Skip to content

feat: add opt-in device node access (--device) - #13

Open
jgforbes wants to merge 1 commit into
fityannugroho:mainfrom
jgforbes:feat/device-access
Open

jgforbes wants to merge 1 commit into
fityannugroho:mainfrom
jgforbes:feat/device-access

Conversation

@jgforbes

Copy link
Copy Markdown
Contributor

Summary

Adds a new opt-in --device /dev/NAME switch (repeatable) that exposes individual host device nodes to programs running directly inside opencodebox — serial ports, webcams, sound cards, etc. — with the same mode, owner, group, and ACLs as on the host. The launcher never recreates device nodes, changes device permissions, or runs sudo; access inside the sandbox is authorized exactly like on the host.

This addresses setups like a typical RHEL 9 server, where there is no graphical login, systemd-logind grants no per-seat ACLs, and device access comes from group membership (dialout, video, audio) that the host user may be missing.

Behavior

  • Each node is validated at launch and bound with bubblewrap's --dev-bind at the same path inside the sandbox's private /dev (the bind shares the host inode).
  • Validation accepts only absolute paths under /dev pointing at an existing character or block device; symlinks, symlinked parent directories, and paths with empty, ., or .. components are refused, and validation failure aborts the launch.
  • Parent directories (e.g. /dev/snd) are created inside the sandbox before the node is bound; repeating an identical --device binds it once.
  • A launch-time advisory warns when the node's mode bits deny the invoking user both read and write on the host (e.g. root:dialout 0660 without dialout membership) and names the host-side fix (udev rule or usermod -aG); POSIX-ACL nodes are skipped, and the bind still happens either way.
  • The synthetic /etc/passwd and /etc/group now include root and nobody plus every host group of the invoking user, so group and owner lookups (getent, initgroups) resolve inside the sandbox; ids not mapped into the sandbox's user namespace still render as 65534/nobody, which is a harmless display artifact. The advisory relies on the same id -G data.
  • Device mounts come after the private /dev but before explicit user binds and the OpenCode 2 isolation overlay, so later bindings still win.
  • The feature is default-off: without --device, no device probes or mounts are added.

Verification

Full test suite: 96 passed (62 existing + 34 new). New tests/test_device.py covers the standalone helper contract (path/type validation, dedup, parent-directory creation, symlink refusal) and the CLI contract (parsing errors, advisory hints, composition with seccomp/isolation, NSS identity contents); it is self-contained against tests/conftest.py and does not depend on #12.

Live checks from a checkout:

$ ./opencodebox --device /etc/hosts
Error: --device requires an absolute path under /dev (got '/etc/hosts').

$ ./opencodebox --device /dev/null models
Warning: --device exposes host device nodes to the sandbox; each node keeps its host mode and permissions.
llama-local/unsloth/gpt-oss-120b
...   # opencode ran inside the sandbox with /dev/null bound

Notes for review

  • No version bump: release numbering is left to the maintainer's release commits.
  • Independent of feat: add opt-in direct GPU access (--gpu) #12 (--gpu); the two PRs share only one identical, additive hunk in tests/conftest.py (run_launcher(script=...)) and adjacent README insertions, so they can merge in either order.
  • The README documents the RHEL 9/bubblewrap 0.4.1→0.6.3 situation: --dev-bind semantics are unchanged across those versions; host permissions decide whether a bound node can be opened.

Expose individual host device nodes inside the sandbox behind a repeatable
--device /dev/NAME flag. Nodes are validated at launch (absolute path under
/dev, existing character or block device only; symlinks, symlinked parent
directories and empty/./.. path components are refused) and bound with
--dev-bind at the same path in the private /dev, sharing the host inode so
the node keeps its host mode, owner, group and ACLs — access inside matches
the host exactly, and the launcher never recreates nodes, changes
permissions or runs sudo. Missing parent directories (e.g. /dev/snd) are
created per launch and duplicate paths are bound once.

A launch-time advisory warns when the node's mode bits deny the invoking
user both read and write on the host (e.g. root:dialout 0660 without
dialout membership) and names the host-side fix (udev rule or usermod);
POSIX-ACL nodes are skipped, and the bind still happens either way.

The synthetic /etc/passwd and /etc/group now include root and nobody plus
every host group of the invoking user (id -G via getent), so group and
owner lookups (getent, initgroups) resolve inside the sandbox while
unmapped ids keep rendering as 65534/nobody; the access advisory relies on
the same USER_GROUPS data.

Device mounts follow the private /dev, so explicit --with/--with-ro binds
can still override them and the OpenCode v2 isolation overlay stays last.
The feature is default-off; without --device no device probes or mounts
are added.

Ported from the sandbox-podman-persistent-tmp working tree, scoped to the
device feature only (no version bump — release numbering is the
maintainer's call). Tests are self-contained against tests/conftest.py
(34 new tests, including the NSS identity content check; full suite: 96
passed) so this PR does not depend on the GPU PR.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant