Skip to content

Fix use-after-free resolving reference type names in wast-parser - #2805

Merged
sbc100 merged 2 commits into
WebAssembly:mainfrom
aizu-m:wast-parser-ref-type-uaf
Aug 6, 2026
Merged

Fix use-after-free resolving reference type names in wast-parser#2805
sbc100 merged 2 commits into
WebAssembly:mainfrom
aizu-m:wast-parser-ref-type-uaf

Conversation

@aizu-m

@aizu-m aizu-m commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

drop the deferred reference-type resolutions a module field registered before it failed to parse, otherwise ParseModuleFieldList dereferences pointers into a field that was destroyed instead of appended.

@sbc100

sbc100 commented Aug 3, 2026

Copy link
Copy Markdown
Member

Should we have ParseModuleField clean up after itself on error instead of expecting the caller to track this stuff?

Move the truncation of resolve_ref_types_/resolve_type_vectors_/
resolve_funcs_ out of ParseModuleFieldList and into ParseModuleField, so
a field that fails to parse drops its own deferred resolutions instead of
the caller tracking them. The dispatch switch moves to
ParseModuleFieldImpl unchanged; ParseModuleField now records the list
sizes, calls it, and truncates on failure.

No behavioural change: the six registration sites still exit 1 with only
the expected parse diagnostics and no ASan report.
@aizu-m

aizu-m commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

Agreed, that's better. Moved it into ParseModuleField, which now records the three resolve list sizes, dispatches, and truncates them if the field failed. ParseModuleFieldList is back to its original shape.

The dispatch switch moved into ParseModuleFieldImpl unchanged so ParseModuleField has a single exit point to hang the cleanup off. Writing it as result = ParseXModuleField(module); break; in place worked too, but clang-format then exploded all eleven cases onto three lines each, which seemed a worse trade than one small helper.

Re-checked after the change: all six registration sites (func param, func result, func local, table elem type, elem segment type, import func signature) exit 1 with only the expected parse diagnostics and no ASan report, and reverting just the truncation brings the heap-use-after-free straight back in ResolveTargetTypeVector. Test suite is green in Release, and green again under a Debug ASan build.

Comment thread src/wast-parser.cc
resolve_type_vectors_.begin() + type_vectors_size,
resolve_type_vectors_.end());
resolve_funcs_.erase(resolve_funcs_.begin() + funcs_size,
resolve_funcs_.end());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still a little confused, if we are returning failure, when why does it matter about the state of these members? If we fail to parse why is anybody accessing these after this point?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The failure doesn't propagate. ParseModuleFieldList swallows it:

if (Failed(ParseModuleField(module))) {
  CHECK_RESULT(Synchronize(IsModuleField));
}

It resynchronises to the next module field and carries on parsing, so wat2wasm reports every parse error in one pass rather than stopping at the first. The failed field's result is dropped right there.

So control still reaches the three resolve loops at the bottom of that same function, which walk resolve_ref_types_ / resolve_type_vectors_ / resolve_funcs_ and dereference the pointers recorded while the field was being parsed. Those point into the field's local unique_ptr, which was destroyed on the error path instead of being handed to AppendField. That's the freed read, and a freed write via local_types.Set() in the func-local case.

The one case where nobody touches them is when Synchronize fails too, i.e. the bad field is the last thing in the file, since then we return early and never reach the loops. That's why the test has a (memory 1) after the broken fields: it gives Synchronize somewhere to land so the loops are actually reached.

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