Uh oh!
There was an error while loading. Please reload this page.
Translate the serial line and modem-control ioctls - #314
Merged
Conversation
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
jotpalchforce-pushed
the
serial-ioctls
branch
2 times, most recently
from
August 20, 2026 17:21
84a2d76 to
ebdd0abComparejserv
reviewed
Aug 20, 2026
| return 0; | ||
| } | ||
| /* Interruptible output drain, mirroring tty_wait_until_sent(): the kernel waits |
Contributor
There was a problem hiding this comment.
Be precise about "the kernel" while specifying macOS, Linux, or general cases.
ContributorAuthor
There was a problem hiding this comment.
Fair point. Reworded in 79d3e0a: every "the kernel" in this diff now names Linux explicitly ("the Linux kernel waits", "Linux returns a plain -EINTR"), and the commit message follows. The macOS side was already named at each mention.
A Linux guest driving a USB serial adapter (pyserial, esptool, picocom) issues TCFLSH, TCSBRK (tcdrain), TCXONC, TCSBRKP, TIOCSBRK/TIOCCBRK, TIOCOUTQ, TIOCEXCL/NXCL and TIOCMGET/MSET/MBIS/MBIC; all of them fell through to ENOTTY, so pyserial's open() died at termios.tcflush() and esptool never reached its DTR/RTS reset sequence. tcflush() on the controlling tty fails the same way for any guest program. The line-control arms route to the POSIX wrappers, since macOS has no ioctl form. The Linux kernel waits for pending output before TCSBRK, TCSBRKP and TIOCSBRK (drivers/tty/tty_io.c) and a pending signal aborts that wait with a plain -EINTR, so the drain is emulated by polling TIOCOUTQ in short slices with the interruption checks from io_wait_fd_or_interrupted() rather than parking the vCPU thread in macOS tcdrain(), which guest signals cannot reach -- a serial port stalled by flow control would otherwise hang the guest unkillably -- and the EINTR forbids the SVC restart, as the Linux one does. The tcflush/tcflow selectors are renumbered explicitly (Linux 0-based in asm-generic/termbits-common.h, macOS 1-based in sys/termios.h). The modem-control set family masks the guest's word to the output lines the way tty_tiocmset() does before it reaches the driver: Darwin has no OUT1/OUT2/LOOP, so the settable intersection is DTR|RTS, and read-only status bits are silently dropped as on Linux. TIOCMSET means "set = val & mask, clear = ~val & mask" on Linux while Darwin's TIOCMSET overwrites the whole word, so it is emulated with a TIOCMBIS/TIOCMBIC pair. TIOCM_* bit values themselves are identical on both (asm-generic/termios.h vs sys/ttycom.h). A pty answers ENOTTY on both systems, only a real serial driver implements them. All request numbers come from asm-generic/ioctls.h and so are the same for aarch64 and x86_64 (Rosetta) guests. tests/test-pty.c covers tcflush/tcdrain/tcflow, TCFLSH selector validation, TIOCOUTQ, TIOCEXCL/NXCL, the ENOTTY of TIOCMGET on a pty, and that a status-bits-only TIOCMBIS still reports ENOTTY on a pty (the driver check precedes the value on Linux).
Uh oh!
There was an error while loading. Please reload this page.
jserv
commented
Aug 21, 2026
Contributor
Thank @jotpalch for contributing! |
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 freeto 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.
Note
Piece 2 of #310, on top of the CBAUD/CIBAUD work merged in #312.
Summary
Thirteen serial line-control and modem-control ioctls fell through to
ENOTTY. They now route to the Darwin equivalents, so pyserial/esptool/picocom-style tools (and any program callingtcflush()on its tty) work on a passthrough serial port.Important
What to look at:
TCSBRK/TCSBRKP/TIOCSBRKis interruptible: the kernel'stty_wait_until_sentaborts on a pending signal with a plain-EINTR(no restart), so the arms pollTIOCOUTQin short slices with theio_wait_fd_or_interrupted()interruption checks instead of parking the vCPU thread in macOStcdrain(), which guest signals cannot reach; a flow-control-stalled port would otherwise hang the guest unkillably. OnlyTCSBRKarg 0 actually breaks (arg != 0 is glibc/musltcdrain()), andTIOCCBRKdoes not drain, matching the kernel's prep block.TCFLSH/TCXONCselectors are renumbered explicitly: Linux 0-based (asm-generic/termbits-common.h), Darwin 1-based (sys/termios.h). Out-of-range ->EINVAL, as the kernel.TIOCMset family masks the guest word to the settable lines the waytty_tiocmset()does (Linux maskDTR|RTS|OUT1|OUT2|LOOP; Darwin has no OUT1/OUT2/LOOP, so the intersection isDTR|RTS), dropping read-only status bits silently as Linux does.TIOCMSETis emulated with aTIOCMBIS/TIOCMBICpair because Linux only touches masked bits while Darwin'sTIOCMSEToverwrites the whole word. Bit values themselves are identical on both. A pty answersENOTTYon both systems, and the driver check precedes the value (a status-bits-onlyTIOCMBISis stillENOTTYon a pty).host_refon all paths.Problem
sys_ioctldefault arm (io.c:2573at bffd6bd) returnsENOTTYfor:TCSBRKTCSBRKPTCFLSHTCXONCTIOCOUTQTIOCEXCLTIOCNXCLTIOCSBRKTIOCCBRKTIOCMGETTIOCMSETTIOCMBISTIOCMBIC.Who hits it (from source): pyserial
open()->_reset_input_buffer()->termios.tcflush(serialposix.py:344,677, fatal);setDTR/setRTS->TIOCMBIS/TIOCMBIC(:706-715);out_waiting->TIOCOUTQ(:755);break_condition->TIOCSBRK/TIOCCBRK(:300-301); esptool reset sequences ->TIOCMSET/TIOCMGET(esptool/reset.py:22-25); glibc/musltcdrain()->TCSBRK 1; Pythontermios.tcflushon the controlling tty fails today for any guest.Change
linux-wire.h:LINUX_TCSBRK/TCXONC/TCFLSH/TIOCEXCL/TIOCNXCL/TIOCOUTQ/TCSBRKP/TIOCSBRK/TIOCCBRK/TIOCMGET/TIOCMBIS/TIOCMBIC/TIOCMSET, values fromasm-generic/ioctls.h(same on aarch64 and x86_64).io.csys_ioctl: arms routing totcdrain/tcsendbreak/tcflush/tcflowand hostioctl(TIOCOUTQ|TIOCEXCL|TIOCNXCL|TIOCSBRK|TIOCCBRK|TIOCM*).tests/test-pty.c:tcflush/tcdrain/tcflowon the slave;TCFLSH(3)->EINVAL;TIOCOUTQ;TIOCEXCL/TIOCNXCL;TIOCMGETon a pty isENOTTY.Evidence
macOS pty, glibc 2.41 guest: before vs after
before:
after:
real hardware: ESP32-S3 (/dev/cu.usbmodem101), reproducer before vs after
before:
after:
Linux esptool 5.3.1 (python:3.12-slim arm64 sysroot) on the ESP32-S3: before vs after
before (dies inside
Serial.open()):after (
--before usb-reset; see note):--before usb-resetis needed because esptool picks its reset strategy from the port's VID/PID, which pyserial on Linux reads from/sys/class/tty/<name>/device/...(serial/tools/list_ports_linux.py:30-53); that sysfs view is piece 3 in #310, not this PR.Tests
build/elfuse build/test-pty: 70 passed, 0 failed (on top of Honor CBAUD/CIBAUD in TCGETS, TCGETS2 and TCSETS #312, rebased onto current main)make check ...: rc=0,All 77 tests passedclang-format --dry-run -WerrorcleanNot in this PR
TIOCGSERIAL/TIOCSSERIALstayENOTTY(no Darwin equivalent; pyserial only uses them in the opt-inset_low_latency_mode()).TCSBRKPduration is approximated: Darwintcsendbreak()ignores the duration (fixed ~0.4 s) vs Linuxarg*100ms./dev/ttyUSB*,/dev/ttyACM*) and the/sys/class/ttywalk -> piece 3 in Map Linux USB-serial device nodes (/dev/ttyUSB*, /dev/ttyACM*) onto macOS /dev/cu.* and translate the serial termios/modem ioctls #310.Summary by cubic
Translate Linux serial line-control and modem-control ioctls to Darwin so passthrough ttys work with pyserial/esptool/picocom and any program calling tcflush()/tcdrain(). Previously these ioctls returned ENOTTY; now they route to tcdrain/tcsendbreak/tcflush/tcflow and
TIOC*equivalents with Linux-matching behavior where it affects semantics.TIOCOUTQin short slices and return plain -EINTR (no restart), avoiding vCPU hangs in macOS tcdrain(); documented inscripts/check-eintr-contract.py. Only TCSBRK arg 0 sends break; nonzero drains.TCFLSH(0..2 ->TCIFLUSH/TCOFLUSH/TCIOFLUSH) andTCXONC(0..3 ->TCOOFF/TCOON/TCIOFF/TCION) are renumbered to Darwin’s 1-based values; out-of-range -> EINVAL.TCSBRKPduration is accepted but approximated by Darwin.TIOCMGET/TIOCMBIS/TIOCMBICforwarded;TIOCMSETemulated with BIS/BIC so only masked bits change. Mask to DTR|RTS (Darwin lacks OUT1/OUT2/LOOP). Bit values are identical; guest 32-bit ints preserved. ptys continue to return ENOTTY.TIOCOUTQandTIOCEXCL/TIOCNXCLforwarded. New request numbers added insrc/syscall/linux-wire.h. Tests intests/test-pty.ccover flush/drain/flow, selector validation,TIOCOUTQ, exclusivity, and pty ENOTTY.Written for commit 79d3e0a. Summary will update on new commits.