Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 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 @@ -76,8 +76,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 @@ -114,7 +112,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
8 changes: 7 additions & 1 deletion e2e/limavm/limavm.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
}

var BinBuildEnv = map[string]string{
"GOOS": "linux",
"GOOS": "linux",

Check failure on line 24 in e2e/limavm/limavm.go

View workflow job for this annotation

GitHub Actions / Lint

File is not properly formatted (gofmt)
"GOARCH": "amd64",
"CGO_ENABLED": "0",
}

func NewWithDocker(mountDir string, buildContext string, bins repo.Bins) (LimaVM, error) {
Expand All @@ -32,6 +34,10 @@
return New("podman", mountDir, buildContext, string(runtimeBin))
}

func NewAlpine(mountDir string, runtimeBin repo.RuntimeBin) (LimaVM, error) {
return New("alpine", mountDir, "", string(runtimeBin))
}

func New(template string, mountDir string, buildContext string, binsToInstall ...string) (LimaVM, error) {
cmd := exec.Command(
prepareLimaVMScript,
Expand Down
28 changes: 22 additions & 6 deletions e2e/limavm/prepare-lima-vm.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,19 @@ BINARIES=()

usage() {
echo "Usage: $0 <template> <mount-dir> <build-context> <binary1> [binary2] ..." >&2
echo " template: Lima template to use (docker or podman)" >&2
echo " template: Lima template to use (docker, podman, or alpine)" >&2

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.

This is quite confusing as docker and podman is a linux package while alpine is a linux distribution. It make the concept 'template' very broad and as a result vague. I think we need two separate container-engine parameter and distro parameter

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 kind of agree, but at the same time that would involve us redefining a template; which I would definitely not want to do for a test.

The templates are called 'docker', 'podman', or 'alpine', by lima. I think adding extra abstraction on top here might be excessive YAGNI, IMO.

echo " mount-dir: Directory attached in the vm" >&2
echo " build-context: Build context directory for test-image build" >&2
echo " binary1...N: Path to binaries to install in /usr/local/bin" >&2
exit 1
}

validate_inputs() {
if [ ! -d "$BUILD_CONTEXT" ]; then
echo "Error: Build context directory not found: $BUILD_CONTEXT" >&2
exit 1
if [ "$TEMPLATE" = "docker" ] || [ "$TEMPLATE" = "podman" ]; then
if [ ! -d "$BUILD_CONTEXT" ]; then
echo "Error: Build context directory not found: $BUILD_CONTEXT" >&2
exit 1
fi
fi

for binary in "${BINARIES[@]}"; do
Expand Down Expand Up @@ -58,7 +60,13 @@ start_vm() {
install_binary() {
local source_path="$1"
local binary_name="$2"
local dest_path="/usr/local/bin/$binary_name"
local dest_dir="/usr/local/bin"

if [ "$TEMPLATE" = "alpine" ]; then
dest_dir="/usr/bin"
fi

local dest_path="$dest_dir/$binary_name"

echo "Installing $binary_name..." >&2

Expand Down Expand Up @@ -115,6 +123,12 @@ build_image() {
exit 1
fi
;;
""|none)
echo "Skipping image build (no build context provided)" >&2
;;
alpine)
echo "Skipping image build for alpine template" >&2
;;
*)
echo "Error: Unsupported template '$TEMPLATE'. Only 'docker' and 'podman' are supported." >&2
cleanup_on_failure
Expand Down Expand Up @@ -149,7 +163,9 @@ main() {
install_binary "$binary_path" "$binary_name"
done

build_image
if [ -n "$BUILD_CONTEXT" ] || [ "$TEMPLATE" = "docker" ] || [ "$TEMPLATE" = "podman" ]; then
build_image
fi

echo "VM setup completed successfully" >&2

Expand Down
135 changes: 134 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 @@ -7,9 +7,11 @@ import (
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
"testing"

"github.com/arm/remoteproc-runtime/e2e/limavm"
"github.com/arm/remoteproc-runtime/e2e/remoteproc"
"github.com/arm/remoteproc-runtime/e2e/repo"
"github.com/opencontainers/runtime-spec/specs-go"
Expand Down Expand Up @@ -128,13 +130,143 @@ func TestRuntimeWriteProcessPid(t *testing.T) {
assertFileContent(t, pidFilePath, fmt.Sprintf("%d", pid))
}

func TestRuntimeProxyKeepsHostNamespaceWhenNotRoot(t *testing.T) {
rootDir := t.TempDir()
remoteprocName := "a-lovely-blue-device"

runtimeBin, err := repo.BuildRuntimeBin(t.TempDir(), rootDir, limavm.BinBuildEnv)
require.NoError(t, err)

vm, err := limavm.NewAlpine(rootDir, runtimeBin)
require.NoError(t, err)
t.Cleanup(vm.Cleanup)

sim := remoteproc.NewSimulator(rootDir).WithName(remoteprocName)
if err := sim.Start(); err != nil {
t.Fatalf("failed to run simulator: %s", err)
}
t.Cleanup(func() { _ = sim.Stop() })

const containerName = "not-root-namespace-container"
bundlePath := filepath.Join(rootDir, "not-root-namespace-bundle")
require.NoError(t, generateBundle(
bundlePath,
remoteprocName,
specs.LinuxNamespace{Type: specs.MountNamespace},
))

_, stderr, err := vm.RunCommand(
"remoteproc-runtime",
"create", "--bundle", bundlePath,
containerName,
)
require.NoError(t, err, "stderr: %s", stderr)
t.Cleanup(func() {
_, _, _ = vm.RunCommand("remoteproc-runtime", "delete", containerName)
})

Comment thread
Luke-Parkin marked this conversation as resolved.
pid, err := checkContainerRunning(func() (specs.State, error) {
stdout, stderr, err := vm.RunCommand("remoteproc-runtime", "state", containerName)
if err != nil {
return specs.State{}, fmt.Errorf("stderr: %s: %w", stderr, err)
}
var state specs.State
if err := json.Unmarshal([]byte(stdout), &state); err != nil {
return specs.State{}, err
}
return state, nil
})
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("readlink", fmt.Sprintf("/proc/%d/ns/mnt", pid))
require.NoError(t, err, "stderr: %s", stderr)

assert.Equal(t, strings.TrimSpace(hostMountNS), strings.TrimSpace(proxyMountNS))
}

func TestRuntimeProxyKeepsHostNamespaceWhenRootInLimaVM(t *testing.T) {
rootDir := t.TempDir()
remoteprocName := "a-lovely-blue-device"

runtimeBin, err := repo.BuildRuntimeBin(t.TempDir(), rootDir, limavm.BinBuildEnv)
require.NoError(t, err)

vm, err := limavm.NewAlpine(rootDir, runtimeBin)
require.NoError(t, err)
t.Cleanup(vm.Cleanup)

sim := remoteproc.NewSimulator(rootDir).WithName(remoteprocName)
if err := sim.Start(); err != nil {
t.Fatalf("failed to run simulator: %s", err)
}
t.Cleanup(func() { _ = sim.Stop() })

const containerName = "root-namespace-container"
bundlePath := filepath.Join(rootDir, "root-namespace-bundle")
require.NoError(t, generateBundle(
bundlePath,
remoteprocName,
specs.LinuxNamespace{Type: specs.MountNamespace},
))

_, stderr, err := vm.RunCommand(
"sudo", "remoteproc-runtime",
"create", "--bundle", bundlePath,
containerName,
)
require.NoError(t, err, "stderr: %s", stderr)
t.Cleanup(func() {
_, _, _ = vm.RunCommand("sudo", "remoteproc-runtime", "delete", containerName)
})

pid, err := checkContainerRunning(func() (specs.State, error) {
stdout, stderr, err := vm.RunCommand("sudo", "remoteproc-runtime", "state", containerName)
if err != nil {
return specs.State{}, fmt.Errorf("stderr: %s: %w", stderr, err)
}
var state specs.State
if err := json.Unmarshal([]byte(stdout), &state); err != nil {
return specs.State{}, err
}
return state, nil
})
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 = vm.RunCommand("sudo", "remoteproc-runtime", "start", containerName)
require.NoError(t, err, "stderr: %s", stderr)
remoteproc.AssertState(t, sim.DeviceDir(), "running")
}

func assertContainerStatus(t testing.TB, bin repo.RuntimeBin, containerName string, wantStatus specs.ContainerState) {
t.Helper()
state, err := getContainerState(bin, containerName)
require.NoError(t, err)
assert.Equal(t, wantStatus, state.Status)
}

func checkContainerRunning(fetch func() (specs.State, error)) (int, error) {
state, err := fetch()
if err != nil {
return 0, err
}
if state.Pid <= 0 {
return 0, fmt.Errorf("container is not running - pid is %d", state.Pid)
}
return state.Pid, nil
}

func getContainerPid(bin repo.RuntimeBin, containerName string) (int, error) {
state, err := getContainerState(bin, containerName)
if err != nil {
Expand All @@ -156,7 +288,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 +311,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
54 changes: 54 additions & 0 deletions internal/proxy/namespaces.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
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
}

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
}
58 changes: 58 additions & 0 deletions internal/proxy/namespaces_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
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},
}

Comment thread
Luke-Parkin marked this conversation as resolved.
Outdated
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)
})
}
Loading
Loading