Skip to content

Process: correct a subtle runLoopSource lifetime issue #3125

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jan 19, 2022

Conversation

compnerd
Copy link
Member

@compnerd compnerd commented Jan 16, 2022

When a process is run with run, a monitoring thread is setup by means
of a socketpair. This socket is used to create a runloop source, which
will monitor the process for termination. waitUntilExit will monitor
the isRunning ivar which is set by the source to indicate the process
termination having been observed. We however would previously mark the
process as terminated before invalidating the runloop source to prevent
any new callouts on the source. However, the operation is multithreaded
the operation is not guaranteed to be serialized and the runloop source
may be nil'ed prior or during the invalidation which also releases as
the refcount is now 0. This would result in the runloopSource passed
to CFRunLoopSourceInvalidate being nil and breaking the contract of
the call or a UaF if the value is nil'ed during the execution. These
states have both been observed in practice. Reordering the isRunning
ivar assignment to occur after the invalidation ensures that the value
remains usable until we no longer reference it.

@compnerd compnerd requested review from pcbeard and millenomi January 16, 2022 19:54
@compnerd
Copy link
Member Author

@swift-ci please test

@compnerd compnerd marked this pull request as draft January 16, 2022 19:59
@compnerd
Copy link
Member Author

Converting to draft - I think that I hit the failure even with the check?

@compnerd
Copy link
Member Author

Okay, caught the thing under a debugger with the change - it reduces the frequency, but is more indicative of a data race - we now hit it as a UaF in __CFRunLoopSourceLock:

(2a10.2a68): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
ntdll!RtlpEnterCriticalSectionContended+0x2a9:
00007ff9`a6a3a039 4883781000      cmp     qword ptr [rax+10h],0 ds:feeefeee`feeefefe=????????????????
0:008> kv
 # Child-SP          RetAddr               : Args to Child                                                           : Call Site
00 000000ff`332ff100 00007ff9`a6a39d82     : 0000c52c`5958dedc 00000000`00000001 00007ff9`62b65860 000000ff`332ff218 : ntdll!RtlpEnterCriticalSectionContended+0x2a9
01 000000ff`332ff160 00007ff9`62c15765     : 000002a7`e39cf720 00007ff9`637fc9a3 00000000`000052ac 00000000`00002a10 : ntdll!RtlEnterCriticalSection+0x42
02 (Inline Function) --------`--------     : --------`-------- --------`-------- --------`-------- --------`-------- : Foundation!_CFRecursiveMutexLock+0x9 (Inline Function @ 00007ff9`62c15765) [S:\b\3\CoreFoundation.framework\PrivateHeaders\CFInternal.h @ 650] 
03 (Inline Function) --------`--------     : --------`-------- --------`-------- --------`-------- --------`-------- : Foundation!__CFRunLoopSourceLock+0xd (Inline Function @ 00007ff9`62c15765) [S:\SourceCache\swift-corelibs-foundation\CoreFoundation\RunLoop.subproj\CFRunLoop.c @ 1036] 
04 000000ff`332ff190 00007ff9`62b66b3c     : 00007ff9`62b65860 00000000`00003708 00000000`00000000 00000000`00003708 : Foundation!CFRunLoopSourceInvalidate+0x35 [S:\SourceCache\swift-corelibs-foundation\CoreFoundation\RunLoop.subproj\CFRunLoop.c @ 3885] 
05 000000ff`332ff1f0 00007ff9`62b65702     : 000002a7`e3575d00 00007ff9`62bc64ad 000002a7`dc17d970 00007ff9`62bceb0d : Foundation!$s10Foundation7ProcessC3runyyKFySo11CFSocketRefaSg_So0D12CallBackTypeVSo06CFDataE0aSgSVSgSvSgtcfU0_Tf4ndddn_n+0x21c [S:\SourceCache\swift-corelibs-foundation\Sources\Foundation\Process.swift @ 662] 
06 000000ff`332ff270 00007ff9`62c1dc14     : 00000000`00000000 00000000`00000000 ffffffff`ffffffff ffffffff`ffffffff : Foundation!$s10Foundation7ProcessC3runyyKFySo11CFSocketRefaSg_So0D12CallBackTypeVSo06CFDataE0aSgSVSgSvSgtcfU0_To+0x32 [S:\<compiler-generated> @ 0] 
07 000000ff`332ff2c0 00007ff9`62c1abbd     : 422d149a`0ba570a4 00000000`00000002 000002a7`e39c38d0 000002a7`e39c38d0 : Foundation!__CFSocketDoCallback+0x204 [S:\SourceCache\swift-corelibs-foundation\CoreFoundation\RunLoop.subproj\CFSocket.c @ 2076] 
08 000000ff`332ff360 00007ff9`62c179d1     : 00000000`00000000 00000000`00000000 00007ff9`97f44160 00000000`50800000 : Foundation!__CFSocketPerformV0+0x17d [S:\SourceCache\swift-corelibs-foundation\CoreFoundation\RunLoop.subproj\CFSocket.c @ 2152] 
09 000000ff`332ff3e0 00007ff9`62c178dd     : 00007ff9`62c177f0 00007ff9`62bc63dd 00000000`00002e88 000002a7`e39c38d0 : Foundation!__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__+0x11 [S:\SourceCache\swift-corelibs-foundation\CoreFoundation\RunLoop.subproj\CFRunLoop.c @ 1896] 
0a 000000ff`332ff410 00007ff9`62c173e0     : 00007ff9`a6a39d40 000002a7`df56cfe0 00007ff9`a6a39d01 00007ff9`a6a399e0 : Foundation!__CFRunLoopDoSource0+0x4d [S:\SourceCache\swift-corelibs-foundation\CoreFoundation\RunLoop.subproj\CFRunLoop.c @ 1938] 
0b 000000ff`332ff450 00007ff9`62c13039     : 000001b3`e1b33ac6 00007ff9`a6a39d40 000002a7`df56cff8 000002a7`df56cff8 : Foundation!__CFRunLoopDoSources0+0x90 [S:\SourceCache\swift-corelibs-foundation\CoreFoundation\RunLoop.subproj\CFRunLoop.c @ 1975] 
0c 000000ff`332ff4d0 00007ff9`62c12a02     : 00000000`00000001 000000ff`332ff810 000000ff`332ff6c8 00000000`00000001 : Foundation!__CFRunLoopRun+0x319 [S:\SourceCache\swift-corelibs-foundation\CoreFoundation\RunLoop.subproj\CFRunLoop.c @ 2732] 
0d 000000ff`332ff650 00007ff9`62b8b59f     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : Foundation!CFRunLoopRunSpecific+0x1a2 [S:\SourceCache\swift-corelibs-foundation\CoreFoundation\RunLoop.subproj\CFRunLoop.c @ 3026] 
0e (Inline Function) --------`--------     : --------`-------- --------`-------- --------`-------- --------`-------- : Foundation!Foundation::run+0xcc (Inline Function @ 00007ff9`62b8b59f) [S:\SourceCache\swift-corelibs-foundation\Sources\Foundation\RunLoop.swift @ 222] 
0f 000000ff`332ff700 00007ff9`62b6126d     : 00000000`00000000 000002a7`dc090000 000000ff`332ff790 00007ff9`62b61063 : Foundation!Foundation::run+0xdf [S:\SourceCache\swift-corelibs-foundation\Sources\Foundation\RunLoop.swift @ 204] 
10 000000ff`332ff770 00007ff9`62ba2ab5     : 00000000`00000000 00000000`00000000 e0000000`00000000 000002a7`dc12e1f0 : Foundation!$s10Foundation7ProcessC5setup33_05FBE738056ADAC488E6D2B534411459LLyyFZyyXEfU_yycfU_+0x2cd [S:\SourceCache\swift-corelibs-foundation\Sources\Foundation\Process.swift @ 255] 
11 000000ff`332ff850 00007ff9`62ba1d4b     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : Foundation!Foundation::Thread::main+0x15 [S:\SourceCache\swift-corelibs-foundation\Sources\Foundation\Thread.swift @ 270] 
12 000000ff`332ff880 00007ff9`62ba2a99     : 00000000`00000057 000002a7`dc17a870 00000000`00000000 00000000`00000000 : Foundation!Foundation::NSThreadStart+0xfb [S:\SourceCache\swift-corelibs-foundation\Sources\Foundation\Thread.swift @ 68] 
13 000000ff`332ff900 00007ff9`a3fb93cc     : 000002a7`dc13bd50 00000000`00000000 00000000`00000000 00000000`00000000 : Foundation!Foundation::NSThreadStart+0x9 [S:\<compiler-generated> @ 0] 
14 000000ff`332ff930 00007ff9`a56b1350     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ucrtbase!thread_start<unsigned int (__cdecl*)(void *),1>+0x4c
15 000000ff`332ff960 00007ff9`a6a71e78     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : KERNEL32!BaseThreadInitThunk+0x10
16 000000ff`332ff990 00000000`00000000     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!RtlUserThreadStart+0x28

We can see in CFRunLoopSourceInvalidate that the rls is partially free'd:

0:008> dt -r rls
Local var @ rsi Type __CFRunLoopSource*
   +0x000 _base            : __CFRuntimeBase
      +0x000 _cfisa           : 0x000002a7`e3a0dd10
      +0x008 _swift_rc        : 0x000002a7`e3a0d7e0
      +0x010 _cfinfoa         : 
   +0x018 _lock            : _RTL_CRITICAL_SECTION
      +0x000 DebugInfo        : 0xfeeefeee`feeefeee _RTL_CRITICAL_SECTION_DEBUG
         +0x000 Type             : ??
         +0x002 CreatorBackTraceIndex : ??
         +0x008 CriticalSection  : ???? 
         +0x010 ProcessLocksList : _LIST_ENTRY
         +0x020 EntryCount       : ??
         +0x024 ContentionCount  : ??
         +0x028 Flags            : ??
         +0x02c CreatorBackTraceIndexHigh : ??
         +0x02e SpareWORD        : ??
      +0x008 LockCount        : 0n-17891602
      +0x00c RecursionCount   : 0n-17891602
      +0x010 OwningThread     : 0xfeeefeee`feeefeee Void
      +0x018 LockSemaphore    : 0xfeeefeee`feeefeee Void
      +0x020 SpinCount        : 0xfeeefeee`feeefeee
   +0x040 _order           : 0n-76843841185972498
   +0x048 _signaledTime    : 
   +0x050 _runLoops        : 0xfeeefeee`feeefeee __CFBag
   +0x058 _context         : __CFRunLoopSource::<unnamed-tag>
      +0x000 version0         : CFRunLoopSourceContext
         +0x000 version          : 0n-76843841185972498
         +0x008 info             : 0xfeeefeee`feeefeee Void
         +0x010 retain           : 0xfeeefeee`feeefeee           void*  +feeefeeefeeefeee
         +0x018 release          : 0xfeeefeee`feeefeee           void  +feeefeeefeeefeee
         +0x020 copyDescription  : 0xfeeefeee`feeefeee           __CFString*  +feeefeeefeeefeee
         +0x028 equal            : 0xfeeefeee`feeefeee           bool  +feeefeeefeeefeee
         +0x030 hash             : 0xfeeefeee`feeefeee           unsigned int64  +feeefeeefeeefeee
         +0x038 schedule         : 0xfeeefeee`feeefeee           void  +feeefeeefeeefeee
         +0x040 cancel           : 0xfeeefeee`feeefeee           void  +feeefeeefeeefeee
         +0x048 perform          : 0xfeeefeee`feeefeee           void  +feeefeeefeeefeee
      +0x000 version1         : CFRunLoopSourceContext1
         +0x000 version          : 0n-76843841185972498
         +0x008 info             : 0xfeeefeee`feeefeee Void
         +0x010 retain           : 0xfeeefeee`feeefeee           void*  +feeefeeefeeefeee
         +0x018 release          : 0xfeeefeee`feeefeee           void  +feeefeeefeeefeee
         +0x020 copyDescription  : 0xfeeefeee`feeefeee           __CFString*  +feeefeeefeeefeee
         +0x028 equal            : 0xfeeefeee`feeefeee           bool  +feeefeeefeeefeee
         +0x030 hash             : 0xfeeefeee`feeefeee           unsigned int64  +feeefeeefeeefeee
         +0x038 getPort          : 0xfeeefeee`feeefeee           void*  +feeefeeefeeefeee
         +0x040 perform          : 0xfeeefeee`feeefeee           void  +feeefeeefeeefeee
Memory read error feeefeeefeeeff1c
0:008> dx rls->_lock.LockCount,x
rls->_lock.LockCount,x : 0xfeeefeee [Type: long]

Clearly rls has been freed at the point that we hit CFRunLoopSourceInvalidate - this seems to be a data race.

When a process is run with `run`, a monitoring thread is setup by means
of a socketpair.  This socket is used to create a runloop source, which
will monitor the process for termination.  `waitUntilExit` will monitor
the `isRunning` ivar which is set by the source to indicate the process
termination having been observed.  We however would previously mark the
process as terminated before invalidating the runloop source to prevent
any new callouts on the source. However, the operation is multithreaded
the operation is not guaranteed to be serialized and the runloop source
may be `nil`'ed prior or during the invalidation which also releases as
the refcount is now 0.  This would result in the `runloopSource` passed
to `CFRunLoopSourceInvalidate` being `nil` and breaking the contract of
the call or a UaF if the value is `nil`'ed during the execution.  These
states have both been observed in practice.  Reordering the `isRunning`
ivar assignment to occur after the invalidation ensures that the value
remains usable until we no longer reference it.
@compnerd compnerd changed the title Process: handle a possible case of a nil runloop source Process: correct a subtle runLoopSource lifetime issue Jan 17, 2022
@compnerd
Copy link
Member Author

@swift-ci please test

@compnerd compnerd marked this pull request as ready for review January 17, 2022 01:10
@compnerd
Copy link
Member Author

The failures seem unrelated - docc timeouts

@compnerd
Copy link
Member Author

@swift-ci please test

@pcbeard
Copy link
Contributor

pcbeard commented Jan 17, 2022

Alternative, which should fix both code paths:

#3126

@compnerd
Copy link
Member Author

I think that the re-ordering is actually correct and desirable (though retaining is also something we should do in addition to the re-ordering). The re-ordering ensures that we do not touch the monitor runloop source after we indicate the process termination. We can simply hoist that above the flagging. Refactoring to share the path seems reasonable as well, though, I'd love to see a more invasive refactoring to share most of the path for the monitor.

@pcbeard
Copy link
Contributor

pcbeard commented Jan 17, 2022

@swift-ci please test

@compnerd
Copy link
Member Author

@swift-ci please test

This fixes a data race between setting `process.isRunning = false`
and then calling `CFRunLoopSourceInvalidate(process.runLoopSource)`.
Process.waitUntilExit() may wake up and clear .runLoopSource which
results in a fairly common crash on Windows.

Put this logic in a common method to reduce drift between the
Darwin / Linux and Windows code paths.

Co-authored-by: Saleem Abdulrasool <[email protected]>
@compnerd
Copy link
Member Author

@swift-ci please test

1 similar comment
@compnerd
Copy link
Member Author

@swift-ci please test

@compnerd
Copy link
Member Author

@swift-ci please test Linux platform

@millenomi
Copy link
Contributor

LGTM. Merging if green.

Please create a PR against release/5.6 as well?

@compnerd
Copy link
Member Author

#3128 for 5.6

@compnerd
Copy link
Member Author

Please test with following PRs:
swiftlang/swift-docc#70

@swift-ci please test Linux platform

@compnerd compnerd merged commit aa44546 into swiftlang:main Jan 19, 2022
@compnerd compnerd deleted the sources branch January 19, 2022 06:03
Sign up for free to 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.

3 participants