-
Notifications
You must be signed in to change notification settings - Fork 5.4k
Description
The .NET DAC on Linux crashes diagnostic tools (SOS, dotnet-dump, ClrMD) when ISOSDacInterface methods receive certain benign or invalid inputs. On Windows these are caught by SEH and returned as error HRESULTs. On Linux there is no SEH, so they become SIGSEGV and kill the process.
We systematically tested every method on ISOSDacInterface through ISOSDacInterface16 with adversarial inputs and found 13 bugs: 12 SIGSEGV crashes and 1 hang.
Crashes found
ISOSDacInterface (base) -- 5 crashes in request.cpp:
GetAppDomainName(addr=0)-- null AppDomain pointer dereferenceTraverseVirtCallStubHeap(addr=garbage, callback=null)-- dereferences garbage domain addressTraverseVirtCallStubHeap(addr=valid, callback=null)-- calls through null callback pointerTraverseModuleMap(module=valid, callback=null)-- calls through null callback pointerGetRegisterName(idx=0, bufLen=2)-- writes full register name past end of caller's buffer (buffer overflow)
ISOSDacInterface7 -- 4 crashes in request.cpp:
GetPendingReJITID(md=0xFFFFFFFFFFFFFFFF)-- dereferences MaxValue as MethodDescGetReJITInformation(md=0xFFFFFFFFFFFFFFFF)-- sameGetProfilerModifiedILInformation(md=0xFFFFFFFFFFFFFFFF)-- sameGetMethodsWithProfilerModifiedIL(module=0xFFFFFFFFFFFFFFFF)-- dereferences MaxValue as Module
ISOSDacInterface8 -- 3 crashes in request.cpp:
GetFinalizationFillPointersSvr(heap=0xDEADBEEFCAFEBABE)-- resolves garbage as gc_heap pointerGetFinalizationFillPointersSvr(heap=0xFFFFFFFFFFFFFFFF)-- same with MaxValueGetAssemblyLoadContext(mt=0xFFFFFFFFFFFFFFFF)-- dereferences MaxValue as MethodTable
ISOSDacInterface3 -- 1 hang:
GetGCGlobalMechanisms-- writes 48 bytes (6 x size_t) into caller buffer, but IDL signature is justsize_t*with no count. If caller provides a smaller buffer, the write corrupts the stack causing undefined behavior (infinite loop on Linux).
Root cause categories
- Missing address validation (8 crashes): DAC dereferences CLRDATA_ADDRESS without validating it points to readable target-process memory.
- Missing null callback check (3 crashes): Traversal APIs call through function pointer without null-checking.
- Buffer overflow (1 crash): GetRegisterName uses wcscpy_s but ignores the buffer length parameter.
- Stack corruption (1 hang): GetGCGlobalMechanisms has no way to communicate required buffer size to callers.
Bonus: shadowed HRESULT bugs
Found 5 instances where HRESULT hr = S_OK; declared inside SOSDacEnter's EX_TRY block shadows the macro's outer hr. The return hr after SOSDacLeave always returns S_OK, silently swallowing errors. Affected: GetGenerationTable, GetFinalizationFillPointers, GetGenerationTableSvr, GetFinalizationFillPointersSvr, GetObjectComWrappersData.
Fixes
All 12 crashes and 5 shadowed HRESULT bugs are fixed in src/coreclr/debug/daccess/request.cpp. Forward-proofing for GetGCGlobalMechanisms buffer size is in src/coreclr/inc/dacprivate.h (RequestGlobal over-allocates to 256 entries). Caller-side hardening with input validation guards added in ClrMD (microsoft/clrmd).
Repro
Test suite at https://github.com/leculver/dac-tests -- 648 tests, standalone (no dependencies), runs against any Linux coredump + matching DAC.