DioxusLabs / DioxusLabs/dioxus

`wasm-split-cli` panics on a growable (no-maximum) ifunc table

Open
#5,764 0 comments 0 reactions 0 assignees View on GitHub
bug
Dominant language
Rust
Stars
39.1k
Forks
1.9k
Avg merge
4d 10h
Merged PRs (30d)
4

Description

# Introduction

I am currently writing a component library for Dioxus.
I use wasm split during development, as I also use some third party libraries which are pretty heavy and are not supposed to defer the initial paint pipeline.

I wanted to add a "key" attribute to my style tags to keep the generated style tags more stable and prevent unneeded recalculations. However, I ran into an unusual crash.

I asked Claude to debug and fix it. Hope it helps!

Turns out the problem is multiple fold, and some other problems where discovered on the way. I decided to split these bugs into 3 separate reports. In the end it was traced back to format!(...).

| Bug | Fixes | Relation to the actual corruption |
|---|---|---|
| growable table panic | Unconditional `.unwrap()` on a table that can lack a max | Blocked builds from completing; not the corruption itself |
| dropped child edges | Single unresolved call-graph child edge silently discarded | Real & measurable (228/run), confirmed *not* the cause |
| data symbol overlap | Pruning a dead symbol zeroes bytes a live one still owns | **This is it** — the actual truncation/crash root cause |

Below I asked Claude to make a writeup for each issue. As I dont really understand the inner workings of this library, but I verified that the fixes actually solved the problem. So please look at it and verify it yourself. I am also going to make a separate pull request for each fix that Claude found.

## Environment

- Dioxus: `0.7.10` (tag `57d6794`)
- Rust: `rustc 1.99.0-nightly (1a98b1e13 2026-08-07)`
- wasm-bindgen-cli: `0.2.127`
- OS: Arch Linux (rolling), Linux 7.1.8, x86_64

## Problem

`wasm-split-cli` can panic with `Option::unwrap() on a None value` inside
`expand_ifunc_table_max` while emitting a split or chunk module, aborting
the whole build.

## Root cause

`expand_ifunc_table_max()` (`packages/wasm-split/wasm-split-cli/src/lib.rs`)
only handled a funcref table with an explicit `maximum`:

```rust
fn expand_ifunc_table_max(&self, out: &mut Module, table: TableId, num_ifuncs: usize) -> Option {
let ifunc_table_ = out.tables.get_mut(table);
if let Some(max) = ifunc_table_.maximum {
ifunc_table_.maximum = Some(max + num_ifuncs as u64);
ifunc_table_.initial += num_ifuncs as u64;
return Some(max as usize);
}
None
}
```

A table with no `maximum` at all - the shape `wasm-ld`'s `--growable-table`
flag produces, and a flag this same codebase's own hot-patch linking path
(`packages/cli/src/build/link.rs`) passes - makes this return `None`. All
3 call sites unconditionally `.unwrap()` the result.

## Minimal reproduction

[growable-ifunc-table-repro.sh](https://github.com/user-attachments/files/31330653/growable-ifunc-table-repro.sh) is a self-contained,
runnable reproduction - it needs only `cargo`/`rustup` and `curl`, touches
nothing outside a throwaway temp directory, and:

1. Generates a ~30-line standalone crate (a `BTreeMap` keyed by an enum +
`u64`, formatted with `format!("{}-{hash:x}", ...)` - deliberately the
same shape as the real-world corruption in
`data-symbol-overlap-truncation.md`, so this one repro exercises both).
2. Builds it with `cargo build --target wasm32-unknown-unknown --release`
under the exact `RUSTFLAGS` `dx` uses for a `--wasm-split` build
(`-C relocation-model=pic -C link-arg=--emit-relocs -C link-arg=--growable-table ...`).
3. Runs it through `wasm-bindgen-cli 0.2.127` and the **published, unpatched
`wasm-split-cli 0.7.10` from crates.io** (installed via `cargo install`,
no local fork or patch involved).
4. Downloads that same `0.7.10` source from crates.io, applies the 4-line
patch below, rebuilds it, and re-runs the identical split.
5. Checks the actual output value (not just the exit code) via `node`.

Captured output, run just now on this machine:

```
--- 3/4: wasm-split-cli split, UNPATCHED - expected to fail ---
thread 'main' (...) panicked at .../wasm-split-cli-0.7.10/src/lib.rs:345:14:
called `Option::unwrap()` on a `None` value
...
Confirmed: unpatched wasm-split-cli failed as expected (exit 101).

--- 4/4: fetching wasm-split-cli 0.7.10 source, patching it, rebuilding, re-running ---
Patched split succeeded (exit 0), where the unpatched one panicked.

--- checking the actual output value, not just the exit code ---
run() returned: "lsx-framework-aaaa"
CORRECT - the patch fixes the data corruption too, not only the crash.

=== Summary ===
unpatched wasm-split-cli exit code: 101 (0 = bug did not reproduce)
patched wasm-split-cli exit code: 0 (should be 0)
```

The same run's stderr also has ~35 `Could not find function symbol
... Ignoring` warnings from this build - see
`relocation-symbol-name-vs-index.md`, a separate, independently-real bug
this exact repro happens to trigger too.

## Patch

```diff
- fn expand_ifunc_table_max(&self, out: &mut Module, table: TableId, num_ifuncs: usize) -> Option {
+ fn expand_ifunc_table_max(&self, out: &mut Module, table: TableId, num_ifuncs: usize) -> usize {
let ifunc_table_ = out.tables.get_mut(table);
if let Some(max) = ifunc_table_.maximum {
ifunc_table_.maximum = Some(max + num_ifuncs as u64);
ifunc_table_.initial += num_ifuncs as u64;
- return Some(max as usize);
+ return max as usize;
}
- None
+ // No maximum to raise - just grow `initial`, the only size the
+ // table has.
+ let start = ifunc_table_.initial as usize;
+ ifunc_table_.initial += num_ifuncs as u64;
+ start
}
```

(plus removing the now-unnecessary `.unwrap()`/`.expect()` at all 3 call
sites, since the function no longer returns an `Option`)

## Why this fixes it

The function's job is to reserve `num_ifuncs` new table slots and return
the offset they start at. With an explicit `maximum`, that offset is the
old maximum, and the maximum grows to match. With no maximum, there's no
ceiling to raise - the table's *only* size is `initial`, so the fix uses
that as both the return value and the field to grow. No `Option` is
needed because both branches now always produce a valid offset.

## Verified against dioxus `main`

`wasm-split-cli`'s logic is unchanged between the `0.7.10` tag and current
`main` (`24f6a829d` at time of writing) - the only diff is cosmetic
import/macro reformatting. This patch cherry-picks cleanly onto `main`
with no conflicts. Separately verified directly against `main`'s own
`Splitter::new()`/`emit()`: forcing a funcref table's `maximum` to `None`
(the exact shape `--growable-table` produces) panics identically on
unpatched `main`, and no longer panics once this patch is applied.

Contributor guide

No contributing guide indexed for this repository

Research direction

Start in packages/wasm-split/wasm-split-cli/src/lib.rs at expand_ifunc_table_max and inspect its three call sites; packages/cli/src/build/link.rs shows how growable tables are produced. Run growable-ifunc-table-repro.sh first, then verify the split completes without a panic and returns the expected output value for a table without a maximum.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust, wasm
Domain
build-system, cli
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.