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..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 { @@ -316,6 +318,34 @@ func (l *Linux) buildUrunitConfig() string { 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 new file mode 100644 index 000000000..c1311dd57 --- /dev/null +++ b/pkg/unikontainers/unikernels/linux_test.go @@ -0,0 +1,135 @@ +// 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) + // 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") + }) + } +} + +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, "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) { + 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, "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 TestBuildUrunitConfigRlimitsAreInOwnBlock(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") + 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) + } + + // 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) { + 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