lib/tldr/src/properties/formats/coff.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const hypothesis = @import("hypothesis");
3 const tldr = @import("tldr");
4
5 const coff = tldr.formats.coff;
6 const model = tldr.model;
7
8 const Allocator = std.mem.Allocator;
9
10 const header_size = @sizeOf(std.coff.Header);
11 const section_header_size = @sizeOf(std.coff.SectionHeader);
12 const symbol_size = std.coff.Symbol.sizeOf();
13 const relocation_size = 10;
14
15 pub fn settings(seed: u64, max_examples: usize) hypothesis.Settings {
16 var out = hypothesis.Settings.quick()
17 .withSeed(seed)
18 .withDatabase("zig-out/hypothesis-failures/tldr");
19 out.max_examples = max_examples;
20 return out;
21 }
22
23 fn drawUsize(data: *hypothesis.ConjectureData, min: usize, max: usize, shrink_towards: usize) !usize {
24 return @intCast(try data.drawInteger(
25 @intCast(min),
26 @intCast(max),
27 @intCast(shrink_towards),
28 ));
29 }
30
31 const ExpectedSection = struct {
32 name: []const u8,
33 size: u32,
34 offset: u32,
35 relocation_offset: u32,
36 relocation_count: u16,
37 flags: u32,
38 alignment: u16,
39 };
40
41 const ExpectedSymbol = struct {
42 raw_index: u32,
43 name: []const u8,
44 value: u32,
45 section_number: i16,
46 kind: u16,
47 storage_class: u8,
48 aux_count: u8,
49 };
50
51 const ExpectedRelocation = struct {
52 section_index: usize,
53 virtual_address: u32,
54 symbol_table_index: u32,
55 kind: u16,
56 };
57
58 const GeneratedObject = struct {
59 bytes: []u8,
60 target: model.Target,
61 sections: []ExpectedSection,
62 relocations: []ExpectedRelocation,
63 symbols: []ExpectedSymbol,
64 };
65
66 fn targetForMachine(machine: std.coff.IMAGE.FILE.MACHINE) model.Target {
67 return switch (machine) {
68 .AMD64 => .windows_x86_64_coff,
69 .ARM64 => .{
70 .object_format = .coff,
71 .architecture = .aarch64,
72 .endianness = .little,
73 .pointer_width_bits = 64,
74 },
75 else => unreachable,
76 };
77 }
78
79 fn addString(
80 allocator: Allocator,
81 table: *std.ArrayListUnmanaged(u8),
82 value: []const u8,
83 ) !u32 {
84 const offset = std.math.cast(u32, table.items.len) orelse return error.InvalidRange;
85 try table.appendSlice(allocator, value);
86 try table.append(allocator, 0);
87 return offset;
88 }
89
90 fn writeStringTableSize(table: []u8) void {
91 std.mem.writeInt(u32, table[0..4], @intCast(table.len), .little);
92 }
93
94 fn drawRelocationKind(
95 data: *hypothesis.ConjectureData,
96 architecture: model.Architecture,
97 section_size: u32,
98 ) !u16 {
99 var kinds: [24]u16 = undefined;
100 var count: usize = 0;
101 appendRelocationKind(&kinds, &count, 0x8000);
102 switch (architecture) {
103 .x86_64 => {
104 appendRelocationKindIfFits(&kinds, &count, architecture, @backingInt(std.coff.IMAGE.REL.AMD64.ADDR64), section_size);
105 appendRelocationKindIfFits(&kinds, &count, architecture, @backingInt(std.coff.IMAGE.REL.AMD64.ADDR32), section_size);
106 appendRelocationKindIfFits(&kinds, &count, architecture, @backingInt(std.coff.IMAGE.REL.AMD64.ADDR32NB), section_size);
107 appendRelocationKindIfFits(&kinds, &count, architecture, @backingInt(std.coff.IMAGE.REL.AMD64.REL32), section_size);
108 appendRelocationKindIfFits(&kinds, &count, architecture, @backingInt(std.coff.IMAGE.REL.AMD64.SECTION), section_size);
109 appendRelocationKindIfFits(&kinds, &count, architecture, @backingInt(std.coff.IMAGE.REL.AMD64.SECREL), section_size);
110 appendRelocationKindIfFits(&kinds, &count, architecture, @backingInt(std.coff.IMAGE.REL.AMD64.SECREL7), section_size);
111 appendRelocationKindIfFits(&kinds, &count, architecture, @backingInt(std.coff.IMAGE.REL.AMD64.TOKEN), section_size);
112 },
113 .aarch64 => {
114 appendRelocationKindIfFits(&kinds, &count, architecture, @backingInt(std.coff.IMAGE.REL.ARM64.ADDR64), section_size);
115 appendRelocationKindIfFits(&kinds, &count, architecture, @backingInt(std.coff.IMAGE.REL.ARM64.ADDR32), section_size);
116 appendRelocationKindIfFits(&kinds, &count, architecture, @backingInt(std.coff.IMAGE.REL.ARM64.ADDR32NB), section_size);
117 appendRelocationKindIfFits(&kinds, &count, architecture, @backingInt(std.coff.IMAGE.REL.ARM64.BRANCH26), section_size);
118 appendRelocationKindIfFits(&kinds, &count, architecture, @backingInt(std.coff.IMAGE.REL.ARM64.PAGEBASE_REL21), section_size);
119 appendRelocationKindIfFits(&kinds, &count, architecture, @backingInt(std.coff.IMAGE.REL.ARM64.SECTION), section_size);
120 appendRelocationKindIfFits(&kinds, &count, architecture, @backingInt(std.coff.IMAGE.REL.ARM64.SECREL), section_size);
121 appendRelocationKindIfFits(&kinds, &count, architecture, @backingInt(std.coff.IMAGE.REL.ARM64.BRANCH19), section_size);
122 appendRelocationKindIfFits(&kinds, &count, architecture, @backingInt(std.coff.IMAGE.REL.ARM64.REL32), section_size);
123 },
124 else => {},
125 }
126 return kinds[try drawUsize(data, 0, count - 1, 0)];
127 }
128
129 fn appendRelocationKind(kinds: *[24]u16, count: *usize, kind: u16) void {
130 kinds[count.*] = kind;
131 count.* += 1;
132 }
133
134 fn appendRelocationKindIfFits(
135 kinds: *[24]u16,
136 count: *usize,
137 architecture: model.Architecture,
138 kind: u16,
139 section_size: u32,
140 ) void {
141 const width = relocationWidth(architecture, kind) orelse 1;
142 if (width <= section_size) appendRelocationKind(kinds, count, kind);
143 }
144
145 fn generatedObject(
146 data: *hypothesis.ConjectureData,
147 allocator: Allocator,
148 ) !GeneratedObject {
149 const section_count = try drawUsize(data, 1, 6, 1);
150 const retained_symbol_count = try drawUsize(data, 0, 8, 1);
151 const machine = if (try data.drawBoolean()) std.coff.IMAGE.FILE.MACHINE.AMD64 else std.coff.IMAGE.FILE.MACHINE.ARM64;
152 const target = targetForMachine(machine);
153
154 var string_table: std.ArrayListUnmanaged(u8) = .empty;
155 try string_table.appendNTimes(allocator, 0, 4);
156
157 const sections = try allocator.alloc(ExpectedSection, section_count);
158 const section_string_offsets = try allocator.alloc(?u32, section_count);
159 var content_cursor = header_size + section_count * section_header_size;
160 var total_relocation_count: usize = 0;
161 for (sections, 0..) |*section, index| {
162 const long_name = try data.drawBoolean();
163 const name = if (long_name)
164 try std.fmt.allocPrint(allocator, ".pbt$section${d}", .{index})
165 else
166 try std.fmt.allocPrint(allocator, ".s{d}", .{index});
167 section_string_offsets[index] = if (long_name) try addString(allocator, &string_table, name) else null;
168 const size = try drawUsize(data, 0, 12, 0);
169 const relocation_count = if (size == 0) 0 else try drawUsize(data, 0, 2, 0);
170 total_relocation_count += relocation_count;
171 const raw_data_offset = if (size == 0) 0 else content_cursor;
172 content_cursor += size;
173 const relocation_offset = if (relocation_count == 0) 0 else content_cursor;
174 content_cursor += relocation_count * relocation_size;
175 const alignment_selector = try drawUsize(data, 0, 4, 0);
176 const alignment_encoded: u32 = @intCast(alignment_selector + 1);
177 const flags = (alignment_encoded << 20) | 0x20;
178 section.* = .{
179 .name = name,
180 .size = @intCast(size),
181 .offset = @intCast(raw_data_offset),
182 .relocation_offset = @intCast(relocation_offset),
183 .relocation_count = @intCast(relocation_count),
184 .flags = flags,
185 .alignment = sectionAlignment(flags),
186 };
187 }
188
189 const symbols = try allocator.alloc(ExpectedSymbol, retained_symbol_count);
190 const symbol_string_offsets = try allocator.alloc(?u32, retained_symbol_count);
191 const symbol_aux_counts = try allocator.alloc(u8, retained_symbol_count);
192 const primary_symbol_indices = try allocator.alloc(u32, retained_symbol_count);
193 var raw_symbol_count: usize = 0;
194 for (symbols, 0..) |*symbol, index| {
195 const long_name = try data.drawBoolean();
196 const name = if (long_name)
197 try std.fmt.allocPrint(allocator, "pbt_long_external_symbol_{d}", .{index})
198 else
199 try std.fmt.allocPrint(allocator, "sym{d}", .{index});
200 symbol_string_offsets[index] = if (long_name) try addString(allocator, &string_table, name) else null;
201 const aux_count = try drawUsize(data, 0, 2, 0);
202 symbol_aux_counts[index] = @intCast(aux_count);
203 primary_symbol_indices[index] = @intCast(raw_symbol_count);
204 raw_symbol_count += 1 + aux_count;
205 symbol.* = .{
206 .raw_index = primary_symbol_indices[index],
207 .name = name,
208 .value = @intCast(try drawUsize(data, 0, 128, 0)),
209 .section_number = @intCast(try drawUsize(data, 0, section_count, 0)),
210 .kind = @intCast(try drawUsize(data, 0, 0xffff, 0)),
211 .storage_class = if (try data.drawBoolean()) @backingInt(std.coff.StorageClass.EXTERNAL) else @backingInt(std.coff.StorageClass.STATIC),
212 .aux_count = @intCast(aux_count),
213 };
214 }
215 writeStringTableSize(string_table.items);
216
217 const relocations = try allocator.alloc(ExpectedRelocation, total_relocation_count);
218
219 const symbol_table_offset = content_cursor;
220 const string_table_offset = symbol_table_offset + raw_symbol_count * symbol_size;
221 const total_size = string_table_offset + string_table.items.len;
222 const bytes = try allocator.alloc(u8, total_size);
223 @memset(bytes, 0);
224
225 writeU16(bytes, 0, @backingInt(machine));
226 writeU16(bytes, 2, @intCast(section_count));
227 writeU32(bytes, 8, @intCast(symbol_table_offset));
228 writeU32(bytes, 12, @intCast(raw_symbol_count));
229
230 for (sections, 0..) |section, index| {
231 const section_offset = header_size + index * section_header_size;
232 if (section_string_offsets[index]) |offset| {
233 var name_buffer: [8]u8 = @as([8]u8, @splat(0));
234 const rendered = try std.fmt.bufPrint(&name_buffer, "/{d}", .{offset});
235 @memcpy(bytes[section_offset..][0..rendered.len], rendered);
236 } else {
237 writeName(bytes, section_offset, 8, section.name);
238 }
239 writeU32(bytes, section_offset + 16, section.size);
240 writeU32(bytes, section_offset + 20, section.offset);
241 writeU32(bytes, section_offset + 24, section.relocation_offset);
242 writeU16(bytes, section_offset + 32, section.relocation_count);
243 writeU32(bytes, section_offset + 36, section.flags);
244 if (section.size != 0) {
245 @memset(bytes[section.offset..][0..section.size], @intCast(index + 1));
246 }
247 }
248
249 var relocation_cursor: usize = 0;
250 for (sections, 0..) |section, section_index| {
251 var index: usize = 0;
252 while (index < section.relocation_count) : (index += 1) {
253 const relocation_offset = @as(usize, section.relocation_offset) + index * relocation_size;
254 const kind = if (retained_symbol_count == 0)
255 0
256 else
257 try drawRelocationKind(data, target.architecture, section.size);
258 const width = relocationWidth(target.architecture, @intCast(kind)) orelse 1;
259 const section_size: u64 = section.size;
260 const symbol_table_index = if (kind == 0 or retained_symbol_count == 0) 0 else primary_symbol_indices[try drawUsize(data, 0, retained_symbol_count - 1, 0)];
261 const relocation = ExpectedRelocation{
262 .section_index = section_index,
263 .virtual_address = @intCast(try drawUsize(data, 0, try checkedUsize(section_size - width), 0)),
264 .symbol_table_index = @intCast(symbol_table_index),
265 .kind = @intCast(kind),
266 };
267 relocations[relocation_cursor] = relocation;
268 writeU32(bytes, relocation_offset, relocation.virtual_address);
269 writeU32(bytes, relocation_offset + 4, relocation.symbol_table_index);
270 writeU16(bytes, relocation_offset + 8, relocation.kind);
271 relocation_cursor += 1;
272 }
273 }
274
275 var raw_symbol_index: usize = 0;
276 for (symbols, 0..) |symbol, index| {
277 const symbol_offset = symbol_table_offset + raw_symbol_index * symbol_size;
278 if (symbol_string_offsets[index]) |offset| {
279 writeU32(bytes, symbol_offset + 4, offset);
280 } else {
281 writeName(bytes, symbol_offset, 8, symbol.name);
282 }
283 writeU32(bytes, symbol_offset + 8, symbol.value);
284 writeI16(bytes, symbol_offset + 12, symbol.section_number);
285 writeU16(bytes, symbol_offset + 14, symbol.kind);
286 bytes[symbol_offset + 16] = symbol.storage_class;
287 bytes[symbol_offset + 17] = symbol.aux_count;
288 raw_symbol_index += 1 + symbol_aux_counts[index];
289 }
290 @memcpy(bytes[string_table_offset..][0..string_table.items.len], string_table.items);
291
292 return .{
293 .bytes = bytes,
294 .target = target,
295 .sections = sections,
296 .relocations = relocations,
297 .symbols = symbols,
298 };
299 }
300
301 pub const MetadataProperty = struct {
302 pub fn property(data: *hypothesis.ConjectureData, property_allocator: Allocator) !void {
303 var arena_state = std.heap.ArenaAllocator.init(property_allocator);
304 defer arena_state.deinit();
305 const allocator = arena_state.allocator();
306
307 const generated = try generatedObject(data, allocator);
308 var object = try coff.parseObject(allocator, generated.bytes);
309 defer object.deinit(allocator);
310
311 try std.testing.expectEqual(generated.target.object_format, object.target.object_format);
312 try std.testing.expectEqual(generated.target.architecture, object.target.architecture);
313 try std.testing.expectEqual(generated.sections.len, object.sections.len);
314 for (generated.sections, object.sections) |expected, actual| {
315 try std.testing.expectEqualStrings(expected.name, actual.name);
316 try std.testing.expectEqual(expected.size, actual.size);
317 try std.testing.expectEqual(expected.offset, actual.offset);
318 try std.testing.expectEqual(expected.relocation_offset, actual.relocation_offset);
319 try std.testing.expectEqual(expected.relocation_count, actual.relocation_count);
320 try std.testing.expectEqual(expected.flags, actual.flags);
321 try std.testing.expectEqual(expected.alignment, actual.alignment);
322 }
323 try std.testing.expectEqual(generated.relocations.len, object.relocations.len);
324 for (generated.relocations, object.relocations) |expected, actual| {
325 try std.testing.expectEqual(expected.section_index, actual.section_index);
326 try std.testing.expectEqual(expected.virtual_address, actual.virtual_address);
327 try std.testing.expectEqual(expected.symbol_table_index, actual.symbol_table_index);
328 try std.testing.expectEqual(expected.kind, actual.kind);
329 }
330 try std.testing.expectEqual(generated.symbols.len, object.symbols.len);
331 for (generated.symbols, object.symbols) |expected, actual| {
332 try std.testing.expectEqual(expected.raw_index, actual.raw_index);
333 try std.testing.expectEqualStrings(expected.name, actual.name);
334 try std.testing.expectEqual(expected.value, actual.value);
335 try std.testing.expectEqual(expected.section_number, actual.section_number);
336 try std.testing.expectEqual(expected.kind, actual.kind);
337 try std.testing.expectEqual(expected.storage_class, actual.storage_class);
338 try std.testing.expectEqual(expected.aux_count, actual.aux_count);
339 }
340
341 var metadata = try coff.parseObjectMetadata(allocator, generated.bytes);
342 defer metadata.deinit(allocator);
343
344 try std.testing.expectEqual(generated.sections.len, metadata.sections.len);
345 try std.testing.expectEqual(generated.symbols.len, metadata.symbols.len);
346 for (generated.sections, metadata.sections) |expected, actual| {
347 try std.testing.expectEqualStrings(expected.name, actual.name);
348 try std.testing.expectEqual(@as(u64, expected.size), actual.size);
349 try std.testing.expectEqual(expected.alignment, actual.alignment);
350 try std.testing.expectEqual(expected.relocation_count, actual.relocation_count);
351 }
352 for (generated.symbols, metadata.symbols) |expected, actual| {
353 try std.testing.expectEqualStrings(expected.name, actual.name);
354 try std.testing.expectEqual(@as(i32, expected.section_number), actual.section_index);
355 try std.testing.expectEqual(@as(u64, expected.value), actual.value);
356 try std.testing.expectEqual(expected.storage_class == @backingInt(std.coff.StorageClass.EXTERNAL), actual.external);
357 try std.testing.expectEqual(expected.section_number == 0, actual.undefined);
358 }
359 }
360 };
361
362 test "pbt: COFF parser preserves generated object metadata" {
363 try hypothesis.checkNamed(
364 MetadataProperty,
365 "tldr-coff-object-metadata",
366 settings(0xC0FF_EE64, 48),
367 );
368 }
369
370 fn relocationWidth(architecture: model.Architecture, kind: u16) ?u64 {
371 return switch (architecture) {
372 .x86_64 => amd64RelocationWidth(kind),
373 .aarch64 => arm64RelocationWidth(kind),
374 else => null,
375 };
376 }
377
378 fn amd64RelocationWidth(kind: u16) ?u64 {
379 return switch (@as(std.coff.IMAGE.REL.AMD64, @fromBackingInt(@intCast(kind)))) {
380 .ADDR64 => 8,
381 .SECTION => 2,
382 .SECREL7 => 1,
383 .ADDR32,
384 .ADDR32NB,
385 .REL32,
386 .REL32_1,
387 .REL32_2,
388 .REL32_3,
389 .REL32_4,
390 .REL32_5,
391 .SECREL,
392 .TOKEN,
393 .SREL32,
394 .SSPAN32,
395 => 4,
396 else => null,
397 };
398 }
399
400 fn arm64RelocationWidth(kind: u16) ?u64 {
401 return switch (@as(std.coff.IMAGE.REL.ARM64, @fromBackingInt(@intCast(kind)))) {
402 .SECTION => 2,
403 .ADDR64 => 8,
404 .ADDR32,
405 .ADDR32NB,
406 .BRANCH26,
407 .PAGEBASE_REL21,
408 .REL21,
409 .PAGEOFFSET_12A,
410 .PAGEOFFSET_12L,
411 .SECREL,
412 .SECREL_LOW12A,
413 .SECREL_HIGH12A,
414 .SECREL_LOW12L,
415 .TOKEN,
416 .BRANCH19,
417 .BRANCH14,
418 .REL32,
419 => 4,
420 else => null,
421 };
422 }
423
424 fn sectionAlignment(flags: u32) u16 {
425 const encoded = @as(u4, @truncate((flags >> 20) & 0xf));
426 if (encoded == 0) return 1;
427 return @as(u16, 1) << (encoded - 1);
428 }
429
430 fn checkedUsize(value: anytype) !usize {
431 return std.math.cast(usize, value) orelse error.InvalidRange;
432 }
433
434 fn writeU16(bytes: []u8, offset: usize, value: u16) void {
435 std.mem.writeInt(u16, bytes[offset..][0..2], value, .little);
436 }
437
438 fn writeI16(bytes: []u8, offset: usize, value: i16) void {
439 std.mem.writeInt(i16, bytes[offset..][0..2], value, .little);
440 }
441
442 fn writeU32(bytes: []u8, offset: usize, value: u32) void {
443 std.mem.writeInt(u32, bytes[offset..][0..4], value, .little);
444 }
445
446 fn writeName(bytes: []u8, offset: usize, comptime size: usize, value: []const u8) void {
447 @memset(bytes[offset..][0..size], 0);
448 @memcpy(bytes[offset..][0..value.len], value);
449 }