-
Notifications
You must be signed in to change notification settings - Fork 465
PTY access fails in sandbox (/dev/ptmx / openpty() => Permission denied), and allowing PTY device paths in policy leaves sandbox stuck in Provisioning #749
Description
Agent Diagnostic
I used the openshell-cli and debug-openshell-cluster skill context to trace the sandbox runtime. This looks like two real bugs sharing the same policy surface.
Findings
• Nested PTY allocation happens after the shell has already dropped to the sandbox user and applied Landlock, via the pre_exec path in crates/openshell-sandbox/src/ssh.rs:1076, crates/openshell-sandbox/src/ssh.rs:1124; the
default restrictive policy only makes /sandbox, /tmp, and /dev/null writable in crates/openshell-policy/src/lib.rs:427, so /dev/ptmx is not allowed by default.
• The sandbox code explicitly documents that Landlock blocks device-file opens unless the device path is present in read_write in crates/openshell-sandbox/src/lib.rs:913; that matches your os.open("/dev/ptmx") and
os.openpty() returning EACCES.
• The reason ssh -tt still gives you a working outer TTY is that the supervisor allocates the first PTY before sandboxing the child in crates/openshell-sandbox/src/ssh.rs:764; the shell just inherits those already-open
slave FDs, so being able to use /dev/pts/0 does not prove the sandbox can allocate a new PTY later.
• Adding /dev/ptmx or /dev/pts to read_write currently goes through prepare_filesystem() before the sandbox becomes ready in crates/openshell-sandbox/src/lib.rs:214; that helper assumes every read_write path should be
created/chowned, rejects symlinks in crates/openshell-sandbox/src/lib.rs:1418, and then unconditionally chown()s the path in crates/openshell-sandbox/src/lib.rs:1430. That is a bad fit for PTY paths: /dev/ptmx is commonly a
symlink on Linux/devpts setups, and /dev/pts is a special filesystem mount, not a normal writable directory to prepare.
• The “stuck in Provisioning” part also makes sense from the server side: policy validation is only lexical today in crates/openshell-policy/src/lib.rs:542 and crates/openshell-server/src/grpc.rs:3306, so it won’t catch
image-specific PTY path behavior up front, and the server keeps ReconcilerError / DependenciesNotReady mapped to Provisioning in crates/openshell-server/src/sandbox/mod.rs:1399, which can hide a permanent startup problem
behind a retrying provisioning state.
• Net: your report is valid as written. Default nested PTY allocation is effectively unsupported by the current default filesystem allowlist, and explicit PTY path allowances are broken by startup filesystem-prep behavior.
Likely Fix
• prepare_filesystem() should stop treating every read_write entry as a path to create/chown; it needs to distinguish normal directories from existing special files, device nodes, devpts mounts, and symlinks in crates/
openshell-sandbox/src/lib.rs:1367.
• PTY support then needs an explicit allowlist story for /dev/pts plus the ptmx endpoint used by openpty(); /dev/ptmx alone is probably not sufficient, because openpty() also opens the slave under /dev/pts/.
Description
PTY allocation appears broken inside a sandbox on openshell 0.0.19.
In a sandbox created from ghcr.io/nvidia/openshell-community/sandboxes/base:latest, the default sandbox user
cannot access PTYs:
• os.open("/dev/ptmx", ...) returns Permission denied
• os.openpty() returns Permission denied
This still happens when the outer session already has a TTY (ssh -tt / equivalent): the remote shell gets /
dev/pts/0, but nested PTY allocation still fails.
I also tested custom policies that explicitly allow PTY device paths:
• /dev/ptmx
• /dev/pts
In both cases, the sandbox never reaches Ready and remains stuck in Provisioning.
Expected behavior
One of these should work:
• the default sandbox runtime should allow PTY allocation for the configured process user, or
• explicitly allowing /dev/ptmx / /dev/pts in policy should enable PTY allocation without breaking sandbox
startup
If PTY device paths are unsupported in policy, I would expect a validation/startup error instead of an
indefinitely provisioning sandbox.
Reproduction Steps
A. Create a control sandbox with a default-like custom policy
Create /tmp/openshell-policy-control.yaml:
version: 1
filesystem_policy:
include_workdir: true
read_only:
- /usr
- /lib
- /proc
- /dev/urandom
- /app
- /etc
- /var/log
read_write:
- /sandbox
- /tmp
- /dev/null
landlock:
compatibility: best_effort
process:
run_as_user: sandbox
run_as_group: sandbox
network_policies: {}
Create the sandbox:
openshell sandbox create \
--name pty-policy-control \
--from ghcr.io/nvidia/openshell-community/sandboxes/base:latest \
--policy /tmp/openshell-policy-control.yaml \
--no-tty -- true
Verify it becomes ready:
openshell sandbox get pty-policy-control
B. Probe PTY access inside the ready sandbox
tmp=$(mktemp)
openshell sandbox ssh-config pty-policy-control > "$tmp"
ssh -F "$tmp" openshell-pty-policy-control -- /bin/sh -lc '
id
python3 - << "PY"
import os, sys
try:
fd = os.open("/dev/ptmx", os.O_RDWR | os.O_NOCTTY)
print("ptmx_open_ok", fd)
os.close(fd)
except OSError as e:
print("ptmx_open_err", e.errno, e.strerror)
try:
m, s = os.openpty()
print("openpty_ok", m, s)
os.close(m); os.close(s)
except OSError as e:
print("openpty_err", e.errno, e.strerror)
sys.exit(1)
PY
'
C. Verify that an outer TTY exists but nested PTY still fails
tmp=$(mktemp)
openshell sandbox ssh-config pty-policy-control > "$tmp"
ssh -tt -F "$tmp" openshell-pty-policy-control -- /bin/sh -lc '
tty
ls -l /dev/pts/0
python3 - << "PY"
import os, sys
try:
m, s = os.openpty()
print("openpty_ok", m, s)
os.close(m); os.close(s)
except OSError as e:
print("openpty_err", e.errno, e.strerror)
sys.exit(1)
PY
'
D. Create a sandbox with /dev/ptmx explicitly allowed
Create /tmp/openshell-policy-ptmx.yaml:
version: 1
filesystem_policy:
include_workdir: true
read_only:
- /usr
- /lib
- /proc
- /dev/urandom
- /app
- /etc
- /var/log
read_write:
- /sandbox
- /tmp
- /dev/null
- /dev/ptmx
landlock:
compatibility: best_effort
process:
run_as_user: sandbox
run_as_group: sandbox
network_policies: {}
Create the sandbox:
openshell sandbox create \
--name pty-policy-ptmx-only \
--from ghcr.io/nvidia/openshell-community/sandboxes/base:latest \
--policy /tmp/openshell-policy-ptmx.yaml \
--no-tty -- true
Check status:
openshell sandbox get pty-policy-ptmx-only
E. Create a sandbox with /dev/ptmx and /dev/pts explicitly allowed
Create /tmp/openshell-policy-pty.yaml:
version: 1
filesystem_policy:
include_workdir: true
read_only:
- /usr
- /lib
- /proc
- /dev/urandom
- /app
- /etc
- /var/log
read_write:
- /sandbox
- /tmp
- /dev/null
- /dev/ptmx
- /dev/pts
landlock:
compatibility: best_effort
process:
run_as_user: sandbox
run_as_group: sandbox
network_policies: {}
Create the sandbox:
openshell sandbox create \
--name pty-policy-test2 \
--from ghcr.io/nvidia/openshell-community/sandboxes/base:latest \
--policy /tmp/openshell-policy-pty.yaml \
--no-tty -- true
Check status:
openshell sandbox get pty-policy-test2
Environment
- OpenShell version: 0.0.19
- Host OS: Darwin 25.3.0
- Host architecture: arm64 (Apple Silicon)
- Sandbox image: ghcr.io/nvidia/openshell-community/sandboxes/base:latest
Logs
PTY probe in ready sandbox
uid=998(sandbox) gid=998(sandbox) groups=998(sandbox)
ptmx_open_err 13 Permission denied
openpty_err 13 Permission denied
Outer TTY exists, nested PTY still fails
/dev/pts/0
crw--w---- 1 root tty 136, 0 Apr 3 09:03 /dev/pts/0
openpty_err 13 Permission denied
Sandbox with /dev/ptmx added to policy
Phase: Provisioning
Sandbox with /dev/ptmx and /dev/pts added to policy
Phase: ProvisioningAgent-First Checklist
- I pointed my agent at the repo and had it investigate this issue
- I loaded relevant skills (e.g.,
debug-openshell-cluster,debug-inference,openshell-cli) - My agent could not resolve this — the diagnostic above explains why