From 6fcd2728179caef566dfa5c0f41653326b08e2f5 Mon Sep 17 00:00:00 2001 From: Naman Sharma Date: Tue, 14 Apr 2026 16:23:26 +0530 Subject: [PATCH] feat(unikernels): pass OCI rlimits to urunit config Serialize process.rlimits from the OCI spec into the URUNIT_CONFIG file as RLIMIT:TYPE:SOFT:HARD lines inside the UCS block. Fixes: #312 Signed-off-by: namansh70747 Signed-off-by: Sankalp --- .github/linters/urunc-dict.txt | 4 +- pkg/unikontainers/hypervisors/qemu.go | 6 +- pkg/unikontainers/types/types.go | 9 +- pkg/unikontainers/unikernels/linux.go | 9 ++ pkg/unikontainers/unikernels/linux_test.go | 113 +++++++++++++++++++++ pkg/unikontainers/unikontainers.go | 1 + 6 files changed, 135 insertions(+), 7 deletions(-) create mode 100644 pkg/unikontainers/unikernels/linux_test.go diff --git a/.github/linters/urunc-dict.txt b/.github/linters/urunc-dict.txt index 538649e0..27950462 100644 --- a/.github/linters/urunc-dict.txt +++ b/.github/linters/urunc-dict.txt @@ -405,4 +405,6 @@ Logr onsi ESRCH Prafful -praffq \ No newline at end of file +praffq +rlimits +Rlimits diff --git a/pkg/unikontainers/hypervisors/qemu.go b/pkg/unikontainers/hypervisors/qemu.go index 712e73da..d4e2f466 100644 --- a/pkg/unikontainers/hypervisors/qemu.go +++ b/pkg/unikontainers/hypervisors/qemu.go @@ -58,9 +58,9 @@ func (q *Qemu) Path() string { func (q *Qemu) BuildExecCmd(args types.ExecArgs, ukernel types.Unikernel) ([]string, error) { qemuMem := BytesToStringMB(args.MemSizeB) cmdString := q.binaryPath + " -m " + qemuMem + "M" - cmdString += " -L /usr/share/qemu" // Set the path for qemu bios/data - cmdString += " -cpu host" // Choose CPU - cmdString += " -enable-kvm" // Enable KVM to use CPU virt extensions + cmdString += " -L /usr/share/qemu" // Set the path for qemu bios/data + cmdString += " -cpu host" // Choose CPU + cmdString += " -enable-kvm" // Enable KVM to use CPU virt extensions cmdString += " -display none -vga none -serial stdio -monitor null" // Disable graphic output if args.VCPUs > 0 { diff --git a/pkg/unikontainers/types/types.go b/pkg/unikontainers/types/types.go index f8898d09..261f151a 100644 --- a/pkg/unikontainers/types/types.go +++ b/pkg/unikontainers/types/types.go @@ -15,6 +15,8 @@ //revive:disable:var-naming package types +import specs "github.com/opencontainers/runtime-spec/specs-go" + type Unikernel interface { Init(UnikernelParams) error CommandString() (string, error) @@ -70,9 +72,10 @@ type RootfsParams struct { // Specific to Linux type ProcessConfig struct { - UID uint32 // The uid of the process inside the guest - GID uint32 // The gid of the process inside the guest - WorkDir string // The workdir of the process inside the guest + UID uint32 // The uid of the process inside the guest + GID uint32 // The gid of the process inside the guest + WorkDir string // The workdir of the process inside the guest + Rlimits []specs.POSIXRlimit // The rlimits for the process inside the guest } // UnikernelParams holds the data required to build the unikernels commandline diff --git a/pkg/unikontainers/unikernels/linux.go b/pkg/unikontainers/unikernels/linux.go index d6bb8095..53bb5178 100644 --- a/pkg/unikontainers/unikernels/linux.go +++ b/pkg/unikontainers/unikernels/linux.go @@ -314,6 +314,15 @@ func (l *Linux) buildUrunitConfig() string { sb.WriteString("WD:") sb.WriteString(l.ProcConfig.WorkDir) sb.WriteString("\n") + for _, rl := range l.ProcConfig.Rlimits { + sb.WriteString("RLIMIT:") + sb.WriteString(rl.Type) + sb.WriteString(":") + sb.WriteString(strconv.FormatUint(rl.Soft, 10)) + sb.WriteString(":") + sb.WriteString(strconv.FormatUint(rl.Hard, 10)) + sb.WriteString("\n") + } sb.WriteString(lpcEndMarker) sb.WriteString("\n") sb.WriteString(blkStartMarker) diff --git a/pkg/unikontainers/unikernels/linux_test.go b/pkg/unikontainers/unikernels/linux_test.go new file mode 100644 index 00000000..a5597419 --- /dev/null +++ b/pkg/unikontainers/unikernels/linux_test.go @@ -0,0 +1,113 @@ +// Copyright (c) 2023-2026, Nubificus LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package unikernels + +import ( + "strings" + "testing" + + specs "github.com/opencontainers/runtime-spec/specs-go" + "github.com/stretchr/testify/assert" + "github.com/urunc-dev/urunc/pkg/unikontainers/types" +) + +func newTestLinux(rlimits []specs.POSIXRlimit) *Linux { + return &Linux{ + Env: []string{"PATH=/usr/local/bin"}, + Monitor: "qemu", + ProcConfig: types.ProcessConfig{ + UID: 1000, + GID: 1000, + WorkDir: "/app", + Rlimits: rlimits, + }, + } +} + +func TestBuildUrunitConfigNoRlimits(t *testing.T) { + tests := []struct { + name string + rlimits []specs.POSIXRlimit + }{ + {name: "nil", rlimits: nil}, + {name: "empty", rlimits: []specs.POSIXRlimit{}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := newTestLinux(tt.rlimits) + conf := l.buildUrunitConfig() + t.Logf("generated urunit.conf:\n%s", conf) + assert.NotContains(t, conf, "RLIMIT:", "expected no RLIMIT lines") + }) + } +} + +func TestBuildUrunitConfigSingleRlimit(t *testing.T) { + l := newTestLinux([]specs.POSIXRlimit{ + {Type: "RLIMIT_NOFILE", Soft: 1024, Hard: 4096}, + }) + conf := l.buildUrunitConfig() + t.Logf("generated urunit.conf:\n%s", conf) + + assert.Contains(t, conf, "RLIMIT:RLIMIT_NOFILE:1024:4096\n") +} + +func TestBuildUrunitConfigMultipleRlimits(t *testing.T) { + l := newTestLinux([]specs.POSIXRlimit{ + {Type: "RLIMIT_NOFILE", Soft: 1024, Hard: 4096}, + {Type: "RLIMIT_NPROC", Soft: 512, Hard: 1024}, + {Type: "RLIMIT_AS", Soft: 0, Hard: 0}, + }) + conf := l.buildUrunitConfig() + t.Logf("generated urunit.conf:\n%s", conf) + + assert.Contains(t, conf, "RLIMIT:RLIMIT_NOFILE:1024:4096\n") + assert.Contains(t, conf, "RLIMIT:RLIMIT_NPROC:512:1024\n") + assert.Contains(t, conf, "RLIMIT:RLIMIT_AS:0:0\n") +} + +func TestBuildUrunitConfigRlimitsInsideProcessBlock(t *testing.T) { + l := newTestLinux([]specs.POSIXRlimit{ + {Type: "RLIMIT_NOFILE", Soft: 1024, Hard: 4096}, + }) + conf := l.buildUrunitConfig() + t.Logf("generated urunit.conf:\n%s", conf) + + ucs := strings.Index(conf, "UCS\n") + uce := strings.Index(conf, "UCE\n") + if ucs < 0 || uce < 0 || ucs >= uce { + t.Fatalf("invalid UCS/UCE markers:\n%s", conf) + } + + block := conf[ucs : uce+4] + assert.Contains(t, block, "RLIMIT:RLIMIT_NOFILE:1024:4096\n", "expected RLIMIT line inside process block") +} + +func TestBuildUrunitConfigUIDGIDWorkdir(t *testing.T) { + l := &Linux{ + ProcConfig: types.ProcessConfig{ + UID: 500, + GID: 501, + WorkDir: "/workdir", + }, + } + conf := l.buildUrunitConfig() + t.Logf("generated urunit.conf:\n%s", conf) + + assert.Contains(t, conf, "UID:500\n") + assert.Contains(t, conf, "GID:501\n") + assert.Contains(t, conf, "WD:/workdir\n") +} diff --git a/pkg/unikontainers/unikontainers.go b/pkg/unikontainers/unikontainers.go index c09508ce..750ae0ef 100644 --- a/pkg/unikontainers/unikontainers.go +++ b/pkg/unikontainers/unikontainers.go @@ -285,6 +285,7 @@ func (u *Unikontainer) Exec(metrics m.Writer) error { UID: u.Spec.Process.User.UID, GID: u.Spec.Process.User.GID, WorkDir: u.Spec.Process.Cwd, + Rlimits: u.Spec.Process.Rlimits, } // UnikernelParams // populate unikernel params