Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
b56d626
Create proxy in the correct namespace defined by specs
Luke-Parkin Oct 6, 2025
c8f0026
Use OK rather than err
Luke-Parkin Oct 6, 2025
670e0a4
Add a test for inheriting the correct namespace
Luke-Parkin Oct 7, 2025
d57170c
If running without root, do not do namespace isolation
Luke-Parkin Oct 7, 2025
3736da6
Add tests rootless run
Luke-Parkin Oct 7, 2025
aad47da
lowercase error messages
Luke-Parkin Oct 7, 2025
9649e44
Remove unneeded arg and use correct terminology
Luke-Parkin Oct 7, 2025
f46b9d3
Don't use root-requiring tests, add a unit test.
Luke-Parkin Oct 9, 2025
9c568d3
Unit testing
Luke-Parkin Oct 9, 2025
bb81f30
oops
Luke-Parkin Oct 9, 2025
e6e1e51
Merge branch 'main' into network-namespaces
Luke-Parkin Oct 15, 2025
b1cc94f
Rework tests, improve logging
Luke-Parkin Oct 20, 2025
6fb1977
Address review comments, allow nil linux namespace
Luke-Parkin Oct 20, 2025
8f01ce8
Ensure logger is added, add alpine img for tests. move tests into cen…
Luke-Parkin Oct 20, 2025
f534a7f
Safety third
Luke-Parkin Oct 20, 2025
827a7ac
DRY!
Luke-Parkin Oct 20, 2025
654d472
use a universal bin path
Luke-Parkin Oct 20, 2025
34c5ff1
arch?
Luke-Parkin Oct 20, 2025
a4bf97d
Seems unlikely..
Luke-Parkin Oct 20, 2025
bee0dcf
Revert vm changes
Luke-Parkin Oct 21, 2025
6663003
Merge branch 'main' into network-namespaces
Luke-Parkin Oct 22, 2025
959ee9e
Merge branch 'main' into network-namespaces
Luke-Parkin Oct 27, 2025
4b743a2
Update to match new test method
Luke-Parkin Oct 28, 2025
e0838bb
ProcessPID fix
Luke-Parkin Oct 28, 2025
b39e1ea
Remove rebase failures, remove --network=host from e2e tests, add rat…
Luke-Parkin Oct 31, 2025
3800ba8
Merge branch 'main' into network-namespaces
Luke-Parkin Oct 31, 2025
379ef50
Collapse tests into a nested t.run()
Luke-Parkin Oct 31, 2025
2974a96
Fix formatting
Luke-Parkin Oct 31, 2025
913f822
Change test names
Luke-Parkin Oct 31, 2025
f7bb9d7
Reuse `Runnable` abstraction for `sudo`
muchzill4 Oct 31, 2025
3429464
Merge pull request #1 from muchzill4/sudo-abstraction
Luke-Parkin Oct 31, 2025
fe5a97d
Linting
Luke-Parkin Oct 31, 2025
f96f1ad
Add string return for InstalledBin
Luke-Parkin Oct 31, 2025
a31f40c
Merge branch 'main' into network-namespaces
Luke-Parkin Oct 31, 2025
5d5b162
Remove no longer needed podman arg
muchzill4 Nov 3, 2025
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
45 changes: 44 additions & 1 deletion e2e/runtime_test.go
Comment thread
Luke-Parkin marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,48 @@ func TestRuntimeWriteProcessPid(t *testing.T) {
assertFileContent(t, pidFilePath, fmt.Sprintf("%d", pid))
}

func TestRuntimeProxyKeepsHostNamespaceWhenNotRoot(t *testing.T) {
if os.Geteuid() == 0 {
t.Skip("this test must be run as non-root")
}
Comment thread
Luke-Parkin marked this conversation as resolved.
Outdated
rootDir := t.TempDir()
remoteprocName := "a-lovely-blue-device"
sim := remoteproc.NewSimulator(rootDir).WithName(remoteprocName)
if err := sim.Start(); err != nil {
t.Fatalf("failed to run simulator: %s", err)
}
defer func() { _ = sim.Stop() }()

bin, err := repo.BuildRuntimeBin(t.TempDir(), rootDir, nil)
require.NoError(t, err)

const containerName = "good-looking-container"
bundlePath := t.TempDir()

require.NoError(t, generateBundle(
bundlePath,
remoteprocName,
specs.LinuxNamespace{Type: specs.MountNamespace},
))

_, err = invokeRuntime(bin, "create", "--bundle", bundlePath, containerName)
require.NoError(t, err)
defer func() {
_, _ = invokeRuntime(bin, "delete", containerName)
}()

state, err := getContainerState(bin, containerName)
require.NoError(t, err)
require.Greater(t, state.Pid, 0)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this necessary? Is this a pre-flight check?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I put this in as just a safeguard; in the case the runtime returned some clearly wrong value we wouldn't continue testing against that PID.

Possibly not needed though, doesn't really hurt much?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair. Those e2e tests are a getting chunky, so I'm trying to keep context to minimum and describe as much as possible why/what/where.

I wonder if this would improve the readability and maintain small context:

pid, err := getRunningContainerPid(bin, containerName)

And then

func getRunningContainerPid() (uint, err) {
    state, err := getContainerState()
    if state.Pid == 0 {
        return 0, fmt.Println("container doesn't seem to be running - pid is 0")
    }
    return state.Pid, nil
}


hostMountNS, err := os.Readlink("/proc/self/ns/mnt")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't /proc/self the go test process? Why do we need to check this?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The go test process is launching the runtime.
The runtime then launches the proxy.

The proxy should have inherited the namespace from the go test proxy.

This is the same flow as a user initiating the proxy (give or take)

require.NoError(t, err)
proxyMountNS, err := os.Readlink(fmt.Sprintf("/proc/%d/ns/mnt", state.Pid))
require.NoError(t, err)

assert.Equal(t, hostMountNS, proxyMountNS)
}

func assertContainerStatus(t testing.TB, bin repo.RuntimeBin, containerName string, wantStatus specs.ContainerState) {
t.Helper()
state, err := getContainerState(bin, containerName)
Expand Down Expand Up @@ -156,7 +198,7 @@ func getContainerState(bin repo.RuntimeBin, containerName string) (specs.State,
return state, nil
}

func generateBundle(targetDir string, remoteprocName string) error {
func generateBundle(targetDir string, remoteprocName string, namespaces ...specs.LinuxNamespace) error {
const bundleRoot = "rootfs"
const firmwareName = "hello_world.elf"

Expand All @@ -179,6 +221,7 @@ func generateBundle(targetDir string, remoteprocName string) error {
Annotations: map[string]string{
"remoteproc.name": remoteprocName,
},
Linux: &specs.Linux{Namespaces: namespaces},
}

specPath := filepath.Join(targetDir, "config.json")
Expand Down
62 changes: 60 additions & 2 deletions internal/proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,75 @@ import (
"os"
"os/exec"
"syscall"

"github.com/opencontainers/runtime-spec/specs-go"
"golang.org/x/sys/unix"
)

func NewProcess(devicePath string) (int, error) {
var namespaceFlags = map[specs.LinuxNamespaceType]uintptr{
specs.CgroupNamespace: unix.CLONE_NEWCGROUP,
specs.IPCNamespace: unix.CLONE_NEWIPC,
specs.MountNamespace: unix.CLONE_NEWNS,
specs.NetworkNamespace: unix.CLONE_NEWNET,
specs.PIDNamespace: unix.CLONE_NEWPID,
specs.TimeNamespace: unix.CLONE_NEWTIME,
specs.UserNamespace: unix.CLONE_NEWUSER,
specs.UTSNamespace: unix.CLONE_NEWUTS,
}

var namespaceCloneFlagsFn = namespaceCloneFlags

func namespaceCloneFlags(spec *specs.Spec) (uintptr, error) {
Comment thread
Luke-Parkin marked this conversation as resolved.
Outdated
if spec == nil {
return 0, nil
}

var flags uintptr
Comment thread
th3james marked this conversation as resolved.
Outdated
for _, ns := range spec.Linux.Namespaces {
if ns.Path != "" {
Comment thread
Luke-Parkin marked this conversation as resolved.
Outdated
continue
}
flag, ok := namespaceFlags[ns.Type]
if !ok {
return 0, fmt.Errorf("unknown namespace type %q", ns.Type)
}
flags |= flag
}
return flags, nil
}

func effectiveNamespaceFlags(isRoot bool, spec *specs.Spec) (uintptr, error) {
Comment thread
Luke-Parkin marked this conversation as resolved.
Outdated
flags, err := namespaceCloneFlagsFn(spec)
if err != nil {
return 0, err
}

if !isRoot {
if flags != 0 {
fmt.Fprintln(os.Stderr, "[WARN] running non-root; namespace isolation disabled")
Comment thread
muchzill4 marked this conversation as resolved.
Outdated
}
return 0, nil
}

return flags, nil
}

func NewProcess(spec *specs.Spec, devicePath string) (int, error) {
execPath, err := os.Executable()
if err != nil {
return -1, fmt.Errorf("failed to get executable path: %w", err)
}

isRoot := os.Geteuid() == 0
namespaceFlags, err := effectiveNamespaceFlags(isRoot, spec)
if err != nil {
return -1, err
}

cmd := exec.Command(execPath, "proxy", "--device-path", devicePath)
cmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true,
Setpgid: true,
Cloneflags: namespaceFlags,
}

if err := cmd.Start(); err != nil {
Expand Down
110 changes: 110 additions & 0 deletions internal/proxy/proxy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package proxy
Comment thread
muchzill4 marked this conversation as resolved.
Outdated

import (
"errors"
"io"
"os"
"testing"

"github.com/opencontainers/runtime-spec/specs-go"
"github.com/stretchr/testify/assert"
"golang.org/x/sys/unix"
)

func stubNamespaceCloneFlags(t *testing.T, fn func(*specs.Spec) (uintptr, error)) {
t.Helper()
original := namespaceCloneFlagsFn
namespaceCloneFlagsFn = fn
t.Cleanup(func() {
namespaceCloneFlagsFn = original
})
}

func TestNamespaceCloneFlags(t *testing.T) {
t.Run("returns zero when spec is nil", func(t *testing.T) {
got, err := namespaceCloneFlags(nil)

assert.NoError(t, err)
assert.Equal(t, uintptr(0), got)
})

t.Run("marks for creation namespaces where paths are not provided", func(t *testing.T) {
spec := &specs.Spec{
Linux: &specs.Linux{
Namespaces: []specs.LinuxNamespace{
{Type: specs.NetworkNamespace},
{Type: specs.PIDNamespace, Path: "/proc/self/ns/pid"},
{Type: specs.UTSNamespace},
},
},
}

got, err := namespaceCloneFlags(spec)

assert.NoError(t, err)
want := uintptr(unix.CLONE_NEWNET | unix.CLONE_NEWUTS)
assert.Equal(t, want, got)
})

t.Run("errors with unknown namespace types", func(t *testing.T) {
spec := &specs.Spec{
Linux: &specs.Linux{
Namespaces: []specs.LinuxNamespace{
{Type: specs.LinuxNamespaceType("unknown")},
},
},
}

_, err := namespaceCloneFlags(spec)

assert.Error(t, err)
assert.Equal(t, "unknown namespace type \"unknown\"", err.Error())
})
}

func TestEffectiveNamespaceFlags(t *testing.T) {
t.Run("returns clone flags when running as root", func(t *testing.T) {
stubNamespaceCloneFlags(t, func(spec *specs.Spec) (uintptr, error) {
Comment thread
muchzill4 marked this conversation as resolved.
Outdated
return 0x1507, nil
})

got, err := effectiveNamespaceFlags(true, &specs.Spec{})

assert.NoError(t, err)
assert.Equal(t, uintptr(0x1507), got)
})

t.Run("warns and does not set namespace if root is not set but flags are given", func(t *testing.T) {
stubNamespaceCloneFlags(t, func(spec *specs.Spec) (uintptr, error) {
return 0x2, nil
})

stderr := os.Stderr
reader, writer, err := os.Pipe()
assert.NoError(t, err)
os.Stderr = writer
t.Cleanup(func() {
os.Stderr = stderr
})

got, err := effectiveNamespaceFlags(false, &specs.Spec{})
assert.NoError(t, err)
assert.Equal(t, uintptr(0), got)

assert.NoError(t, writer.Close())
out, readErr := io.ReadAll(reader)
assert.NoError(t, readErr)
assert.Contains(t, string(out), "[WARN] running non-root; namespace isolation disabled")
assert.NoError(t, reader.Close())
})

t.Run("propagates namespaceCloneFlags errors", func(t *testing.T) {
expected := errors.New("error!")
stubNamespaceCloneFlags(t, func(spec *specs.Spec) (uintptr, error) {
return 0, expected
})

_, err := effectiveNamespaceFlags(true, &specs.Spec{})
assert.ErrorIs(t, err, expected)
})
}
2 changes: 1 addition & 1 deletion internal/runtime/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func Create(containerID string, bundlePath string, pidFile string) error {
}
}()

pid, err := proxy.NewProcess(devicePath)
pid, err := proxy.NewProcess(spec, devicePath)
Comment thread
Luke-Parkin marked this conversation as resolved.
Outdated
if err != nil {
return fmt.Errorf("failed to start proxy process: %w", err)
}
Expand Down