Skip to content

types: eliminate some blowups when finalizing and dropping types - #375

Open
apoelstra wants to merge 4 commits into
BlockstreamResearch:masterfrom
apoelstra:2026-08/type-bomb
Open

types: eliminate some blowups when finalizing and dropping types#375
apoelstra wants to merge 4 commits into
BlockstreamResearch:masterfrom
apoelstra:2026-08/type-bomb

Conversation

@apoelstra

Copy link
Copy Markdown
Collaborator

Improves types::Type::finalize so that it doesn't unshare the type DAG when finalizing. This is important because we need to finalize the types before we can get static bounds, meaning that we can't reject exponentially-sized programs until after this method has succeeded.

Along the way, because I was testing with an extremely deep program found by the Bitcoin Red Team, I ran into stack overflows dropping both Node and types::Incomplete. This PR replaces those with manually-written non-recursive impls. It also copies the display-limiting logic from types::Type to types::Incomplete.

I have a 300kb program (produced by a clanker) that has an
exponentially-sized type DAG. This is fine until we call
`Type::finalize`, which has the amusing comment

    // Now that we know our types have finite size, we can safely use a
    // post-order iterator to finalize them.

Well, this -would- be safe if we weren't using the `NoSharing` tracker in
our post-order iterator. AFAICT I did this just to be lazy. Stop being
lazy and implement a sharing tracker that tracks BoundRefs, thereby only
iterating through as many nodes an we actually allocated.

I don't have a good unit test; for one thing, my test vector is 300kb so
I don't want to put it in this repo (I'll throw it in qa-assets so it's
at least available, though I need to think what kind of harness we
should write for it). But also, the failure mode is that this code just
iterates forever, allocating 600Gb+ of RAM, which Rust makes a bit hard
to detect. We should investigate using an alternate allocator that can
limit memory, or something, in the fuzzer.

But the existing unit and fuzz tests should confirm that this doesn't
break anything. In fact the fuzzer should run much faster now.
For `Node` this is actually not too bad, because the type is mutally
recursive with `Inner`, so we can move out of the Inner type without
getting stupid "cannot move out of type that implements Drop" errors.

High-level structure was written by ChatGPT 5.6 Sol, but I rewrote it
to factor out the `into_dag` method and make the other code more terse.
Just copy exactly the same logic we added for Type in src/types/mod.rs.
When we panic on type-inference errors we often try to debug-dump an
entire Incomplete, which may be exponential in size.

Possibly we want BoundRef-sharing here too? At least for Debug output?
I dunno. Certainly what we -don't- want is to run forever outputting
nothing, which is the existing behavior that this commit fixes.
Unlike the case for `Node`, `types::Incomplete` is a directly recursive
type (or rather, it holds an `Arc<Incomplete>`, but we can't implement
anything on `Arc` so for our purposes it may as well just be `Incomplete`)
which means that we hit a Rust bug preventing us moving stuff out of it.

We need unsafe code to move out of the Rust bug. Hopefully this code,
which is mostly comments and directly analogous to the code we just
added to Node, is easy enough to follow.

You can run the `root_unit_to_unit` in Miri which exercises this path,
and if you introduce UB (say, by removing the call to `mem::forget`)
it'll detect it.
@apoelstra

Copy link
Copy Markdown
Collaborator Author

On 51992e5 successfully ran local tests

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