Skip to content

Don't leave a fresh tty's c_cflag at zero - #2783

Open
emkey1 wants to merge 1 commit into
ish-app:masterfrom
emkey1:tty_cflag_not_zero
Open

Don't leave a fresh tty's c_cflag at zero#2783
emkey1 wants to merge 1 commit into
ish-app:masterfrom
emkey1:tty_cflag_not_zero

Conversation

@emkey1

@emkey1 emkey1 commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

tty_alloc() copies c_iflag, c_oflag, c_lflag and c_cc from Linux's tty_std_termios, but leaves c_cflag at 0.

Zero is not a neutral default. The baud rate lives in the CBAUD bits of c_cflag, so 0 reads back as B0, which means "hang up the line", and also as CS5 with CREAD clear.

How I ran into it

ssh(1) from iSH to an OpenBSD host authenticated, printed the motd, and dropped immediately with Exit status 129 (128 + SIGHUP). The same client worked against every Linux host.

cfgetospeed() in both musl and glibc reads straight out of c_cflag, so ssh sees a terminal running at B0 and forwards it as TTY_OP_OSPEED in 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 calls tcsetattr to set up line editing; 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 SIGHUPs the session leader when it sees c_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.c takes tty_std_termios as-is for the console, which includes HUPCL
  • drivers/tty/pty.c overrides c_cflag to B38400|CS8|CREAD for both the master and the slave

The test is on the device type rather than the driver: 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.

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'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.

Verification

Freshly allocated pty, tcgetattr on the slave. Same static i386 binary on iSH and on real Linux:

before after real Linux
c_cflag 0000000 0000277 0000277
cfgetospeed() 0 (B0) 017 (B38400) 017 (B38400)
CSIZE 0 (CS5) 060 (CS8) 060 (CS8)
CREAD clear set set

Console tty, tcgetattr on fd 0 over a real pty:

before after Linux tty_std_termios
c_cflag 0000000 0002277 0002277

And the CLI's console tty, via busybox:

before after real Linux
stty speed 0 38400 38400

Run on the Alpine 3.19.0 rootfs the App Store build downloads (ROOTFS_URL in app/iSH.xcconfig), against master 7864dd6. Real-Linux column is the same binary in a linux/386 container. stty -a still 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. HUPCL is 0x400, i.e. octal 0002000, while octal 0000400 is PARENB. PARENB_ is defined alongside it here so the mistake is harder to make, since ssh(1) forwards PARENB to the far end over the same wire path that makes the speed matter.

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
emkey1 force-pushed the tty_cflag_not_zero branch from c457337 to e571ef9 Compare August 3, 2026 16:35
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
emkey1 force-pushed the tty_cflag_not_zero branch from e571ef9 to ce0a0de Compare August 3, 2026 16:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants