From e4317d71f3cf1997d1a7d3cf014157b9be87d63c Mon Sep 17 00:00:00 2001 From: Juan Hoyos <19413848+hoyosjs@users.noreply.github.com> Date: Fri, 24 Jul 2026 18:52:52 -0700 Subject: [PATCH] Harden dbgutil Windows PE export walk against untrusted image data The Windows-host PE export-directory walk in dbgutil's `TryGetSymbol` parses an image's export table using data read from an `ICorDebugDataTarget`. All of that data must be treated as untrusted: it can come from a corrupt/unmapped data in the target process. Zero out the initial value defensively, be defensive about modules with no export directory to about interpretting data that's not present, and ensure there's enough ordinal exports to support the request. --- src/coreclr/debug/dbgutil/dbgutil.cpp | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/coreclr/debug/dbgutil/dbgutil.cpp b/src/coreclr/debug/dbgutil/dbgutil.cpp index 93011b9d1fe54b..04f77f4d9ca23e 100644 --- a/src/coreclr/debug/dbgutil/dbgutil.cpp +++ b/src/coreclr/debug/dbgutil/dbgutil.cpp @@ -446,12 +446,21 @@ HRESULT ReadFromDataTarget(ICorDebugDataTarget* pDataTarget, extern "C" bool TryGetSymbol(ICorDebugDataTarget* dataTarget, uint64_t baseAddress, const char* symbolName, uint64_t* symbolAddress) { + *symbolAddress = 0; + DWORD exportTableRva; if (FAILED(GetMachineAndDirectoryAddress(dataTarget, baseAddress, IMAGE_DIRECTORY_ENTRY_EXPORT, nullptr, &exportTableRva))) { return false; } + // A module with no export directory reports a zero RVA here; there is nothing to search and + // reading at baseAddress would parse the PE headers as an IMAGE_EXPORT_DIRECTORY. + if (exportTableRva == 0) + { + return false; + } + // Manually read the export directory from the target to find the requested symbol. IMAGE_EXPORT_DIRECTORY exportDir{}; if (FAILED(ReadFromDataTarget(dataTarget, baseAddress + exportTableRva, (BYTE*)&exportDir, sizeof(exportDir)))) @@ -460,6 +469,7 @@ TryGetSymbol(ICorDebugDataTarget* dataTarget, uint64_t baseAddress, const char* } uint32_t namePointerCount = VAL32(exportDir.NumberOfNames); + uint32_t exportAddressCount = VAL32(exportDir.NumberOfFunctions); uint32_t addressTableRVA = VAL32(exportDir.AddressOfFunctions); uint32_t ordinalTableRVA = VAL32(exportDir.AddressOfNameOrdinals); uint32_t nameTableRVA = VAL32(exportDir.AddressOfNames); @@ -481,7 +491,7 @@ TryGetSymbol(ICorDebugDataTarget* dataTarget, uint64_t baseAddress, const char* } // Allocate a buffer for the memory that we'll read out of the target image. // We can allocate as much space as the target symbol name as we gracefully handle reading - // inaccessable memory. + // inaccessible memory. std::unique_ptr namePointer(new char[symbolNameLength + 1]); // If we fail to read the memory or the name doesn't match, then we don't have the right export. if (SUCCEEDED(ReadFromDataTarget(dataTarget, baseAddress + namePointerRVA, (BYTE*)namePointer.get(), (UINT32)symbolNameLength + 1)) @@ -493,6 +503,12 @@ TryGetSymbol(ICorDebugDataTarget* dataTarget, uint64_t baseAddress, const char* { return false; } + // The ordinal indexes the export address table; reject an out-of-range value from + // untrusted image data before using it to compute a read offset. + if (ordinalForNamedExport >= exportAddressCount) + { + return false; + } // If the name matches, we should be able to get the export uint32_t exportRVA = 0; if (FAILED(ReadFromDataTarget(dataTarget, baseAddress + addressTableRVA + sizeof(uint32_t) * ordinalForNamedExport, (BYTE*)&exportRVA, sizeof(exportRVA)))) @@ -505,7 +521,6 @@ TryGetSymbol(ICorDebugDataTarget* dataTarget, uint64_t baseAddress, const char* } } - *symbolAddress = 0; return false; } #endif