. "$BST_LIB" # libptytty 2.0 (poky recipes-sato) -- "OS independent and secure pty/tty and # utmp/wtmp/lastlog handling", the library rxvt-unicode and its relatives use to # get a pty pair and to record the session that runs on it. # # --------------------------------------------------------------------------- # WHY THIS SUITE IS SHAPED THE WAY IT IS # # The package is four files: libptytty.h, libptytty.pc, libptytty.3 and # libptytty.a. There is no CLI, nothing else on this image links it, and there # is no C compiler on the target -- so a shell suite on its own could only say # that four files exist. Every failure mode that matters here is the library # answering WRONGLY: handing back a pty pair that does not carry bytes, or # naming a slave device other than the one it opened. So the functional half # comes from a linked consumer, /usr/libexec/libptytty-tests/libptytty-probe, # built by the recipe against the very ${B}/libptytty.a that ships. # # The port is the LIBRARY, not a session. Nothing here opens a terminal for a # person to type into, and the suite closes non-interactively. # # WHAT THE PROBE IS REALLY ASKING ABOUT is not the 1300 lines of libptytty but # the four platform mechanisms they stand on: # # the devpts pair getpt/grantpt/unlockpt/ptsname, then open(2) of the # name ptsname reported. get_pty() strdup()s that name # and never consults the slave fd again, so "the name it # reports is the device it opened" is a real claim -- # checked by st_dev/st_ino, not by string equality alone. # the line discipline bytes crossing the pair in both directions, with the # slave in raw mode so ECHO and ONLCR are not in the way. # termios on a master ptytty_set_utf8_mode is tcgetattr+tcsetattr of IUTF8 on # the MASTER fd, measured in both directions. # the session database utmp through glibc's pututline, wtmp through updwtmp, # lastlog through a plain open+pwrite. THREE DIFFERENT # platform requirements, which is why they are three # groups below and not one. # # EVERY SKIP HERE IS EARNED BY A RUNTIME PROBE, never by a hardcoded assumption # about this arm. The probe reports whether an fcntl(2) write lock works, and # whether each of the three database files exists, BEFORE the library touches # any of them -- so a case that skips names the measurement that made it skip, # and the day the platform gains the facility the case starts asserting on its # own with no edit here. # # EXPECTATIONS COME FROM A NATIVE x86_64 CONTROL, not from reading the source. # data/libptytty-native.txt is the verbatim output of the same probe source # built against a native build of the identical patched 2.0 tree and run under a # private user+mount namespace with a tmpfs /var/run and /var/log -- i.e. as a # uid that can actually write the session database, so every case in it is a # measured 1 rather than an assumed one. The two device-name lines are stripped # from the control: a pty name is allocation-order dependent, and they are # checked against EACH OTHER by name-agree instead. # --------------------------------------------------------------------------- HDR=/usr/include/libptytty.h PC=/usr/lib/pkgconfig/libptytty.pc ARCHIVE=/usr/lib/libptytty.a MAN=/usr/share/man/man3/libptytty.3 PROBE=/usr/libexec/libptytty-tests/libptytty-probe NATIVE=$BST_DATA/libptytty-native.txt nl=' ' if [ ! -s "$HDR" ]; then bst_skip libptytty/present "libptytty not installed on this image" exit 0 fi bst_ok libptytty/present # --- the package is whole ---------------------------------------------------- check libptytty/header test -s "$HDR" check libptytty/pkgconfig test -s "$PC" check libptytty/staticlib test -s "$ARCHIVE" # The man page goes to libptytty-doc and this image installs no -doc packages, # so its absence is a packaging choice rather than a fault. if [ -s "$MAN" ]; then bst_ok libptytty/man else bst_skip libptytty/man "libptytty-doc is not installed on this image" fi # The distro is static-only. A shared object or a libtool archive here would # mean the bbappend's -DBUILD_SHARED_LIBS=OFF stopped taking effect -- which # matters more for this package than for most, because libptytty's own # CMakeLists defaults that option to ON. noglob_ok() { _id=$1; shift if [ ! -e "$1" ]; then bst_ok "$_id"; else bst_notok "$_id" "present: $1"; fi } set -- /usr/lib/libptytty.so*; noglob_ok libptytty/no-shared "$@" set -- /usr/lib/libptytty.la; noglob_ok libptytty/no-libtool "$@" # eg/c-sample is built by upstream and installed by nobody; -DBUILD_EXAMPLES=OFF # should mean it never reaches the image under any name. set -- /usr/bin/c-sample; noglob_ok libptytty/no-example "$@" # --- the header declares both APIs ------------------------------------------- # # libptytty.h serves the C++ class to a C++ compiler and the PTYTTY handle API # to a C one. Both halves are asserted: the port's consumers on this image are C # (the probe is), and the C++ half is what a future rxvt-unicode-shaped consumer # would compile against. hdr=$(cat "$HDR" 2>/dev/null) has() { case "$3" in *"$2"*) bst_ok "$1" ;; *) bst_notok "$1" "not found: $2" ;; esac; } has libptytty/hdr-c-typedef "typedef void *PTYTTY;" "$hdr" has libptytty/hdr-c-create "PTYTTY ptytty_create ();" "$hdr" has libptytty/hdr-c-get "int ptytty_get (PTYTTY" "$hdr" has libptytty/hdr-c-login "void ptytty_login (PTYTTY" "$hdr" has libptytty/hdr-c-pty "int ptytty_pty (PTYTTY" "$hdr" has libptytty/hdr-c-tty "int ptytty_tty (PTYTTY" "$hdr" has libptytty/hdr-cxx-struct "struct ptytty" "$hdr" has libptytty/hdr-cxx-error "class ptytty_error" "$hdr" # THE PATCH DOES NOT TOUCH THE INTERFACE, and this is where that is checked. # The bbappend routes seven `throw ptytty_error` statements through PTYTTY_THROW # because wasmc++-glibc bakes -fno-exceptions; ptytty_error itself is untouched # and is still declared here, so a consumer built on an arm that has exceptions # sees the unchanged class. A header that had lost it would mean the patch had # grown past its remit. # The needle is the DECLARATOR alone: upstream writes the return type on its # own line above it, so a one-line spelling of this would be red against a # perfectly correct header. (It was, on the first dry run against the built # .deb -- which is what that dry run is for.) has libptytty/hdr-error-what "what () const noexcept override" "$hdr" # --- pkg-config --------------------------------------------------------------- pct=$(cat "$PC" 2>/dev/null) has libptytty/pc-name "Name: libptytty" "$pct" has libptytty/pc-version "Version: 2.0" "$pct" has libptytty/pc-libs "-lptytty" "$pct" # --- the archive -------------------------------------------------------------- check_eq libptytty/archive-format '!' "$(head -c 7 "$ARCHIVE" 2>/dev/null)" # On this arm the archive is ThinLTO bitcode, so ar(1)/nm(1) are not on the # image and string CONSTANTS are not plain text -- but the C entry points are # `extern "C"` and their names survive in the object's name section, which # strings(1) can read. A skip here is the tool being absent, never a finding. if command -v strings >/dev/null 2>&1; then syms=$(strings "$ARCHIVE" 2>/dev/null) for s in ptytty_create ptytty_get ptytty_login ptytty_pty ptytty_tty \ ptytty_delete ptytty_init ptytty_close_tty \ ptytty_make_controlling_tty ptytty_set_utf8_mode \ ptytty_sanitise_stdfd ptytty_drop_privileges; do has "libptytty/sym-$s" "$s" "$syms" done else bst_skip libptytty/sym-ptytty_create "strings not installed on this image" fi # --- the session-database files the image does not provision ----------------- # # glibc's pututline() opens _PATH_UTMP but never creates it, updwtmp() is the # same for _PATH_WTMP, and libptytty's lastlog writer is a plain open(O_WRONLY) # that likewise creates nothing. This image runs no populate-volatile step and # mounts none of the tmpfs filesystems its /etc/fstab lists, so on a fresh boot # none of the three exists and /var/log is a symlink to a volatile/log that is # not there either. That is an image gap and not libptytty's, so each file gets # its own named case and the suite then provisions all three itself -- otherwise # every functional case below would go red for a reason that has nothing to do # with the library. What makes it possible at all is that / is mounted rw here. # # EVERY creation runs in a SUBSHELL, which is load-bearing rather than # stylistic: `:` is a POSIX special builtin, so a failed redirection on it exits # the shell outright -- and on this runner that means the rest of this file # never executes and the suite reports a truncated pass list with no failure in # it at all. (78-libutempter paid for that lesson; this suite inherits it.) for f in /var/run/utmp /var/log/wtmp /var/log/lastlog; do if [ -f "$f" ]; then bst_ok "libptytty/provisioned-${f##*/}" else bst_skip "libptytty/provisioned-${f##*/}" \ "image provisions no $f at boot (no populate-volatile); suite creates it" fi done ( mkdir -p /var/volatile/log /var/volatile/run ) 2>/dev/null [ -f /var/run/utmp ] || ( : > /var/run/utmp ) 2>/dev/null [ -f /var/log/wtmp ] || ( : > /var/log/wtmp ) 2>/dev/null [ -f /var/log/lastlog ] || ( : > /var/log/lastlog ) 2>/dev/null # --- the probe: the library actually running --------------------------------- if [ ! -f "$PROBE" ]; then bst_skip libptytty/probe-runs "libptytty-tests is not installed on this image" exit 0 fi pout=${TMPDIR:-/tmp}/bst-libptytty-out.$$ "$PROBE" > "$pout" 2>/dev/null prc=$? out=$(cat "$pout" 2>/dev/null) if [ "$prc" -eq 0 ]; then bst_ok libptytty/probe-runs else bst_notok libptytty/probe-runs "probe exited rc=$prc $(_bst_1line "$out")"; fi # The probe's last line. A capture without it is a probe that died part-way, and # every `(absent)` below would otherwise read as a wrong answer rather than as a # missing one. case "$nl$out$nl" in *"${nl}PT-end reached${nl}"*) bst_ok libptytty/probe-complete ;; *) bst_notok libptytty/probe-complete "the probe did not reach its end marker" ;; esac # PV is set by pv, in the caller's shell, deliberately: `$( )` forks, and on the # wasm32-hwjs runtime the forking IS the cost. 81-cjson was killed by the # runner's 60s per-suite bound at 156 cases written the forking way, so the ~50 # assertions below cost no process at all. pv() { _r=$nl$out$nl case "$_r" in *"${nl}PT-$1 "*) _r=${_r#*"${nl}PT-$1 "}; PV=${_r%%"$nl"*} ;; *) PV='(absent)' ;; esac } peq() { pv "$2"; check_eq "libptytty/$1" "$3" "$PV"; } # --- the platform facilities, as the probe measured them --------------------- # # These four are REPORTS, not gates, and each is asserted at the value this arm # actually has so that a CHANGE in either direction shows up. The two that hold # everything else together are fcntl-lock and ptmx. # POSIX record locking. glibc takes an fcntl write lock before every pututline() # and every updwtmp(), so a refusal here makes the utmp and wtmp halves of this # library inert however correct the library is. # # Already tracked: brintos.io/brintos/linux/issues/16 -- sys_fcntl64 is not # wired in the wasm32 syscall table, and glibc on this arch (__TIMESIZE == 64) # always sends the LFS lock commands F_GETLK64/F_SETLK64/F_SETLKW64 (12/13/14), # which are that syscall's alone. 78-libutempter measured the same refusal for # the same reason; this port adds a second witness, not a new report. # # This case stays a LOUD `not ok` when the lock is refused, and downgrading it # would hide the one fact that explains everything under it. The cases that are # CONSEQUENCES of it skip with the issue named -- runtime-blocked coverage # rather than an independent failure; five identical reds would bury the one # that matters. # # Measured 2026-08-27 on the batch-1648 image: the lock is GRANTED, so this is # green and the three consequence cases assert instead of skipping. The gate is # kept exactly as it is, so a regression in the fcntl64 wiring reports as one # red rather than four. LOCKBLOCK="blocked by brintos.io/brintos/linux/issues/16 (sys_fcntl64 unwired; glibc's utmp/wtmp writers cannot take their record lock)" pv fcntl-lock if [ "$PV" = "ok" ]; then bst_ok libptytty/fcntl-record-locking locked=0 else bst_notok libptytty/fcntl-record-locking "fcntl(F_SETLK) -> $PV; $LOCKBLOCK" locked=1 fi peq ptmx-present ptmx-chardev 1 peq devpts-present devpts-dir 1 # --- allocation --------------------------------------------------------------- peq init init ok peq create create ok peq get get ok peq pty-fd pty-fd-valid 1 peq tty-fd tty-fd-valid 1 # --- the name the library reports IS the device it opened -------------------- # # THE CENTRAL CLAIM OF THIS PORT. get_pty() takes ptsname(master), strdup()s it # and open(2)s it, then never looks at the result again -- so a wrong name is a # silent wrong answer, which is exactly the class a file listing cannot reach. # Checked three ways: the strings agree, the slave path is a devpts node, and # the (st_dev, st_ino) the name resolves to is the one the slave fd refers to. # The third is the one that would still catch a name re-created between the two # calls. peq name-agree name-agree 1 peq slave-devpts slave-under-devpts 1 peq slave-same-node slave-same-node 1 peq slave-chardev slave-is-chardev 1 peq master-isatty master-isatty 1 peq slave-isatty slave-isatty 1 # --- the pair carries bytes, both ways --------------------------------------- peq slave-raw slave-raw 1 peq write-master write-master 1 peq read-slave read-slave 1 peq write-slave write-slave 1 peq read-master read-master 1 # --- ptytty_set_utf8_mode ------------------------------------------------------ peq utf8-on utf8-on 1 peq utf8-off utf8-off 1 # --- the controlling terminal and the fd sanitiser --------------------------- # # Both run in a forked child of the probe: control_tty() calls setsid() and # TIOCSCTTY and then proves itself by open("/dev/tty"), and sanitise_stdfd is # only meaningful with 0/1/2 closed. peq ctty ctty 1 peq sanitise-stdfd sanitise-stdfd 1 peq child-clean child-exit-clean 1 # --- the session database ----------------------------------------------------- # # Three groups, three different platform requirements, three different verdicts. # The pre-state cases come first and are asserted at 0: a pty line is reused, so # /dev/pts/N already carries whatever the last session left, and a check that # matched on the line alone would report a stranger's record as this one's. That # is measured rather than feared -- the first native run of this probe returned # 1 for both before ut_pid and ut_host were added to the match. peq utmp-pre utmp-pre 0 peq lastlog-pre lastlog-pre 0 peq login-returns login-returns ok # utmp and wtmp: both go through an fcntl record lock, so both skip -- attributed # -- when the probe measured that lock refused. They MUST skip rather than # assert 0: asserting the value this arm happens to produce would turn green the # day the platform is fixed, which is backwards. pequ() { if [ "$locked" -eq 1 ]; then bst_skip "libptytty/$1" "$LOCKBLOCK" else peq "$1" "$2" "$3"; fi } pequ utmp-user-record utmp-user-record 1 pequ wtmp-grew wtmp-grew 1 pequ dead-record dead-record 1 # lastlog is the one session-database path that takes NO lock: libptytty writes # it itself, with a plain open(O_WRONLY) + pwrite at getuid()*sizeof(struct # lastlog) (logging.C write_record), never through glibc's utmp layer. So it is # asserted unconditionally, and it is the case that distinguishes "this library # cannot record anything here" from "the platform's locked paths are the ones # that are out". A red here with fcntl-record-locking already red would be a # genuine libptytty finding rather than a consequence of #16. peq lastlog-line lastlog-line 1 # --- release ------------------------------------------------------------------- # # The half a file listing cannot reach at all: ptytty_delete() restores the # slave's mode, closes both fds, and -- devpts being devpts -- the slave node # goes with the last close of the master. peq delete-returns delete-returns ok peq pty-fd-closed pty-fd-closed 1 peq tty-fd-closed tty-fd-closed 1 peq slave-node-gone slave-node-gone 1 # --- a second pair, so the first release is shown to have left nothing behind -- peq second-get second-get 1 peq second-fd second-distinct-fd 1 peq second-name second-name 1 peq second-delete second-delete ok # --- the aggregate, against the native control ------------------------------- # # Compared IN THE GUEST rather than eyeballed from the console: a console dump # truncates long lines (the BRINTOS-260 lesson), and a whole-capture comparison # catches a line this suite forgot to name individually. The two device-name # lines are filtered from both sides for the reason the header gives. # # Skipped, attributed, when the record lock is refused: with #16 present three # lines legitimately differ, and a red here would be reporting the platform gap # a second time rather than reporting libptytty. if [ ! -f "$NATIVE" ]; then bst_skip libptytty/matches-native "data/libptytty-native.txt absent from the image" elif [ "$locked" -eq 1 ]; then bst_skip libptytty/matches-native "$LOCKBLOCK" else filt=${TMPDIR:-/tmp}/bst-libptytty-filt.$$ grep -v -E '^PT-(master-ptsname|slave-ttyname) ' "$pout" > "$filt" 2>/dev/null if cmp -s "$filt" "$NATIVE"; then bst_ok libptytty/matches-native else bst_notok libptytty/matches-native \ "probe output differs from the native x86_64 control; see the per-case results above" fi rm -f "$filt" fi rm -f "$pout"