From 65354e49f8548001ff81487b192815c2bf03e57c Mon Sep 17 00:00:00 2001 From: Naman Sharma Date: Tue, 14 Apr 2026 16:23:26 +0530 Subject: [PATCH 1/2] 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 | 2 + pkg/unikontainers/types/types.go | 12 ++- pkg/unikontainers/unikernels/linux.go | 9 ++ pkg/unikontainers/unikernels/linux_test.go | 113 +++++++++++++++++++++ pkg/unikontainers/unikontainers.go | 1 + 5 files changed, 133 insertions(+), 4 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 51956d01c..704ab751d 100644 --- a/.github/linters/urunc-dict.txt +++ b/.github/linters/urunc-dict.txt @@ -420,3 +420,5 @@ onsi ESRCH Prafful praffq +rlimits +Rlimits diff --git a/pkg/unikontainers/types/types.go b/pkg/unikontainers/types/types.go index c6388e2cc..b4c8fb15b 100644 --- a/pkg/unikontainers/types/types.go +++ b/pkg/unikontainers/types/types.go @@ -15,7 +15,10 @@ //revive:disable:var-naming package types -import "golang.org/x/sys/unix" +import ( + specs "github.com/opencontainers/runtime-spec/specs-go" + "golang.org/x/sys/unix" +) type Unikernel interface { Init(UnikernelParams) error @@ -74,9 +77,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 d6bb80952..53bb51784 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 000000000..a55974192 --- /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 a3a00bb5f..1a6f46212 100644 --- a/pkg/unikontainers/unikontainers.go +++ b/pkg/unikontainers/unikontainers.go @@ -387,6 +387,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 From b8cff40e9c67c14204a852b0727be92562ed13b3 Mon Sep 17 00:00:00 2001 From: Naman Sharma Date: Mon, 1 Jun 2026 14:04:39 +0530 Subject: [PATCH 2/2] fix(linux): move rlimits to separate RLS/RLE block in urunit config Follow mentor protocol: rlimits get own RLS/RLE block with NUM: header. Format: RLS NUM: TYPE: SOFT: HARD: RLE Block only emitted when rlimits present. Existing images unaffected. Companion urunit change: namansh70747/urunit#14 Fixes #312 Signed-off-by: Naman Sharma --- pkg/unikontainers/unikernels/linux.go | 57 +++++++++++++++------- pkg/unikontainers/unikernels/linux_test.go | 42 ++++++++++++---- 2 files changed, 71 insertions(+), 28 deletions(-) diff --git a/pkg/unikontainers/unikernels/linux.go b/pkg/unikontainers/unikernels/linux.go index 53bb51784..76e7df1c7 100644 --- a/pkg/unikontainers/unikernels/linux.go +++ b/pkg/unikontainers/unikernels/linux.go @@ -27,15 +27,17 @@ import ( ) const ( - LinuxUnikernel string = "linux" - urunitConfPath string = "/urunit.conf" - retainInitrdPath string = "/sys/firmware/initrd" - envStartMarker string = "UES" - envEndMarker string = "UEE" - lpcStartMarker string = "UCS" // Linux process config start marker - lpcEndMarker string = "UCE" // Linux process config end marker - blkStartMarker string = "UBS" // Block-based mounts start marker - blkEndMarker string = "UBE" // Block-based mounts end marker + LinuxUnikernel string = "linux" + urunitConfPath string = "/urunit.conf" + retainInitrdPath string = "/sys/firmware/initrd" + envStartMarker string = "UES" + envEndMarker string = "UEE" + lpcStartMarker string = "UCS" // Linux process config start marker + lpcEndMarker string = "UCE" // Linux process config end marker + blkStartMarker string = "UBS" // Block-based mounts start marker + blkEndMarker string = "UBE" // Block-based mounts end marker + rlimitStartMarker string = "RLS" // Resource limits (rlimits) start marker + rlimitEndMarker string = "RLE" // Resource limits (rlimits) end marker ) type Linux struct { @@ -314,17 +316,36 @@ 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") + // Resource limits (rlimits) are passed in their own block, following the + // same KEY:VALUE convention as the rest of the protocol. The block begins + // with the number of entries (NUM), so urunit can allocate the rlimit + // arrays exactly once. Each entry is described by three lines: TYPE, SOFT + // and HARD. The block is only emitted when there is at least one rlimit, so + // that the generated configuration stays identical to before for guests + // without any rlimits. + // Format: RLS\nNUM:\nTYPE:\nSOFT:\nHARD:\n...\nRLE\n + if len(l.ProcConfig.Rlimits) > 0 { + sb.WriteString(rlimitStartMarker) + sb.WriteString("\n") + sb.WriteString("NUM:") + sb.WriteString(strconv.Itoa(len(l.ProcConfig.Rlimits))) + sb.WriteString("\n") + for _, rl := range l.ProcConfig.Rlimits { + sb.WriteString("TYPE:") + sb.WriteString(rl.Type) + sb.WriteString("\n") + sb.WriteString("SOFT:") + sb.WriteString(strconv.FormatUint(rl.Soft, 10)) + sb.WriteString("\n") + sb.WriteString("HARD:") + sb.WriteString(strconv.FormatUint(rl.Hard, 10)) + sb.WriteString("\n") + } + sb.WriteString(rlimitEndMarker) + sb.WriteString("\n") + } sb.WriteString(blkStartMarker) sb.WriteString("\n") for _, b := range l.Blk { diff --git a/pkg/unikontainers/unikernels/linux_test.go b/pkg/unikontainers/unikernels/linux_test.go index a55974192..c1311dd57 100644 --- a/pkg/unikontainers/unikernels/linux_test.go +++ b/pkg/unikontainers/unikernels/linux_test.go @@ -50,7 +50,12 @@ func TestBuildUrunitConfigNoRlimits(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") + // With no rlimits the whole RLS block is omitted, so the + // generated configuration stays identical to guests that + // predate the rlimit support. + assert.NotContains(t, conf, "RLS\n", "expected no rlimit block") + assert.NotContains(t, conf, "RLE\n", "expected no rlimit block") + assert.NotContains(t, conf, "TYPE:", "expected no rlimit entries") }) } } @@ -62,7 +67,12 @@ func TestBuildUrunitConfigSingleRlimit(t *testing.T) { 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, "RLS\n") + assert.Contains(t, conf, "NUM:1\n") + assert.Contains(t, conf, "TYPE:RLIMIT_NOFILE\n") + assert.Contains(t, conf, "SOFT:1024\n") + assert.Contains(t, conf, "HARD:4096\n") + assert.Contains(t, conf, "RLE\n") } func TestBuildUrunitConfigMultipleRlimits(t *testing.T) { @@ -74,12 +84,13 @@ func TestBuildUrunitConfigMultipleRlimits(t *testing.T) { 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") + assert.Contains(t, conf, "NUM:3\n", "NUM must match the number of entries") + assert.Contains(t, conf, "TYPE:RLIMIT_NOFILE\nSOFT:1024\nHARD:4096\n") + assert.Contains(t, conf, "TYPE:RLIMIT_NPROC\nSOFT:512\nHARD:1024\n") + assert.Contains(t, conf, "TYPE:RLIMIT_AS\nSOFT:0\nHARD:0\n") } -func TestBuildUrunitConfigRlimitsInsideProcessBlock(t *testing.T) { +func TestBuildUrunitConfigRlimitsAreInOwnBlock(t *testing.T) { l := newTestLinux([]specs.POSIXRlimit{ {Type: "RLIMIT_NOFILE", Soft: 1024, Hard: 4096}, }) @@ -88,12 +99,23 @@ func TestBuildUrunitConfigRlimitsInsideProcessBlock(t *testing.T) { 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) + rls := strings.Index(conf, "RLS\n") + rle := strings.Index(conf, "RLE\n") + ubs := strings.Index(conf, "UBS\n") + if ucs < 0 || uce < 0 || rls < 0 || rle < 0 || ubs < 0 { + t.Fatalf("missing one of UCS/UCE/RLS/RLE/UBS 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") + // The rlimit entries must live in their own RLS..RLE block and not leak + // into the UCS..UCE process-config block. + procBlock := conf[ucs : uce+len("UCE\n")] + assert.NotContains(t, procBlock, "TYPE:", "rlimits must not be inside the process block") + + // Block ordering must be UCS..UCE, then RLS..RLE, then UBS, so urunit + // parses each block in the expected sequence. + assert.Less(t, uce, rls, "RLS block must come after the UCE marker") + assert.Less(t, rls, rle, "RLS must precede RLE") + assert.Less(t, rle, ubs, "RLS block must come before the UBS block") } func TestBuildUrunitConfigUIDGIDWorkdir(t *testing.T) {