Skip to content

Repository files navigation

PTY Playground

A small Unix process laboratory: terminal attachment topology is an explicit experimental variable. Rust touches the machine; Erlang owns session lifecycle.

Operator workshop: complete. Six bounded exhibits explored waits, intervention receipts, caller/worker death and inherited output descriptors. All passed; the take-home result is an evidence-backed operator wishlist, not a larger platform. Exhibit results.

Initial terminal width: observed PASS. Three PTY/PTY pairs of unmodified /bin/ls emitted one row at 80 columns and three lines at 8 columns. Only the column count changed; native readback, fixture integrity and cleanup checks passed. Results.

Canonical input delivery: observed PASS. With echo off in both cells, three pairs showed that ICANON withheld h during the early observation window; noncanonical input delivered it before newline. Both modes ultimately delivered the same h\n. Kernel readback and receipt phase ordering verified the controlled change. Results.

First terminal-state axis: echo on/off, observed PASS. Three PTY/PTY pairs kept the same hello-world subject and input. Turning off ECHO removed the terminal's echoed input while preserving the application's prompt and reply. Kernel readback confirmed exactly the intended configuration change. Result and raw bytes.

First unmodified application witness: observed PASS. Installed /bin/ls, identical empty argv/input, environment and fixed directory: three pipe runs printed one filename per line; three PTY runs printed one row with tabs. Receipts, fixture integrity and bounded cleanup checks passed. One earlier digest-text checker failure is preserved separately. Result and both attempts.

Plumbing isolation and the thin Elixir experiment layer: observed PASS. Elixir schedules bounded hello-world pairs through the public Erlang session API. Three normal pairs passed; separate deliberate mismatch and execution-failure experiments classified their anomalies and completed the later pairs. All 18 session journals were sealed and verified; all 50 reported OS PIDs were absent. Elixir results · Elixir API · Erlang API · Isolation results.

First behavioral witness: observed PASS on macOS 26.6.1 arm64. The same hello-world program received hello\n through a pipe and a PTY. Both replied received: hello and exited 0; only the PTY run enabled input> . Original echo/CRLF bytes remain in the receipts. This is one intentionally constructed pair, not a claim about unmodified applications. Results · Raw comparison.

The earlier Phase D receipt checkpoint passed: Standalone session receipts now include binary fingerprints, helper/runtime versions and a language-neutral environment hash. A real BEAM SIGKILL left a partial journal that a fresh BEAM recovered byte-for-byte as incomplete/unknown. All 14 receipt checks and the nine-session ownership regression passed. See receipt semantics and recovery.

The preceding Phase C ownership checkpoint also passed: Nine OTP sessions covered topology, binary input, helper murder under pipes and PTY, worker kill, timeout, and successful execution after faults in the same BEAM. All reported processes in the fault cells were absent within three seconds. The SIGHUP-ignoring lifetime probe makes cleanup independent of terminal hangup. No NIF or UI exists. ECHO, ICANON and initial column count are individually tested axes; no factorial matrix exists.

The earlier Phase A/B gate reproduced the real macOS Terminal reference on the terminal dimensions declared before measurement:

Measured property Pipes PTY slave only PTY + controlling terminal Real Terminal
isatty stdin/stdout/stderr all false all true all true all true
terminal settings ENOTTY match reference match reference frozen reference
/dev/tty accessible no no yes yes
foreground pgrp ENOTTY ENOTTY subject pgrp subject pgrp

Matched fields include all termios flag words, control characters, input/output speeds, rows/columns and pixel dimensions. Numeric process identities, ancestry and session leadership differ and remain visible in the original observations. The complete probe can distinguish the processes; its declared terminal projection cannot distinguish the controlling-PTY cell from the reference.

Evidence and source custody

The first ownership run failed because Rust buffered non-newline protocol events. Direct descriptor writes fixed the transport; a new, separately recorded run passed. The failed binary-input journal is incomplete because the test BEAM halted on the assertion; it is preserved, never synthesized into a complete run.

OTP ownership

pty_lab_sup
├── receipt_writer (independent worker monitor and durable journal owner)
└── session_sup
    └── session_worker (temporary, never replayed)
        └── external Port: Rust relay
            └── Rust guardian
                └── subject

The extra Rust process closes the macOS SIGKILL cleanup gap: the guardian owns the subject and observes EOF when its relay dies. OTP deadlines send termination; worker death closes the Port. The guardian kills/reaps the direct child. Killing the guardian itself and descendant containment remain outside the guarantee.

Session journals are synchronously appended, sealed on completion/failure, and monitored independently of workers. Original wire events, input/output hex, command ordering, exit signals and timeout/failure outcomes remain inspectable. The protocol contract explains bounds and error semantics.

Requires OTP 27+ for native JSON (tested with OTP 29.0.6), and rebar3 (3.27.0). The Elixir layer additionally requires Elixir 1.20 (tested with 1.20.4). On this laptop add /opt/homebrew/bin alongside the Rust toolchain to PATH:

make otp-check        # compile, EUnit, cross-reference analysis
make protocol-check   # real fragmented/coalesced/rejected frames; new evidence
make ownership-check  # nine bounded sessions; new evidence, no retries
make receipt-check    # standalone metadata, BEAM kill, lossless recovery
make witness-check    # hello-world prompt flip, one matched pair
make isolation-check  # identity, writer/Port failures, healthy siblings
make elixir-test      # formatting and classification unit test
make elixir-check     # nine pairs including two deliberate anomalies
make ls-check         # three unmodified ls pairs, fixed fixture, no input
make echo-check       # three PTY/PTY echo pairs with kernel configuration readback
make canonical-check  # early byte delivery versus canonical line delivery
make width-check      # initial 80/8-column PTY layouts with installed ls
make operator-check   # bounded operator workshop and take-home evidence

The probe, argv, explicit environment, cwd and empty input are identical across the topology cells. A side-channel snapshot preserves stdio attachment. Raw pipe and PTY bytes are retained separately, including terminal newline processing. Terminal's screen is not claimed as a captured byte stream. The probe does not read input; this experiment establishes nothing about EOF or interactive timing.

Reference source was committed before capture (f30c10d), reference evidence before helper implementation (fe204cd), and helper/check source before execution (da17020). Per-run SHA-256 manifests, read-only files and Git preserve evidence. These are local tamper-evident artifacts, not WORM storage. Failures are retained; there are no automatic experimental retries.

Build and check

Requires Rust 1.95.0 with rustfmt/clippy, Python 3, and macOS for reference capture. Cargo.lock is committed. Cargo must find rustc on PATH. On this laptop:

export PATH="$HOME/.rustup/toolchains/1.95.0-aarch64-apple-darwin/bin:$PATH"
make build
make lint
make check

make check creates a new receipt and verifies the frozen probe binary hash before executing. A different compiler, checkout location or build profile can change that hash; investigate such a mismatch instead of accepting it silently. To deliberately capture a new reference with the current binary, run make reference, then pass its new directory explicitly:

make check REFERENCE=receipts/reference-YYYYMMDDTHHMMSSZ

Reference capture opens a fresh macOS Terminal window and runs a foreground Bash job with 24 x 80 cells; native pixel dimensions and termios are observed. It leaves that window available for inspection. Each cell has a bounded wait. Terminal launch failure/timeout is retained and needs inspection; this temporary capture script does not provide OTP resource recovery.

The functional checks cover slave-only attachment, controlling-terminal equivalence, exact ENOENT on exec failure, deadline termination and reaping of a sleeping child, and child exit-code propagation. They compare observed behavior, not implementation text. There are no Rust unit tests at this checkpoint.

Legacy Phase B CLI

pty_helper CONFIG_JSON slave|ctty DEADLINE_MS -- EXECUTABLE [ARGS...]

The configuration contains termios and dimensions (see a receipt's terminal-config.json). The helper inherits the caller's cwd and environment; the experiment script supplies the fixed environment. Both PTY modes create a fresh session, isolating inherited controlling-terminal membership. ctty also attaches the slave as controlling terminal and sets the subject foreground group.

Stdout carries raw, merged PTY bytes, buffered up to 1 MiB; stderr carries local JSON diagnostics. The helper owns one direct child, closes the parent's slave copies, drains the master, and observes/reaps exit. It accepts a 1..60000 ms I/O-loop deadline and allows at most one second for kill/reap cleanup. Exit 125 means helper failure; child exit details distinguish it from a subject exiting 125. No stdin forwarding or terminal state matrix is implemented.

This legacy CLI remains for gate_001 regression and has no crash-containment promise. OTP uses pty_helper --port and the framed protocol instead. Only that new path has the tested guardian ownership chain. Descendant cleanup remains deferred in both paths.

The post-fork hook uses only session/terminal syscalls and immediate errno capture, following Rust's pre_exec safety requirements. Only macOS has been runtime-tested; terminal constants and configurations are platform-specific. Go vet/staticcheck are inapplicable to this Rust/Python tree; Clippy is the Rust static-analysis gate.

License

MIT. Copyright (c) 2026 c3mb0 and contributors. Third-party dependencies and quoted reference material retain their own licenses.

About

A Unix process and PTY laboratory in Rust and Erlang/OTP: reproducible experiments, session ownership, byte-exact transport, and interactive shell machinery.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages