Uploaded image for project: 'Core ReactOS'
  1. Core ReactOS
  2. CORE-20774

[CRT/x64] x64 PSEH2: inverted SEH scope tables and a stale RBP in filter funclets (system freezes after ejecting a CD)

    XMLWordPrintable

Details

    • Bug
    • Resolution: Unresolved
    • Major
    • None
    • None
    • None

    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

      1. Boot the debug build, log in and let My Computer enumerate both CD/DVD drives.
      2. Insert a data disc in the secondary drive and open it once, so CDFS has mounted and cached the volume.
      3. 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".)

      1. 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 manually
      • CORE-15361 - ReactOS "freezes" when you try to open a folder on the ejected disk

      Attachments

        Activity

          People

            Unassigned Unassigned
            Yausen Yausen
            Votes:
            0 Vote for this issue
            Watchers:
            1 Start watching this issue

            Dates

              Created:
              Updated: