Skip to content

PTY access fails in sandbox (/dev/ptmx / openpty() => Permission denied), and allowing PTY device paths in policy leaves sandbox stuck in Provisioning #749

Description

@kamilbakierzynski

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: Provisioning

Agent-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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:sandboxSandbox runtime and isolation workos:linuxBug affects Linux hosts

    Type

    Fields

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions