lib/tldr/src/properties/formats/macho.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const hypothesis = @import("hypothesis");
  3 const tldr = @import("tldr");
  4 
  5 const macho = tldr.formats.macho;
  6 const model = tldr.model;
  7 
  8 const Allocator = std.mem.Allocator;
  9 
 10 const header_size = @sizeOf(std.macho.mach_header_64);
 11 const segment_command_64_size = @sizeOf(std.macho.segment_command_64);
 12 const section_64_size = @sizeOf(std.macho.section_64);
 13 const symtab_command_size = @sizeOf(std.macho.symtab_command);
 14 const nlist_64_size = @sizeOf(std.macho.nlist_64);
 15 const relocation_info_size = @sizeOf(std.macho.relocation_info);
 16 
 17 pub fn settings(seed: u64, max_examples: usize) hypothesis.Settings {
 18     var out = hypothesis.Settings.quick()
 19         .withSeed(seed)
 20         .withDatabase("zig-out/hypothesis-failures/tldr");
 21     out.max_examples = max_examples;
 22     return out;
 23 }
 24 
 25 fn drawUsize(data: *hypothesis.ConjectureData, min: usize, max: usize, shrink_towards: usize) !usize {
 26     return @intCast(try data.drawInteger(
 27         @intCast(min),
 28         @intCast(max),
 29         @intCast(shrink_towards),
 30     ));
 31 }
 32 
 33 const ExpectedSection = struct {
 34     segment_name: []const u8,
 35     name: []const u8,
 36     address: u64,
 37     size: u64,
 38     offset: u32,
 39     alignment_shift: u32,
 40     relocation_offset: u32,
 41     relocation_count: u32,
 42     flags: u32,
 43 };
 44 
 45 const ExpectedSymbol = struct {
 46     name: []const u8,
 47     section_index: u8,
 48     value: u64,
 49     kind: u8,
 50     external: bool,
 51 };
 52 
 53 const ExpectedRelocation = struct {
 54     section_index: usize,
 55     address: i32,
 56     symbol_number: u32,
 57     pc_relative: bool,
 58     length: u8,
 59     external: bool,
 60     kind: u8,
 61 };
 62 
 63 const GeneratedObject = struct {
 64     bytes: []u8,
 65     target: model.Target,
 66     sections: []ExpectedSection,
 67     relocations: []ExpectedRelocation,
 68     symbols: []ExpectedSymbol,
 69 };
 70 
 71 fn targetForCpuType(cpu_type: std.macho.cpu_type_t) model.Target {
 72     if (cpu_type == std.macho.CPU_TYPE_X86_64) return .macos_x86_64_macho;
 73     return .{
 74         .object_format = .macho,
 75         .architecture = .aarch64,
 76         .endianness = .little,
 77         .pointer_width_bits = 64,
 78     };
 79 }
 80 
 81 fn addString(
 82     allocator: Allocator,
 83     table: *std.ArrayListUnmanaged(u8),
 84     value: []const u8,
 85 ) !u32 {
 86     const offset = std.math.cast(u32, table.items.len) orelse return error.InvalidRange;
 87     try table.appendSlice(allocator, value);
 88     try table.append(allocator, 0);
 89     return offset;
 90 }
 91 
 92 fn generatedObject(
 93     data: *hypothesis.ConjectureData,
 94     allocator: Allocator,
 95 ) !GeneratedObject {
 96     const section_count = try drawUsize(data, 0, 6, 1);
 97     const symbol_count = try drawUsize(data, 0, 8, 1);
 98     const cpu_type: std.macho.cpu_type_t = if (try data.drawBoolean()) std.macho.CPU_TYPE_X86_64 else std.macho.CPU_TYPE_ARM64;
 99     const target = targetForCpuType(cpu_type);
100 
101     const sections = try allocator.alloc(ExpectedSection, section_count);
102     const segment_load_size = segment_command_64_size + section_count * section_64_size;
103     const load_size = segment_load_size + symtab_command_size;
104     var content_cursor = header_size + load_size;
105     var total_relocation_count: usize = 0;
106 
107     for (sections, 0..) |*section, index| {
108         const size = try drawUsize(data, 0, 12, 0);
109         const relocation_count = if (size == 0) 0 else try drawUsize(data, 0, 2, 0);
110         total_relocation_count += relocation_count;
111         const data_offset = if (size == 0) 0 else content_cursor;
112         content_cursor += size;
113         const relocation_offset = if (relocation_count == 0) 0 else content_cursor;
114         content_cursor += relocation_count * relocation_info_size;
115         const alignment_shift = try drawUsize(data, 0, 5, 0);
116         section.* = .{
117             .segment_name = if (index % 2 == 0) "__TEXT" else "__DATA",
118             .name = try std.fmt.allocPrint(allocator, "__sect{d}", .{index}),
119             .address = try data.drawInteger(0, 128, 0),
120             .size = size,
121             .offset = @intCast(data_offset),
122             .alignment_shift = @intCast(alignment_shift),
123             .relocation_offset = @intCast(relocation_offset),
124             .relocation_count = @intCast(relocation_count),
125             .flags = @intCast(try drawUsize(data, 0, 0xff, 0)),
126         };
127     }
128 
129     const relocations = try allocator.alloc(ExpectedRelocation, total_relocation_count);
130     var string_table: std.ArrayListUnmanaged(u8) = .empty;
131     try string_table.append(allocator, 0);
132     const symbols = try allocator.alloc(ExpectedSymbol, symbol_count);
133     const string_offsets = try allocator.alloc(u32, symbol_count);
134     for (symbols, 0..) |*symbol, index| {
135         const name = try std.fmt.allocPrint(allocator, "_pbt_macho_sym_{d}", .{index});
136         string_offsets[index] = try addString(allocator, &string_table, name);
137         const section_index = if (section_count == 0) 0 else try drawUsize(data, 0, section_count, 0);
138         const external = try data.drawBoolean();
139         const kind: u8 = if (section_index == 0) @intFromBool(external) else if (external) 0x0f else 0x0e;
140         symbol.* = .{
141             .name = name,
142             .section_index = @intCast(section_index),
143             .value = try data.drawInteger(0, 256, 0),
144             .kind = kind,
145             .external = external,
146         };
147     }
148 
149     const symbol_offset = content_cursor;
150     const string_offset = symbol_offset + symbol_count * nlist_64_size;
151     const total_size = string_offset + string_table.items.len;
152     const bytes = try allocator.alloc(u8, total_size);
153     @memset(bytes, 0);
154 
155     writeU32(bytes, 0, std.macho.MH_MAGIC_64);
156     writeU32(bytes, 4, @bitCast(cpu_type));
157     writeU32(bytes, 12, std.macho.MH_OBJECT);
158     writeU32(bytes, 16, 2);
159     writeU32(bytes, 20, @intCast(load_size));
160 
161     const segment_offset = header_size;
162     writeU32(bytes, segment_offset, @backingInt(std.macho.LC.SEGMENT_64));
163     writeU32(bytes, segment_offset + 4, @intCast(segment_load_size));
164     writeName(bytes, segment_offset + 8, 16, "__TEXT");
165     writeU32(bytes, segment_offset + 64, @intCast(section_count));
166 
167     for (sections, 0..) |section, index| {
168         const section_offset = segment_offset + segment_command_64_size + index * section_64_size;
169         writeName(bytes, section_offset, 16, section.name);
170         writeName(bytes, section_offset + 16, 16, section.segment_name);
171         writeU64(bytes, section_offset + 32, section.address);
172         writeU64(bytes, section_offset + 40, section.size);
173         writeU32(bytes, section_offset + 48, section.offset);
174         writeU32(bytes, section_offset + 52, section.alignment_shift);
175         writeU32(bytes, section_offset + 56, section.relocation_offset);
176         writeU32(bytes, section_offset + 60, section.relocation_count);
177         writeU32(bytes, section_offset + 64, section.flags);
178         if (section.size != 0) {
179             const size: usize = @intCast(section.size);
180             @memset(bytes[section.offset..][0..size], @intCast(index + 1));
181         }
182     }
183 
184     var relocation_cursor: usize = 0;
185     for (sections, 0..) |section, section_index| {
186         var index: usize = 0;
187         while (index < section.relocation_count) : (index += 1) {
188             const relocation_offset = @as(usize, section.relocation_offset) + index * relocation_info_size;
189             const external = symbol_count != 0 and try data.drawBoolean();
190             const symbol_number = if (external)
191                 try drawUsize(data, 0, symbol_count - 1, 0)
192             else if (section_count == 0)
193                 0
194             else
195                 try drawUsize(data, 1, section_count, 1);
196             const length = try drawUsize(data, 0, maxRelocationLength(section.size), 0);
197             const width = relocationWidth(@intCast(length));
198             const relocation = ExpectedRelocation{
199                 .section_index = section_index,
200                 .address = @intCast(try drawUsize(data, 0, try checkedUsize(section.size - width), 0)),
201                 .symbol_number = @intCast(symbol_number),
202                 .pc_relative = try data.drawBoolean(),
203                 .length = @intCast(length),
204                 .external = external,
205                 .kind = @intCast(try drawUsize(data, 0, 10, 0)),
206             };
207             relocations[relocation_cursor] = relocation;
208             writeRelocation(bytes, relocation_offset, relocation);
209             relocation_cursor += 1;
210         }
211     }
212 
213     const symtab_offset = segment_offset + segment_load_size;
214     writeU32(bytes, symtab_offset, @backingInt(std.macho.LC.SYMTAB));
215     writeU32(bytes, symtab_offset + 4, symtab_command_size);
216     writeU32(bytes, symtab_offset + 8, @intCast(symbol_offset));
217     writeU32(bytes, symtab_offset + 12, @intCast(symbol_count));
218     writeU32(bytes, symtab_offset + 16, @intCast(string_offset));
219     writeU32(bytes, symtab_offset + 20, @intCast(string_table.items.len));
220 
221     for (symbols, 0..) |symbol, index| {
222         const offset = symbol_offset + index * nlist_64_size;
223         writeU32(bytes, offset, string_offsets[index]);
224         bytes[offset + 4] = symbol.kind;
225         bytes[offset + 5] = symbol.section_index;
226         writeU64(bytes, offset + 8, symbol.value);
227     }
228     @memcpy(bytes[string_offset..][0..string_table.items.len], string_table.items);
229 
230     return .{
231         .bytes = bytes,
232         .target = target,
233         .sections = sections,
234         .relocations = relocations,
235         .symbols = symbols,
236     };
237 }
238 
239 pub const MetadataProperty = struct {
240     pub fn property(data: *hypothesis.ConjectureData, property_allocator: Allocator) !void {
241         var arena_state = std.heap.ArenaAllocator.init(property_allocator);
242         defer arena_state.deinit();
243         const allocator = arena_state.allocator();
244 
245         const generated = try generatedObject(data, allocator);
246         var object = try macho.parseObject(allocator, generated.bytes);
247         defer object.deinit(allocator);
248 
249         try std.testing.expectEqual(generated.target.object_format, object.target.object_format);
250         try std.testing.expectEqual(generated.target.architecture, object.target.architecture);
251         try std.testing.expectEqual(generated.sections.len, object.sections.len);
252         for (generated.sections, object.sections) |expected, actual| {
253             try std.testing.expectEqualStrings(expected.segment_name, actual.segment_name);
254             try std.testing.expectEqualStrings(expected.name, actual.name);
255             try std.testing.expectEqual(expected.address, actual.address);
256             try std.testing.expectEqual(expected.size, actual.size);
257             try std.testing.expectEqual(expected.offset, actual.offset);
258             try std.testing.expectEqual(expected.alignment_shift, actual.alignment_shift);
259             try std.testing.expectEqual(expected.relocation_offset, actual.relocation_offset);
260             try std.testing.expectEqual(expected.relocation_count, actual.relocation_count);
261             try std.testing.expectEqual(expected.flags, actual.flags);
262         }
263         try std.testing.expectEqual(generated.relocations.len, object.relocations.len);
264         for (generated.relocations, object.relocations) |expected, actual| {
265             try std.testing.expectEqual(expected.section_index, actual.section_index);
266             try std.testing.expectEqual(expected.address, actual.address);
267             try std.testing.expectEqual(expected.symbol_number, actual.symbol_number);
268             try std.testing.expectEqual(expected.pc_relative, actual.pc_relative);
269             try std.testing.expectEqual(expected.length, actual.length);
270             try std.testing.expectEqual(expected.external, actual.external);
271             try std.testing.expectEqual(expected.kind, actual.kind);
272         }
273         try std.testing.expectEqual(generated.symbols.len, object.symbols.len);
274         for (generated.symbols, object.symbols) |expected, actual| {
275             try std.testing.expectEqualStrings(expected.name, actual.name);
276             try std.testing.expectEqual(expected.section_index, actual.section_index);
277             try std.testing.expectEqual(expected.value, actual.value);
278             try std.testing.expectEqual(expected.kind, actual.kind);
279             try std.testing.expectEqual(expected.external, actual.external);
280         }
281 
282         var metadata = try macho.parseObjectMetadata(allocator, generated.bytes);
283         defer metadata.deinit(allocator);
284 
285         try std.testing.expectEqual(generated.sections.len, metadata.sections.len);
286         try std.testing.expectEqual(generated.symbols.len, metadata.symbols.len);
287         for (generated.sections, metadata.sections) |expected, actual| {
288             try std.testing.expectEqualStrings(expected.segment_name, actual.segment_name);
289             try std.testing.expectEqualStrings(expected.name, actual.name);
290             try std.testing.expectEqual(expected.size, actual.size);
291             try std.testing.expectEqual(try alignmentFromShift(expected.alignment_shift), actual.alignment);
292             try std.testing.expectEqual(expected.relocation_count, actual.relocation_count);
293         }
294         for (generated.symbols, metadata.symbols) |expected, actual| {
295             try std.testing.expectEqualStrings(expected.name, actual.name);
296             try std.testing.expectEqual(@as(i32, expected.section_index), actual.section_index);
297             try std.testing.expectEqual(expected.external, actual.external);
298             try std.testing.expectEqual(expected.section_index == 0, actual.undefined);
299         }
300     }
301 };
302 
303 test "pbt: Mach-O parser preserves generated object metadata" {
304     try hypothesis.checkNamed(
305         MetadataProperty,
306         "tldr-macho-object-metadata",
307         settings(0xBADC_0FFE, 48),
308     );
309 }
310 
311 fn alignmentFromShift(shift: u32) !u64 {
312     if (shift >= 63) return error.InvalidAlignment;
313     return @as(u64, 1) << @intCast(shift);
314 }
315 
316 fn relocationWidth(length: u8) u64 {
317     return @as(u64, 1) << @intCast(length);
318 }
319 
320 fn maxRelocationLength(section_size: u64) usize {
321     if (section_size >= 8) return 3;
322     if (section_size >= 4) return 2;
323     if (section_size >= 2) return 1;
324     return 0;
325 }
326 
327 fn checkedUsize(value: anytype) !usize {
328     return std.math.cast(usize, value) orelse error.InvalidRange;
329 }
330 
331 fn writeU32(bytes: []u8, offset: usize, value: u32) void {
332     std.mem.writeInt(u32, bytes[offset..][0..4], value, .little);
333 }
334 
335 fn writeI32(bytes: []u8, offset: usize, value: i32) void {
336     std.mem.writeInt(i32, bytes[offset..][0..4], value, .little);
337 }
338 
339 fn writeU64(bytes: []u8, offset: usize, value: u64) void {
340     std.mem.writeInt(u64, bytes[offset..][0..8], value, .little);
341 }
342 
343 fn writeName(bytes: []u8, offset: usize, comptime size: usize, value: []const u8) void {
344     @memset(bytes[offset..][0..size], 0);
345     @memcpy(bytes[offset..][0..value.len], value);
346 }
347 
348 fn writeRelocation(bytes: []u8, offset: usize, relocation: ExpectedRelocation) void {
349     writeI32(bytes, offset, relocation.address);
350     const encoded = (relocation.symbol_number & 0x00ff_ffff) |
351         (@as(u32, @intFromBool(relocation.pc_relative)) << 24) |
352         (@as(u32, relocation.length) << 25) |
353         (@as(u32, @intFromBool(relocation.external)) << 27) |
354         (@as(u32, relocation.kind) << 28);
355     writeU32(bytes, offset + 4, encoded);
356 }