Eight bytes is not much, and that is the point. Before a WebAssembly runtime will do anything at all — before it reads a type, allocates a memory, or looks for a single instruction — it checks eight bytes, and those eight bytes have one job: to establish that the rest of the file is worth parsing.
That sounds trivial, and it would be in most formats. But consider what a runtime is being asked to do. It is being handed bytes from somewhere untrusted and asked to allocate memory, compile code, and run it. The header is the cheapest possible gate, and it is deliberately designed to fail on the wrong input with the least possible work.
There is also something specific about these eight bytes that is easy to miss on a first reading, and it is the reason this concept exists at all. They are the only fixed-width integers in the entire container framing. The next concept covers the variable-width encoding that governs everything after them, and the contrast between the two is not a curiosity — it tells you which fields a runtime checks before it trusts anything, and it is the reason a WebAssembly file can be rejected without a single allocation.
The whole header, from a module that does nothing:
00 61 73 6d 01 00 00 00 | | | | | | | | | | | | | | | | | +-- 0x00 | | | | | | | +----- 0x00 | | | | | | +-------- 0x00 | | | | | +----------- 0x01 | | | | +-------------- 'm' = 0x6d | | | +-----------------'s' = 0x73 | | +--------------------'a' = 0x61 | +----------------------0x00 +------------------------- byte 0
Two fields, four bytes each, both fixed-width:
| Offset | Field | Value | Encoding |
|---|---|---|---|
| 0 | magic | 00 61 73 6d | four raw bytes, compared one at a time |
| 4 | version | 01 00 00 00 | four bytes, little-endian, interpreted as a number |
And there is the detail. The magic is four bytes compared individually. The version is four bytes interpreted as a little-endian 32-bit integer, which is why 01 comes first and the three zeroes follow. A reader that compares the version as four separate bytes would be checking for the literal byte sequence 01 00 00 00 rather than for the number 1, and would reject a hypothetical future version 256, whose encoding is 00 01 00 00.
That is not a hypothetical distinction being made for its own sake. It is the difference between a format that can be extended with new versions and one that cannot, and it was decided in eight bytes. The version field is a number, not a pattern, precisely so that a later version is a later number rather than a different byte arrangement.
00 61 73 6d is a NUL followed by the three ASCII letters asm. The format is named by its own first four bytes, and the fourth byte is there to stop that reading from working.
Without the leading NUL, a file starting asm would be plausible as text, and the tools that sniff files by content — file, an editor, a browser deciding how to display a download — would have no reliable signal. With it, the first byte is 0x00, which is not a legal start for any of them. The three letters are for humans and for hex dumps; the NUL is for the machines. Both halves of the field are doing work, and they are doing different work.
$ xxd minimal.wasm 00000000: 0061 736d 0100 0000 .asm.... $ file minimal.wasm minimal.wasm: WebAssembly (wasm) binary module version 0x1 (MVP)
That ASCII gutter is worth a second look. xxd prints every unprintable byte as a dot, so the four magic bytes come out as .asm and the four version bytes as ..... The format's own name is visible in a standard hex dump with no tooling, which is a small courtesy with a real payoff: a person looking at an unknown file in a hex editor sees .asm at offset zero and knows immediately what they are looking at.
Take a valid module and corrupt one byte at a time, then see how the three readers respond. This is the cheapest possible test of a runtime's gate, and it is worth doing by hand before writing one.
| Corruption | wasm-validate | llvm-objdump | wasm-objdump |
|---|---|---|---|
| byte 1 of the magic changed | rejects | rejects, no sections printed | rejects |
| the version set to 2 | rejects | rejects | rejects |
version bytes reordered to 00 00 00 01 | the same rejection — it is the number 16777216, not 1 | ||
| a section's length byte changed | rejects with an offset | rejects, no sections printed | prints a wrong table |
The last row is the one to notice, because it is a difference of kind rather than degree. On a file with a bad length, the validator names the offset and the rule; LLVM prints nothing, because it declines to print a structure it cannot trust; and wasm-objdump prints a section table anyway, which is wrong and which it presents without complaint.
None of that is a bug. Each tool is answering a different question. The validator answers "is this file usable", LLVM answers "can I make sense of this file", and the disassembler answers "what is in this file". Only the first of those has a use for a header check at all, and the header exists because of it.
Put the header in the context of what a runtime does with an untrusted module, because the order of operations is the design:
Two properties of that sequence are the point of the format's design. The header and the section walk both happen before any allocation, because both need only the length fields, and a length field cannot cause an allocation. And step 3 is a whole pass over the module that proves properties without executing anything — that every index is in range, that every function body has the arity its type claims, that no memory limit exceeds what the runtime permits, that no branch lands mid-instruction.
That is what makes WebAssembly safe to compile ahead of time and ship to a machine you do not control. A JIT does not need to be careful about what it is handed, because the module was validated before it arrived — and the validator is a piece of code whose only job is to say no. The eight-byte header is the first line of that, and it is the cheapest.
And here is the observation the concept is built around, stated as a count. In the container framing of a WebAssembly module, exactly two integers are fixed-width:
Everything else in the framing is a LEB128: every section length, every name length, every entry count, every index, every limit. The next concept covers that encoding, and the useful thing to carry into it is why these two are exceptions.
The answer is that they are the fields a reader must trust before it can trust anything else, and a fixed-width field can be checked with a single memory comparison. A LEB128 of unknown length would require a loop, and a loop on untrusted input before validation is exactly what you do not want to write. The two places the format is most conservative about width are the two places it is least able to afford to guess.
There is a pleasant symmetry in that, and it is the reason the concept ends where it does. The header is the part of the format that has to be readable by a reader who knows nothing, so it is the part that uses the simplest possible encoding. The body is the part that a reader may have read the header first, so it can afford the compact variable-width form and use the saved bytes for something. Every binary format makes this trade somewhere, and knowing where a format is most conservative tells you where its designers were most worried.
Writing the gate, which is the part of a runtime that must be right before anything else can be. Eight bytes, four checks, and the reasoning about what each one buys:
Each check is a different kind of rejection, and the third is the one that is easy to get wrong. Rejecting an unknown version is not the same as rejecting a bad magic, and conflating them produces a runtime that either refuses future modules forever or attempts modules it cannot read. The version check is a statement about this runtime's capabilities: "I understand these versions and no others", which is a legitimate and conservative position.
What the gate must not do is interpret anything. Note that nothing above looks at a section, a type, or an instruction. That is deliberate and it is the performance argument as much as the safety one: a file that begins with the right eight bytes but is otherwise nonsense has still cost a runtime eight bytes of work to begin with, whereas a file that begins wrongly is dismissed in a single comparison. And since the overwhelmingly common case for a wrong file is "not a WebAssembly module at all" — a mislabelled download, a text file, another binary format — the magic check is the one that fires nearly every time.
Now the harder question, which is what to do about the third check. There are three reasonable positions, and the choice is a product decision rather than a technical one:
| Policy | Behaviour on an unknown version | Cost |
|---|---|---|
| Strict | reject | A module valid everywhere else will not load here, including modules produced by a newer toolchain for a newer feature you do not need |
| Permissive | attempt to parse, trusting the length fields | You may misinterpret a construct whose meaning changed, which is worse than failing |
| Capability-based | parse the section table, and reject only if a section kind is present that you do not understand | More work up front, and a design that has to be right about what "understand" means |
The third is the interesting one, and it is where the custom section earns its keep. A custom section can be skipped by a reader that has no idea what is in it, so a file containing only custom sections this runtime has never heard of is still perfectly loadable — which is what makes it safe for a toolchain to attach provenance and target-feature metadata without coordinating with runtimes. The strict policy rejects such a file at the version check only if the version changed; the capability-based policy accepts it, because it never looks inside a section it does not recognise.
Which is the real answer to a question this course keeps returning to. The format's extensibility is not achieved by making the version number optional. It is achieved by having a section kind that means "here are some bytes, ignore them", and a framing that lets a reader skip it. The eight-byte header is what tells a reader which interpretation to use; the custom section is what lets the interpretation stay the same while the content changes.
$ printf '\x00asm\x01\x00\x00\x00' > raw.wasm
$ xxd raw.wasm
$ wasm-validate raw.wasm
$ printf '\x00asm\x02\x00\x00\x00' > v2.wasm && wasm-validate v2.wasm
$ printf '\x00asm\x00\x00\x00\x01' > end.wasm && wasm-validate end.wasmprintf. The escape sequences make the two fields visible in the command itself, which is the fastest way to internalise that the magic is bytes and the version is a number. Then build the same file with wat2wasm from an empty module and confirm they are byte-identical — the hand-written version and the tool's version, agreeing with no shared code.00 00 00 01. A reader comparing four bytes against 01 00 00 00 rejects it; a reader computing a 32-bit little-endian integer gets 16777216 and also rejects it — but for the right reason, and it would behave correctly on a future version 256 whose encoding is also 00 01 00 00. Build both and check which implementation gives which message.asm. Remove it and run file and xxd on the result. Then try the file through a text-oriented tool and notice how much more willing everything is to try to interpret it. One byte is the entire difference between "binary" and "maybe text", and that is a design decision someone made on purpose.wasm-validate's. Where they differ, find out which is right — and the version policies will differ legitimately, which is itself worth seeing.externs.o and identify every integer in it that is not a LEB128. This course's claim is that there are exactly two, both in the header. Verify it, because a counterexample would mean the claim needs qualifying and it is much better to find that out here than in someone else's reader.Without looking back: a runtime is given the eight bytes 00 61 73 6d 00 00 00 01. The magic is correct. As a four-byte comparison against the known version it does not match. As a 32-bit little-endian integer the version field is 16777216. Has this file got a valid header, and what does the answer depend on?
You are building a service that accepts uploaded .wasm modules, validates them, and stores them. Users are on a mix of toolchains, and about one in twenty uploads is rejected by your validator with a version error. The file is a valid module produced by a real toolchain — you can confirm that by opening it in a browser, where it runs. What are the plausible explanations, and what is the cheapest way to tell which one you are looking at?
The general habit: when a gate rejects something that works elsewhere, dump the bytes the gate looked at. The gate's verdict is one bit and the reason is the information; a gate that logs only the bit has thrown away the useful part.
The header is the smallest possible version of a problem every format in this collection solves at a larger scale, and comparing the four is the fastest way to place WebAssembly in the space. ELF opens with 7f 45 4c 46 — four bytes, also a magic, also unmissable in a hex dump — but is followed by a class byte, a data encoding byte, a version, and then a header size that varies. PE opens with 4d 5a and then a 64-byte DOS header of largely historical interest before the real header. COFF has no magic at all, which is a real problem it solves by checking the machine field's plausibility instead.
WebAssembly's eight bytes are the compressed answer to all of that, and the compression is only possible because there is almost nothing to describe. A module has no machine type, because it is not for a machine. It has no data encoding, because the format defines its own. It has no class, because there is only one kind of thing. Remove everything that can be inferred and the header collapses to a magic and a version — and a file format that can be identified in one comparison is a file format that a runtime can reject before it allocates anything.
The next concept is the one that explains why those eight bytes are unusual in a second way, and it is worth holding the contrast in mind while you read it: LEB128 governs every number in the format except these eight bytes' worth. The header's conservatism and the body's compactness are the same design decision seen from two ends.
Next: Sections — what comes after the header, and a place where two reference tools report different numbers for the same bytes.