Skip to content

Fix multiple PE/COFF parsing bugs (#1502) - #8538

Open
Weitao-Sun wants to merge 4 commits into
devfrom
test_peparsing_bug_fix
Open

Fix multiple PE/COFF parsing bugs (#1502)#8538
Weitao-Sun wants to merge 4 commits into
devfrom
test_peparsing_bug_fix

Conversation

@Weitao-Sun

Copy link
Copy Markdown
Contributor

Fixes three PE/COFF parsing issues reported in Vector35/binaryninja#1502 (referenced there as Ghidra #9169, #9170, #9171):

  • COFF symbol table OOM via unbounded symbol count and string names
  • PointerToRawData not rounded to 0x200 per Windows loader behavior
  • SizeOfRawData not rounded up to FileAlignment, hiding code caves

This is the api-repo counterpart of Vector35/binaryninja#1625, which will be updated to just bump the api submodule pointer once this merges.

Review feedback addressed

An earlier version of this change went through a round of review. Summary of what changed as a result:

  1. virtualSize is grown to max(virtualSize, sizeOfRawData) so segments, sections, RVA characteristics, and symbol placement all agree with the corrected raw-data extent.
  2. The symbol-name budget is checked before each string-table read instead of after, names are cached by string-table offset so repeated offsets are read once, and the running total accounts for the extra copies a resolved name ends up retained in.
  3. The aligned raw-data size is clamped to UINT32_MAX before narrowing to uint32_t.
  4. coffSymbolCount * sizeofCOFFSymbol arithmetic is widened to 64-bit in both files, and the COFF section-name-resolution path uses the runtime symbol-record size instead of a hardcoded 18.
  5. Section-name and symbol-name string table offsets are validated against the declared table length before being read.
  6. All six loader settings (PE + COFF, count / name-length / total-budget) now treat 0 as uncapped.
  7. Magic numbers moved into named constants in peview.h, shared by both files.
  8. header.coffSymbolCount is no longer mutated. Every raw symbol-table slot still gets a data variable and marker symbol, which relocation resolution looks up by address; the configurable count limit only gates the more expensive per-symbol work (name resolution, typing, aux record definitions).

Follow-up round

A second look raised four more points:

  • Sector rounding was unconditional and didn't account for low-alignment PE images. Fixed in the latest commit: when SectionAlignment < page size and SectionAlignment == FileAlignment (the PE spec's low-alignment case), raw offsets map directly to RVAs without sector rounding. Checked against a synthetic low-alignment image (SectionAlignment = FileAlignment = 0x20, PointerToRawData = 0x220) — the section's file offset and content now match the declared layout instead of getting rounded down to 0x200.
  • Symbol-count limits discarding valid relocations: with maxCoffSymbolCount = 1, 2 symbols, and a relocation referencing symbol index 1, the marker for index 1 is still created and the relocation resolves — this is what point 8 above covers.
  • Rounded raw bytes ending up outside the mapped section: covered by the virtualSize = max(virtualSize, sizeOfRawData) change above.
  • Raw-size rounding wrapping past 4 GiB to zero: the clamp to UINT32_MAX happens before narrowing (point 3 above), so this doesn't wrap.

If any of the last three still show up against this branch's current tip, a repro would help track it down further.

Known follow-up (not blocking)

The per-slot marker-symbol loop in the COFF symbol table walk (point 8) still iterates the full declared coffSymbolCount even when the count limit caps the heavier per-symbol work, so a very large declared count still costs a linear pass just to place markers. A bound derived from the file's actual size (a legitimate count can't exceed what fits in the remaining file) would close this without reintroducing the relocation-lookup issue point 8 fixed. Can follow up separately if useful.

Testing

This changes PE section-layout logic, so part of the local analysis-output test suite differs from its stored oracles — expected, since section boundaries legitimately shift for several PE test corpus binaries. The full suite gives 81 differences against this branch; running the same suite against plain dev (no commits from this branch) narrows it down:

  • 23 already show up on plain dev — unrelated to this change (test_Architecture, test_memory_leaks, test_struct_return_win32/win64, PDB tests, plus a handful of non-layout tests on acpipagr.sys/acpitime.sys).
  • 58 are specific to this branch, on the PE/COFF corpus binaries the change touches (acpipagr.sys, acpitime.sys, jumptable_multiple_indirect, partial_register_dataflow(_rebasing), pe_thumb, quick3dcoreplugin.dll, win32_x86.dll, win32_x86_warp.dll). Same set with or without the latest (low-alignment) commit. These are the layout-shift kind — uniform address/length changes and/or new, well-formed content in the newly-exposed byte range.

Will regenerate oracles for the 58 once the code here settles, scoped to just those binaries.

🤖 Generated with Claude Code

Weitao-Sun and others added 3 commits September 9, 2026 16:19
Bug #9169 - COFF symbol table entries (peview.cpp + coffview.cpp):
- Cap section name string table reads at 1024 bytes per name
- Validate section- and symbol-name string table offsets against the
  declared string table size before reading, and use 64-bit arithmetic
  (and the actual per-record size in coffview.cpp) when computing string
  table offsets
- Add a configurable limit (default 1M) on how many COFF symbol table
  entries receive full name resolution, typing, and aux record
  definitions; every table slot still gets a data variable and marker
  symbol in coffview.cpp so relocations can resolve any symbol index the
  file declares
- Add a per-name length cap (default 32 KB) via ReadCString(maxSymNameLen)
- Add a total budget (default 1 GB) on accumulated symbol name bytes,
  weighted for the additional copies a resolved name ends up retained in
  once a symbol is created for it; cache names by string table offset so
  repeated offsets are read and counted once
- Expose all limits as user-configurable loader settings (loader.pe.* and
  loader.coff.*), each accepting 0 to disable the corresponding check

Bug #9170 - PointerToRawData not 0x200-aligned (peview.cpp):
- For PE32/PE32+, always round PointerToRawData DOWN to the nearest 0x200
  boundary, matching the Windows loader's hardcoded behaviour regardless of
  the FileAlignment field value

Bug #9171 - SizeOfRawData not rounded to FileAlignment (peview.cpp):
- For PE32/PE32+, round SizeOfRawData UP to the next FileAlignment multiple
  so that bytes between the declared raw size and the alignment boundary
  are included in the binary view, matching what Windows maps; cap the
  result at the remaining bytes in the file, clamping before narrowing to
  the 32-bit section field
- Use uint64_t arithmetic for the rounding to prevent overflow when
  sizeOfRawData is near UINT32_MAX

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Segment/section creation and RVA characteristics/symbol placement bound a
section's length using two different fields: file-backed reads use
sizeOfRawData, while everything else uses virtualSize. When a section's
raw data extends past its declared virtual size, the extra bytes end up
readable via RVA-to-file-offset translation but outside the mapped
segment, section, and symbol range. Grow virtualSize to cover sizeOfRawData
so all of these stay consistent. This applies regardless of PE32/PE32+
magic, since it isn't describing loader-specific alignment behavior, just
keeping the view's own internal bookkeeping self-consistent.
Section raw-data rounding to the 0x200 sector boundary only applies
to normally page-aligned images. Per the PE spec, when SectionAlignment
is below the architecture's page size, FileAlignment must equal
SectionAlignment and raw offsets map directly to RVAs without the
usual sector padding. Detect that case and skip both the
PointerToRawData and SizeOfRawData rounding for it, so low-alignment
images keep their declared file layout.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@Weitao-Sun
Weitao-Sun requested a review from zznop September 9, 2026 22:17
Comment thread view/pe/peview.cpp
Comment thread view/pe/coffview.cpp
Comment thread view/pe/peview.cpp
Comment thread view/pe/peview.cpp
… bounds

- Charge every retained COFF symbol name against the total-name-bytes
  budget, including cache hits.
- Resolve external symbol names on demand in the relocation loop for
  symbol table entries outside the per-file annotation limit, sharing
  the same budget and cache as the initial pass.
- Make RVAToFileOffset and GetRVACharacteristics prefer the most
  recently added section when sections overlap, matching the rest of
  the loader.
- Require the string table length to be at least 4 bytes, validate
  name offsets against the table bounds, and cap name reads to the
  bytes remaining in the table, across all three name lookups in these
  files (COFF symbol names, PE symbol names, COFF section names).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@Weitao-Sun

Copy link
Copy Markdown
Contributor Author

Fixed all four in bc9cad8: name-budget now charges every retained reference including cache hits; relocations to undefined-external symbols beyond the annotation limit are resolved on demand under the same budget; RVAToFileOffset/GetRVACharacteristics now keep the last matching section on overlap; string-table reads are bounds-checked and capped to the table's remaining length in all three lookup sites in these files (also found and fixed the same missing bounds check in the section-name lookup, which wasn't flagged but has the same issue).

@Weitao-Sun
Weitao-Sun requested a review from zznop September 10, 2026 22: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.

2 participants