← board

xtensa TerminalSize: -ENOTTY, cause found 2026-09-24 (see the bottom)

Found while deleting ansiterm's four private per-target syscall number tables and routing its five bodies through the PAL. The PAL has ioctl for all six targets (xtensa 66), so the syscall now happens on xtensa where before the number was simply absent.

Measured, 2026-09-04

Same program, run under script -qec 'stty rows 40 cols 132; ...':

target PalIoctl(1, $5413, @ws) cols x rows
x86-64 0 132 x 40
i386, arm32, aarch64 0 132 x 40
riscv32 0 132 x 40 (was FALSE 80x24 before the PAL move)
xtensa -25 80 x 24 fallback
wasm32 PAL_ERR_UNSUPPORTED 80 x 24 fallback (wasi has no ioctl; correct)

WHY THIS TICKET IS ABOUT AN INSTRUMENT, NOT A NUMBER

The obvious hypothesis was that xtensa Linux uses the BSD-style ioctl command encoding (TIOCGWINSZ = $40087468) rather than the generic $5413, the way MIPS and SPARC do. It was tested rather than assumed, and the test refuted nothing and confirmed nothing:

xtensa : generic $5413 rc=-25   bsd-style $40087468 rc=-25
x86-64 : generic $5413 rc=0     bsd-style $40087468 rc=-25

-25 is -ENOTTY, and on x86-64 that is exactly what the WRONG constant returns on a REAL tty. So on xtensa the two candidate causes produce the identical observation and this probe cannot discriminate. The x86-64 row is what makes that statement checkable rather than a shrug: it shows the probe CAN tell a right constant from a wrong one when the tty is real, which is precisely why its silence on xtensa is informative about the instrument.

What would separate them

Something that answers about the tty rather than about the ioctl. Either:

Until one of those is done, do NOT copy an ioctl constant out of a header into ansiterm and call it fixed -- a wrong syscall or command number does not fail like a missing one, it calls something else, which is the exact reasoning that kept the old private table empty for xtensa in the first place.

Not a regression, and the reason matters

Before 2026-09-04 GetSysIoctl had no xtensa row, returned -1, and AnsiSetRawMode/TerminalSize exited early -- the same 80x24 fallback. The change moved xtensa from "refuses because nobody filled in a number" to "asks and is told -ENOTTY". That is strictly more information and the same behaviour, and it is why this is prio 20 rather than higher: no TUI drew anything on xtensa before and none draws less now.

2026-09-24 — solved, and it was neither candidate (frankS)

The ticket asked for one of three things. The second one — "PalIsatty/TCGETS on fd 0 AND fd 1 AND a plain file, three rows, so the pattern separates 'this fd is not a terminal' from 'this command is not recognised'" — was the right instinct, and the version that worked added a row nobody had thought of.

The probe that separates them

row x86-64 riscv32 xtensa
A ioctl on a bad fd (-1), $5413 -9 -9 -25
B FIONREAD $541B on a regular file 0 0 -25
C FIONREAD BSD-spelled, regular file -25 -25 -25
E TIOCGWINSZ $5413 on fd 1 (pty) 0 0 -25
H TCGETS $5401 on fd 1 (pty) 0 0 0

Row A is the finding and it is about the instrument. ioctl() validates the fd before it decodes the command, so a bad fd must answer EBADF. x86-64 and riscv32 do. xtensa answers -25 — which the kernel cannot produce. So -25 on xtensa was never ENOTTY at all. It is what qemu-xtensa returns for a command absent from its table, fd unexamined. The original probe's positive control had been failing silently the whole time, and every -25 read as evidence about a terminal.

Row H refutes the no-tty candidate outright. TCGETS succeeds on fd 1, and TCGETS succeeds only on a terminal. qemu's own -strace shows it decoded:

ioctl(1,TCGETS,...) = 0 ({c_iflag = ICRNL|IXON, c_cflag = B38400,CS8,CREAD,
                          c_lflag = ISIG|ICANON|ECHO|...})

A live tty's settings. Note qemu printed TCGETS by name and the unknown commands as raw hex — the formatting itself distinguishes "in the table" from "not in the table".

The cause

xtensa keeps the BSD-style 't' encoding for the window-size family while using asm-generic for everything else (TCGETS $5401 and TCSETS $5402 are the generic numbers on xtensa too — measured, which is why only one constant is conditional).

The direction bits are the trap. Linux's _IOC_READ is 2, so _IOR sets $80000000; BSD's sets $40000000. _IOR('t',104,struct winsize) is therefore $80087468. The 2026-09-04 probe tried $40087468 — the BSD spelling — so it tested a constant that is wrong under both hypotheses it was meant to separate. That is the whole reason it "refuted nothing and confirmed nothing".

xtensa, in a 132x40 pty:
  generic   $5413     rc=-25
  bsd-dir   $40087468 rc=-25     <- what the old probe tried
  linux_IOR $80087468 rc=0 rows=40 cols=132

Fixed

lib/rtl/ansiterm.pas behind {$ifdef CPU_XTENSA}, and both crtl C headers that carried the same constant. TIOCSWINSZ $40087467 was measured (read the size, write it back) rather than derived from the same macro.

Verified at HEAD, all six targets, each in a pty: xtensa now reads TRUE 132x40 like everything else, and forcing the old constant back reproduces FALSE 80x24. The Makefile's atpal recipe gains an xtensa pty row, and its comment — which stated this ticket's old uncertainty — is retired. The existing x86-64 pty row is that row's control: a missing pty makes a correct constant answer FALSE 80x24, so both red means the harness lost its pty and xtensa alone red means the constant regressed.

The caveat that remains

Every xtensa row here is qemu-xtensa, not silicon. qemu's target ioctl table is derived from the kernel's uapi headers, so it is not an independent witness. What raises it above "qemu says so" is that the accepted value is exactly the canonical _IOR('t',104,8) a kernel header generates, and that qemu decoded and named the request. The third option in "What would separate them" — a run on real xtensa Linux — is still worth having. Nothing here depends on it.

Two side findings

Log