Back to course

The Data Section and DataCount

21 minadvancedModule 4: Data PlacementContinues from wasm-elements
21 min · Module 4: Data Placement · Algorithms

Why This Matters

The data section is the element section's twin: it fills memories instead of tables. It has three forms rather than eight, it is the last section in the file, and it brings with it a section that exists for no other reason — datacount, which appears before the code section purely so the code section can be checked against it.

That section is the most interesting thing here, because it inverts the format's normal shape. Every other section is self-contained: the type section is valid on its own, the code section is valid on its own, the data section is valid on its own. The code section, though, contains memory.init and data.drop instructions that name data segment indices — and those instructions live in a section that appears before the data section that defines them. Without help a validator would have to either accept the indices unchecked or read the whole file twice.

So the format inserts a four-byte section between the element section and the code section containing nothing but a count. It is a forward declaration, and it is the only section in the format whose entire purpose is to make a later section checkable. It is a compromise that trades bytes for single-pass validation, and the reason the compromise was necessary rather than merely convenient is worth understanding.

A Simple Model

Three forms, and the split is the same one the element section made — active or passive, implicit or explicit memory — minus the expression variant, because a data element is always a raw byte string:

data section: count how many segments follow segment: flags 0, 1 or 2 ...then: flags = 0 active, memory 0 offset a constant expression size a LEB128 byte count bytes exactly that many raw bytes flags = 1 passive size a LEB128 byte count bytes exactly that many raw bytes flags = 2 active, explicit memory index memory_index a LEB128 offset a constant expression size a LEB128 byte count bytes exactly that many raw bytes

There is no expression form for a data segment, and there never will be one — the element section has four extra forms because a table element can be a computed reference, and a data element is bytes. That is not an oversight in the element section's design; it is the reason the two sections have different shapes, and it is the clearest evidence that the eight element forms were added for a capability the data section never needed.

And notice what the byte count is doing: it is a size in bytes, not a count of elements. A data segment is an opaque string, so the only way to know where it ends is to be told its length. This is the first place in the format where a payload is a counted blob rather than a sequence of self-describing records, and it is why a data section cannot be walked by reading structure — you read the count and then skip that many bytes.

The Actual Detail

All three forms, each in its own minimal module, hand-assembled and each accepted by wasm-validate and read back by wabt. The segment bytes first, then the readers' verdicts:

  form 0   00 41 00 0b 04 61 62 63 64
          |   |  |     |  +------- "abcd", four raw bytes
          |   |  |     +---------- size = 4
          |   |  +---------------- offset: i32.const 0 ; end
          |   +------------------- flags = 0, active into memory 0
          +-----------------------

  form 1   01 04 70 61 73 73
          |   |  +----------- "pass"
          |   +-------------- size = 4
          +------------------ flags = 1, PASSIVE: no memory, no offset

  form 2   02 00 41 00 0b 04 61 62 63 64
          |   |  |  |     |
          |   |  |  |     +------- "abcd"
          |   |  |  +------------ size = 4
          |   |  +--------------- offset: i32.const 0 ; end
          |   +------------------ memory index = 0
          +---------------------- flags = 2, active into an EXPLICIT memory

And the two readers, independently:

  $ wasm-validate data0.wasm   && echo VALID
  VALID
  $ wasm-objdump -x data0.wasm | sed -n '/^Data/,$p'
  Data[1]:
   - segment[0] memory=0 size=4 - init i32=0
    - 0000000: 6162 6364          abcd

  ...and for the passive one:
   - segment[0] passive size=4
    - 0000000: 7061 7373          pass

Three observations, each of which is a place a reader can go wrong. Form 0 and form 2 differ only by one byte — the explicit memory index — and for a module with one memory they are semantically identical, which is exactly the pattern the element section followed and exactly the reason a reader that treats 0x00 and 0x02 as unrelated shapes will miss the difference. The passive form has no offset and no memory index at all, so its size byte sits immediately after the flags byte, and a reader that expects an offset expression will read the size as an opcode. And the data is raw: 61 62 63 64 is "abcd" and there is no encoding step, no escaping, no NUL terminator, no length prefix beyond the section's own framing.

That last point is worth contrasting with the string encoding in the PE import directory, where names are NUL-terminated, or a COFF symbol name, which is 8 bytes with a length in a spare field and where longer names go in the string table. WebAssembly's data segments are the only place in the format with a genuinely arbitrary byte string, and they are length-prefixed rather than terminated — which is the right choice, because a length prefix permits embedded NULs and a terminator does not.

The datacount section

Here is the whole of it, in a module with one data segment:

  0c 01 00 00 00
    |  |  |
    |  |  +-- the count: ONE data segment follows, later
    |  +----- payload is one byte
    +-------- id 0x0c = datacount

  and the section it is describing, which comes LAST:
  0b 01 00 41 00 0b 04 61 62 63 64

Four bytes carrying one number, and that number is the segment count of a section that has not been read yet. The ordering constraint is the whole design: datacount is section 12, and it must appear between the element section (9) and the code section (10), so it is the one place where a section's position is not simply ascending by id. The section ordering concept covers why the ids ascend; this is the documented exception, and it exists precisely because the code section has to come before the data section while also being checkable against it.

And a real object confirms it. The simple.o sample from this course has a data section and wabt reports it:

  $ wasm-objdump -x simple.o | grep -A1 DataCount
  DataCount:
   - data count: 1

One number, and the validator uses it to check every memory.init and data.drop in the code section. Without it, memory.init 3 in a module with one data segment would have to be either trusted or deferred, and both are worse: trusting it lets a validated module trap at run time on an index the validator was supposed to have caught, and deferring means reading the data section before the code section, which the format's ordering forbids.

The subtlety: datacount is optional, and omitting it is not automatically an error. It is required when the code section contains a bulk-memory instruction that names a data segment. A module with a data section and no such instruction may legitimately omit it — so a reader must not treat its absence as "zero data segments", and must not treat its absence as an error without checking whether the code section actually needs it. A reader that requires datacount unconditionally will reject valid modules; a reader that treats it as optional unconditionally will fail to catch an out-of-range index. The correct rule is conditional, and it is the kind of rule that has to be stated explicitly because there is no way to infer it from the bytes of any one section. This is the same shape as the limits flags in the memory section: a bit that changes what is required, where getting the condition wrong produces a silent misreading rather than an error.

A Real Example

Validating a memory.init, which is the operation datacount exists to make possible, and it is a nice illustration of a general principle about validation order:

validate the code section, with `data_count` read from datacount: for each instruction: 0xfc 0x08 <segidx> 0x00 memory.init if datacount is ABSENT: reject the code names a data segment, so the forward declaration was required and is missing if segidx >= data_count: reject a module with 1 data segment cannot init segment 3 # and only NOW is a data_count of 0 meaningful: # it means "no segments", so any memory.init is invalid 0xfc 0x09 <segidx> data.drop same check, same datacount and for ELEMENT segments the same problem exists, which is why the element section needs no count section: its segments are described entirely by their own flags byte and count, and the table.init instruction is checked against a count that is already available in the section the instruction's own ordering permits.

Two things there are worth pulling apart. The first is that datacount gives the validator the number without giving it the content — it can bound-check an index against a count it already holds, and only read the data section later when it actually needs the bytes. That is what makes single-pass validation possible, and it is why the section is a count and not a copy of the data section's header.

The second is more interesting and slightly awkward: the element section needs no equivalent, and the reason is a subtlety in the ordering rules.table.init names an element segment, and the element section (9) comes before the code section (10), so a validator reading in order has already read the element section and knows the count. The data section (11) comes after the code section, so the same validator has not. Datacount is a workaround for an asymmetry in the section ordering, and it exists only on the side that needed it.

That is worth sitting with, because it shows a format being patched in the least invasive way rather than restructured. The alternative would have been to move the data section before the code section, which would have broken every existing module, or to have the validator make two passes, which costs more than four bytes for everyone. Datacount is four bytes paid by the producer to avoid two passes by the consumer — and it is a good trade precisely because almost every module either needs it or is not much hurt by the four bytes.

The last piece is what a passive data segment is for, and it is the same pattern as a passive element segment with a different payoff. A passive segment's bytes live in the module, not in a memory, until the module runs memory.init to copy them somewhere it chose. That is how a program gets read-only constant data into writable memory, which is a real problem: WebAssembly memories are writable, and a program that wants a large table of constants has to copy them in at start-up, because the data section can only write into a memory at instantiation. A native ELF or PE image maps a read-only section straight into memory with page protections set by the loader, and needs no copy at all. WebAssembly has no page protection, so it has to copy — and the passive form is how that copy is expressed without paying for it in every module that does not need it.

Try It Yourself

$ wasm-objdump -x data0.wasm | sed -n '/^Data/,$p'
$ wasm-objdump -x simple.o | sed -n '/DataCount/,+1p'

Check Your Understanding

Without looking back: a module has two data segments, both passive, and its code section contains no bulk-memory instructions. It has no datacount section. Is the module valid? And separately: what does a reader that requires datacount unconditionally do to a module like this, and why is that the wrong answer?

Apply It

You are writing a validator. It accepts every module with a data section, including ones whose code section contains memory.init 5 when only one data segment exists. wasm-validate rejects those. Your validator's data section reader is correct — it finds all the segments and their bytes. What is missing, what is the smallest correct change, and why does your validator currently accept a module that will trap at run time?

The general habit: a validation pass exists to make a promise about run-time behaviour. When you find a gap, ask which promise it breaks — and note that "validated code can still trap" breaks a much stronger promise than "validated code can still misbehave".

Connect

The datacount section is the clearest example in this format of a piece of metadata whose only consumer is a validator, and this collection has a few of those. A COFF section's NumberOfRelocations is read by a linker rather than by the code, and an ELF section header's sh_info is meaningful only to whichever tool interprets that section. In all three the value describes something the reader cannot see yet, and the tool that will see it has to be trusted to check it. Datacount is the strictest of the three, because the specification requires it exactly when a later section makes a claim the earlier one has to bound.

The counted-blob encoding of a data segment connects to the way every other format in this collection stores a string table or a resource blob. A COFF string table is a length-prefixed pool shared by every name in the file. A PE resource is a tree of offsets, and its data entries are a base-relative offset plus a length — the same two fields as a data segment, in a different order. A DWARF .debug_str entry is a NUL-terminated string at an offset, so it is the one case here that cannot hold an embedded NUL — which is a real limitation of that format and the reason the others chose differently.

And the passive segment's purpose — getting read-only constants into a writable memory because the format has no page protection — is the direct consequence of a design decision this course has met in several places. WebAssembly memories have no execute, read or write permissions, unlike an ELF PT_GNU_RELRO segment or a PE section's VirtualProtect flags. That simplicity is what makes a WebAssembly sandbox small enough to implement correctly in a browser tab, and it is also why the data section has to copy instead of map, and why a memory's optional maximum has to be enforced by the host rather than by the hardware.

That completes the core format. Every section has now been covered: the framing, the eight declaration and data sections, the code section, the instruction encoding, and the one section that exists only for validation. What is left is what happens before a module is a module — the object files a compiler emits, the linking and reloc sections that live in custom sections the core specification does not define, and the 5-byte LEB128 that a producer pads its section sizes to. That is the final concept.