Skip to content

tools: let a policy license the class data a promoted TU emits - #1986

Closed
andrewboudreau wants to merge 1 commit into
mainfrom
tools/deadstrip-data
Closed

tools: let a policy license the class data a promoted TU emits#1986
andrewboudreau wants to merge 1 commit into
mainfrom
tools/deadstrip-data

Conversation

@andrewboudreau

Copy link
Copy Markdown
Collaborator

Part 3 of the #1878 split, after #1977 (the duplicate-body policy) and #1979 (the
promotion that needed it). Tools only — no manifest row uses the new disposition yet,
so this build is expected to be byte-for-byte identical to main.

The problem

Promoting a TU consolidates N one-function objects into one, which takes objisolate's
intact multi-symbol path: nothing may survive that the manifest has not licensed,
and deadstrip_plan accepted STT_FUNC only. A class's _ZTV/_ZTI/_ZTS are
STT_OBJECT, so the only way to promote a TU was to strip the destructor that makes
mwcc emit them — silently trading away the one thing that makes a vtable comparable to
the cartridge at all.

Measured on ov100/daObjPathLift_c (the TU #1979 just promoted): 14 ROM-data records
to 0.
No gate reports this. rombuild's 106/106 is blind to vtables and
romdata_check is advisory, so every promotion so far has traded vtable coverage for
consolidation without anyone noticing — ActorBase_SceneNode, the first promoted TU,
emits no ROM-data records either.

Three pieces

1. deadstrip_plan accepts STT_OBJECT. Data goes to externalise, not dead
the destructor must go on storing the vptr — so a surviving section may reference one by
name, but only for the RTTI trio whose relocation shapes are surveyed, and never through
an unnamed section symbol.

mwcc's _ZTV<C> addresses the vtable object; symbols.txt's is the slot array.
So a vptr store carries either the 8-byte preamble skip (a definition in this object —
correct it) or nothing at all (the source declared the ROM symbol directly, as
daObjPathLift_c.cpp does today with extern int _ZTV10dBgActor_c[]; — leave it).
Anything strictly between the two is refused rather than guessed at. The plan names the
sections needing the correction in a new rebase key; plans without one keep the old
rule exactly.

2. A new deadstrip-data disposition, licensed on an address argument rather
than a byte-identity one. A promoted entry claims exact ranges; a data home outside
every one of them is content dsd delinks from the cartridge whatever this object emits,
so discarding the copy cannot cost the image a byte. A home inside a claimed range is
refused — that is a range the source undertook to build — and so is an entry that
declares no module of its own, which would otherwise clear the range test vacuously for
every address in the ROM.

Sound is not the same as right: a vtable whose slots disagree with the cartridge means
the class model is wrong, and quietly dropping it would bury that evidence in the very
file that produced it. So romdata_check's relocation-resolved, word-by-word comparison
is made binding for exactly the symbols a policy row names, and run before the
surgery zeroes them. It stays advisory everywhere else. PARTIAL is the normal verdict
for a vtable — the ROM extent is distance-to-next-symbol, so a trailing alignment gap
that belongs to nobody shortens a fully-matching body — and only DIFFERS refuses.

3. deadstrip-duplicate's body proof was wrong, and this fixes it. It compared raw
section bytes against the cartridge, which can only be right for a body with no
relocations: every bl and every vptr store holds an addend in the object and a
linked address in the image. A 4-byte bx lr (_ZN7Vector3D1Ev) is the only body
flat enough ever to have passed it; anything real would have been rejected. objisolate
now compares the words no relocation covers, and rombuild._duplicate_body_reasons
links the body through linkcheck and compares the ones it masked. Both halves are
checked now.

Verification

  • pytest tools/test_objisolate.py tools/test_rombuild.py tools/test_tubuild.py
    98 passed, +12 new cases covering both accepted vptr spellings, the shape between
    them, the non-RTTI import refusal, function-only duplicate evidence, and all four
    deadstrip-data licence conditions.
  • python tools/rombuild.py -j 16106/106 exact, 100.000000% of compared bytes,
    PASS
    ; 446 verified / 239 partial / 15 differ from 7,798 object records, identical
    to main.

What comes next

The manifest PR that uses it restores ov100/daObjPathLift_c's real destructor and
takes it from 0 to 14 ROM-data records — 12 named and checked against the cartridge,
360 bytes compared, 3 VERIFIED / 9 PARTIAL / 0 DIFFERS, including this class's own
128-byte _ZTV15daObjPathLift_c at ov100:0x0214857c. It has to be a separate PR
because the validator restores all of tools/ from base, so no PR can test its own
tool change.

🤖 Generated with Claude Code

Promoting a TU consolidates N one-function objects into one, which takes
objisolate's intact multi-symbol path: nothing may survive that the manifest
has not licensed, and `deadstrip_plan` accepted STT_FUNC only. A class's
`_ZTV`/`_ZTI`/`_ZTS` are STT_OBJECT, so the only way to promote was to strip
the destructor that makes mwcc emit them -- silently trading away the one
thing that makes a vtable comparable to the cartridge at all. Measured on
ov100/daObjPathLift_c: the promoted TU went from 14 data records to 0.

Three pieces:

* `deadstrip_plan` accepts STT_OBJECT. Data goes to `externalise`, not `dead`
  -- the destructor must go on storing the vptr -- so a surviving section may
  reference one by name, but only for the RTTI trio whose relocation shapes
  are surveyed, and never through an unnamed section symbol. mwcc's `_ZTV<C>`
  addresses the vtable object; symbols.txt's addresses the slots, so the plan
  names the code sections whose vptr stores still carry the preamble skip in a
  new `rebase` key and `_apply` corrects them. Existing plans carry no such
  key and keep the old rule exactly.

* A new `deadstrip-data` disposition in rombuild/tubuild, licensed on an
  address argument: the home lies outside every range the entry claims, so dsd
  delinks it from the cartridge either way and discarding this object's copy
  cannot cost the image a byte. A home INSIDE a claimed range is refused --
  that is content the source must build. Soundness is not enough, though: a
  vtable whose slots disagree means the class model is wrong, so
  `_data_body_reasons` makes romdata_check's relocation-resolved comparison
  binding for exactly the symbols a policy row names, before the surgery
  zeroes them. An entry with no module of its own now fails closed rather than
  answering "no claimed range" for every address.

* `deadstrip-duplicate`'s body proof compared raw section bytes, which can
  only be right for a body with no relocations -- a 4-byte `bx lr` was the
  only one that had ever taken this path. Every `bl` and vptr store holds an
  addend here and a linked address in the cartridge. objisolate now compares
  the words no relocation covers, and `rombuild._duplicate_body_reasons` links
  the body through linkcheck and compares the ones it masked. Both halves of
  every duplicate body are checked now, where before one flat case passed and
  anything real would have been rejected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QhhAeJwXBnfPp7B5DNjCwh
@tangos-validator

tangos-validator Bot commented Aug 30, 2026

Copy link
Copy Markdown

✅ PR validation — Passed

noverify: no source/build-data changes in this PR

Each changed src/*.c|*.cpp is compiled and its relocated bytes compared to the binary data on a private build box. Passing requires every changed file to reproduce the ROM byte-for-byte with correct relocation targets — this catches WRONG-DEST relocations and non-reproducing near-misses that ledger-scoped linkcheck skips.

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