Adding Custom Sections to a Wasm Binary

This page answers one task: attach your own data to a .wasm file — a version string, a build ID, licence text, a small configuration block — in a way that engines ignore and your code can read back.

Prerequisites

  • [ ] A Rust or C toolchain targeting wasm32, or wasm-tools for post-build editing.
  • [ ] A browser or Node to read sections with WebAssembly.Module.customSections.
  • [ ] A clear idea of what the data is for and who reads it.

What custom sections are for

A WebAssembly binary is a sequence of sections. Known sections — types, imports, functions, code — have fixed ids and meanings. Section id 0 is reserved for custom sections: each has a name string and arbitrary content, and the specification requires engines to accept and ignore custom sections they do not recognise. That makes them the standard place for metadata that travels with the module without affecting how it runs.

The ecosystem already uses them for function names (name), debug information (.debug_*), toolchain records (producers), feature records (target_features) and source map links (sourceMappingURL), as described in reading the name custom section. You can add your own for the same reasons: so a deployed module can say which build it is, which licence applies, or which configuration it was compiled with — information that otherwise lives in a separate file that drifts out of sync.

Where a custom section sits in a module Known sections such as type, import, function and code appear in a fixed order with their numeric ids. Custom sections, with id 0 and a name, can appear before, between or after them, and engines skip any they do not recognise. header: \0asm + version 8 bytes type (1), import (2), function (3), … known sections in fixed order code (10), data (11) the module's executable content custom (0) "build_info" your metadata: ignored by engines custom (0) "name", "producers" toolchain metadata

Step 1 — emit a section from Rust

Rust can place static data in a custom section with the link_section attribute. The linker collects every static with the same section name into one custom section:

// src/build_info.rs
#[used]
#[link_section = "build_info"]
static BUILD_INFO: [u8; 46] = *br#"{"version":"2.4.0","commit":"9c41e7a0d5b2f3c8"}"#;

#[used] stops the compiler discarding a static that no code references. The content must be a byte array of fixed size known at compile time; a build script can generate the file with the current version and commit so it stays accurate. For values that change per build, keep them deterministic — derived from the commit rather than the clock — so builds stay reproducible, as discussed in producing reproducible Wasm binaries.

Step 2 — or from C

With clang, the section attribute does the same:

// build_info.c
__attribute__((used, section("build_info")))
static const char build_info[] = "{\"version\":\"2.4.0\",\"commit\":\"9c41e7a0\"}";

Note that a C string literal includes a trailing NUL byte; readers should trim it or the C side should use an explicit byte array.

Step 3 — or add it after the build

When you cannot change the source, or want to stamp metadata in a release step, add the section to the finished binary with wasm-tools:

printf '{"version":"2.4.0","built_by":"ci-412"}' > build_info.json
wasm-tools metadata add --name app --version 2.4.0 app.wasm -o app.meta.wasm      # standard metadata
wasm-tools custom-section add app.wasm build_info build_info.json -o app.tagged.wasm   # if your version supports it

Different releases of wasm-tools spell this slightly differently; wasm-tools --help lists the subcommands available. A dozen lines of JavaScript can also append a section, since the format is simple:

// append-section.mjs — usage: node append-section.mjs in.wasm out.wasm name payload-file
import { readFileSync, writeFileSync } from "node:fs";
const [, , input, output, name, payloadFile] = process.argv;
const uleb = (n) => { const out = []; do { let b = n & 0x7f; n >>>= 7; if (n) b |= 0x80; out.push(b); } while (n); return out; };
const nameBytes = new TextEncoder().encode(name);
const payload = readFileSync(payloadFile);
const body = [...uleb(nameBytes.length), ...nameBytes, ...payload];
const section = Buffer.from([0x00, ...uleb(body.length), ...body]);
writeFileSync(output, Buffer.concat([readFileSync(input), section]));

Appending at the end is always valid: custom sections may appear after the last known section.

Step 4 — read it back from JavaScript

WebAssembly.Module.customSections returns an array of ArrayBuffers for every custom section with a given name, without instantiating the module:

const module = await WebAssembly.compileStreaming(fetch("/wasm/app.wasm"));
const [raw] = WebAssembly.Module.customSections(module, "build_info");
const info = raw ? JSON.parse(new TextDecoder().decode(raw).replace(/\0+$/, "")) : null;
console.log("module build", info);          // { version: "2.4.0", commit: "9c41e7a0d5b2f3c8" }

Including this in error reports is the main payoff: every crash report can carry the exact build of the module that crashed, read from the module itself, rather than from a version string in JavaScript that might belong to a different deploy. That is the link reporting Wasm crashes to an error tracker relies on.

Build metadata from compile time to a crash report The build embeds a build_info custom section. At runtime the page compiles the module, reads the section with customSections without running any module code, and attaches the build information to any error report the module produces. build page error tracker emit custom section build_info (version, commit) compileStreaming; customSections(module, build_info) instantiate and run; a trap occurs report: trap + build_info.commit

Step 5 — make sure optimizers keep it

Post-processing tools may strip custom sections. wasm-opt removes custom sections it does not know when stripping (--strip-debug removes debug sections; --strip removes more), and wasm-strip removes them all. Check after the full pipeline:

wasm-objdump -h dist/app.wasm | grep -i 'custom.*build_info' || echo "build_info section missing"

If a step removes it, add the section after that step — or use wasm-opt’s options to keep named sections. Putting the check in CI catches a pipeline change that silently drops the metadata.

Designing the payload

A custom section’s content is yours to define, and a little design up front makes it more useful later. Choose a stable section name scoped to your organisation or project — acme:build_info rather than info — so it cannot collide with a toolchain’s sections or another library’s. Version the payload format itself, with a field such as "v": 1, so readers can handle older and newer modules. Keep it small and self-describing: JSON is easy to read in every language and easy to inspect with a hex viewer, and for a few dozen bytes its overhead is irrelevant. If the payload grows, consider CBOR or another compact binary encoding, but only once size actually matters.

Decide who writes the section and when. If the compiler writes it from source attributes, every build includes it automatically, which suits build identity. If a release step adds it after the build, it can include information only known at release time — the release channel, the signing identity, the deployment name — without changing the compiled code. Many teams use both: compile-time metadata for what the code is, release-time metadata for where it was shipped.

Finally, document the format next to the code that reads it. A custom section that only one script understands becomes unreadable the day that script is deleted; a short schema comment keeps it useful for the life of the deployed modules.

What not to put in custom sections

Custom sections are visible to anyone who downloads the module — they are plain bytes in a public file. Do not put secrets, API keys or internal hostnames there. Avoid large payloads too: custom sections are downloaded with every load of the module, so a megabyte of embedded configuration costs every user a megabyte. And do not rely on custom sections for anything that affects behaviour: if code needs the data, put it in a data segment where the code can read it, and use the custom section only for information about the module — provenance, licences, build details — that tools and error handlers consume from outside.

Expected output

wasm-objdump -x -j build_info dist/app.wasm
Custom:
 - name: "build_info"
 - payload: {"version":"2.4.0","commit":"9c41e7a0d5b2f3c8"}

and the JavaScript snippet logs the same object.

Gotchas

  • The section disappears in release builds. A strip step removed it. Add it after stripping, or configure the strip to keep it.
  • The static was optimized away. Without #[used] (Rust) or __attribute__((used)) ©, an unreferenced static may be discarded.
  • Trailing NUL in C strings. Trim it when decoding, or emit a byte array without the terminator.
  • Non-deterministic content. Timestamps or machine names in metadata break reproducible builds. Derive values from the commit.

Performance note

Custom sections cost exactly their size in download and nothing in compilation — engines skip them. A 60-byte build-info section is invisible in any measurement. Reading it with customSections took a few microseconds; the module has to be compiled first, but that is work the page was doing anyway.

Cost of a small metadata section Added size and time for a 60-byte build_info custom section on a 610 KB module — download bytes after compression, compile time difference, and the time to read it back with customSections. added cost bytes on the wire (Brotli) 52 compile time difference (µs) 0 customSections read (µs) 4

Frequently Asked Questions

Can a module read its own custom sections? Not with WebAssembly instructions — custom sections are not mapped into memory. If code needs the data, use a data segment instead.

Can I have several sections with the same name? Yes. customSections returns all of them in order. Linkers merge same-named sections from object files.

Do custom sections count toward size budgets? They should — they are downloaded like everything else. Include them in size checks so a growing payload is noticed.

Is there a standard metadata format? The producers section is standardised by tool conventions, and newer tooling adds registry metadata (name, version, licence) for packages. For your own data, a small JSON payload is practical.

Do custom sections survive the component model? Core modules inside a component keep their custom sections; components can carry their own as well.

← Back to Wasm Binary Format Deep Dive