Skip to content

Add COFF objects whose .bss has no bytes in the file - #184

Open
zardus wants to merge 5 commits into
masterfrom
feature/coff-bss
Open

Add COFF objects whose .bss has no bytes in the file#184
zardus wants to merge 5 commits into
masterfrom
feature/coff-bss

Conversation

@zardus

@zardus zardus commented Aug 18, 2026

Copy link
Copy Markdown
Member

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Problem

A COFF section that holds no bytes in the file states PointerToRawData 0 and
still states its length in SizeOfRawData. No object here has one. Scanning the
files tracked under tests/ at this pull request's merge base finds 8 COFF
objects and 0 sections shaped that way: both fauxware.obj builds put every
section in the file, and the hand-assembled coff_*.obj fixtures have no .bss
at all. So there is nothing here to test that placement against, nothing to test a
bound against -- SizeOfRawData is 32 bits wide and, for a section with no bytes
in the file, nothing else in the file bounds it -- and nothing to test the
distinction the format draws between a section that claims to be uninitialized
data and one that does not.

Root cause

The existing COFF objects were added for symbol and relocation coverage and were
compiled from sources with no uninitialised data, so the zero-offset section shape
never entered the repository at all.

Fix

Three objects, so the same scan at this head finds 11 COFF objects and 3 such
sections, two with IMAGE_SCN_CNT_UNINITIALIZED_DATA set and one with it clear.

tests/x86/coff_bss.obj, 580 bytes, .bss SizeOfRawData 0x1000 at
PointerToRawData 0 with the flag set -- larger than the whole file, so the span a
loader gives it covers the file header and the whole of .text.

tests/x86/coff_huge_bss.obj, 580 bytes, the same object with .bss stating
0x20000000: half a gigabyte of zero-filled space asked for by a file of a few
hundred bytes.

tests/x86/coff_bss_no_flag.obj, 120 bytes, states PointerToRawData 0 with
SizeOfRawData 0x4000000 while its characteristics mark it code rather than
uninitialized data. It is hand-assembled: a survey of 731,573 COFF objects found
1,480 sections with no bytes in the file and every one of them sets the
uninitialized-data flag, so nothing there produced this shape.

tests_src/coff/coff_bss.c and coff_huge_bss.c are the sources for the first
two; the second is the first with a buffer 0x20000 times the size and the matching
index mask, under a header comment of its own. Both are built by the same
i686-w64-mingw32-gcc line and both rebuild byte for byte from it. That line is
corrected here: it was missing -fzero-call-used-regs=used-gpr, so as recorded it
produced a different object. The committed coff_bss.obj is unchanged and always
was reproducible from the compiler its comment names.

coff_bss_no_flag.obj comes from tests_src/coff/build_coff_objects.py, whose
docstring now names this shape beside the others it owns, and whose writer gained
one optional section argument to express a section that states a size it does not
carry. The six objects that script already owned rebuild byte for byte.

Testing

Read back with cle at master, all three load through the COFF backend as X86
and each .bss is given a span starting at the image base, over .text:
[0x400000, 0x401000) for the first, [0x400000, 0x20400000) for the second out
of a 580-byte file, and [0x400000, 0x4400000) for the third out of 120 bytes.
angr/cle#764 consumes all three, one test per object, and those tests cannot run
without them.

This commit introduces a conflict with binaries 223: the two heads merged clean
before it, and now collide in one place, the module docstring of
build_coff_objects.py, where both pull requests describe what the script
assembles. Every code hunk merges clean -- both builder functions and both
OBJECTS entries. 223 rewrites that paragraph into two, so the resolution is to
take its two paragraphs and carry this clause into the first, not to concatenate
the two versions. Land 223 first and rebase this over it.

Validation: #184 (comment)

session: sharpen

@zardus

zardus commented Aug 27, 2026

Copy link
Copy Markdown
Member Author

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Validation record for head 8269ca79b61511e0ba80f8e639b8f59dd7a4ec66 against baseline 981989f741dad616983a685f66a5566d2aed5bcf. This is a fixture-only change, so the evidence is the objects read back out of the branch and a rebuild, rather than a test run.

  • tests/x86/coff_bss.obj — 580 bytes, sha256 8794bf26b7d4f8a476a7184d778c66df86240b14d752d60d10acf08a215e90b8, unchanged by this commit
  • tests/x86/coff_huge_bss.obj — 580 bytes, sha256 09deb513103e0a0268479df218e5bc9381b249c8e4d07d78c17e600840f7ff9b
  • tests/x86/coff_bss_no_flag.obj — 120 bytes, sha256 09f22396d03f87f23e6da0219d6cb7fa213d6bcbae675416bd3857ebe5eea7be
  • The first two: Machine 0x014C (IMAGE_FILE_MACHINE_I386), TimeDateStamp 0x00000000, 4 sections, no optional header, characteristics 0x0104, 15 symbols. Section table, name / SizeOfRawData / PointerToRawData / characteristics:
    • .text / 0x40 / 0xb4 / 0x60500020
    • .data / 0 / 0 / 0xC0300040
    • .bss / 0x1000 in coff_bss.obj and 0x20000000 in coff_huge_bss.obj / 0 / 0xC0600080
    • /4 (.rdata$zzz) / 0x14 / 0xf4 / 0x40300040
  • The third: 2 sections, 0 symbols, .text / 0x10 / 0x64 / 0x60000020 and .bss / 0x4000000 / 0 / 0x60000020. Its .bss characteristics equal its .text characteristics because CoffObjectWriter writes one value for every section; what matters for the consumer is that IMAGE_SCN_CNT_UNINITIALIZED_DATA is clear. It is hand-assembled: a survey of 731,573 COFF objects found 1,480 sections with no bytes in the file, and every one sets that flag, so nothing there produced this shape.
  • So two objects state PointerToRawData 0 with the uninitialized-data flag set and one with it clear, which is the distinction COFF: Give a section with no file bytes an address of its own cle#764 keys on
  • Shape scan over every tracked file under tests/, opened and magic-checked rather than filtered by name: at the baseline 1,068 files, 8 parse as COFF objects, 0 sections with PointerToRawData 0 and non-zero SizeOfRawData. At this head 1,071 files, 11 COFF objects, 3 such sections, one per new object. One file, tests/armel/fpijr_cortexm_console.bin, is rejected on both sides as a false candidate: its first two bytes read as a machine type and its declared section count does not fit the file. The object counts were derived by hand, after the scan script was found to skip objects with zero sections and so to drop tests/mips/coff_r4000.obj from both sides.
  • Both compiled objects rebuild byte for byte from their recorded commands — i686-w64-mingw32-gcc -c -O2 -fno-asynchronous-unwind-tables -fzero-call-used-regs=used-gpr <source>.c, with the i686-w64-mingw32 gcc 15.3.0 from the workspace nix store, into a scratch output path; cmp clean against both committed fixtures. That rebuild was run at 75729ada and is not repeated here: git diff --stat 75729ada 8269ca79 touches only tests_src/coff/build_coff_objects.py, so neither object nor the recorded command line changed
  • tests_src/coff/build_coff_objects.py reproduces all seven objects it owns, the six it already had included, leaving the working tree clean after a rerun

Correcting the recorded command, not the fixture. An earlier line here said coff_bss.obj rebuilds byte-identically, and it does. What was wrong was the command written in tests_src/coff/coff_bss.c, which omitted -fzero-call-used-regs=used-gpr; run as recorded it produced sha256 9e1199e76ce46df957cd92c84349d2e3a896f698eb22d9149e36c8e78477d547, differing from the committed object in 6 bytes. With the flag the recorded command reproduces 8794bf26… exactly. The comment is corrected in this commit and the object is untouched.

Correcting the survey total, 2026-09-06. That survey first reported 731,580 objects. It listed angr/binaries with find rather than git ls-files, so it also walked an untracked nested worktree inside that checkout — a second copy of the fixture tree — and counted every COFF object in it twice: 7 objects and 79 section headers that do not exist. The 1,480 sections and the claim about them are unaffected: none of them is in angr/binaries, which contributes no section with PointerToRawData 0 and a non-zero size. angr/cle#764's validation record carries the full correction. The shape scan bullet above reads its objects out of git at a revision rather than off the working tree, so it never saw that worktree and is unchanged.

The message of commit 8269ca79b61511e0ba80f8e639b8f59dd7a4ec66 carries the same superseded 731,580, and a commit message cannot be edited in place. The correct figure is 731,573.

Merge order. This commit introduces the conflict with angr/binaries 223, and did not inherit it: git merge-tree --write-tree af5978f6 5ef7fa13 is clean, and the same command against this head reports CONFLICT (content) in tests_src/coff/build_coff_objects.py. It is exactly one region, the module docstring, where both pull requests describe what the script assembles; every code hunk merges clean, both builder functions and both OBJECTS entries. 223 replaces that two-line paragraph with two paragraphs of its own, so the resolution is to keep 223's text and fold this clause into its first paragraph rather than to concatenate the two versions. Land 223 first and rebase this over it.

Caveats: no test was run here; the regressions that consume these objects live in angr/cle#764. The rebuild used the workspace's pinned toolchain, so it shows the recipe is faithful rather than that any gcc 15.3.0 build reproduces it.

@zardus

zardus commented Aug 28, 2026

Copy link
Copy Markdown
Member Author

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

The three objects read back with cle at master (2f7657fda2a657ec76a201d5c63245c6262f60b7),
against a checkout of angr/binaries at this pull request's merge base and against this
branch. The command is the same on both sides; BINARIES is the checkout under test.

the reproducer
import os
import struct

import cle

BIN = os.environ["BINARIES"]

for name in ("coff_bss.obj", "coff_huge_bss.obj", "coff_bss_no_flag.obj"):
    path = os.path.join(BIN, "tests", "x86", name)
    print(f"== {name}")
    try:
        data = open(path, "rb").read()
    except FileNotFoundError as error:
        print(f"   {type(error).__name__}: {error}")
        continue
    print(f"   file size: {len(data)} bytes")
    machine, nsec, _, _, _, optsz, _ = struct.unpack_from("<HHLLLHH", data, 0)
    print("   raw COFF section headers (name, SizeOfRawData, PointerToRawData, uninit flag):")
    for i in range(nsec):
        sec = struct.unpack_from("<8sLLLLLLHHL", data, 20 + optsz + 40 * i)
        print(f"     {sec[0].rstrip(chr(0).encode()).decode('latin1'):<10} "
              f"SizeOfRawData {sec[3]:#010x}  PointerToRawData {sec[4]:#010x}  "
              f"{'uninit' if sec[9] & 0x80 else '  --  '}")
    ld = cle.Loader(path, main_opts={"backend": "COFF"})
    print("   cle places them at:")
    for s in ld.main_object.sections:
        print(f"     {s.name:<10} vaddr {s.vaddr:#010x} memsize {s.memsize:#010x}")

Before — none of the three is in the repository:

angr/binaries at 981989f741dad616983a685f66a5566d2aed5bcf
== coff_bss.obj
   FileNotFoundError: [Errno 2] No such file or directory: 'binaries/tests/x86/coff_bss.obj'
== coff_huge_bss.obj
   FileNotFoundError: [Errno 2] No such file or directory: 'binaries/tests/x86/coff_huge_bss.obj'
== coff_bss_no_flag.obj
   FileNotFoundError: [Errno 2] No such file or directory: 'binaries/tests/x86/coff_bss_no_flag.obj'

After — all three load, and each .bss, which states PointerToRawData 0, is given a
span starting at the image base and running over .text. The last column of the header
dump is the IMAGE_SCN_CNT_UNINITIALIZED_DATA bit, set on the first two and clear on the
third, which is the distinction the consumer's fix turns on:

with this change
== coff_bss.obj
   file size: 580 bytes
   raw COFF section headers (name, SizeOfRawData, PointerToRawData, uninit flag):
     .text      SizeOfRawData 0x00000040  PointerToRawData 0x000000b4    --  
     .data      SizeOfRawData 0x00000000  PointerToRawData 0x00000000    --  
     .bss       SizeOfRawData 0x00001000  PointerToRawData 0x00000000  uninit
     /4         SizeOfRawData 0x00000014  PointerToRawData 0x000000f4    --  
   cle places them at:
     .text      vaddr 0x004000b4 memsize 0x00000040
     .data      vaddr 0x00400000 memsize 0x00000000
     .bss       vaddr 0x00400000 memsize 0x00001000
     .rdata$zzz vaddr 0x004000f4 memsize 0x00000014
== coff_huge_bss.obj
   file size: 580 bytes
   raw COFF section headers (name, SizeOfRawData, PointerToRawData, uninit flag):
     .text      SizeOfRawData 0x00000040  PointerToRawData 0x000000b4    --  
     .data      SizeOfRawData 0x00000000  PointerToRawData 0x00000000    --  
     .bss       SizeOfRawData 0x20000000  PointerToRawData 0x00000000  uninit
     /4         SizeOfRawData 0x00000014  PointerToRawData 0x000000f4    --  
   cle places them at:
     .text      vaddr 0x004000b4 memsize 0x00000040
     .data      vaddr 0x00400000 memsize 0x00000000
     .bss       vaddr 0x00400000 memsize 0x20000000
     .rdata$zzz vaddr 0x004000f4 memsize 0x00000014
== coff_bss_no_flag.obj
   file size: 120 bytes
   raw COFF section headers (name, SizeOfRawData, PointerToRawData, uninit flag):
     .text      SizeOfRawData 0x00000010  PointerToRawData 0x00000064    --  
     .bss       SizeOfRawData 0x04000000  PointerToRawData 0x00000000    --  
   cle places them at:
     .text      vaddr 0x00400064 memsize 0x00000010
     .bss       vaddr 0x00400000 memsize 0x04000000

A COFF section holding no bytes in the file states PointerToRawData 0 while
SizeOfRawData still states its length. No object here has one: both fauxware.obj
builds put every section in the file and the hand-assembled coff_*.obj fixtures
have no .bss, so cle's COFF loader has nothing to test that placement against.

tests/x86/coff_bss.obj is 580 bytes with a 0x1000-byte .bss and a 0x40-byte
.text at file offset 0xb4, so the .bss spans the file header and the whole of
.text. tests_src/coff/coff_bss.c is the source and records the command.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
zardus and others added 2 commits September 1, 2026 12:44
Brings the branch up to date with master so cle pull requests that resolve
this branch also get the mipsn32, ppc64, riscv64 UEFI and s390x fixtures
their tests load.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
SizeOfRawData is 32 bits wide, and for a section with no bytes in the file
nothing else in the file bounds it. coff_bss.obj states 0x1000 bytes, which a
loader can honour. Before this commit nothing here stated a size the file cannot
stand behind, and nothing had a section with no bytes in the file that does not
claim to be uninitialized data -- the two shapes a loader has to tell apart.

tests/x86/coff_huge_bss.obj is 580 bytes and its .bss states 0x20000000 at
PointerToRawData 0: half a gigabyte of zero fill asked for by a file of a few
hundred bytes. tests_src/coff/coff_huge_bss.c is coff_bss.c with a buffer
0x20000 times the size and the matching index mask, built by the same command
line, so the two objects differ in 20 bytes.

tests/x86/coff_bss_no_flag.obj is 120 bytes and states PointerToRawData 0 with
SizeOfRawData 0x4000000 while marking itself code rather than uninitialized
data. A survey of 731,580 COFF objects found 1,480 sections with no bytes in the
file and every one sets the uninitialized-data flag, so this shape is assembled
by build_coff_objects.py, whose docstring now names it alongside the others it
owns. Its writer gained one optional section argument to express a section that
states a size it does not carry; the six objects it already owned rebuild byte
for byte.

coff_bss.c's recorded command was missing -fzero-call-used-regs=used-gpr, so it
rebuilt to 9e1199e7... rather than the committed 8794bf26.... The command is
corrected here; the object is unchanged and was always reproducible from the
compiler the comment names.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@zardus zardus changed the title Add a COFF object whose .bss covers its own .text Add COFF objects whose .bss has no bytes in the file Sep 2, 2026
zardus and others added 2 commits September 3, 2026 17:03
cle's tests on master now load tests/aarch64/langdetect_go.macho and
tests/aarch64/relocatable_object.macho, which #193 and #224 added after this
branch was cut. angr/cle#764 and angr/cle#804 name this pull request in their
sync: lines, so CI checks this branch out instead of master; once either is
rebased onto current cle master those two files would be missing and the macOS
job would fail. Merging master in supplies them and leaves this branch's own
three objects and build script untouched.
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.

1 participant