From aad32602777bdcf6b7b76265d5e0245c410fb80b Mon Sep 17 00:00:00 2001 From: Martijn Date: Tue, 10 Nov 2026 16:09:26 +0100 Subject: [PATCH 1/5] chore(linter): Update the Makefile's golangci-lint to v2.15 The lint workflow moved to golangci-lint v2.15 with the Go v1.27.2 update in ee1476e, but the Makefile stayed at v2.9, which can't load a go1.27.3 module, so make lint stopped before linting. Fixes: #1201 Signed-off-by: Martijn --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index afe535844..5dc264ace 100644 --- a/Makefile +++ b/Makefile @@ -87,8 +87,8 @@ CNTR_OPTS ?= run --rm -it # Linking variables LINT_CNTR_OPTS ?= $(CNTR_OPTS) -v $(CURDIR):/app -w /app -#? LINT_CNTR_IMG The linter image to use (default: golangci/golangci-lint:v1.53.3) -LINT_CNTR_IMG ?= golangci/golangci-lint:v2.9 +#? LINT_CNTR_IMG The linter image to use (default: golangci/golangci-lint:v2.15) +LINT_CNTR_IMG ?= golangci/golangci-lint:v2.15 LINT_CNTR_CMD ?= golangci-lint run -v --timeout=5m #? DOCS_CNTR_IMG The mkdocs image to use (default: harbor.nbfc.io/nubificus/urunc/mkdocs:test) From 23369adfd389dcf99663e59649596016081125c0 Mon Sep 17 00:00:00 2001 From: Martijn Date: Tue, 10 Nov 2026 16:15:23 +0100 Subject: [PATCH 2/5] refactor(hypervisors): Share the solo5 cli between hvt and spt The hvt and spt tenders take the same command line, and their BuildExecCmd bodies were identical, which dupl reported. Build it in one solo5ExecCmd. Fixes: #1201 Signed-off-by: Martijn --- pkg/unikontainers/hypervisors/hvt.go | 20 +------------ pkg/unikontainers/hypervisors/solo5.go | 40 ++++++++++++++++++++++++++ pkg/unikontainers/hypervisors/spt.go | 20 +------------ 3 files changed, 42 insertions(+), 38 deletions(-) create mode 100644 pkg/unikontainers/hypervisors/solo5.go diff --git a/pkg/unikontainers/hypervisors/hvt.go b/pkg/unikontainers/hypervisors/hvt.go index 3e1932b56..83d8a9719 100644 --- a/pkg/unikontainers/hypervisors/hvt.go +++ b/pkg/unikontainers/hypervisors/hvt.go @@ -76,25 +76,7 @@ func (h *HVT) Ok() error { } func (h *HVT) BuildExecCmd(args types.ExecArgs, ukernel types.Unikernel) ([]string, error) { - hvtMem := BytesToStringMB(args.MemSizeB) - exArgs := []string{h.binaryPath, "--mem=" + hvtMem} - if args.Net.TapDev != "" { - netCli := ukernel.MonitorNetCli(args.Net.TapDev, args.Net.MAC) - exArgs = append(exArgs, netCli...) - } - bArgs := ukernel.MonitorBlockCli() - for _, blockArg := range bArgs { - if blockArg.Path != "" { - exArgs = append(exArgs, "--block:"+blockArg.ID+"="+blockArg.Path) - } - } - extraMonArgs := ukernel.MonitorCli() - exArgs = append(exArgs, extraMonArgs.OtherArgs...) - exArgs = append(exArgs, solo5ArgsSeparator, args.UnikernelPath) - if args.Command != "" { - exArgs = append(exArgs, args.Command) - } - return exArgs, nil + return solo5ExecCmd(h.binaryPath, args, ukernel), nil } // PreExec performs pre-execution setup for HVT. diff --git a/pkg/unikontainers/hypervisors/solo5.go b/pkg/unikontainers/hypervisors/solo5.go new file mode 100644 index 000000000..87a2c3601 --- /dev/null +++ b/pkg/unikontainers/hypervisors/solo5.go @@ -0,0 +1,40 @@ +// 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 hypervisors + +import "github.com/urunc-dev/urunc/pkg/unikontainers/types" + +// solo5ExecCmd builds the command line of a solo5 tender (hvt or spt). Both +// share the cli grammar "[ OPTIONS ] [ -- ] KERNEL [ ARGS ]". +func solo5ExecCmd(binaryPath string, args types.ExecArgs, ukernel types.Unikernel) []string { + exArgs := []string{binaryPath, "--mem=" + BytesToStringMB(args.MemSizeB)} + if args.Net.TapDev != "" { + netCli := ukernel.MonitorNetCli(args.Net.TapDev, args.Net.MAC) + exArgs = append(exArgs, netCli...) + } + bArgs := ukernel.MonitorBlockCli() + for _, blockArg := range bArgs { + if blockArg.Path != "" { + exArgs = append(exArgs, "--block:"+blockArg.ID+"="+blockArg.Path) + } + } + extraMonArgs := ukernel.MonitorCli() + exArgs = append(exArgs, extraMonArgs.OtherArgs...) + exArgs = append(exArgs, solo5ArgsSeparator, args.UnikernelPath) + if args.Command != "" { + exArgs = append(exArgs, args.Command) + } + return exArgs +} diff --git a/pkg/unikontainers/hypervisors/spt.go b/pkg/unikontainers/hypervisors/spt.go index 80b622e78..7844b0d3b 100644 --- a/pkg/unikontainers/hypervisors/spt.go +++ b/pkg/unikontainers/hypervisors/spt.go @@ -74,25 +74,7 @@ func (s *SPT) Ok() error { } func (s *SPT) BuildExecCmd(args types.ExecArgs, ukernel types.Unikernel) ([]string, error) { - sptMem := BytesToStringMB(args.MemSizeB) - exArgs := []string{s.binaryPath, "--mem=" + sptMem} - if args.Net.TapDev != "" { - netCli := ukernel.MonitorNetCli(args.Net.TapDev, args.Net.MAC) - exArgs = append(exArgs, netCli...) - } - bArgs := ukernel.MonitorBlockCli() - for _, blockArg := range bArgs { - if blockArg.Path != "" { - exArgs = append(exArgs, "--block:"+blockArg.ID+"="+blockArg.Path) - } - } - extraMonArgs := ukernel.MonitorCli() - exArgs = append(exArgs, extraMonArgs.OtherArgs...) - exArgs = append(exArgs, solo5ArgsSeparator, args.UnikernelPath) - if args.Command != "" { - exArgs = append(exArgs, args.Command) - } - return exArgs, nil + return solo5ExecCmd(s.binaryPath, args, ukernel), nil } // PreExec performs pre-execution setup. SPT has no special pre-exec requirements. From 993f0803644d05bb81bc813d262260b03312ac1e Mon Sep 17 00:00:00 2001 From: Martijn Date: Tue, 10 Nov 2026 16:18:41 +0100 Subject: [PATCH 3/5] refactor(mount): Compare statfs flags as int64 Statfs_t.Flags is an int64 on amd64 and arm64, and the ST_* constants are untyped, so keep them as int64 instead of converting the flags to uintptr, which gosec reported as G115. Fixes: #1201 Signed-off-by: Martijn --- pkg/unikontainers/mount.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/unikontainers/mount.go b/pkg/unikontainers/mount.go index b0a8f036a..769d7f745 100644 --- a/pkg/unikontainers/mount.go +++ b/pkg/unikontainers/mount.go @@ -276,7 +276,8 @@ func getUnprivilegedMountFlags(path string) (uintptr, error) { // so map each ST_* bit that statfs reports to the MS_* flag used for the // remount explicitly. lockedFlags := []struct { - st, ms uintptr + st int64 + ms uintptr }{ {unix.ST_RDONLY, unix.MS_RDONLY}, {unix.ST_NODEV, unix.MS_NODEV}, @@ -289,7 +290,7 @@ func getUnprivilegedMountFlags(path string) (uintptr, error) { var flags uintptr for _, f := range lockedFlags { - if uintptr(st.Flags)&f.st == f.st { + if st.Flags&f.st == f.st { flags |= f.ms } } From 3805f23286de6bf581f7eb5bb9e71f3ad20e2361 Mon Sep 17 00:00:00 2001 From: Martijn Date: Tue, 10 Nov 2026 16:20:20 +0100 Subject: [PATCH 4/5] chore(linter): Annotate the gosec false positives in cmd/urunc runcExec hands non-unikernel containers to runc with urunc's own arguments, and prepareXDGRuntimeDir only creates $XDG_RUNTIME_DIR/urunc for a rootless user, so G702 and G703 do not apply there. Fixes: #1201 Signed-off-by: Martijn --- cmd/urunc/utils.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/urunc/utils.go b/cmd/urunc/utils.go index 9bbaa7c9a..6a83db47e 100644 --- a/cmd/urunc/utils.go +++ b/cmd/urunc/utils.go @@ -100,7 +100,7 @@ func runcExec() error { return err } args[0] = binPath - return syscall.Exec(binPath, args, os.Environ()) + return syscall.Exec(binPath, args, os.Environ()) //nolint:gosec // runc gets urunc's own arguments } // newSockPair returns a new SOCK_STREAM unix socket pair. @@ -154,10 +154,10 @@ func ShouldHonorXDGRuntimeDir() bool { // prepareXDGRuntimeDir prepares the XDG_RUNTIME_DIR directory according to the XDG specification. // It creates the directory with proper permissions and sets the sticky bit to prevent auto-pruning. func prepareXDGRuntimeDir(root string) error { - if err := os.MkdirAll(root, 0o700); err != nil { + if err := os.MkdirAll(root, 0o700); err != nil { //nolint:gosec // root is $XDG_RUNTIME_DIR/urunc, the user's own dir return fmt.Errorf("the path in $XDG_RUNTIME_DIR must be writable by the user: %w", err) } - if err := os.Chmod(root, os.FileMode(0o700)|os.ModeSticky); err != nil { + if err := os.Chmod(root, os.FileMode(0o700)|os.ModeSticky); err != nil { //nolint:gosec // as above return fmt.Errorf("you should check permission of the path in $XDG_RUNTIME_DIR: %w", err) } return nil From 5e4a33417296b8be9e5d721fbe1343eab0078b28 Mon Sep 17 00:00:00 2001 From: Martijn Date: Tue, 10 Nov 2026 16:56:02 +0100 Subject: [PATCH 5/5] fix(cli): Create the XDG runtime dir through os.Root prepareXDGRuntimeDir opens $XDG_RUNTIME_DIR as an os.Root and creates urunc's directory in it, so a symlink can't send MkdirAll or Chmod outside, and G703 holds without the nolint. $XDG_RUNTIME_DIR itself must now exist, as the XDG specification requires. Fixes: #1201 Signed-off-by: Martijn --- cmd/urunc/main.go | 4 +-- cmd/urunc/utils.go | 23 +++++++++------ cmd/urunc/utils_test.go | 62 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 10 deletions(-) create mode 100644 cmd/urunc/utils_test.go diff --git a/cmd/urunc/main.go b/cmd/urunc/main.go index c2136f249..b68a4a67d 100644 --- a/cmd/urunc/main.go +++ b/cmd/urunc/main.go @@ -128,8 +128,8 @@ func main() { if !cmd.IsSet("root") { xdgRuntimeDir := os.Getenv("XDG_RUNTIME_DIR") if xdgRuntimeDir != "" && ShouldHonorXDGRuntimeDir() { - root := xdgRuntimeDir + "/urunc" - if err := prepareXDGRuntimeDir(root); err != nil { + root, err := prepareXDGRuntimeDir(xdgRuntimeDir) + if err != nil { return nil, err } if err := cmd.Set("root", root); err != nil { diff --git a/cmd/urunc/utils.go b/cmd/urunc/utils.go index 6a83db47e..e99dcdedf 100644 --- a/cmd/urunc/utils.go +++ b/cmd/urunc/utils.go @@ -151,16 +151,23 @@ func ShouldHonorXDGRuntimeDir() bool { return !ok || u != "root" } -// prepareXDGRuntimeDir prepares the XDG_RUNTIME_DIR directory according to the XDG specification. -// It creates the directory with proper permissions and sets the sticky bit to prevent auto-pruning. -func prepareXDGRuntimeDir(root string) error { - if err := os.MkdirAll(root, 0o700); err != nil { //nolint:gosec // root is $XDG_RUNTIME_DIR/urunc, the user's own dir - return fmt.Errorf("the path in $XDG_RUNTIME_DIR must be writable by the user: %w", err) +// prepareXDGRuntimeDir creates urunc's directory in $XDG_RUNTIME_DIR according to +// the XDG specification, with the sticky bit set to prevent auto-pruning, and +// returns its path. os.Root keeps both steps inside $XDG_RUNTIME_DIR. +func prepareXDGRuntimeDir(xdgRuntimeDir string) (string, error) { + const name = "urunc" + dir, err := os.OpenRoot(xdgRuntimeDir) + if err != nil { + return "", fmt.Errorf("the path in $XDG_RUNTIME_DIR must be writable by the user: %w", err) } - if err := os.Chmod(root, os.FileMode(0o700)|os.ModeSticky); err != nil { //nolint:gosec // as above - return fmt.Errorf("you should check permission of the path in $XDG_RUNTIME_DIR: %w", err) + defer dir.Close() + if err := dir.MkdirAll(name, 0o700); err != nil { + return "", fmt.Errorf("the path in $XDG_RUNTIME_DIR must be writable by the user: %w", err) } - return nil + if err := dir.Chmod(name, os.FileMode(0o700)|os.ModeSticky); err != nil { + return "", fmt.Errorf("you should check permission of the path in $XDG_RUNTIME_DIR: %w", err) + } + return filepath.Join(xdgRuntimeDir, name), nil } // validateID validates the given ID string against the allowed characters. diff --git a/cmd/urunc/utils_test.go b/cmd/urunc/utils_test.go new file mode 100644 index 000000000..e289b6ce6 --- /dev/null +++ b/cmd/urunc/utils_test.go @@ -0,0 +1,62 @@ +// 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 main + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestPrepareXDGRuntimeDir(t *testing.T) { + t.Parallel() + xdg := t.TempDir() + + root, err := prepareXDGRuntimeDir(xdg) + require.NoError(t, err) + assert.Equal(t, filepath.Join(xdg, "urunc"), root) + + fi, err := os.Stat(root) + require.NoError(t, err) + assert.True(t, fi.IsDir()) + assert.Equal(t, os.FileMode(0o700)|os.ModeSticky, fi.Mode()&(os.ModePerm|os.ModeSticky)) + + _, err = prepareXDGRuntimeDir(xdg) + require.NoError(t, err, "an existing directory is reused") +} + +func TestPrepareXDGRuntimeDirMissing(t *testing.T) { + t.Parallel() + _, err := prepareXDGRuntimeDir(filepath.Join(t.TempDir(), "missing")) + assert.Error(t, err) +} + +func TestPrepareXDGRuntimeDirStaysInside(t *testing.T) { + t.Parallel() + xdg := t.TempDir() + outside := t.TempDir() + require.NoError(t, os.Chmod(outside, 0o755)) + require.NoError(t, os.Symlink(outside, filepath.Join(xdg, "urunc"))) + + _, err := prepareXDGRuntimeDir(xdg) + require.Error(t, err) + + fi, err := os.Stat(outside) + require.NoError(t, err) + assert.Equal(t, os.FileMode(0o755), fi.Mode()&(os.ModePerm|os.ModeSticky), "the directory outside is not changed") +}