Description
Environment
- ReactOS 0.4.17-dev, revision 023d8af5566 (master), AMD64, GCC/MinGW-w64 build, CMAKE_BUILD_TYPE=Debug and DBG=1 (bootcd)
- RosBE 2.3.0 Beta / GCC 13.2.0 (x86_64-w64-mingw32-gcc) with sdk/tools/gcc_plugin_seh enabled
- Host: Windows + VirtualBox 7.2.16; guest type Windows 2003 (64-bit), PIIX4 IDE controller with two DVD drives (IDE-0-0 = bootcd.iso, IDE-0-1 = a second data disc)
- The branch fixes two x64 PSEH2 defects, described below; the second one has to be fixed before the first one becomes reachable at all
Steps to reproduce
- Boot the debug build, log in and let My Computer enumerate both CD/DVD drives.
- Insert a data disc in the secondary drive and open it once, so CDFS has mounted and cached the volume.
- With the Explorer window showing D: still open, eject the disc from the virtual drive:
VBoxManage storageattach <vm> --storagectl IDE --port 0 --device 1 --type dvddrive --medium emptydrive
|
(In the GUI: Devices -> Optical Drives -> "Remove disk from virtual drive".)
- Refresh the drive in the window (F5), or run "dir d:\" from a command prompt.
Expected behavior
The read fails with a normal error (STATUS_VERIFY_REQUIRED handled by CdProcessException / CdPerformVerify -> IoVerifyVolume, i.e. "The device is not ready"), the drive is shown as empty and the system stays responsive - as it does on Windows.
Actual behavior
The whole system freezes. On a debug build the Kernel Debugger comes up on an embedded INT3 inside KiDoubleFaultAbort (ntoskrnl/ke/amd64/trap.S:343):
MMFAULT addr 00000000000196a9 code 2
|
rip fffff880`74dd86a7 rsp fffff880`73537658
|
base fffff880`73539000 limit fffff880`73532000
|
Entered debugger on embedded INT3 at 0x0010:0xFFFFF80000403F83
|
cdfs.sys is loaded at fffff880`74dbf000, so the faulting RIP is RVA 0x196a7, which is the symbol _seh2$$filter_214 - the PSEH2 filter funclet belonging to CdPerformVerify (drivers/filesystems/cdfs/verfysup.c). The instruction that faulted is an ordinary frame-relative spill emitted by GCC at the funclet entry:
mov %r15,0x10(%rbp) ; %rbp = 0x19699 here - not a stack address
|
%rbp holds the leftover value 0x19699 (a code-like RVA), not the frame of CdPerformVerify, so the store goes to a wild address. The resulting page fault is taken while an exception is already being dispatched, so the dispatcher faults again while unwinding and re-enters itself:
KiPageFault -> InternalDispatchException -> KiDispatchException -> RtlDispatchException
|
-> RtlpUnwindInternal -> RtlVirtualUnwind -> RtlpExecuteHandlerForUnwind
|
-> SetRegFromStackValue -> KiPageFault -> ...
|
The cycle repeats and consumes kernel stack on every iteration (there were still ~24 KB free when CdVerifyVolume ran, so this is not a plain stack overflow to begin with), until the guard page is reached and the fault taken inside the dispatcher is reported as a double fault.
Root cause
On x64 PSEH2 declares the filter as a label inside the guarded function (sdk/lib/pseh/include/pseh/pseh2_64.h:78):
"\t__seh2$$filter__" #Line "=%l2\n" /* Filter function */
|
and _SEH2_TRY / _SEH2_EXCEPT reach it with a jmp through the global filter trampoline. The funclet is therefore not entered by the unwinder - which on other platforms/compilers gives funclet code its own frame or a defined register setup - but by a direct call, so it inherits the caller's register state. GCC compiles it as ordinary code of the enclosing function and is free to access the frame through %rbp at that label.
__C_specific_handler (sdk/lib/crt/except/amd64/ehandler.c) used to call the filter and the termination handler directly:
/* before */
|
FilterResult = ExceptionFilter(&ExceptionPointers, EstablisherFrame);
|
which leaves %rbp at whatever the exception dispatcher happened to have - exactly the value the funclet must not see.
This is not specific to CDFS: in cdfs.sys alone, 13 of the 44 _seh2$$filter_* funclets begin with an [%rbp +/- n] access instead of mov %rbp,%rax. Any x64 module built with GCC + PSEH2 can therefore corrupt memory the first time such a filter runs; the CDFS volume verify path just makes it reachable by simply ejecting a disc.
Fix (first defect)
Call filter and termination handler funclets through a small assembly helper that sets %rbp to the establisher frame for the duration of the call and restores the caller's value afterwards. Fixed in PR <PR link>, 3 files (1 new):
- sdk/lib/crt/except/amd64/callfilter.s (new) - __C_specific_handler_call_filter(Filter, Argument, EstablisherFrame)
- sdk/lib/crt/except/amd64/ehandler.c - use it for both the filter and the termination handler
- sdk/lib/crt/except/except.cmake - build the new file into libcntpr, which is linked into both ntoskrnl and ntdll, so kernel mode and user mode are covered
Verified in the same VM with a clean build of the branch: after ejecting the disc, running "dir d:\" answers
Directory of d:\
|
File not found
|
the system stays responsive, no MMFAULT and no KDB, and re-inserting a disc makes the drive list its contents again.
Second defect fixed by the same branch: inverted SEH scope table intervals
The scope table records emitted by gcc_plugin_seh reference the local labels _seh2$$begin_try_ / _seh2$$end_try_ by their final addresses, and __C_specific_handler matches the faulting IP against the resulting [BeginAddress, EndAddress) interval. With GCC 13 at -O1, -freorder-blocks and -fivopts can move code across those labels so that the end label lands before the begin label, which makes the interval empty: the scope never matches and the handler is skipped.
CdFsdDispatch in cdfs.sys, for example:
12c7c: jmp 12e55 <__seh2$$begin_try__440> ; entry jumps to the out-of-line try block
|
12d00: call CdCommonCreate ; faulting IP = 0x12d05
|
12d06: <__seh2$$end_try__440> ; end label *before* the try body
|
12e55: <__seh2$$begin_try__440>
|
This emits Begin=0x2e55 / End=0x2d06, an empty interval, so the STATUS_VERIFY_REQUIRED raised by CdVerifyVcb -> RtlRaiseStatus is not caught at all and the ejection ends in an unhandled last-chance exception.
Fix: tag every function the plugin gives SEH to with optimize("-fno-reorder-blocks","-fno-ivopts") in sdk/tools/gcc_plugin_seh/main.cpp, so the emitted intervals stay canonical. Both fixes are part of the same PR.
Consequences of the two defects together, on the eject-and-refresh repro:
- both present -> the try scope is empty, _SEH2_EXCEPT(CdExceptionFilter) never runs, unhandled STATUS_VERIFY_REQUIRED (last-chance exception, KDB)
- only the scope table ordering fixed -> the filter does run and the system dies with the double fault described above
- both fixed -> the drive is reported as empty and the system stays responsive
Related issues
CORE-20316- PSEH: Problematic RegistrationFrame in GCC-compiled SEH test app (x64 PSEH2 ABI)CORE-14037- System unresponsive after ejecting CD manuallyCORE-15361- ReactOS "freezes" when you try to open a folder on the ejected disk