Don't leave a fresh tty's c_cflag at zero - #2783
Open
emkey1 wants to merge 1 commit into
Open
Conversation
emkey1
pushed a commit
to emkey1/ish-AOK
that referenced
this pull request
Aug 3, 2026
d532698 gave a freshly allocated tty a sane c_cflag, but missed the second place one is built. termios_from_real() zero-initializes and then translates only the i/o/l flags and the control characters, never cflags -- so the CLI's console tty was re-zeroed immediately after tty_alloc had got it right, and still reported B0 with CS5 and CREAD clear. The app never showed this because its terminal is a pty, which tty_alloc covers on its own; only the CLI goes through tty-real.c. It matters anyway, since ssh(1) run from the CLI forwards the same ospeed 0 that hung up the BSD login shell in d532698. The host's c_cflag is not worth translating: iSH models no baud rate or character size, and BSD keeps the speed in a separate c_ospeed field rather than in CBAUD. Seed the same nominal default tty_alloc uses. busybox stty speed, CLI console tty: before 0 after 38400 fresh pty c_cflag (unchanged): 0677 = B38400|CS8|CREAD|HUPCL tests/manual/pty_line_discipline.c passes, default_cflag included. Found while porting d532698 upstream (ish-app#2783), where the same gap would have made the fix look ineffective to anyone testing on the command-line build. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
emkey1
force-pushed
the
tty_cflag_not_zero
branch
from
August 3, 2026 16:35
c457337 to
e571ef9
Compare
tty_alloc() copies c_iflag, c_oflag, c_lflag and c_cc from Linux's tty_std_termios but leaves c_cflag at 0, which is not a neutral default: the baud rate lives in the CBAUD bits of c_cflag, so 0 decodes as B0 -- "hang up the line" -- and also as CS5 with CREAD clear. cfgetospeed() in both musl and glibc reads straight out of those bits, so a guest sees a terminal running at B0, and ssh(1) forwards that: it sends TTY_OP_OSPEED in its pty-req, and a BSD sshd honours the value literally. sshd applies the modes before the fork, so nothing is signalled yet; the kernel just records an ospeed of 0. The login shell then takes the pty as its controlling terminal and calls tcsetattr to set up line editing, and a shell that copies the whole struct and edits only c_iflag/c_lflag/c_cc carries the zero speed back in. A BSD tty driver sends SIGHUP to the session leader when it sees c_ospeed == 0, so the shell hangs itself up the instant it starts: ssh authenticates, prints the motd, and drops with exit status 129 (128 + SIGHUP). Linux servers are unaffected because their pty driver ignores B0, which is what makes this look like a problem on the server side. Linux keeps the default per driver, so do the same rather than picking one value for every tty: drivers/tty/vt/vt.c takes tty_std_termios as-is for the console, which includes HUPCL, while drivers/tty/pty.c overrides c_cflag to B38400|CS8|CREAD for both the master and the slave. The test here is on the device type rather than the driver, because the app drives its terminals through a tty_driver of its own -- but pty_open_fake registers them under TTY_PSEUDO_SLAVE_MAJOR, so to the guest they are pty slaves and should not come up looking like a console. The bits are inert inside iSH, which models no line discipline hardware, but guests read them back and forward them over the wire. Note the kernel's termbits header spells these in hex, and the two neighbouring bits are easy to swap: HUPCL is 0x400, i.e. octal 0002000, while octal 0000400 is PARENB. PARENB_ is defined alongside it so the mistake is harder to make -- ssh(1) forwards PARENB to the far end, over the same wire path that makes the speed matter. termios_from_real() needs the same treatment: it zero-initializes and then translates only the i/o/l flags and the control characters, so the CLI's console tty came out at B0 as well. The host's c_cflag is not worth translating -- BSD keeps the speed in a separate c_ospeed field rather than in CBAUD, and iSH models no baud rate -- so it seeds the same nominal default instead.
emkey1
force-pushed
the
tty_cflag_not_zero
branch
from
August 3, 2026 16:56
e571ef9 to
ce0a0de
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
tty_alloc()copiesc_iflag,c_oflag,c_lflagandc_ccfrom Linux'stty_std_termios, but leavesc_cflagat 0.Zero is not a neutral default. The baud rate lives in the
CBAUDbits ofc_cflag, so 0 reads back asB0, which means "hang up the line", and also asCS5withCREADclear.How I ran into it
ssh(1)from iSH to an OpenBSD host authenticated, printed the motd, and dropped immediately withExit status 129(128 + SIGHUP). The same client worked against every Linux host.cfgetospeed()in both musl and glibc reads straight out ofc_cflag, so ssh sees a terminal running at B0 and forwards it asTTY_OP_OSPEEDin its pty-req. A BSD sshd honours that literally. The modes are applied before the fork, so nothing is signalled yet and the kernel just records an ospeed of 0. The login shell then takes the pty as its controlling terminal and callstcsetattrto set up line editing; a shell that copies the whole struct and edits onlyc_iflag/c_lflag/c_cccarries the zero speed back in. A BSD tty driver SIGHUPs the session leader when it seesc_ospeed == 0, so the shell hangs itself up the instant it starts.Linux servers are unaffected because their pty driver ignores B0, which is what makes this look like a server-side problem.
The change
Linux keeps the default per driver, so this does the same rather than picking one value for every tty:
drivers/tty/vt/vt.ctakestty_std_termiosas-is for the console, which includesHUPCLdrivers/tty/pty.coverridesc_cflagtoB38400|CS8|CREADfor both the master and the slaveThe test is on the device type rather than the driver: the app drives its terminals through a
tty_driverof its own, butpty_open_fakeregisters them underTTY_PSEUDO_SLAVE_MAJOR, so to the guest they are pty slaves and should not come up looking like a console.The bits are inert inside iSH, which models no line discipline hardware, but guests read them back and forward them over the wire.
termios_from_real()needs the same treatment. It zero-initializes and then translates only the i/o/l flags and the control characters, so the CLI's own console tty came out at B0 as well. The host'sc_cflagis not worth translating (BSD keeps the speed in a separatec_ospeedfield rather than inCBAUD, and iSH models no baud rate), so it seeds the same nominal default instead.Verification
Freshly allocated pty,
tcgetattron the slave. Same static i386 binary on iSH and on real Linux:c_cflag000000000002770000277cfgetospeed()0(B0)017(B38400)017(B38400)CSIZE0(CS5)060(CS8)060(CS8)CREADConsole tty,
tcgetattron fd 0 over a real pty:tty_std_termiosc_cflag000000000022770002277And the CLI's console tty, via busybox:
stty speed03840038400Run on the Alpine 3.19.0 rootfs the App Store build downloads (
ROOTFS_URLinapp/iSH.xcconfig), against master 7864dd6. Real-Linux column is the same binary in alinux/386container.stty -astill reports canonical mode, echo and the usual control characters.The console row is checked against the kernel's value for that driver rather than against a measurement: a container's stdin is a pts, so there was nothing to measure it on directly.
One trap worth flagging for review: the kernel's termbits header spells these in hex, and the two neighbouring bits are easy to swap.
HUPCLis0x400, i.e. octal0002000, while octal0000400isPARENB.PARENB_is defined alongside it here so the mistake is harder to make, sincessh(1)forwardsPARENBto the far end over the same wire path that makes the speed matter.