Skip to content

leculver/dac-tests

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

DAC Interface Test Suite

Adversarial test harness for the .NET DAC (Data Access Component) on Linux. Systematically exercises every method on ISOSDacInterface through ISOSDacInterface16 with valid, null, garbage, and boundary inputs to find crash-inducing bugs.

648 tests covering all DAC interface methods.

Related issue: dotnet/runtime#124640

What This Is

The .NET DAC (libmscordaccore.so) is the component that diagnostic tools like SOS, dotnet-dump, and ClrMD use to inspect crash dumps and live processes. It exposes a set of COM interfaces (ISOSDacInterface through ISOSDacInterface16) that return information about the CLR's internal state: threads, GC heaps, method tables, modules, assemblies, and more.

On Windows, the DAC runs inside a structured exception handling (SEH) context that catches access violations and converts them to error HRESULTs. On Linux, there is no SEH -- an access violation becomes a SIGSEGV that kills the entire diagnostic tool process. This means input validation bugs that are silent on Windows become fatal crashes on Linux.

This test suite was built to find those bugs by calling every DAC method with inputs designed to trigger missing validation: null pointers, garbage addresses, boundary values like ulong.MaxValue, misaligned pointers, and cross-type confusion (passing a GC heap address where a module address is expected).

Architecture

The test harness is entirely standalone with no ClrMD dependency. It implements its own infrastructure from scratch:

dac-tests/
  src/
    CoreReader/         # Standalone ELF core file reader
      ElfReader.cs      #   ELF header and program header parsing
      ElfStructs.cs     #   ELF64 structure definitions
      CoreDumpReader.cs #   PT_NOTE parsing (threads, aux vector, mapped files)
      MemoryReader.cs   #   Virtual address to file offset mapping, ReadMemory API
    DacInterop/         # COM interop layer for the DAC
      ComHelpers.cs     #   COM vtable construction, QI/AddRef/Release
      DataTarget.cs     #   ICLRDataTarget/2/3 + IXCLRDataTarget3 backed by CoreReader
      DacLoader.cs      #   Loads libmscordaccore.so, calls CLRDataCreateInstance
      Interfaces/       #   One wrapper file per interface version
        DacStructs.cs   #     All Dacp*Data structs matching dacprivate.h layouts
        ISOSDacInterface.cs through ISOSDacInterface16.cs
        IXCLRDataProcess.cs
  tests/
    DacTests/
      DacTestFixture.cs           # Shared xUnit fixture: loads dump + DAC once
      SmokeTests.cs               # End-to-end infrastructure validation
      ISOSDacInterfaceTests_Group1.cs through Group6.cs  # Base interface tests
      ISOSDacInterface2Tests.cs through ISOSDacInterface16Tests.cs
      CoreDumpReaderTests.cs      # Unit tests for the ELF reader
      ElfReaderTests.cs
      MemoryReaderTests.cs
      ComHelpersTests.cs
      DataTargetTests.cs
      DacStructSizeTests.cs       # Validates struct layouts match native sizes
  tools/
    CrashApp/           # Minimal .NET app that crashes to produce a coredump
  generate-coredump.sh  # Runs CrashApp with dump collection env vars
  run-tests.sh          # Test runner with crash dump collection enabled

CoreReader

A standalone ELF core file reader that parses:

  • ELF headers -- identifies the file as an ELF core dump, reads program header table location and count
  • Program headers -- iterates PT_LOAD segments to build the virtual memory map, PT_NOTE segments for metadata
  • PT_NOTE segments -- extracts NT_PRSTATUS (thread register contexts), NT_PRPSINFO (process info), NT_AUXV (auxiliary vector), and NT_FILE (mapped file list with paths)
  • Virtual memory mapping -- given a virtual address, finds the PT_LOAD segment containing it and translates to a file offset for reading

This replaces ClrMD's ElfCoreFile/CoredumpReader for test purposes. It is thread-safe (giant lock) and uses BinaryReader/Span for I/O.

DacInterop

A COM interop layer built with ComWrappers and [UnmanagedCallersOnly] static methods. The key components:

  • ComHelpers -- builds COM vtables manually with function pointers, implements QueryInterface/AddRef/Release, and provides ComInterfaceDispatch helpers for getting back to managed state from native callbacks
  • DataTarget -- implements the four callback interfaces the DAC calls into: ICLRDataTarget (ReadVirtual, GetMachineType, GetPointerSize, etc.), ICLRDataTarget2 (AllocVirtual/FreeVirtual), ICLRDataTarget3 (GetExceptionRecord/Context/ThreadID), and IXCLRDataTarget3 (GetMetaData). All backed by the CoreReader.
  • DacLoader -- uses NativeLibrary.Load to load libmscordaccore.so, gets the CLRDataCreateInstance export, calls it with our DataTarget to get IXCLRDataProcess, then QueryInterfaces for all ISOSDacInterface versions
  • Interface wrappers -- one file per interface version (ISOSDacInterface through ISOSDacInterface16 plus IXCLRDataProcess). Each defines the vtable layout as a struct of delegate* unmanaged[Stdcall] function pointers and provides thin managed wrappers. Struct definitions in DacStructs.cs match the native layouts from dacprivate.h exactly.

Test Infrastructure

  • CrashApp -- a minimal .NET console app that allocates various object types (strings, arrays, generic types), creates threads with interesting stacks, sets up static fields, then throws an unhandled exception to crash and produce a coredump
  • generate-coredump.sh -- configures .NET dump collection via environment variables (DOTNET_DbgEnableMiniDump=1, DOTNET_DbgMiniDumpType=4 for full dumps, DOTNET_EnableCrashReport=1) and runs CrashApp against the locally built runtime
  • DacTestFixture -- a shared xUnit class fixture that opens the test coredump, creates the DataTarget, loads the DAC, and QueryInterfaces for all interface versions. All test classes share this single fixture instance so the DAC is loaded once per test run.
  • run-tests.sh -- runs dotnet test with dump collection enabled so that if a DAC bug crashes the test process, a dump is produced for analysis

Test Methodology

Each DAC method is tested with multiple categories of input:

Category 1: Normal Usage

Call the API the way a real diagnostic tool would, using addresses discovered from prior API calls (e.g., get a MethodTable address from GetMethodTableData, pass it to GetMethodTableName). Validates that the method returns expected results.

Category 2: Null/Zero/Boundary Inputs

For every pointer parameter: test with 0 (null), ulong.MaxValue (0xFFFFFFFFFFFFFFFF). For every length parameter: test with 0 and -1. For every output pointer: test combinations of null output pointers. The API must return an error HRESULT (E_INVALIDARG, E_FAIL, etc.) but NOT crash.

Category 3: Garbage/Adversarial Addresses

For every target-process-pointer parameter:

  • Unallocated address: 0xDEAD_0000_BEEF_0000
  • Wild pointer: a valid address pointing to unrelated data (e.g., pass a GC heap address where a module address is expected)
  • Offset address: a real valid address + 0x16
  • Misaligned address: a real valid address + 0x15
  • Small address: 0x1000 (mapped but not a valid CLR structure)

The function must return an error HRESULT but NOT crash.

Crash/Hang Protocol

When a test crashes the process (SIGSEGV), a dump is produced in tmp/dac/. The test is annotated with the crashing input, native call stack, and root cause, then marked as skipped. When a test hangs (100% CPU), a dump is collected via dotnet-dump collect, analyzed with lldb+SOS to find the spinning thread, and similarly annotated and skipped.

Prerequisites

  • Linux x64
  • .NET SDK 10.0 or later
  • A locally built .NET runtime (for the DAC and test coredump)

Setup

  1. Build the .NET runtime:

    cd /path/to/runtime
    ./build.sh -s clr -c Release
  2. Generate a test coredump:

    ./generate-coredump.sh /path/to/runtime

    This produces a coredump in tmp/dac/ using the locally built runtime.

  3. Run the tests:

    ./run-tests.sh
    # Or directly:
    dotnet test dac-tests.sln

The test fixture auto-discovers the coredump in tmp/dac/ and the DAC from the runtime's testhost artifacts directory.

Results

Testing found 13 bugs (12 SIGSEGV crashes + 1 hang) across the DAC interface surface:

Interface Method Input Bug Type
ISOSDacInterface GetAppDomainName addr=0 Null dereference (SIGSEGV)
ISOSDacInterface TraverseVirtCallStubHeap garbage addr, null callback Null dereference (SIGSEGV)
ISOSDacInterface TraverseVirtCallStubHeap valid addr, null callback Null callback (SIGSEGV)
ISOSDacInterface TraverseModuleMap valid module, null callback Null callback (SIGSEGV)
ISOSDacInterface GetRegisterName idx=0, bufLen=2 Buffer overflow (SIGSEGV)
ISOSDacInterface3 GetGCGlobalMechanisms (no addr params) Stack corruption causing infinite loop
ISOSDacInterface7 GetPendingReJITID md=MaxValue Dereferences 0xFFFF...FFFF (SIGSEGV)
ISOSDacInterface7 GetReJITInformation md=MaxValue Dereferences 0xFFFF...FFFF (SIGSEGV)
ISOSDacInterface7 GetProfilerModifiedILInformation md=MaxValue Dereferences 0xFFFF...FFFF (SIGSEGV)
ISOSDacInterface7 GetMethodsWithProfilerModifiedIL module=MaxValue Dereferences 0xFFFF...FFFF (SIGSEGV)
ISOSDacInterface8 GetFinalizationFillPointersSvr heap=garbage Bad gc_heap pointer (SIGSEGV)
ISOSDacInterface8 GetFinalizationFillPointersSvr heap=MaxValue Bad gc_heap pointer (SIGSEGV)
ISOSDacInterface8 GetAssemblyLoadContext mt=MaxValue Bad MethodTable dereference (SIGSEGV)

Root Cause Categories

  1. Missing address validation (8 crashes) -- the DAC takes a CLRDATA_ADDRESS and dereferences it without checking if it points to valid target-process memory. On Windows, SEH catches the AV. On Linux, SIGSEGV kills the process.

  2. Missing null callback validation (3 crashes) -- traversal APIs accept a function pointer callback and call through it without null-checking.

  3. Buffer overflow (1 crash) -- GetRegisterName writes the full register name string without checking the caller's buffer size.

  4. Stack corruption / infinite loop (1 hang) -- GetGCGlobalMechanisms writes 6 size_t values (48 bytes) into the caller's buffer, but the IDL signature is just size_t* with no count parameter. If the caller provides a buffer smaller than 48 bytes, the write corrupts the stack frame, causing undefined behavior (infinite loop on Linux).

Interfaces That Passed Clean

Every method on these interfaces handled all adversarial inputs correctly with no crashes and no hangs:

  • ISOSDacInterface2, ISOSDacInterface4, ISOSDacInterface5, ISOSDacInterface6
  • ISOSDacInterface9 through ISOSDacInterface16

Bonus: Shadowed HRESULT Bugs

During fix development, 5 instances of a shadowed variable bug were found in the DAC source. The pattern:

SOSDacEnter();  // expands to: HRESULT hr = S_OK; EX_TRY {
HRESULT hr = S_OK;  // SHADOWS the macro's hr
// ... code that sets hr on error ...
SOSDacLeave();  // expands to: } EX_CATCH { ... } return hr;

The return hr after SOSDacLeave uses the outer (macro) hr which is always S_OK, silently swallowing errors. Found in: GetGenerationTable, GetFinalizationFillPointers, GetGenerationTableSvr, GetFinalizationFillPointersSvr, GetObjectComWrappersData.

Notable Implementation Details

  • CLRDataCreateInstance requires an ICLRDataTarget interface pointer (via Marshal.QueryInterface), not a raw IUnknown. Passing IUnknown causes a SIGSEGV because the DAC calls ICLRDataTarget vtable slots (3+) through the IUnknown pointer which only has 3 valid slots.
  • IXCLRDataTarget3 inherits ICLRDataTarget2 -- its vtable has 17 slots total, not just IUnknown's 3 plus GetMetaData.
  • GetMetaData has 9 parameters with mvid as Guid* (IntPtr), not uint. A signature mismatch here causes subtle memory corruption.
  • GetThreadpoolData returns E_NOTIMPL on modern .NET (managed threadpool). This is expected behavior, not a bug.
  • GetAssemblyList(SystemDomain) returns E_INVALIDARG because SystemDomain does not hold assemblies in .NET Core. Also expected.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •