Bad wasm encoding due to std.debug.print()

I’m running into a really strange issue. The wasm files I’m getting from Zig 0.15.2 all have bad data segments whenever they contain calls to std.debug.print(). The following compiles correctly:

const std = @import("std");

pub fn hello() !void {
    var buffer: [256]u8 = undefined;
    const writer = std.fs.File.stderr().writer(&buffer);
    var stderr = writer.interface;
    try stderr.print("Hello\n", .{});
    // std.debug.print("Hello\n", .{});
}

As soon as I uncommented the debug print call, the output would be broken. Here’s the partial output from wasm-objdump -s --section=data <file>:

as-return-value.wasm:	file format wasm 0x1
000d527: error: invalid data segment flags: 0xb

Contents of section Data:
000d519: 0200 4180 8010 0b05 0041 8080 100b 0500  ..A......A......
000d529: 0000 0000 0041 8580 100b b617 0000 00aa  .....A..........
000d539: aaaa aaaa aaaa aa15 0000 0000 0000 00aa  ................
000d549: aaaa aaaa aaaa aa00 0000 0000 0000 00aa  ................

And a break down of the first line:

02                      // segment count

00                      // type (0, 1, 2)
41 80 80 10 0b          // opcodes (ending with 0b)
05                      // byte length
00 41 80 80 10          // 5 bytes
             
0b                      // bad type
05 00

The actual second segment actually seems to sit in the second line, starting with 00 41.

Here’s the output when the debug print is omitted:

as-return-value.wasm:	file format wasm 0x1

Contents of section Data:
000d09d: 0200 4180 8010 0b01 0000 4181 8010 0be8  ..A.......A.....
000d0ad: 1600 0000 0000 0000 aaaa aaaa aaaa aaaa  ................
000d0bd: 1500 0000 0000 0000 aaaa aaaa aaaa aaaa  ................
000d0cd: 0000 0000 0000 0000 aaaa aaaa aaaa aaaa  ................
02                      // segment count

00                      // type
41 80 80 10 0b          // opcodes
01                      // byte length
00                      // 1 byte

00                      // type
41 81 80 10 0b          // opcodes
e8 16                   // byte length
00 ...                  // 2920 bytes 

I’m really puzzled by this, given how frequently std.debug.print() is used. Wasn’t able to find anything in the issue tracker.

Does this happen in debug with the self-hosted x86 backend or in general?

I think maybe people mostly use wasm with release modes which could be the reason it wasn’t noticed yet?

You’re making a copy of the stderr Writer interface; you need to take a reference instead (with &)

5 Likes

It happens at all optimization levels. I’ve also tried -Dcpu=bleeding_edge and
-Dcpu=generic. Linking in libc yield the following warnings:

warning(object): unimplemented: element section in /home/cleong/.cache/zig/o/b732b09b1f3d21ce1b83c9cc8f803c5a/libc.a /home/cleong/.cache/zig/o/a4bcede13b1c8babe68df1355c3867bb/scandirat.o
warning(object): unimplemented: element section in /home/cleong/.cache/zig/o/b732b09b1f3d21ce1b83c9cc8f803c5a/libc.a /home/cleong/.cache/zig/o/93e053a668595ec9545bc2784f4139f8/preopens.o
warning(object): unimplemented: element section in /home/cleong/.cache/zig/o/b732b09b1f3d21ce1b83c9cc8f803c5a/libc.a /home/cleong/.cache/zig/o/40161c11888d600861002b3215a04f3c/posix.o
warning(object): unimplemented: element section in /home/cleong/.cache/zig/o/b732b09b1f3d21ce1b83c9cc8f803c5a/libc.a /home/cleong/.cache/zig/o/dc0433cd97e3c17c3e73bbc832d27a21/stdout.o
warning(object): unimplemented: element section in /home/cleong/.cache/zig/o/b732b09b1f3d21ce1b83c9cc8f803c5a/libc.a /home/cleong/.cache/zig/o/f0566cdd72f2e2d53d1a07588cd424bc/stderr.o
warning(object): unimplemented: element section in /home/cleong/.cache/zig/o/b732b09b1f3d21ce1b83c9cc8f803c5a/libc.a /home/cleong/.cache/zig/o/839094a270b3eedcb89f137dab3e64d4/fopencookie.o
warning(object): unimplemented: element section in /home/cleong/.cache/zig/o/b732b09b1f3d21ce1b83c9cc8f803c5a/libc.a /home/cleong/.cache/zig/o/e6d5b8871c3700df4c1ffbb9ba309673/stdin.o
warning(object): unimplemented: element section in /home/cleong/.cache/zig/o/b732b09b1f3d21ce1b83c9cc8f803c5a/libc.a /home/cleong/.cache/zig/o/153c1e39ad07a7d4bf92a641e0618214/qsort_nr.o
warning(object): unimplemented: element section in /home/cleong/.cache/zig/o/b732b09b1f3d21ce1b83c9cc8f803c5a/libc.a /home/cleong/.cache/zig/o/cc456186ab6bf200915dc0d8a5da1962/atexit.o
...

I should note that I’m not compiling the file in isolation. The code in question is compiled alongside other stuff. If I compile just the one file, the problem does not occur. In that that case the .wasm file would only contain one data segment.

I’m going to investigate this further to see if the problem is triggered by my code or if it happens when there’re multiple data segments.

You were actually on the right track. I had forgotten that I added this line in my build file because code generated by the x86-64 backend wasn’t working quite right when multithreaded:

        .use_llvm = cfg.multithreaded,

That ends up disabling llvm for wasm. I wonder if I should report the issue nonetheless. It appears to have something to do with threadlocal variables.

1 Like

FWIW I have this std.debug.print() call here in the sokol samples since forever:

…and that hasn’t caused any trouble so far when running in the browser.

I’m linking with Emscripten though. E.g. if it’s a bug in Zig then most likely in the Zig link step when targeting WASM.

const std = @import("std");

pub fn hello() !void {
    var buffer: [256]u8 = undefined;
    const writer = std.fs.File.stderr().writer(&buffer);
    const stderr = &writer.interface;
    try stderr.print("Hello\n", .{});
    std.debug.print("Hello\n", .{});
}

Will there still be problems using this code?

Your original code incorrectly copied writer.interface to stderr. stderr.print internally calls File.Writer.drain(), you can look at its implementation. It attempts to find the writer to which stderr belongs and modify its state. However, your stderr is a copy, so it no longer belongs to a writer, making the state modification a memory corruption.