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

Closed as not planned

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

Activity

  1. springToNl commented on Apr 21, 2026

    @springToNl

    Any updates on this? 😊

  2. TaylorMutch commented on Jul 13, 2026

    @TaylorMutch
    Collaborator

    📋 triage-agent

    Triage Assessment

    Classification: bug-confirmed

    Summary

    This report remains valid on current main, although PR #827 partially fixed the original provisioning failure. Nested PTY allocation is still denied by the default filesystem policy, and explicitly listing /dev/ptmx can still prevent startup because that path is normally a symlink and current filesystem preparation rejects symlinked read_write entries.

    Investigation

    The restrictive default policy in crates/openshell-policy/src/lib.rs still grants read-write access only to /sandbox, /tmp, and /dev/null; it does not include /dev/pts. The SSH supervisor allocates the outer PTY before preparing and enforcing Landlock for the child in crates/openshell-supervisor-process/src/ssh.rs, explaining why an inherited outer terminal works while a nested openpty() remains denied.

    Merged PR #827 changed filesystem preparation to preserve existing non-symlink paths. That should allow /dev/pts itself to pass startup and makes /dev/pts alone the likely current workaround. However, prepare_read_write_path() in crates/openshell-supervisor-process/src/process.rs still rejects every symlink, including the usual /dev/ptmx -> pts/ptmx, before the sandbox becomes ready. Server phase derivation also continues to treat ReconcilerError and DependenciesNotReady as Provisioning conditions.

    Targeted current-main tests confirmed symlink rejection and Provisioning mapping. A repository-wide issue search found no duplicate; #543, #936, and #2228 cover different PTY behavior.

    Recommendation

    Keep this as a focused Linux sandbox-runtime bug. The fix should define supported nested-PTY policy behavior, safely handle the canonical /dev/pts/ptmx relationship without weakening symlink protections, and add Linux regression coverage for both nested openpty() and startup failure reporting. Until that is implemented, document /dev/pts without /dev/ptmx as a provisional workaround only after validating it end to end.

  3. added
    retired:area:sandboxRetired; do not apply. See #5151 for item-specific migration to the current taxonomy.
    os:linuxIssue affects Linux hosts
    and removed
    state:triage-neededOpened without agent diagnostics and needs triage
    on Jul 13, 2026
  4. github-actions commented on Aug 29, 2026

    @github-actions

    This issue has had no activity for 14 days and is now marked stale. It may be closed in 7 days if there is no further activity. Comment or remove the state:stale label to keep it open.

  5. egalanos commented on Sep 1, 2026

    @egalanos

    still current. Prevents --editor vscode sessions from opening a terminal

  6. github-actions commented on Sep 16, 2026

    @github-actions

    This issue has had no activity for 14 days and is now marked stale. It may be closed in 7 days if there is no further activity. Comment or remove the state:stale label to keep it open.

  7. nixon89 commented on Oct 23, 2026

    @nixon89

    works for me in 0.1.5 version of openshell

    read_write:l
    - /dev/ptmx
    - /dev/pts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    os:linuxIssue affects Linux hostsretired:area:sandboxRetired; do not apply. See #5151 for item-specific migration to the current taxonomy.state:staleInactive item at risk of automatic closure.

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions