Skip to content

rv32: a retained function relocating against a declined INTERNAL emits a dangling synth_func_N and exits 0 — the #851 guard #1013 gave aarch64 is not on the RISC-V path #1102

Description

@avrabe

[fathom (gale) — cross-arch driver seam]

#1013 fixed the aarch64 ELF builder's panic, and both the issue and the fix rest
on the premise that "the ARM and RISC-V backends already do the right thing on
the identical module"
/ "matching the sibling backends."

On v0.60.0 the RISC-V backend does not, when the declined target is an
internal function rather than a requested export. It exits 0 and ships
an object that cannot be linked.

Repro

Any of gale's thin-seam drivers. gpio-thin is a 13-export real-world wasm:

$ synth compile gpio-thin.wasm -b riscv --target esp32c3 --all-exports --relocatable -o out.o
warning: skipping function 'func_18': backend 'riscv' failed: compilation failed:
  RISC-V selector: immediate 1048588 too large for memory offset
warning: 1 of 18 functions were skipped (not in output): func_18
Compiled 17 functions to out.o
  Total code size: 1696 bytes
  Relocations: 22 (requires linking with Kiln bridge)

$ echo $?
0

Exit 0, and the decline is only a warning. But the object references the
declined function:

$ nm out.o
...
0000039c T func_10
000003b0 T func_11
00000518 T func_16
00000520 T func_17
00000000 T func_2
         U read32
         U synth_func_18      <-- declined func_18, referenced anyway
         U write32

read32/write32 are the legitimate seam imports. synth_func_18 is not — it
is the function the backend just declined, and nothing defines it:

$ ld.lld out.o -o firmware
ld.lld: error: undefined symbol: synth_func_18
>>> referenced by out.o:(gust:hal/gpio@0.1.0#set)
>>> referenced by out.o:(gust:hal/gpio@0.1.0#read)

Two exported entry points reference it, so this is not dead code that a
--gc-sections pass would drop.

Why the export guard does not catch it

The #952 guard fires only when a requested export is skipped. On the same
corpus, mpu-thin skips the export gust:mpu/iso@0.1.0#size-field and synth
correctly exits 1 with no object. gpio-thin skips only the internal func_18,
so nothing fires — even though the resulting object is equally unusable.

Secondary: the dangling name does not match the definition namespace

Definitions in the object are func_N; the dangling reference is
synth_func_N. So the symbol could not be satisfied by another object either —
even a hand-written func_18 would not resolve it.

Scope on our corpus (v0.60.0, 13 thin-seam driver wasms)

rv32 result
12 of 13 exit 0, object emitted, 1–10 dangling synth_func_N
1 of 13 (mpu-thin) exit 1, no object — correct, caught by the export guard

switch-thin is the worst: 13 of 30 functions declined, 10 dangling symbols.

The same 13 modules lower cleanly on cortex-m3 — every object's undefined set
is exactly its wasm imports, and internals are emitted as local t func_N. The
divergence is specific to the RISC-V path.

Not a regression: 0.52.0 / 0.53.0 / 0.54.0 / 0.55.0 / 0.57.0 / 0.58.0 / 0.60.0
all produce identical output on these inputs.

Two underlying selector gaps (separate from the guard)

These are what triggers the declines; the guard issue stands regardless of
whether they are fixed:

  • RISC-V selector: immediate 1048588 too large for memory offset — RV32 I-type
    offsets are 12-bit signed; this needs lui/addi materialization. Hit by 12
    of 13 drivers.
  • unsupported wasm op for RV32 skeleton: GlobalGet(0)mpu-thin, 8 of 20
    functions.

What we would expect

Exit non-zero with a message naming the declined symbol, the way #952 does for
exports and the way #1013 made aarch64 behave — rather than shipping an object
whose link is guaranteed to fail. If a partial object is sometimes wanted, an
opt-in flag (mirroring --allow-skipped-exports) would fit the existing shape.

Why this mattered to us

Our cross-arch gate asserted undefined_count > 0 — a rule written against the
0.49 truncation shape, where declined functions took their call sites with them.
A dangling synth_func_N satisfies > 0 just as well as a real seam symbol, so
the gate read green across every one of these objects. We have since changed the
rule to undefined set == wasm imports, which is what surfaced this.

Minor, while here: the success output suggests Link with: arm-none-eabi-ld for
a RISC-V object.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions