Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/linters/urunc-dict.txt
Original file line number Diff line number Diff line change
Expand Up @@ -420,3 +420,5 @@ onsi
ESRCH
Prafful
praffq
rlimits
Rlimits
12 changes: 8 additions & 4 deletions pkg/unikontainers/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
48 changes: 39 additions & 9 deletions pkg/unikontainers/unikernels/linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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:<n>\nTYPE:<type>\nSOFT:<soft>\nHARD:<hard>\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 {
Expand Down
135 changes: 135 additions & 0 deletions pkg/unikontainers/unikernels/linux_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
1 change: 1 addition & 0 deletions pkg/unikontainers/unikontainers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down