Skip to content

Honor CBAUD/CIBAUD in TCGETS, TCGETS2 and TCSETS - #312

Merged
jserv merged 1 commit into
sysprog21:mainfrom
jotpalch:termios-baud
Aug 20, 2026
Merged

Honor CBAUD/CIBAUD in TCGETS, TCGETS2 and TCSETS#312
jserv merged 1 commit into
sysprog21:mainfrom
jotpalch:termios-baud

Conversation

@jotpalch

@jotpalchjotpalch commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Note

Piece 1 of #310, sent separately per the discussion there.

Summary

Plain TCSETS dropped the baud rate and TCGETS reported B0. TCGETS/TCGETS2/TCSETS now encode and decode CBAUD/CIBAUD like the Linux tty core, so tcsetattr(B115200) from a musl or glibc <= 2.41 guest reaches the host line and cfgetospeed() round-trips.

Important

What to look at:

  1. apply_linux_cflag_speeds(): the two values that must not touch the host speed: B0 in CBAUD and a bare BOTHER. The kernel resolves BOTHER on a plain TCSETS from the tty's currentc_ospeed (drivers/tty/tty_ioctl.cset_termios starts from tty->termios; drivers/tty/tty_baudrate.ctty_termios_baud_rate). An earlier draft decoded it to 0 and collapsed a 74880-baud line to B0 on the next tcgetattr/tcsetattr pair (see evidence).
  2. linux_cflag_speed_bits(): CIBAUD is set only when input != output, and TCGETS2 now uses the same encoding instead of always BOTHER, matching tty_termios_encode_baud_rate().
  3. TCSETS2 decodes CIBAUD symmetrically (B0 = same as output, BOTHER = numeric c_ispeed, else table): since TCGETS2 now reports a split rate as CBAUD+CIBAUD indexes rather than the old blanket BOTHER, an unchanged tcgetattr/tcsetattr pair from glibc 2.42 would otherwise collapse the input rate to the output rate (found in self-review on the staging fork, Honor CBAUD/CIBAUD in TCGETS, TCGETS2 and TCSETS jotpalch/elfuse#1).

Problem

observed
src/syscall/io.c TCSETS arm (io.c:2289-2316 at bffd6bd)never calls cfsetispeed/cfsetospeed; only the TCSETS2 arm did
TCGETSnever encodes the host rate into CBAUD, so cfgetospeed() == 0
who is affectedmusl (always) and glibc <= 2.41 issue plain TCSETS with the rate in CBAUD; glibc 2.42+ switched to TCGETS2/TCSETS2 unconditionally (sysdeps/unix/sysv/linux/tcsetattr.c, kernel-features.h__ASSUME_TERMIOS2)
why it stayed hiddenon a host pty the rate is meaningless; it becomes visible the moment a guest opens a real serial port, which the /dev passthrough already allows (/dev/cu.usbmodem*)

Change

  • io.c: linux_cflag_speed_bits() (host termios -> CBAUD/CIBAUD bits) and apply_linux_cflag_speeds() (Linux c_cflag -> cfsetospeed/cfsetispeed), used by the TCGETS, TCGETS2 and TCSETS/TCSETSW/TCSETSF arms. LINUX_IBSHIFT added next to LINUX_CBAUD.
  • tests/test-pty.c: slave-side block (5 checks): B115200 round trip; unchanged termios keeps the rate; 74880 set via raw TCSETS2 survives a plain tcgetattr/tcsetattr pair; split rate through CIBAUD and re-unification; TCGETS/TCGETS2 agree.

Kernel references (Linux v7.2): include/uapi/asm-generic/termbits.h (CBAUD 0x100f, BOTHER 0x1000, CIBAUD = CBAUD << 16, B115200 0x1002), drivers/tty/tty_baudrate.c (tty_termios_baud_rate, tty_termios_input_baud_rate, tty_termios_encode_baud_rate), drivers/tty/tty_ioctl.c (set_termios, get_termios). x86_64 (Rosetta) guests share the asm-generic layout.

Evidence

Harness: open a macOS pty pair, preset the host side to 9600, hand the slave to a static aarch64 guest that does tcsetattr(B115200) and reads it back.

glibc 2.41 guest, before (bffd6bd) vs after

before:

== 3. baud rate round-trip (TCSETS/TCSETS2 depending on libc) ==
tcsetattr(B115200) = 0
cfgetospeed -> 0 (want 4098)
== 4. line / modem-control ioctls ==
TIOCMGET = -1Inappropriate ioctl for device bits=0x0
TIOCMBIS(DTR|RTS)= -1Inappropriate ioctl for device
TIOCMBIC(DTR|RTS)= -1Inappropriate ioctl for device
tcflush/TCFLSH = -1Inappropriate ioctl for device
tcdrain/TCSBRK = -1Inappropriate ioctl for device
TIOCOUTQ = -1Inappropriate ioctl for device outq=-1
TIOCEXCL = -1Inappropriate ioctl for device
host-side speed after guest tcsetattr(B115200): ispeed=9600 ospeed=9600

after:

== 3. baud rate round-trip (TCSETS/TCSETS2 depending on libc) ==
tcsetattr(B115200) = 0
cfgetospeed -> 4098 (want 4098)
== 4. line / modem-control ioctls ==
TIOCMGET = -1Inappropriate ioctl for device bits=0x0
TIOCMBIS(DTR|RTS)= -1Inappropriate ioctl for device
TIOCMBIC(DTR|RTS)= -1Inappropriate ioctl for device
tcflush/TCFLSH = 0
tcdrain/TCSBRK = 0
TIOCOUTQ = 0 outq=0
TIOCEXCL = 0
host-side speed after guest tcsetattr(B115200): ispeed=115200 ospeed=115200
musl 1.2.5 guest, after
== 3. baud rate round-trip (TCSETS/TCSETS2 depending on libc) ==
tcsetattr(B115200) = 0
cfgetospeed -> 4098 (want 4098)
== 4. line / modem-control ioctls ==
TIOCMGET = -1Not a tty bits=0x0
TIOCMBIS(DTR|RTS)= -1Not a tty
TIOCMBIC(DTR|RTS)= -1Not a tty
tcflush/TCFLSH = 0
tcdrain/TCSBRK = 0
TIOCOUTQ = 0 outq=0
TIOCEXCL = 0
host-side speed after guest tcsetattr(B115200): ispeed=115200 ospeed=115200
74880 (BOTHER) regression: draft vs final

draft (decoded bare BOTHER to 0):

host pty slave = /dev/ttys002, host speed preset to ispeed=74880 ospeed=74880
guest tcgetattr: cfgetospeed=4096 (BOTHER=0x1000) c_cflag&CBAUD=0x1000
guest tcsetattr(unchanged speed) = 0 TCFLSH(TCIFLUSH=0) = 0 TCFLSH(3 invalid) = -1 Invalid argument (want EINVAL)
TCXONC(TCOON=1) = 0 TCXONC(4 invalid) = -1 Invalid argument (want EINVAL)
TCSBRKP(0) = 0 TIOCNXCL = 0 host-side speed after guest round-trip: ispeed=0 ospeed=0 (want 74880)

final:

host pty slave = /dev/ttys002, host speed preset to ispeed=74880 ospeed=74880
guest tcgetattr: cfgetospeed=4096 (BOTHER=0x1000) c_cflag&CBAUD=0x1000
guest tcsetattr(unchanged speed) = 0 TCFLSH(TCIFLUSH=0) = 0 TCFLSH(3 invalid) = -1 Invalid argument (want EINVAL)
TCXONC(TCOON=1) = 0 TCXONC(4 invalid) = -1 Invalid argument (want EINVAL)
TCSBRKP(0) = 0 TIOCNXCL = 0 host-side speed after guest round-trip: ispeed=74880 ospeed=74880 (want 74880)
native macOS build of the same program, same pty (control)
== 3. baud rate round-trip (TCSETS/TCSETS2 depending on libc) ==
tcsetattr(B115200) = 0
cfgetospeed -> 115200 (want 115200)
== 4. line / modem-control ioctls ==
TIOCMGET = -1Inappropriate ioctl for device bits=0x0
TIOCMBIS(DTR|RTS)= -1Inappropriate ioctl for device
TIOCMBIC(DTR|RTS)= -1Inappropriate ioctl for device
tcflush/TCFLSH = 0
tcdrain/TCSBRK = 0
TIOCOUTQ = 0 outq=0
TIOCEXCL = 0
host pty /dev/ttys002
host-side speed after native tcsetattr: 115200 115200
real hardware: ESP32-S3 USB-Serial/JTAG (/dev/cu.usbmodem101), before vs after

before:

== 2. open(/dev/cu.usbmodem101) ==
open = 3 ok
== 3. baud rate round-trip (TCSETS/TCSETS2 depending on libc) ==
tcsetattr(B115200) = 0
cfgetospeed -> 0 (want 4098)
== 4. line / modem-control ioctls ==
TIOCMGET = -1Inappropriate ioctl for device bits=0x0
TIOCMBIS(DTR|RTS)= -1Inappropriate ioctl for device
TIOCMBIC(DTR|RTS)= -1Inappropriate ioctl for device
tcflush/TCFLSH = -1Inappropriate ioctl for device
tcdrain/TCSBRK = -1Inappropriate ioctl for device
TIOCOUTQ = -1Inappropriate ioctl for device outq=-1
TIOCEXCL = -1Inappropriate ioctl for device

after (this PR + piece 2):

== 2. open(/dev/cu.usbmodem101) ==
open = 3 ok
== 3. baud rate round-trip (TCSETS/TCSETS2 depending on libc) ==
tcsetattr(B115200) = 0
cfgetospeed -> 4098 (want 4098)
== 4. line / modem-control ioctls ==
TIOCMGET = 0 bits=0x6
TIOCMBIS(DTR|RTS)= 0
TIOCMBIC(DTR|RTS)= 0
tcflush/TCFLSH = 0
tcdrain/TCSBRK = 0
TIOCOUTQ = 0 outq=0
TIOCEXCL = 0

Tests

  • build/elfuse build/test-pty: 63 passed, 0 failed (includes the split-rate TCGETS2/TCSETS2 regression test)
  • make check BAREMETAL_CROSS=aarch64-elf- LINUX_TOOLCHAIN=/opt/homebrew/opt/aarch64-unknown-linux-gnu -j8: rc=0, All 77 tests passed (fixture-dependent musl/dyn-glibc cases skipped as on a stock checkout)
  • clang-format --dry-run -Werror clean

Not in this PR

  • The line/modem ioctls (TCFLSH, TCSBRK, TIOCM*, ...) -> piece 2 (stacked PR).
  • B0 hang-up (drop DTR/RTS) is not emulated; documented in the comment.

Summary by cubic

Honor CBAUD/CIBAUD in TCGETS, TCGETS2, and TCSETS so baud rates round-trip correctly. Previously TCSETS ignored baud bits and TCGETS reported B0; now standard, custom (BOTHER), and split input rates are preserved.

  • TCGETS/TCGETS2: encode host ospeed/ispeed into CBAUD and CIBAUD like the Linux tty core; TCGETS2 no longer forces BOTHER and still fills numeric speeds.
  • TCSETS/TCSETS2: apply CBAUD/CIBAUD; CIBAUD B0 means “same as output”; B0 in CBAUD and a bare BOTHER leave the host speed unchanged; Linux’s B0 hang-up is not emulated.
  • Tests: cover B115200 round-trip via plain TCSETS, preserving a BOTHER-only rate (74880) across tcgetattr/tcsetattr, split-rate round-trip and re-unification, and TCGETS vs TCGETS2 agreement.

Written for commit 3a6c880. Summary will update on new commits.

Review in cubic

cubic-dev-ai[bot]

This comment was marked as resolved.

@jotpalch
jotpalchforce-pushed the termios-baud branch 2 times, most recently from 394558a to ee6805aCompareAugust 20, 2026 09:12

@jservjserv left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rebase latest main branch as #313 installs Git hooks and a CI gate to enforce commit‑message rules and staged checks.

musl and glibc <= 2.41 store the baud rate as a B* index in CBAUD (and
in CIBAUD for a split input rate) and issue plain TCSETS; glibc 2.42+
switched to TCGETS2/TCSETS2 unconditionally. elfuse only read a speed in
the termios2 arm, so on a real serial port tcsetattr(B115200) returned
0 and left the host line at whatever speed it last had, and TCGETS
reported cfgetospeed() == 0. Host ptys hid this because their rate is
meaningless; a guest opening /dev/cu.usbmodem* through the /dev
passthrough observes it on the first tcsetattr.
TCGETS and TCGETS2 now encode the host rates the way
tty_termios_encode_baud_rate() does: CBAUD carries the output rate (B*
index, or BOTHER for a rate Linux has no name for) and CIBAUD is set
only when the input rate differs. TCGETS2 no longer forces BOTHER, so
both requests agree as they do on Linux. TCSETS decodes CBAUD/CIBAUD
following drivers/tty/tty_baudrate.c: CIBAUD B0 means "same as output";
B0 in CBAUD (drop DTR/RTS on Linux, not emulated) and a bare BOTHER
leave the host speed alone -- on a plain TCSETS the kernel resolves
BOTHER from the tty's current c_ospeed since the struct carries no
speed fields, and without that rule a tcgetattr/tcsetattr pair on a
port set to 74880 via termios2 collapsed the line to B0.
tests/test-pty.c gains a slave-side block covering the B115200 round
trip, the BOTHER-preserving plain pair at 74880, a split rate through
CIBAUD and its re-unification, and TCGETS/TCGETS2 agreement.
The TCSETS2 arm now decodes CIBAUD the same way: since TCGETS2 reports a
split host rate as CBAUD+CIBAUD indexes rather than the previous blanket
BOTHER, an unchanged tcgetattr/tcsetattr pair from a termios2 libc
(glibc 2.42) would otherwise collapse the input rate to the output rate.
@jotpalch

Copy link
Copy Markdown
ContributorAuthor

Rebased onto main at 7cc000b (#313). Both commits pass the new gate (scripts/check-commit-log.sh) and scripts/test-git-hooks.sh locally; comment reflow clean, make check 77/77, test-pty 70/70 on the stacked pair.

@jserv
jserv merged commit ae46dbf into sysprog21:mainAug 20, 2026
14 checks passed
@jserv

Copy link
Copy Markdown
Contributor

Thank @jotpalch for contributing!

@jotpalch

Copy link
Copy Markdown
ContributorAuthor

Thank you! This is my first contribution to your projects and I'm thrilled it landed. The review gates here (commentflow, the commit-log hooks, the EINTR contract checker) taught me a lot along the way. Piece 2 of #310 follows next.

Sign up for freeto 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

@jotpalch@jserv