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 }