Incremental build reuses stale artifacts after a module-interface layout change, producing a corrupt binary #27
Labels
No labels
bug
claude:blocked
claude:done
claude:failed
claude:in-progress
claude:ready
documentation
duplicate
enhancement
good first issue
help wanted
invalid
question
wontfix
bug
duplicate
enhancement
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Catcrafts/Crafter.Build#27
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Adding a data member to a class defined in a module interface does not cause every dependent
translation unit to be rebuilt. The resulting binary mixes translation units compiled against the
old and new class layouts, which is undefined behaviour — in practice a
SIGSEGVinside adestructor.
The failure mode is what makes this worth fixing rather than the inconvenience: there is no link
error and no warning. The build succeeds, and the corruption surfaces later as a crash that looks
like a bug in the library you just pulled. I spent a while preparing a regression report against
Crafter.Network's HTTPS merge before establishing that the merge was fine and the build wasstale.
Reproduction
Deterministic, using
Crafter.Networkat the commits below.219a31ais the commit before theHTTPS merge;
0827e1fis the merge. The merge adds astd::string defaultSchememember toHTTP1::MessageParserininterfaces/Crafter.Network-HTTP1.cppm.Step 2 and step 3 build identical source. The only difference is the stale
bin/andbuild/.Crash
All five failures are the tests that exercise
ClientHTTP1over plaintext. Under gdb:A crash in the destructor of exactly the class whose layout changed is consistent with a
size/offset mismatch between TUs: one side constructs an object of the old size, another destroys
it believing the new layout, and the
std::stringmember added at the end is destroyed at thewrong address.
Note that
ShouldInteropCurlHTTP1passes in step 2. It drives the listener withcurlrather than
ClientHTTP1, so it never constructs the mismatched object — which is why the failurepattern looks like "the plaintext client is broken" rather than "the build is stale."
Diagnosis (inferred, not isolated)
I have not narrowed down which specific artifact is stale, so treat the mechanism as a hypothesis
supported by the evidence rather than a confirmed root cause. What is confirmed is that identical
source produces a crashing binary incrementally and a working binary from clean.
The likely shape: the dependency tracking treats an interface change as requiring a rebuild of the
module's own BMI and of TUs that import it, but something in the chain — a cached BMI, an object
file, or a static archive member — is reused when it should not be. Adding a member is the
interesting case because it changes layout while keeping every signature and mangled name
identical, so nothing downstream fails to link.
Suggested direction
Some options, roughly in increasing order of effort:
.cppminvalidates everything that imports it, not just its own BMI. Coarse but correct, andmodule interfaces do not change often enough for the rebuild cost to matter.
interface they were built against, that should be a build error. A wrong answer is much worse
than a slow one here.
--clean/crafter-build cleansubcommand would at least make the workaround discoverable.Right now the fix is
rm -rf bin build, which you only try if you already suspect the build.Option 2 seems most valuable regardless of which of the others is taken: this class of bug should
never reach a running binary.
Possibly related: project args are not part of the variant identity
Separate symptom, possibly the same underlying theme of "the variant id does not capture everything
that affects the output", so mentioning it here — happy to split it into its own issue.
crafter-buildandcrafter-build -- --no-webgpu(aCrafter.Graphicsproject arg that changeswhich files
cfg.filescontains) both producebin/Catcrafts.Net-wasm32-wasip1-native-native-df37fe0fe124fe57/. Because the directory is sharedand reused rather than replaced, switching between the two leaves a bundle matching neither config:
index.htmlis regenerated without thedom-webgpu.jsscript tag while the staledom-webgpu.jsfile is still sitting in the directory.
rm -rf binbetween flag changes is the workaround.Environment
Crafter.Networkat0827e1f, reproduced against219a31a