1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
|
//! Build tool that patches a WASM binary to make the function table
//! growable by removing its maximum size limit.
//!
//! Zig's WASM linker doesn't support `--growable-table`, so the table
//! is emitted with max == min. This tool finds the table section (id 4)
//! and changes the limits flag from 0x01 (has max) to 0x00 (no max),
//! removing the max field.
//!
//! Usage: wasm_growable_table <input.wasm> <output.wasm>
const std = @import("std");
const testing = std.testing;
const Allocator = std.mem.Allocator;
pub fn main(init: std.process.Init) !void {
// This is a one-off patcher, so we leak all our memory on purpose
// and let the OS clean it up when we exit.
const alloc = init.arena.allocator();
// Parse args: program input output
const args = try init.minimal.args.toSlice(alloc);
if (args.len != 3) {
std.log.err("usage: wasm_growable_table <input.wasm> <output.wasm>", .{});
std.process.exit(1);
unreachable;
}
// Patch the file.
const output: []const u8 = try patchTableGrowable(
alloc,
try std.Io.Dir.cwd().readFileAlloc(
init.io,
args[1],
alloc,
.unlimited,
),
);
// Write our output
const out_file = try std.Io.Dir.cwd().createFile(init.io, args[2], .{});
defer out_file.close(init.io);
try out_file.writePositionalAll(init.io, output, 0);
}
/// Patch the WASM binary's table section to remove the maximum size
/// limit, making the table growable. If the table already has no max
/// or no table section is found, the input is returned unchanged.
///
/// The WASM table section (id=4) encodes table limits as:
/// - flags=0x00, min (LEB128) — no max, growable
/// - flags=0x01, min (LEB128), max (LEB128) — bounded, not growable
///
/// This function rewrites the section to use flags=0x00, dropping the
/// max field entirely.
fn patchTableGrowable(
alloc: Allocator,
input: []const u8,
) (error{InvalidWasm} || std.Io.Writer.Error)![]const u8 {
if (input.len < 8) return error.InvalidWasm;
// Start after the 8-byte WASM header (magic + version).
var pos: usize = 8;
while (pos < input.len) {
const section_id = input[pos];
pos += 1;
const section_size = readLeb128(input, &pos);
const section_start = pos;
// We're looking for section 4 (the table section).
if (section_id != 4) {
pos = section_start + section_size;
continue;
}
_ = readLeb128(input, &pos); // table count
pos += 1; // elem_type (0x70 = funcref)
const flags = input[pos];
// flags bit 0 indicates whether a max is present.
if (flags & 1 == 0) {
// Already no max, nothing to patch.
return input;
}
// Record positions of each field so we can reconstruct
// the section without the max value.
const flags_pos = pos;
pos += 1; // skip flags byte
const min_start = pos;
_ = readLeb128(input, &pos); // min
const max_start = pos;
_ = readLeb128(input, &pos); // max
const max_end = pos;
const section_end = section_start + section_size;
// Build the new section payload with the max removed:
// [table count + elem_type] [flags=0x00] [min] [trailing data]
var payload: std.Io.Writer.Allocating = .init(alloc);
try payload.writer.writeAll(input[section_start..flags_pos]);
try payload.writer.writeByte(0x00); // flags: no max
try payload.writer.writeAll(input[min_start..max_start]);
try payload.writer.writeAll(input[max_end..section_end]);
// Reassemble the full binary:
// [everything before this section] [section id] [new size] [new payload] [everything after]
const before_section = input[0 .. section_start - 1 - uleb128Size(section_size)];
var result: std.Io.Writer.Allocating = .init(alloc);
try result.writer.writeAll(before_section);
try result.writer.writeByte(4); // table section id
try result.writer.writeUleb128(@as(u32, @intCast(payload.written().len)));
try result.writer.writeAll(payload.written());
try result.writer.writeAll(input[section_end..]);
return result.written();
}
// No table section found; return input unchanged.
return input;
}
/// Decode an unsigned LEB128 value from `bytes` starting at `pos.*`,
/// advancing `pos` past the encoded bytes.
fn readLeb128(bytes: []const u8, pos: *usize) u32 {
var result: u32 = 0;
var shift: u5 = 0;
while (true) {
const byte = bytes[pos.*];
pos.* += 1;
result |= @as(u32, byte & 0x7f) << shift;
if (byte & 0x80 == 0) return result;
shift +%= 7;
}
}
/// Return the number of bytes needed to encode `value` as unsigned LEB128.
fn uleb128Size(value: u32) usize {
var v = value;
var size: usize = 0;
while (true) {
v >>= 7;
size += 1;
if (v == 0) return size;
}
}
/// Minimal valid WASM module with a bounded table (min=1, max=1).
/// Sections: type(1), table(4), export(7).
const test_wasm_bounded_table = [_]u8{
0x00, 0x61, 0x73, 0x6d, // magic
0x01, 0x00, 0x00, 0x00, // version
// Type section (id=1): 1 type, () -> ()
0x01, 0x04, 0x01, 0x60,
0x00, 0x00,
// Table section (id=4): 1 table, funcref, flags=1, min=1, max=1
0x04, 0x05,
0x01, 0x70, 0x01, 0x01,
0x01,
// Export section (id=7): 0 exports
0x07, 0x01, 0x00,
};
/// Same module but the table already has no max (flags=0).
const test_wasm_growable_table = [_]u8{
0x00, 0x61, 0x73, 0x6d, // magic
0x01, 0x00, 0x00, 0x00, // version
// Type section (id=1)
0x01, 0x04, 0x01, 0x60,
0x00, 0x00,
// Table section (id=4): 1 table, funcref, flags=0, min=1
0x04, 0x04,
0x01, 0x70, 0x00, 0x01,
// Export section (id=7): 0 exports
0x07, 0x01, 0x00,
};
/// Module with no table section at all.
const test_wasm_no_table = [_]u8{
0x00, 0x61, 0x73, 0x6d, // magic
0x01, 0x00, 0x00, 0x00, // version
// Type section (id=1)
0x01, 0x04, 0x01, 0x60,
0x00, 0x00,
// Export section (id=7): 0 exports
0x07, 0x01,
0x00,
};
test "patches bounded table to remove max" {
// We use a non-checking allocator because the patched result is
// intentionally leaked (matches the real main() usage).
const result = try patchTableGrowable(
std.heap.page_allocator,
&test_wasm_bounded_table,
);
// Result should differ from input (max was removed).
try testing.expect(!std.mem.eql(
u8,
result,
&test_wasm_bounded_table,
));
// Find the table section in the output and verify flags=0x00.
var pos: usize = 8;
while (pos < result.len) {
const id = result[pos];
pos += 1;
const size = readLeb128(result, &pos);
if (id == 4) {
_ = readLeb128(result, &pos); // table count
pos += 1; // elem_type
// flags should now be 0x00 (no max).
try testing.expectEqual(@as(u8, 0x00), result[pos]);
return;
}
pos += size;
}
return error.TableSectionNotFound;
}
test "already growable table is returned unchanged" {
const result = try patchTableGrowable(
testing.allocator,
&test_wasm_growable_table,
);
try testing.expectEqual(
@as([*]const u8, &test_wasm_growable_table),
result.ptr,
);
}
test "no table section returns input unchanged" {
const result = try patchTableGrowable(
testing.allocator,
&test_wasm_no_table,
);
try testing.expectEqual(@as([*]const u8, &test_wasm_no_table), result.ptr);
}
test "too short input returns InvalidWasm" {
try testing.expectError(
error.InvalidWasm,
patchTableGrowable(testing.allocator, "short"),
);
}
test "readLeb128 single byte" {
const bytes = [_]u8{0x05};
var pos: usize = 0;
try testing.expectEqual(@as(u32, 5), readLeb128(&bytes, &pos));
try testing.expectEqual(@as(usize, 1), pos);
}
test "readLeb128 multi byte" {
// 300 = 0b100101100 → LEB128: 0xAC 0x02
const bytes = [_]u8{ 0xAC, 0x02 };
var pos: usize = 0;
try testing.expectEqual(@as(u32, 300), readLeb128(&bytes, &pos));
try testing.expectEqual(@as(usize, 2), pos);
}
test "uleb128Size" {
try testing.expectEqual(@as(usize, 1), uleb128Size(0));
try testing.expectEqual(@as(usize, 1), uleb128Size(0x7f));
try testing.expectEqual(@as(usize, 2), uleb128Size(0x80));
try testing.expectEqual(@as(usize, 2), uleb128Size(300));
try testing.expectEqual(@as(usize, 5), uleb128Size(std.math.maxInt(u32)));
}
|