Skip to content

Rebaseline the vendored demangler and bound stack use - #1

Merged
bdash merged 11 commits into
mainfrom
harden-demangling
Oct 2, 2026
Merged

bdash merged 11 commits into
mainfrom
harden-demangling

Conversation

@bdash

@bdash bdash commented Sep 28, 2026 •

Copy link
Copy Markdown
Collaborator

Rebaselines the vendored Swift demangler to swift-DEVELOPMENT-SNAPSHOT-2026-09-21-a, which fixes signed integer overflow in demangleNatural and demangleIndex. The crate also now fails cleanly instead of overflowing the stack on pathological symbols.

  • Symbol wrapper chains are built, dropped and formatted without recursion.
  • Symbol::parse, demangle and the other printing functions return None for trees deeper than MAX_NODE_DEPTH, counting nested specialization payloads that the printer recurses into.
  • The vendored C++ is always built optimized.
  • Patches the vendored code to cut NodePrinter::print's stack use on Windows from ~8 KiB to ~500 bytes per level of recursion.
  • Tests run on 512 KiB threads on Unix platforms and 1MiB threads on Windows. A macOS CI job runs them under UBSan.

@bdash
bdash force-pushed the harden-demangling branch 9 times, most recently from 0ad61ad to 5f6bb30 Compare September 28, 2026 23:27
At `-O0`, `NodePrinter` overflows a 512 KiB thread stack on real symbols.

On MSVC, set the configuration's compiler flags ourselves. The `cmake`
crate otherwise replaces them with flags that drop optimization.
They weren't updated after the repository was reorganized.
…-2026-09-21-a

The previous baseline has signed integer overflow in
`Demangler::demangleNatural` and `demangleIndex`, fixed upstream in
swiftlang/swift@7f2612f.
This is the first tag with the fix.

Map the new and renamed node kinds in the Rust layer, keeping the old
names as deprecated aliases. Upstream now attaches propagated names in
function signature specializations as `Identifier` children, so
`FunctionSignatureParam::payloads()` reads those too.

Closure-propagated payloads now print as mangled names rather than
demangled, an upstream regression that affects about 0.25% of real
symbols. No symbol that demangled before fails now, and 1,286 more
demangle.
`NodePrinter::print` recurses once per level of the tree, and on Windows
each call used about 8 KiB of stack, against about 0.3 KiB on macOS and
Linux. The Windows x64 calling convention passes a 16-byte `StringRef`
argument via a copy on the caller's stack, and neither MSVC nor clang-cl
shares those copies' slots, so each of `print`'s hundreds of
`Printer << "..."` calls got a slot of its own.

Add a `const char *` overload of `DemanglerPrinter::operator<<` so string
literals are passed in a register. This is a local patch to `vendor/`,
noted in the vendoring README. Also build `NodePrinter.cpp` without
inlining on MSVC, since the temporaries that inlining adds to `print`
don't share slots either. Together these bring `print` to 480 bytes per
call on MSVC.
The parser represents N stacked wrappers as N+1 siblings under `Global`.
Wrappers include specializations, async and metadata markers, and merged
or inlined thunks. `build_specialization_chain` and `build_marker_chain`
recursed once per sibling, and dropping the resulting `Box<Symbol>`
chain recursed once per layer, as did `Symbol::raw()` for `Suffixed`.
So `$s4main5helloSSyYaKF` followed by 5000 copies of `yTg5` overflowed
the stack, although its tree is only 7 deep.

Build the chains from the innermost symbol outwards, make `raw()` a
loop, and implement `Drop` for `Symbol` to detach nested symbols into a
work list.

Because `Symbol` now implements `Drop`, nested symbols can't be moved
out of it by value. Match on a reference instead, as the tests now do.
`TypeRef::classify` recursed through `Type` wrappers and signature-less
`DependentGenericType` nodes, and `NamedType::extract_name` recursed
through `Type` wrappers, once per level of nesting. Unwrap both in
loops. The classification is unchanged.
`Debug` output for `Symbol`, `TypeRef` and `Node` nests once per wrapper
layer or tree level, and wrapper chains can be arbitrarily long, so
`{:?}` on a long specialization chain overflowed the stack.

Route all three through a shared, thread-local nesting budget of 128
levels, eliding anything deeper as `..`. `Symbol`'s `Debug` impl is now
written by hand, with the same output as the derive, so it can take
part. The budget is well above the nesting of any real symbol and keeps
stack use well below thread stack limits.
`Symbol::parse` and `Symbol::from_node` now return `None` for trees
deeper than `MAX_NODE_DEPTH`, so the high-level API's recursive walks
are bounded. It is 768, matching `NodePrinter`'s own recursion limit and
well above the depth of any real symbol.

The printer can also recurse without bound by itself. It demangles
function signature specialization payloads that are themselves mangled
symbols, such as constant-propagated functions and globals, and each
nested demangle restarts its depth count, so payloads nested in payloads
can overflow the stack even though the outer tree stays shallow.

The C wrapper therefore measures depth as the printer sees it, counting
each text node that is a Swift symbol as its demangled tree nested at
that point. `swift_demangle_symbol`, `swift_demangle_node_to_string` and
`swift_demangle_get_function_info` now fail on deeper trees instead of
printing them, which covers `demangle()`, `Node`'s `Display` and
`FunctionInfo::parse`. The check is exposed as
`swift_demangle_node_depth_within` and `Node::depth_within`.
`print_symbol` recurses into each wrapped symbol and indents it further.
For a long wrapper chain the recursion grows without bound and the
output grows quadratically. Print at most 64 nested levels and `...`
after that.

`print_symbol` now takes the nesting depth rather than an indent string.
An `Indent` type's `Display` impl writes the indentation, so no strings
are built for it.
With `SWIFT_DEMANGLE_UBSAN=1`, `build.rs` compiles the vendored C++ with
`-fsanitize=undefined -fno-sanitize-recover=undefined`, so any undefined
behavior aborts the test run. It also links Clang's UBSan runtime
explicitly, because rustc links with `-nodefaultlibs` and the linker
driver won't add it. Only macOS is supported for now.

Run it in CI as a separate macOS job.
@bdash
bdash merged commit ee53de6 into main Oct 2, 2026
4 checks passed
@bdash
bdash deleted the harden-demangling branch October 2, 2026 04:52
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