Skip to content
Merged
Show file tree
Hide file tree
Changes from 32 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
2 changes: 1 addition & 1 deletion cmd/remoteproc-runtime/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ var createCmd = &cobra.Command{
if bundlePath == "" {
bundlePath = "."
}
return runtime.Create(containerID, bundlePath, pidFile)
return runtime.Create(logger, containerID, bundlePath, pidFile)
},
}

Expand Down
3 changes: 0 additions & 3 deletions docs/DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,6 @@ Useful for development without access to hardware with Remoteproc support.

#### Testing with Docker

⚠️ Docker network must be set to 'Host' (`--network=host`), as the proxy runs in the host's network namespace.

1. **Build and install the shim and runtime with custom root**

```bash
Expand Down Expand Up @@ -139,7 +137,6 @@ Useful for development without access to hardware with Remoteproc support.
docker run \
--runtime io.containerd.remoteproc.v1 \
--annotation remoteproc.name="test-processor" \
--network=host \
Comment thread
Luke-Parkin marked this conversation as resolved.
test-remoteproc-image
```

Expand Down
21 changes: 17 additions & 4 deletions e2e/limavm/limavm.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,19 +56,32 @@ func (vm VM) RunCommand(name string, args ...string) (stdout, stderr string, err
return stdout, stderr, nil
}

type Runnable interface {
Run(args ...string) (stdout, stderr string, err error)
}

type InstalledBin struct {
vm VM
pathToBin string
}

func (b InstalledBin) String() string {
return b.pathToBin
}

func (b InstalledBin) Run(args ...string) (stdout, stderr string, err error) {
return b.vm.RunCommand(b.pathToBin, args...)
}

type Sudo struct {
vm VM
pathToBin string
}

func NewSudo(b InstalledBin) Sudo {
return Sudo(b)
}

func (r Sudo) Run(args ...string) (stdout, stderr string, err error) {
return r.vm.RunCommand("sudo", append([]string{r.pathToBin}, args...)...)
}

func Require(t *testing.T) {
if _, err := exec.LookPath("limactl"); err != nil {
t.Skip("limactl not found. Install limavm: https://lima-vm.io/")
Expand Down
97 changes: 93 additions & 4 deletions 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 @@ -141,6 +141,94 @@ func TestRuntime(t *testing.T) {
require.FileExists(t, pidFile)
assertFileContent(t, pidFile, fmt.Sprintf("%d", pid))
})

t.Run("proxy process namespacing", func(t *testing.T) {
installedRuntimeSudo := limavm.NewSudo(installedRuntime)

t.Run("creates process in requested namespace when root", func(t *testing.T) {
remoteprocName := "lovely-blue-device"
sim := remoteproc.NewSimulator(rootpathPrefix).WithName(remoteprocName)
if err := sim.Start(); err != nil {
t.Fatalf("failed to run simulator: %s", err)
}
defer func() { _ = sim.Stop() }()

uniqueID := testID(t)
containerName := uniqueID
bundlePath := filepath.Join(dirMountedInVM, uniqueID)
require.NoError(t, generateBundle(
bundlePath,
remoteprocName,
specs.LinuxNamespace{Type: specs.MountNamespace},
))
_, stderr, err := installedRuntimeSudo.Run(
"create",
"--bundle", bundlePath,
containerName)
require.NoError(t, err, "stderr: %s", stderr)
t.Cleanup(func() {
_, _, _ = installedRuntimeSudo.Run("delete", containerName)
})

pid, err := getContainerPid(installedRuntimeSudo, containerName)
require.NoError(t, err)

hostMountNS, stderr, err := vm.RunCommand("readlink", "/proc/self/ns/mnt")
require.NoError(t, err, "stderr: %s", stderr)

proxyMountNS, stderr, err := vm.RunCommand("sudo", "readlink", fmt.Sprintf("/proc/%d/ns/mnt", pid))
require.NoError(t, err, "stderr: %s", stderr)
assert.NotEqual(t, strings.TrimSpace(hostMountNS), strings.TrimSpace(proxyMountNS))

remoteproc.AssertState(t, sim.DeviceDir(), "offline")

_, stderr, err = installedRuntimeSudo.Run("start", containerName)
require.NoError(t, err, "stderr: %s", stderr)
remoteproc.AssertState(t, sim.DeviceDir(), "running")
})

t.Run("creates process in user's namespace when not root", func(t *testing.T) {
remoteprocName := "lovely-blue-device"
sim := remoteproc.NewSimulator(rootpathPrefix).WithName(remoteprocName)
if err := sim.Start(); err != nil {
t.Fatalf("failed to run simulator: %s", err)
}
defer func() { _ = sim.Stop() }()

uniqueID := testID(t)
containerName := uniqueID
bundlePath := filepath.Join(dirMountedInVM, uniqueID)
require.NoError(t, generateBundle(
bundlePath,
remoteprocName,
specs.LinuxNamespace{Type: specs.MountNamespace},
))
_, stderr, err := installedRuntime.Run(
"create",
"--bundle", bundlePath,
containerName)
require.NoError(t, err, "stderr: %s", stderr)
t.Cleanup(func() {
_, _, _ = installedRuntimeSudo.Run("delete", containerName)
})

pid, err := getContainerPid(installedRuntimeSudo, containerName)
require.NoError(t, err)

hostMountNS, stderr, err := vm.RunCommand("readlink", "/proc/self/ns/mnt")
require.NoError(t, err, "stderr: %s", stderr)

proxyMountNS, stderr, err := vm.RunCommand("sudo", "readlink", fmt.Sprintf("/proc/%d/ns/mnt", pid))
require.NoError(t, err, "stderr: %s", stderr)
assert.Equal(t, strings.TrimSpace(hostMountNS), strings.TrimSpace(proxyMountNS))

remoteproc.AssertState(t, sim.DeviceDir(), "offline")

_, stderr, err = installedRuntimeSudo.Run("start", containerName)
require.NoError(t, err, "stderr: %s", stderr)
remoteproc.AssertState(t, sim.DeviceDir(), "running")
})
})
}

func testID(t testing.TB) string {
Expand All @@ -150,22 +238,22 @@ func testID(t testing.TB) string {
return name
}

func assertContainerStatus(t testing.TB, runtime limavm.InstalledBin, containerName string, wantStatus specs.ContainerState) {
func assertContainerStatus(t testing.TB, runtime limavm.Runnable, containerName string, wantStatus specs.ContainerState) {
t.Helper()
state, err := getContainerState(runtime, containerName)
require.NoError(t, err)
assert.Equal(t, wantStatus, state.Status)
}

func getContainerPid(runtime limavm.InstalledBin, containerName string) (int, error) {
func getContainerPid(runtime limavm.Runnable, containerName string) (int, error) {
state, err := getContainerState(runtime, containerName)
if err != nil {
return 0, err
}
return state.Pid, err
}

func getContainerState(runtime limavm.InstalledBin, containerName string) (specs.State, error) {
func getContainerState(runtime limavm.Runnable, containerName string) (specs.State, error) {
var state specs.State
out, stderr, err := runtime.Run("state", containerName)
if err != nil {
Expand All @@ -178,7 +266,7 @@ func getContainerState(runtime limavm.InstalledBin, containerName string) (specs
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 @@ -201,6 +289,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
3 changes: 0 additions & 3 deletions e2e/shim_via_docker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ func TestDocker(t *testing.T) {

containerID, stderr, err := vm.RunCommand(
"docker", "run", "-d",
"--network=host",
"--runtime", "io.containerd.remoteproc.v1",
"--annotation", fmt.Sprintf("remoteproc.name=%s", remoteprocName),
imageName)
Expand Down Expand Up @@ -76,7 +75,6 @@ func TestDocker(t *testing.T) {

_, stderr, err := vm.RunCommand(
"docker", "run", "-d",
"--network=host",
"--runtime", "io.containerd.remoteproc.v1",
"--annotation", fmt.Sprintf("remoteproc.name=%s", "other-processor"),
imageName)
Expand All @@ -94,7 +92,6 @@ func TestDocker(t *testing.T) {

containerID, stderr, err := vm.RunCommand(
"docker", "run", "-d",
"--network=host",
"--runtime", "io.containerd.remoteproc.v1",
"--annotation", fmt.Sprintf("remoteproc.name=%s", remoteprocName),
imageName)
Expand Down
56 changes: 56 additions & 0 deletions internal/proxy/namespaces.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package proxy

import (
"fmt"
"log/slog"

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

var specNamespacesToUnixCloneFlags = 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,
}

func ParseNamespaceFlags(namespaces []specs.LinuxNamespace) (uintptr, error) {
if namespaces == nil {
return 0, nil
}

var flags uintptr
for _, ns := range namespaces {
if ns.Path != "" {
continue
}
flag, ok := specNamespacesToUnixCloneFlags[ns.Type]
if !ok {
err := fmt.Errorf("unknown namespace type %q", ns.Type)
return 0, err
}
flags |= flag
}
return flags, nil
}

/* We mimic runc here - if not root, we ignore namespace isolation due to insufficient permissions
* https://github.com/opencontainers/runc/blob/a75076b4a413f628c4b6aa4c5568b159aa128a56/libcontainer/specconv/example.go */
func LinuxCloneFlags(logger *slog.Logger, isRoot bool, namespaces []specs.LinuxNamespace) (uintptr, error) {
flags, err := ParseNamespaceFlags(namespaces)
if err != nil {
return 0, err
}

if !isRoot && flags != 0 {
Comment thread
Luke-Parkin marked this conversation as resolved.
logger.Warn("running non-root; namespace isolation disabled")
Comment thread
yejseo01 marked this conversation as resolved.
return 0, nil
}

return flags, nil
}
55 changes: 55 additions & 0 deletions internal/proxy/namespaces_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package proxy_test
Comment thread
muchzill4 marked this conversation as resolved.

import (
"io"
"log/slog"
"testing"

proxy "github.com/arm/remoteproc-runtime/internal/proxy"
"github.com/opencontainers/runtime-spec/specs-go"
"github.com/stretchr/testify/require"
"golang.org/x/sys/unix"
)

func TestLinuxCloneFlags(t *testing.T) {
t.Run("converts known linux namespace flags to unix", func(t *testing.T) {
isRoot := true
namespaces := []specs.LinuxNamespace{
{Type: specs.CgroupNamespace},
{Type: specs.UserNamespace},
}
logger := slog.New(slog.NewTextHandler(io.Discard, nil))

got, err := proxy.LinuxCloneFlags(logger, isRoot, namespaces)

require.NoError(t, err)
want := uintptr(unix.CLONE_NEWCGROUP | unix.CLONE_NEWUSER)
require.Equal(t, want, got)
})

t.Run("non-root disables cloning", func(t *testing.T) {
isRoot := false
namespaces := []specs.LinuxNamespace{
{Type: specs.CgroupNamespace},
{Type: specs.UserNamespace},
}
logger := slog.New(slog.NewTextHandler(io.Discard, nil))

got, err := proxy.LinuxCloneFlags(logger, isRoot, namespaces)

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

t.Run("errors given unknown namespace", func(t *testing.T) {
isRoot := true
namespaces := []specs.LinuxNamespace{
{Type: specs.LinuxNamespaceType("weird-name")},
}
logger := slog.New(slog.NewTextHandler(io.Discard, nil))

_, err := proxy.LinuxCloneFlags(logger, isRoot, namespaces)

require.Error(t, err)
})
}
15 changes: 13 additions & 2 deletions internal/proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,31 @@ package proxy

import (
"fmt"
"log/slog"
"os"
"os/exec"
"syscall"

"github.com/opencontainers/runtime-spec/specs-go"
)

func NewProcess(devicePath string) (int, error) {
func NewProcess(logger *slog.Logger, namespaces []specs.LinuxNamespace, 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 := LinuxCloneFlags(logger, isRoot, namespaces)
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
Loading
Loading