mirror of
https://github.com/RhapsodiOS/RhapsodiOS.git
synced 2026-11-03 22:01:40 -07:00
[GH-ISSUE #30] drvPortServer: opening a data tty (/dev/cuaa, /dev/ttyda) crashes kernel-7 #26
Labels
No labels
bug
documentation
enhancement
enhancement
good first issue
help wanted
help wanted
pull-request
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
github-starred/RhapsodiOS#26
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Originally created by @raynorpat on GitHub (Sep 25, 2026).
Original GitHub issue: https://github.com/RhapsodiOS/RhapsodiOS/issues/30
Symptom
With the reconstructed PortServer driver loaded on kernel-7 (kernel-154.5.1-7 RELEASE_I386), the first
open()of a data tty takes the whole kernel down. Both/dev/cuaa(call-out) and/dev/ttyda(dial-in) do it, every time. ssh and the console stop answering, and nothing is printed.Apple's DR2
PortServer_relocon the same guest, with the same ISASerialPort (COM1, 0x3f8/IRQ 4), passes: bytes go both ways through/dev/cuaa.What works
driverLoader D=PortServerloads our driver. It registersPDPseudo,ttydaandpdservd, the same as Apple's.5eca8b43c) makes the same nodes for our driver as for Apple's:ttyda0,ttyida64,cuaa32,cuiaa96,pdservd192,rpski01-16193-208./dev/pdservdopens./dev/ttyida(initial-state device) opens, andtcgetattr/tcsetattron it work.So the crash is on the data-device open path:
ttyiops_acquireSession,ttyiops_init/ttyiops_param, the rx/txIOForkThreads, and the line discipline open.Evidence
Uninstrumented build. The CPU ends up at EIP
0x1din kernel CS (segment base 0xc0000000) in a trap-7 (#NM) fault loop: the frame at0xe7cdfff0is7, 0, 0x1d, 0x8, 0x10202. All general registers are 0 exceptEDX=0x2015c804, andESP=0x27ce0004, four bytes past the top of an 8 KB kernel stack. That is the shape of a thread entering at EIP 0 on a fresh stack; the "instructions" from 0 to 0x1d are the real-mode IVT. Identical registers on two runs.With
IOLogprobes inttyiops_openand the rx/tx threads, the open itself completes, then the kernel panicsthread_dispatchinstead:thread_dispatch(kern/sched_prim.c:1030) panics when the outgoing thread's state is not one of the legal combinations, so something has left a thread's scheduler state inconsistent. The -714 isIO_R_IO;ttyiops.mcalls-0x2ca"interrupted" in several comments, which it isn't.Ruled out so far
IOForkThread/ioThreadStart: a thread function that returns falls intoIOExitThread()(driverkit-3/libDriver/Kernel/generalFuncsPrivate.m), not EIP 0. Bothttyiops_rxFuncandttyiops_txFuncend inIOExitThread()anyway.thread_sleep,tsleepandthread_block()in kernel-7 all block with a NULL continuation, andsimple_lockis live in driverkit kernels (MACH_SLOCKSincludesDRIVERKIT). Apple's binary imports the same_thread_sleep,_thread_wait_resultand_thread_wakeup_prim.initForDevice:) fills its IMP slots in the order the accessors read them.Next
Compare the functions on this path against the reference; several were never re-verified after the fix pass (
reconstruction/ledger.json):ttyiops_acquireSession(unexamined),ttyiops_waitForDCD(unexamined),ttyiops_init(intentional mismatch),-[IOPortSession acquirePort:sleep:],watchState:mask:, andAppleIOPSSafeCondLock'slock/unlockWith:/lockWhen:. The tx thread'swatchState:mask:returning at once with the state unchanged looks like the first wrong turn.Reproduction: private i386 QEMU guest from
rhap-i386-bootstrapped.img, COM1 as-serial tcp:..., ourPortServer.configinstalled in/usr/Devices,driverLoader D=PortServer, then open/dev/cuaawithO_RDWR|O_NONBLOCK.@raynorpat commented on GitHub (Sep 26, 2026):
Root cause: two reconstruction defects in
ttyiops.m1.
ttyiops_convertFlowCtrloverwrote its own saved frame pointer. It passedunsigned char response[2](at[ebp-2]) torequestEvent:data:, which stores 32 bits. The upper two bytes of that store landed on the low half of the saved EBP, so0x27d63e04came back as0x27d60000.ttyiops_ioctlthen ran with a frame pointer 16 KB below its stack. That explains every variant in the report: an epilogue returning through zeroed memory to EIP 0, or writes through[ebp-n]into another thread's kernel stack, which gavethread_dispatch, the suspended idle thread, "assert_wait: already asserted", and a double fault. Apple's code uses a 4-byte slot at[ebp-4].TIOCGETAand everyTIOCSETA*go through it.Caught with QEMU's gdbstub: a hardware breakpoint at the return of every call in
ttyiops_ioctl, comparing EBP with its value at entry.convertFlowCtrlwas the only call that returned it changed, to exactly0x27d60000. The ledger had this function asunexamined, and the reconstruction's built-binary length table showed it as equal length, which a[ebp-2]versus[ebp-4]difference doesn't change.2.
ttyiops_txloadsent a selector that doesn't exist. With (1) fixed, the first write panicked withIOPortSession does not recognize selector -enqueueData:bufferSize:transferCount:minCount:. Apple's code sendsenqueueData:bufferSize:transferCount:sleep:with the same four arguments. It was the only selector our binary sent that Apple's doesn't: 65 against 64. The ledger had called this functionassembly-matched.Fix and verification
6619654c2(buffer is now anunsigned int) and69a5e1871(selector), onclaude/vigilant-franklin-c9fa40.On the i386 QEMU guest under kernel-7, driverLoader loads the driver and runs our pdservd, which makes the usual nodes. A test program sets raw 9600 8N1 with CLOCAL, writes a line and reads the reply. Bytes went both ways three times in a row: through
/dev/cuaa, through/dev/ttyda, and through/dev/cuaaagain after close and reopen. The guest stayed up with a clean console.convertFlowCtrlis now instruction-identical to the reference, making 45 of 113 functions identical. Details are insrc/drvPortServer/reconstruction/divergences.md§23 (Findings 90 and 91).