← board

crtl has no pty family at all

Found attempting busybox rung 2 (feature-c-corpus-busybox-multi-applet) while auditing crtl against busybox's HAVE_* default table (include/platform.h :403-433) — the table is a list of things busybox will call WITHOUT a guard, so every entry crtl lacks is a compile error waiting for the config that reaches it.

What is missing

posix_openpt, grantpt, unlockpt, ptsname, ptsname_r. Nothing in lib/crtl/include/** or lib/crtl/src/** mentions any of them.

Why it is one ticket and not five

On Linux the whole family is three ioctls on /dev/ptmx plus string formatting:

Implementing one without the others gives a caller a master fd it cannot use.

The trap to avoid

grantpt returning 0 unconditionally is the right implementation and the wrong comment. A "succeeded" that never checked anything is the shape this codebase spends its time on — the body should note that devpts makes the grant a mount-time property, so that a future reader does not read the 0 as evidence about the slave's ownership.

Not blocking today

The 141-applet busybox configuration does not compile libbb/getpty.c (checked against the harness's generated tulist.txt, which is read off busybox_unstripped.map). This is latent, not live.

Done 2026-09-06 (frankC)

Implemented as one unit, which is how it was filed. posix_openpt (open /dev/ptmx), grantpt, unlockpt (TIOCSPTLCK), ptsname_r, ptsname.

grantpt returns 0 without checking anything and the comment says why. devpts makes the grant a mount-time property (gid/mode on the mount do what the old setuid helper did), so the 0 is correct — and it is not evidence about the slave's ownership. Those are different claims and this function can only make the first. The ticket asked for exactly that and it is in the body.

Declared in <stdlib.h>, not a <pty.h>, because that is where glibc puts them and where callers include them from. busybox's getpty.c calls ptsname_r with <stdlib.h> in scope and no guard; a <pty.h> would have compiled and never been found.

The two ioctls are SPELLED, not transcribed. sys/ioctl.h's tty block says the _IOR/_IOW-shaped entries are deliberately absent because "transcribing their expansion would bake in this box's _IOC layout". Writing them as _IOR('T', 0x30, unsigned int) / _IOW('T', 0x31, int) against the _IOC machinery already in that file is that rule's remedy, not an exception to it: a target with a different layout produces a different and correct number. Verified they expand to 0x80045430 / 0x40045431, which is what <asm/ioctls.h> gives here — a real check, because a wrong layout yields a number rather than an error.

ptsname_r returns the ERROR NUMBER, not −1, and sets errno as well. Measured against glibc rather than recalled: rc=34 for a small buffer, rc=25 for a non-pty fd, errno matching each time. Both conventions look right at a call site that only tests for zero, which is why this was measured.

ERANGE rather than truncation, with the length computed in full before a byte is copied. A truncated /dev/pts/1 for slave 17 is a path that EXISTS and belongs to another session, so silently shortening hands the caller someone else's terminal.

The test, and the ablation that says which row is load-bearing

test/c_pty_family.c, 11 rows, diffed whole against gcc — byte-identical, so no expected value is transcribed and a row added later needs none re-derived.

Row 8 is the one that matters and I proved it rather than asserting it. With ptsname_r ablated to always answer /dev/pts/0 — a well-shaped WRONG name, which is this family's actual failure mode — row 5's strncmp prefix check still passes. Only row 8 (open the slave the name actually names and round-trip a byte) and row 9 (a second pair must get a different name) drop to 0. A shape assertion alone would have certified the bug.

Row 8 is non-blocking by construction, after the first version HUNG. A pty slave starts in canonical mode, so read returns only on a complete line and a bare "Z" waits forever. Both fixes are kept — the newline, and O_NONBLOCK with a bounded retry — because a test that can hang has no verdict and stops the run instead of reporting one.

Not done here

openpty and forkpty are <pty.h> functions and are not implemented. They are a layer above this one (allocate, then open the slave, then optionally fork and login_tty), and nothing measured needs them yet.

Log