lib/closure/src/binary/strict.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const binary = @import("root.zig");
2 const read = @import("read.zig");
3 const std = @import("std");
4
5 pub const loads_max: usize = 32;
6 pub const relocations_max: usize = 1_024;
7
8 pub const Policy = struct {
9 kind: binary.Kind,
10 sections_max: u16,
11 symbols_max: u32,
12 executable_bytes_max: u64,
13 initialized_bytes_max: u64,
14 memory_bytes_max: u64,
15 stack_bytes_max: u64,
16 relocations_max: u32,
17 pe_directories_allowed: u16 = 0,
18 pe_timestamp: u32 = 0,
19 pe_repro_timestamp: ?u32 = null,
20 };
21
22 pub const Load = struct {
23 offset: u64,
24 address: u64,
25 file_bytes: u64,
26 content_bytes: u64,
27 memory_bytes: u64,
28 readable: bool,
29 writable: bool,
30 executable: bool,
31
32 pub fn fileEnd(self: Load) Error!u64 {
33 return added(self.offset, self.file_bytes);
34 }
35
36 pub fn addressEnd(self: Load) Error!u64 {
37 return added(self.address, self.memory_bytes);
38 }
39 };
40
41 pub const Relocation = struct {
42 offset: u64,
43 address: u64,
44 };
45
46 pub const Scratch = struct {
47 loads: []Load,
48 relocations: []Relocation,
49 };
50
51 pub const Report = struct {
52 kind: binary.Kind,
53 loads: []const Load,
54 relocations: []const Relocation,
55 executable_bytes: u64,
56 initialized_bytes: u64,
57 memory_bytes: u64,
58 stack_bytes: u64,
59 timestamp: u32,
60 image_base: u64,
61 work: u64,
62
63 pub fn loadForAddress(
64 self: *const Report,
65 address: u64,
66 length: u64,
67 ) ?Load {
68 const end = added(address, length) catch return null;
69 for (self.loads) |load| {
70 const load_end = load.addressEnd() catch return null;
71 if (address >= load.address and end <= load_end) return load;
72 }
73 return null;
74 }
75 };
76
77 const InspectError = error{
78 BaseRelocationCapacityExceeded,
79 DataDirectoryForbidden,
80 DebugDirectoryPresent,
81 DynamicLinkagePresent,
82 EntryPointUnmapped,
83 ExecutableBudgetExceeded,
84 ExecutableStack,
85 InvalidBaseRelocation,
86 InvalidHeader,
87 InvalidLoad,
88 InvalidSection,
89 LoadCapacityExceeded,
90 LoadOverlap,
91 MemoryBudgetExceeded,
92 MissingStackPolicy,
93 NonZeroTimestamp,
94 PeSymbolTablePresent,
95 RelocationTargetInvalid,
96 SectionBudgetExceeded,
97 SymbolBudgetExceeded,
98 TlsPresent,
99 UndefinedSymbol,
100 UnsupportedBinary,
101 WritableExecutable,
102 };
103
104 pub const Error = read.Error || InspectError;
105
106 const elf_header_bytes: usize = 64;
107 const elf_program_bytes: usize = 56;
108 const elf_section_bytes: usize = 64;
109 const elf_symbol_bytes: usize = 24;
110 const elf_machine_x86_64: u16 = 62;
111 const elf_type_executable: u16 = 2;
112 const elf_program_load: u32 = 1;
113 const elf_program_dynamic: u32 = 2;
114 const elf_program_interpreter: u32 = 3;
115 const elf_program_tls: u32 = 7;
116 const elf_program_stack: u32 = 0x6474_e551;
117 const elf_flag_execute: u32 = 1;
118 const elf_flag_write: u32 = 2;
119 const elf_flag_read: u32 = 4;
120 const elf_section_symbols: u32 = 2;
121 const elf_section_rela: u32 = 4;
122 const elf_section_dynamic: u32 = 6;
123 const elf_section_nobits: u32 = 8;
124 const elf_section_rel: u32 = 9;
125 const elf_section_dynamic_symbols: u32 = 11;
126 const elf_section_relr: u32 = 19;
127 const elf_section_flag_write: u64 = 1;
128 const elf_section_flag_alloc: u64 = 2;
129 const elf_section_flag_execute: u64 = 4;
130 const elf_section_flag_tls: u64 = 0x400;
131
132 const pe_coff_bytes: usize = 20;
133 const pe_section_bytes: usize = 40;
134 const pe_machine_x86_64: u16 = 0x8664;
135 const pe_optional_magic: u16 = 0x20b;
136 const pe_executable_image: u16 = 0x0002;
137 const pe_subsystem_efi_application: u16 = 10;
138 const pe_nx_compatible: u16 = 0x0100;
139 const pe_section_code: u32 = 0x0000_0020;
140 const pe_section_discardable: u32 = 0x0200_0000;
141 const pe_section_execute: u32 = 0x2000_0000;
142 const pe_section_read: u32 = 0x4000_0000;
143 const pe_section_write: u32 = 0x8000_0000;
144 const pe_directory_base_relocation: usize = 5;
145 const pe_directory_debug: usize = 6;
146 const pe_relocation_absolute: u16 = 0;
147 const pe_relocation_dir64: u16 = 10;
148 const pe_debug_entry_bytes: u32 = 28;
149 const pe_debug_type_repro: u32 = 16;
150
151 const Counts = struct {
152 load_count: usize = 0,
153 relocation_count: usize = 0,
154 relocation_entry_count: u32 = 0,
155 symbol_count: u32 = 0,
156 executable_bytes: u64 = 0,
157 initialized_bytes: u64 = 0,
158 memory_bytes: u64 = 0,
159 stack_bytes: u64 = 0,
160 timestamp: u32 = 0,
161 image_base: u64 = 0,
162 work: u64 = 1,
163 };
164
165 const ElfHeader = struct {
166 entry: u64,
167 programs: usize,
168 program_count: usize,
169 sections: usize,
170 section_count: usize,
171 };
172
173 const PeHeader = struct {
174 coff: usize,
175 optional: usize,
176 optional_bytes: usize,
177 sections: usize,
178 section_count: usize,
179 image_base: u64,
180 entry_rva: u32,
181 directory_count: usize,
182 };
183
184 pub fn inspect(
185 bytes: []const u8,
186 policy: Policy,
187 scratch: Scratch,
188 ) Error!Report {
189 if (scratch.loads.len > loads_max or
190 scratch.relocations.len > relocations_max)
191 {
192 return error.InvalidHeader;
193 }
194 var counts: Counts = .{};
195 switch (policy.kind) {
196 .elf64 => try inspectElf(bytes, policy, scratch, &counts),
197 .pe32_plus => try inspectPe(bytes, policy, scratch, &counts),
198 }
199 try inspectLoadOverlap(scratch.loads[0..counts.load_count], &counts.work);
200 if (counts.executable_bytes > policy.executable_bytes_max or
201 counts.initialized_bytes > policy.initialized_bytes_max)
202 {
203 return error.ExecutableBudgetExceeded;
204 }
205 if (counts.memory_bytes > policy.memory_bytes_max) {
206 return error.MemoryBudgetExceeded;
207 }
208 return .{
209 .kind = policy.kind,
210 .loads = scratch.loads[0..counts.load_count],
211 .relocations = scratch.relocations[0..counts.relocation_count],
212 .executable_bytes = counts.executable_bytes,
213 .initialized_bytes = counts.initialized_bytes,
214 .memory_bytes = counts.memory_bytes,
215 .stack_bytes = counts.stack_bytes,
216 .timestamp = counts.timestamp,
217 .image_base = counts.image_base,
218 .work = counts.work,
219 };
220 }
221
222 fn inspectElf(
223 bytes: []const u8,
224 policy: Policy,
225 scratch: Scratch,
226 counts: *Counts,
227 ) Error!void {
228 const header = try elfHeader(bytes, policy);
229 var stack_count: u8 = 0;
230 for (0..header.program_count) |index| {
231 counts.work += 1;
232 const offset = try read.indexed(
233 header.programs,
234 index,
235 elf_program_bytes,
236 );
237 const kind = try read.u32le(bytes, offset);
238 const flags = try read.u32le(bytes, offset + 4);
239 switch (kind) {
240 elf_program_dynamic, elf_program_interpreter => return error.DynamicLinkagePresent,
241 elf_program_tls => return error.TlsPresent,
242 elf_program_stack => {
243 stack_count += 1;
244 if (stack_count != 1 or
245 flags & elf_flag_execute != 0 or
246 flags & (elf_flag_read | elf_flag_write) !=
247 elf_flag_read | elf_flag_write)
248 {
249 return error.ExecutableStack;
250 }
251 counts.stack_bytes = try read.u64le(bytes, offset + 40);
252 if (counts.stack_bytes > policy.stack_bytes_max) {
253 return error.MemoryBudgetExceeded;
254 }
255 },
256 elf_program_load => try appendElfLoad(
257 bytes,
258 offset,
259 flags,
260 scratch.loads,
261 counts,
262 ),
263 else => {},
264 }
265 }
266 if (stack_count != 1) return error.MissingStackPolicy;
267 try inspectElfSections(
268 bytes,
269 header,
270 policy,
271 scratch.loads,
272 counts,
273 );
274 if (!addressExecutable(
275 scratch.loads[0..counts.load_count],
276 header.entry,
277 1,
278 )) {
279 return error.EntryPointUnmapped;
280 }
281 }
282
283 fn elfHeader(bytes: []const u8, policy: Policy) Error!ElfHeader {
284 _ = try read.take(bytes, 0, elf_header_bytes);
285 if (!std.mem.eql(u8, bytes[0..4], "\x7fELF") or
286 bytes[4] != 2 or
287 bytes[5] != 1 or
288 bytes[6] != 1 or
289 try read.u16le(bytes, 16) != elf_type_executable or
290 try read.u16le(bytes, 18) != elf_machine_x86_64 or
291 try read.u32le(bytes, 20) != 1 or
292 try read.u16le(bytes, 52) != elf_header_bytes or
293 try read.u16le(bytes, 54) != elf_program_bytes or
294 try read.u16le(bytes, 58) != elf_section_bytes)
295 {
296 return error.UnsupportedBinary;
297 }
298 const program_count = try read.u16le(bytes, 56);
299 const section_count = try read.u16le(bytes, 60);
300 if (program_count == 0 or
301 program_count > loads_max or
302 section_count == 0 or
303 section_count > policy.sections_max)
304 {
305 return error.SectionBudgetExceeded;
306 }
307 const programs = try read.toOffset(try read.u64le(bytes, 32));
308 const sections = try read.toOffset(try read.u64le(bytes, 40));
309 _ = try read.take(
310 bytes,
311 try read.indexed(programs, program_count - 1, elf_program_bytes),
312 elf_program_bytes,
313 );
314 _ = try read.take(
315 bytes,
316 try read.indexed(sections, section_count - 1, elf_section_bytes),
317 elf_section_bytes,
318 );
319 return .{
320 .entry = try read.u64le(bytes, 24),
321 .programs = programs,
322 .program_count = program_count,
323 .sections = sections,
324 .section_count = section_count,
325 };
326 }
327
328 fn appendElfLoad(
329 bytes: []const u8,
330 offset: usize,
331 flags: u32,
332 loads: []Load,
333 counts: *Counts,
334 ) Error!void {
335 if (counts.load_count == loads.len) {
336 return error.LoadCapacityExceeded;
337 }
338 if (flags & ~(elf_flag_read | elf_flag_write | elf_flag_execute) != 0 or
339 flags & elf_flag_read == 0 or
340 flags & (elf_flag_write | elf_flag_execute) ==
341 elf_flag_write | elf_flag_execute)
342 {
343 return error.WritableExecutable;
344 }
345 const file_offset = try read.u64le(bytes, offset + 8);
346 const address = try read.u64le(bytes, offset + 16);
347 const physical = try read.u64le(bytes, offset + 24);
348 const file_bytes = try read.u64le(bytes, offset + 32);
349 const memory_bytes = try read.u64le(bytes, offset + 40);
350 const alignment = try read.u64le(bytes, offset + 48);
351 if (physical != address or
352 file_bytes > memory_bytes or
353 memory_bytes == 0 or
354 (flags & elf_flag_execute != 0 and file_bytes != memory_bytes) or
355 !powerOfTwoOrZero(alignment) or
356 (alignment > 1 and file_offset % alignment != address % alignment))
357 {
358 return error.InvalidLoad;
359 }
360 _ = try read.take(
361 bytes,
362 try read.toOffset(file_offset),
363 try read.toLength(file_bytes),
364 );
365 loads[counts.load_count] = .{
366 .offset = file_offset,
367 .address = address,
368 .file_bytes = file_bytes,
369 .content_bytes = file_bytes,
370 .memory_bytes = memory_bytes,
371 .readable = true,
372 .writable = flags & elf_flag_write != 0,
373 .executable = flags & elf_flag_execute != 0,
374 };
375 counts.load_count += 1;
376 counts.memory_bytes = try added(counts.memory_bytes, memory_bytes);
377 if (flags & elf_flag_execute != 0) {
378 counts.executable_bytes =
379 try added(counts.executable_bytes, file_bytes);
380 } else {
381 counts.initialized_bytes =
382 try added(counts.initialized_bytes, file_bytes);
383 }
384 }
385
386 fn inspectElfSections(
387 bytes: []const u8,
388 header: ElfHeader,
389 policy: Policy,
390 loads: []const Load,
391 counts: *Counts,
392 ) Error!void {
393 for (0..header.section_count) |index| {
394 counts.work += 1;
395 const offset = try read.indexed(
396 header.sections,
397 index,
398 elf_section_bytes,
399 );
400 const kind = try read.u32le(bytes, offset + 4);
401 const flags = try read.u64le(bytes, offset + 8);
402 if (kind == elf_section_dynamic or
403 kind == elf_section_dynamic_symbols)
404 {
405 return error.DynamicLinkagePresent;
406 }
407 if (kind == elf_section_rela or
408 kind == elf_section_rel or
409 kind == elf_section_relr)
410 {
411 return error.InvalidSection;
412 }
413 if (flags & elf_section_flag_tls != 0) return error.TlsPresent;
414 if (kind == elf_section_symbols) {
415 try inspectElfSymbols(bytes, header, offset, policy, counts);
416 }
417 if (flags & elf_section_flag_alloc == 0) continue;
418 const address = try read.u64le(bytes, offset + 16);
419 const file_offset = try read.u64le(bytes, offset + 24);
420 const length = try read.u64le(bytes, offset + 32);
421 const executable = flags & elf_section_flag_execute != 0;
422 const writable = flags & elf_section_flag_write != 0;
423 if (executable and writable) return error.WritableExecutable;
424 if (length == 0) continue;
425 if (kind == elf_section_nobits and executable) {
426 return error.InvalidSection;
427 }
428 const admitted = (if (kind == elf_section_nobits)
429 loadForAddress(loads, address, length)
430 else
431 loadForSection(loads, address, file_offset, length)) orelse
432 return error.InvalidSection;
433 if (admitted.executable != executable or
434 (writable and !admitted.writable))
435 {
436 return error.InvalidSection;
437 }
438 }
439 }
440
441 fn inspectElfSymbols(
442 bytes: []const u8,
443 header: ElfHeader,
444 section_offset: usize,
445 policy: Policy,
446 counts: *Counts,
447 ) Error!void {
448 const table_offset = try read.u64le(bytes, section_offset + 24);
449 const table_bytes = try read.u64le(bytes, section_offset + 32);
450 const entry_bytes = try read.u64le(bytes, section_offset + 56);
451 if (entry_bytes != elf_symbol_bytes or
452 table_bytes % elf_symbol_bytes != 0)
453 {
454 return error.InvalidSection;
455 }
456 const symbol_count = table_bytes / elf_symbol_bytes;
457 const total = try added(counts.symbol_count, symbol_count);
458 if (total > policy.symbols_max) {
459 return error.SymbolBudgetExceeded;
460 }
461 counts.symbol_count = @intCast(total);
462 const symbols = try read.take(
463 bytes,
464 try read.toOffset(table_offset),
465 try read.toLength(table_bytes),
466 );
467 for (0..symbols.len / elf_symbol_bytes) |index| {
468 counts.work += 1;
469 if (index == 0) continue;
470 const offset = index * elf_symbol_bytes;
471 const binding = symbols[offset + 4] >> 4;
472 const section_index = std.mem.readInt(
473 u16,
474 symbols[offset + 6 ..][0..2],
475 .little,
476 );
477 if (section_index == 0 and (binding == 1 or binding == 2)) {
478 return error.UndefinedSymbol;
479 }
480 }
481 _ = header;
482 }
483
484 fn inspectPe(
485 bytes: []const u8,
486 policy: Policy,
487 scratch: Scratch,
488 counts: *Counts,
489 ) Error!void {
490 const header = try peHeader(bytes, policy);
491 counts.image_base = header.image_base;
492 counts.timestamp = try read.u32le(bytes, header.coff + 4);
493 if (counts.timestamp != policy.pe_timestamp) {
494 return error.NonZeroTimestamp;
495 }
496 if (try read.u32le(bytes, header.coff + 8) != 0 or
497 try read.u32le(bytes, header.coff + 12) != 0)
498 {
499 return error.PeSymbolTablePresent;
500 }
501 for (0..header.section_count) |index| {
502 counts.work += 1;
503 try appendPeLoad(
504 bytes,
505 header,
506 index,
507 scratch.loads,
508 counts,
509 );
510 }
511 try inspectPeDirectories(bytes, header, policy, scratch, counts);
512 const entry = try added(header.image_base, header.entry_rva);
513 if (!addressExecutable(
514 scratch.loads[0..counts.load_count],
515 entry,
516 1,
517 )) {
518 return error.EntryPointUnmapped;
519 }
520 }
521
522 fn peHeader(bytes: []const u8, policy: Policy) Error!PeHeader {
523 _ = try read.take(bytes, 0, 64);
524 if (!std.mem.eql(u8, bytes[0..2], "MZ")) {
525 return error.UnsupportedBinary;
526 }
527 const pe_offset = try read.toOffset(try read.u32le(bytes, 0x3c));
528 if (!std.mem.eql(
529 u8,
530 try read.take(bytes, pe_offset, 4),
531 "PE\x00\x00",
532 )) {
533 return error.UnsupportedBinary;
534 }
535 const coff = try addOffset(pe_offset, 4);
536 _ = try read.take(bytes, coff, pe_coff_bytes);
537 if (try read.u16le(bytes, coff) != pe_machine_x86_64 or
538 try read.u16le(bytes, coff + 18) & pe_executable_image == 0)
539 {
540 return error.UnsupportedBinary;
541 }
542 const section_count = try read.u16le(bytes, coff + 2);
543 if (section_count == 0 or section_count > policy.sections_max) {
544 return error.SectionBudgetExceeded;
545 }
546 const optional_bytes = try read.u16le(bytes, coff + 16);
547 const optional = try addOffset(coff, pe_coff_bytes);
548 _ = try read.take(bytes, optional, optional_bytes);
549 if (optional_bytes < 112 or
550 try read.u16le(bytes, optional) != pe_optional_magic or
551 try read.u16le(bytes, optional + 68) !=
552 pe_subsystem_efi_application or
553 try read.u16le(bytes, optional + 70) & pe_nx_compatible == 0)
554 {
555 return error.InvalidHeader;
556 }
557 const directory_count = try read.u32le(bytes, optional + 108);
558 if (directory_count > 16 or
559 112 + @as(usize, directory_count) * 8 > optional_bytes)
560 {
561 return error.InvalidHeader;
562 }
563 const sections = try addOffset(optional, optional_bytes);
564 _ = try read.take(
565 bytes,
566 try read.indexed(sections, section_count - 1, pe_section_bytes),
567 pe_section_bytes,
568 );
569 return .{
570 .coff = coff,
571 .optional = optional,
572 .optional_bytes = optional_bytes,
573 .sections = sections,
574 .section_count = section_count,
575 .image_base = try read.u64le(bytes, optional + 24),
576 .entry_rva = try read.u32le(bytes, optional + 16),
577 .directory_count = directory_count,
578 };
579 }
580
581 fn appendPeLoad(
582 bytes: []const u8,
583 header: PeHeader,
584 index: usize,
585 loads: []Load,
586 counts: *Counts,
587 ) Error!void {
588 if (counts.load_count == loads.len) {
589 return error.LoadCapacityExceeded;
590 }
591 const offset = try read.indexed(
592 header.sections,
593 index,
594 pe_section_bytes,
595 );
596 const virtual_bytes = try read.u32le(bytes, offset + 8);
597 const virtual_address = try read.u32le(bytes, offset + 12);
598 const file_bytes = try read.u32le(bytes, offset + 16);
599 const file_offset = try read.u32le(bytes, offset + 20);
600 const flags = try read.u32le(bytes, offset + 36);
601 const executable = flags & pe_section_execute != 0;
602 const writable = flags & pe_section_write != 0;
603 const readable = flags & pe_section_read != 0;
604 if (executable and
605 (writable or
606 !readable or
607 flags & pe_section_code == 0 or
608 flags & pe_section_discardable != 0 or
609 virtual_bytes == 0 or
610 virtual_bytes > file_bytes))
611 {
612 return error.WritableExecutable;
613 }
614 if (file_bytes == 0 and virtual_bytes == 0) {
615 return error.InvalidSection;
616 }
617 _ = try read.take(bytes, file_offset, file_bytes);
618 const memory_bytes = @max(virtual_bytes, file_bytes);
619 loads[counts.load_count] = .{
620 .offset = file_offset,
621 .address = try added(header.image_base, virtual_address),
622 .file_bytes = file_bytes,
623 .content_bytes = if (executable) virtual_bytes else file_bytes,
624 .memory_bytes = memory_bytes,
625 .readable = readable,
626 .writable = writable,
627 .executable = executable,
628 };
629 counts.load_count += 1;
630 counts.memory_bytes = try added(counts.memory_bytes, memory_bytes);
631 if (executable) {
632 counts.executable_bytes =
633 try added(counts.executable_bytes, virtual_bytes);
634 } else {
635 counts.initialized_bytes =
636 try added(counts.initialized_bytes, file_bytes);
637 }
638 }
639
640 fn inspectPeDirectories(
641 bytes: []const u8,
642 header: PeHeader,
643 policy: Policy,
644 scratch: Scratch,
645 counts: *Counts,
646 ) Error!void {
647 for (0..header.directory_count) |index| {
648 counts.work += 1;
649 const offset = header.optional + 112 + index * 8;
650 const rva = try read.u32le(bytes, offset);
651 const length = try read.u32le(bytes, offset + 4);
652 if ((rva == 0) != (length == 0)) return error.InvalidHeader;
653 if (rva == 0) continue;
654 const bit = @as(u16, 1) << @intCast(index);
655 if (policy.pe_directories_allowed & bit == 0) {
656 if (index == 6) return error.DebugDirectoryPresent;
657 return error.DataDirectoryForbidden;
658 }
659 if (index == pe_directory_base_relocation) {
660 try inspectBaseRelocations(
661 bytes,
662 header,
663 rva,
664 length,
665 policy,
666 scratch,
667 counts,
668 );
669 } else if (index == pe_directory_debug) {
670 try inspectReproDirectory(
671 bytes,
672 header,
673 rva,
674 length,
675 policy,
676 scratch.loads[0..counts.load_count],
677 );
678 } else {
679 _ = peFileRange(
680 scratch.loads[0..counts.load_count],
681 header.image_base,
682 rva,
683 length,
684 ) orelse return error.InvalidSection;
685 }
686 }
687 }
688
689 fn inspectReproDirectory(
690 bytes: []const u8,
691 header: PeHeader,
692 rva: u32,
693 length: u32,
694 policy: Policy,
695 loads: []const Load,
696 ) Error!void {
697 const expected_timestamp = policy.pe_repro_timestamp orelse
698 return error.DebugDirectoryPresent;
699 if (length != pe_debug_entry_bytes) {
700 return error.DebugDirectoryPresent;
701 }
702 const offset = peFileRange(
703 loads,
704 header.image_base,
705 rva,
706 length,
707 ) orelse return error.DebugDirectoryPresent;
708 const entry = try read.take(bytes, offset, pe_debug_entry_bytes);
709 if (std.mem.readInt(u32, entry[0..4], .little) != 0 or
710 std.mem.readInt(u32, entry[4..8], .little) !=
711 expected_timestamp or
712 std.mem.readInt(u16, entry[8..10], .little) != 0 or
713 std.mem.readInt(u16, entry[10..12], .little) != 0 or
714 std.mem.readInt(u32, entry[12..16], .little) !=
715 pe_debug_type_repro or
716 std.mem.readInt(u32, entry[16..20], .little) != 0 or
717 std.mem.readInt(u32, entry[20..24], .little) != 0 or
718 std.mem.readInt(u32, entry[24..28], .little) != 0)
719 {
720 return error.DebugDirectoryPresent;
721 }
722 }
723
724 fn inspectBaseRelocations(
725 bytes: []const u8,
726 header: PeHeader,
727 rva: u32,
728 length: u32,
729 policy: Policy,
730 scratch: Scratch,
731 counts: *Counts,
732 ) Error!void {
733 const directory_offset = peFileRange(
734 scratch.loads[0..counts.load_count],
735 header.image_base,
736 rva,
737 length,
738 ) orelse return error.InvalidBaseRelocation;
739 const directory = try read.take(bytes, directory_offset, length);
740 var cursor: usize = 0;
741 var previous_address: u64 = 0;
742 while (cursor < directory.len) {
743 counts.work += 1;
744 if (cursor + 8 > directory.len) {
745 return error.InvalidBaseRelocation;
746 }
747 const page = std.mem.readInt(
748 u32,
749 directory[cursor..][0..4],
750 .little,
751 );
752 const block_bytes = std.mem.readInt(
753 u32,
754 directory[cursor + 4 ..][0..4],
755 .little,
756 );
757 if (page % 4096 != 0 or
758 block_bytes < 8 or
759 block_bytes % 2 != 0 or
760 cursor + block_bytes > directory.len)
761 {
762 return error.InvalidBaseRelocation;
763 }
764 const entry_count = (block_bytes - 8) / 2;
765 for (0..entry_count) |entry_index| {
766 counts.work += 1;
767 if (counts.relocation_entry_count == policy.relocations_max) {
768 return error.BaseRelocationCapacityExceeded;
769 }
770 counts.relocation_entry_count += 1;
771 const entry_offset = cursor + 8 + entry_index * 2;
772 const entry = std.mem.readInt(
773 u16,
774 directory[entry_offset..][0..2],
775 .little,
776 );
777 const kind = entry >> 12;
778 if (kind == pe_relocation_absolute) continue;
779 if (kind != pe_relocation_dir64) {
780 return error.InvalidBaseRelocation;
781 }
782 if (counts.relocation_count == scratch.relocations.len or
783 counts.relocation_count == policy.relocations_max)
784 {
785 return error.BaseRelocationCapacityExceeded;
786 }
787 const target_rva = try added(page, entry & 0x0fff);
788 const target_address = try added(
789 header.image_base,
790 target_rva,
791 );
792 const load = loadForAddress(
793 scratch.loads[0..counts.load_count],
794 target_address,
795 8,
796 ) orelse return error.RelocationTargetInvalid;
797 if (!load.writable or load.executable) {
798 return error.RelocationTargetInvalid;
799 }
800 if (counts.relocation_count != 0 and
801 target_address <= previous_address)
802 {
803 return error.InvalidBaseRelocation;
804 }
805 const relative = target_address - load.address;
806 scratch.relocations[counts.relocation_count] = .{
807 .offset = try added(load.offset, relative),
808 .address = target_address,
809 };
810 counts.relocation_count += 1;
811 previous_address = target_address;
812 }
813 cursor += block_bytes;
814 }
815 if (cursor != directory.len) return error.InvalidBaseRelocation;
816 }
817
818 fn inspectLoadOverlap(loads: []const Load, work: *u64) Error!void {
819 for (loads, 0..) |left, left_index| {
820 for (loads[left_index + 1 ..]) |right| {
821 work.* += 1;
822 const left_address_end = try left.addressEnd();
823 const right_address_end = try right.addressEnd();
824 if (left.address < right_address_end and
825 right.address < left_address_end)
826 {
827 return error.LoadOverlap;
828 }
829 const left_file_end = try left.fileEnd();
830 const right_file_end = try right.fileEnd();
831 if (left.file_bytes != 0 and
832 right.file_bytes != 0 and
833 left.offset < right_file_end and
834 right.offset < left_file_end)
835 {
836 return error.LoadOverlap;
837 }
838 }
839 }
840 }
841
842 fn loadForSection(
843 loads: []const Load,
844 address: u64,
845 offset: u64,
846 length: u64,
847 ) ?Load {
848 const address_end = added(address, length) catch return null;
849 const offset_end = added(offset, length) catch return null;
850 for (loads) |load| {
851 const load_address_end = load.addressEnd() catch return null;
852 const load_file_end = load.fileEnd() catch return null;
853 if (address >= load.address and
854 address_end <= load_address_end and
855 offset >= load.offset and
856 offset_end <= load_file_end)
857 {
858 return load;
859 }
860 }
861 return null;
862 }
863
864 fn loadForAddress(
865 loads: []const Load,
866 address: u64,
867 length: u64,
868 ) ?Load {
869 const end = added(address, length) catch return null;
870 for (loads) |load| {
871 const load_end = load.addressEnd() catch return null;
872 if (address >= load.address and end <= load_end) return load;
873 }
874 return null;
875 }
876
877 fn addressExecutable(
878 loads: []const Load,
879 address: u64,
880 length: u64,
881 ) bool {
882 const load = loadForAddress(loads, address, length) orelse return false;
883 return load.executable;
884 }
885
886 fn peFileRange(
887 loads: []const Load,
888 image_base: u64,
889 rva: u32,
890 length: u32,
891 ) ?usize {
892 const address = added(image_base, rva) catch return null;
893 const load = loadForAddress(loads, address, length) orelse return null;
894 const relative = address - load.address;
895 const relative_end = added(relative, length) catch return null;
896 if (relative_end > load.file_bytes) return null;
897 return read.toOffset(added(load.offset, relative) catch return null) catch
898 return null;
899 }
900
901 fn powerOfTwoOrZero(value: u64) bool {
902 return value == 0 or std.math.isPowerOfTwo(value);
903 }
904
905 fn addOffset(left: usize, right: anytype) Error!usize {
906 return std.math.add(usize, left, @intCast(right)) catch
907 error.BinaryArithmeticOverflow;
908 }
909
910 fn added(left: anytype, right: anytype) Error!u64 {
911 return std.math.add(
912 u64,
913 @intCast(left),
914 @intCast(right),
915 ) catch error.BinaryArithmeticOverflow;
916 }
917
918 fn put16(bytes: []u8, offset: usize, value: u16) void {
919 std.mem.writeInt(u16, bytes[offset..][0..2], value, .little);
920 }
921
922 fn put32(bytes: []u8, offset: usize, value: u32) void {
923 std.mem.writeInt(u32, bytes[offset..][0..4], value, .little);
924 }
925
926 fn put64(bytes: []u8, offset: usize, value: u64) void {
927 std.mem.writeInt(u64, bytes[offset..][0..8], value, .little);
928 }
929
930 fn elfFixture() [512]u8 {
931 var bytes: [512]u8 = @splat(0);
932 @memcpy(bytes[0..4], "\x7fELF");
933 bytes[4] = 2;
934 bytes[5] = 1;
935 bytes[6] = 1;
936 put16(&bytes, 16, elf_type_executable);
937 put16(&bytes, 18, elf_machine_x86_64);
938 put32(&bytes, 20, 1);
939 put64(&bytes, 24, 0x1000);
940 put64(&bytes, 32, 64);
941 put64(&bytes, 40, 192);
942 put16(&bytes, 52, elf_header_bytes);
943 put16(&bytes, 54, elf_program_bytes);
944 put16(&bytes, 56, 2);
945 put16(&bytes, 58, elf_section_bytes);
946 put16(&bytes, 60, 2);
947 put32(&bytes, 64, elf_program_load);
948 put32(&bytes, 68, elf_flag_read | elf_flag_execute);
949 put64(&bytes, 72, 384);
950 put64(&bytes, 80, 0x1000);
951 put64(&bytes, 88, 0x1000);
952 put64(&bytes, 96, 16);
953 put64(&bytes, 104, 16);
954 put64(&bytes, 112, 1);
955 const stack = 64 + elf_program_bytes;
956 put32(&bytes, stack, elf_program_stack);
957 put32(&bytes, stack + 4, elf_flag_read | elf_flag_write);
958 put64(&bytes, stack + 40, 4096);
959 const text = 192 + elf_section_bytes;
960 put32(&bytes, text + 4, 1);
961 put64(
962 &bytes,
963 text + 8,
964 elf_section_flag_alloc | elf_section_flag_execute,
965 );
966 put64(&bytes, text + 16, 0x1000);
967 put64(&bytes, text + 24, 384);
968 put64(&bytes, text + 32, 16);
969 @memset(bytes[384..400], 0xcc);
970 return bytes;
971 }
972
973 fn elfPolicy() Policy {
974 return .{
975 .kind = .elf64,
976 .sections_max = 4,
977 .symbols_max = 16,
978 .executable_bytes_max = 16,
979 .initialized_bytes_max = 1,
980 .memory_bytes_max = 16,
981 .stack_bytes_max = 4096,
982 .relocations_max = 0,
983 };
984 }
985
986 fn peFixture() [1_024]u8 {
987 var bytes: [1_024]u8 = @splat(0);
988 @memcpy(bytes[0..2], "MZ");
989 put32(&bytes, 0x3c, 64);
990 @memcpy(bytes[64..68], "PE\x00\x00");
991 const coff = 68;
992 put16(&bytes, coff, pe_machine_x86_64);
993 put16(&bytes, coff + 2, 2);
994 put16(&bytes, coff + 16, 240);
995 put16(&bytes, coff + 18, pe_executable_image);
996 const optional = coff + pe_coff_bytes;
997 put16(&bytes, optional, pe_optional_magic);
998 put32(&bytes, optional + 16, 0x1000);
999 put64(&bytes, optional + 24, 0x400000);
1000 put16(&bytes, optional + 68, pe_subsystem_efi_application);
1001 put16(&bytes, optional + 70, pe_nx_compatible);
1002 put32(&bytes, optional + 108, 16);
1003 const text = optional + 240;
1004 @memcpy(bytes[text..][0..5], ".text");
1005 put32(&bytes, text + 8, 32);
1006 put32(&bytes, text + 12, 0x1000);
1007 put32(&bytes, text + 16, 64);
1008 put32(&bytes, text + 20, 512);
1009 put32(
1010 &bytes,
1011 text + 36,
1012 pe_section_code | pe_section_execute | pe_section_read,
1013 );
1014 const data = text + pe_section_bytes;
1015 @memcpy(bytes[data..][0..5], ".data");
1016 put32(&bytes, data + 8, 64);
1017 put32(&bytes, data + 12, 0x2000);
1018 put32(&bytes, data + 16, 64);
1019 put32(&bytes, data + 20, 576);
1020 put32(&bytes, data + 36, pe_section_read | pe_section_write);
1021 @memset(bytes[512..576], 0xcc);
1022 return bytes;
1023 }
1024
1025 fn pePolicy() Policy {
1026 return .{
1027 .kind = .pe32_plus,
1028 .sections_max = 4,
1029 .symbols_max = 0,
1030 .executable_bytes_max = 32,
1031 .initialized_bytes_max = 64,
1032 .memory_bytes_max = 128,
1033 .stack_bytes_max = 0,
1034 .relocations_max = 0,
1035 };
1036 }
1037
1038 test "strict ELF admits a bounded static NX executable" {
1039 const bytes = elfFixture();
1040 var loads: [2]Load = undefined;
1041 var relocations: [1]Relocation = undefined;
1042 const report = try inspect(&bytes, elfPolicy(), .{
1043 .loads = &loads,
1044 .relocations = relocations[0..0],
1045 });
1046 try std.testing.expectEqual(binary.Kind.elf64, report.kind);
1047 try std.testing.expectEqual(@as(usize, 1), report.loads.len);
1048 try std.testing.expect(report.loads[0].executable);
1049 try std.testing.expectEqual(@as(u64, 16), report.executable_bytes);
1050 try std.testing.expectEqual(@as(u64, 4096), report.stack_bytes);
1051 }
1052
1053 test "strict ELF rejects dynamic linkage and writable code" {
1054 var dynamic = elfFixture();
1055 put32(&dynamic, 64, elf_program_dynamic);
1056 var loads: [2]Load = undefined;
1057 var relocations: [1]Relocation = undefined;
1058 try std.testing.expectError(
1059 error.DynamicLinkagePresent,
1060 inspect(&dynamic, elfPolicy(), .{
1061 .loads = &loads,
1062 .relocations = relocations[0..0],
1063 }),
1064 );
1065 var writable = elfFixture();
1066 put32(
1067 &writable,
1068 68,
1069 elf_flag_read | elf_flag_write | elf_flag_execute,
1070 );
1071 try std.testing.expectError(
1072 error.WritableExecutable,
1073 inspect(&writable, elfPolicy(), .{
1074 .loads = &loads,
1075 .relocations = relocations[0..0],
1076 }),
1077 );
1078 }
1079
1080 test "strict ELF bounds symbol inspection work" {
1081 var bytes = elfFixture();
1082 const text = 192 + elf_section_bytes;
1083 put32(&bytes, text + 4, elf_section_symbols);
1084 put64(&bytes, text + 24, 384);
1085 put64(&bytes, text + 32, 48);
1086 put64(&bytes, text + 56, elf_symbol_bytes);
1087 var policy = elfPolicy();
1088 policy.symbols_max = 1;
1089 var loads: [2]Load = undefined;
1090 var relocations: [1]Relocation = undefined;
1091 try std.testing.expectError(
1092 error.SymbolBudgetExceeded,
1093 inspect(&bytes, policy, .{
1094 .loads = &loads,
1095 .relocations = relocations[0..0],
1096 }),
1097 );
1098 }
1099
1100 test "strict PE admits zero-timestamp path-free firmware" {
1101 const bytes = peFixture();
1102 var loads: [2]Load = undefined;
1103 var relocations: [1]Relocation = undefined;
1104 const report = try inspect(&bytes, pePolicy(), .{
1105 .loads = &loads,
1106 .relocations = relocations[0..0],
1107 });
1108 try std.testing.expectEqual(binary.Kind.pe32_plus, report.kind);
1109 try std.testing.expectEqual(@as(usize, 2), report.loads.len);
1110 try std.testing.expectEqual(@as(u64, 32), report.executable_bytes);
1111 try std.testing.expectEqual(@as(u32, 0), report.timestamp);
1112 }
1113
1114 test "strict PE rejects timestamp debug and import state" {
1115 var timestamp = peFixture();
1116 put32(×tamp, 68 + 4, 1);
1117 var loads: [2]Load = undefined;
1118 var relocations: [1]Relocation = undefined;
1119 try std.testing.expectError(
1120 error.NonZeroTimestamp,
1121 inspect(×tamp, pePolicy(), .{
1122 .loads = &loads,
1123 .relocations = relocations[0..0],
1124 }),
1125 );
1126 var imported = peFixture();
1127 const optional = 68 + pe_coff_bytes;
1128 put32(&imported, optional + 112 + 8, 0x2000);
1129 put32(&imported, optional + 112 + 12, 8);
1130 try std.testing.expectError(
1131 error.DataDirectoryForbidden,
1132 inspect(&imported, pePolicy(), .{
1133 .loads = &loads,
1134 .relocations = relocations[0..0],
1135 }),
1136 );
1137 var debug = peFixture();
1138 put32(&debug, optional + 112 + 6 * 8, 0x2000);
1139 put32(&debug, optional + 112 + 6 * 8 + 4, 28);
1140 try std.testing.expectError(
1141 error.DebugDirectoryPresent,
1142 inspect(&debug, pePolicy(), .{
1143 .loads = &loads,
1144 .relocations = relocations[0..0],
1145 }),
1146 );
1147 }
1148
1149 test "strict PE admits only a matching path-free repro record" {
1150 var bytes = peFixture();
1151 const optional = 68 + pe_coff_bytes;
1152 put32(&bytes, optional + 112 + pe_directory_debug * 8, 0x2000);
1153 put32(
1154 &bytes,
1155 optional + 112 + pe_directory_debug * 8 + 4,
1156 pe_debug_entry_bytes,
1157 );
1158 put32(&bytes, 576 + 4, 0x1234_5678);
1159 put32(&bytes, 576 + 12, pe_debug_type_repro);
1160 var policy = pePolicy();
1161 policy.pe_directories_allowed =
1162 @as(u16, 1) << pe_directory_debug;
1163 policy.pe_repro_timestamp = 0x1234_5678;
1164 var loads: [2]Load = undefined;
1165 var relocations: [1]Relocation = undefined;
1166 _ = try inspect(&bytes, policy, .{
1167 .loads = &loads,
1168 .relocations = relocations[0..0],
1169 });
1170 put32(&bytes, 576 + 16, 1);
1171 try std.testing.expectError(
1172 error.DebugDirectoryPresent,
1173 inspect(&bytes, policy, .{
1174 .loads = &loads,
1175 .relocations = relocations[0..0],
1176 }),
1177 );
1178 }
1179
1180 test "strict PE base relocations target writable non-executable data" {
1181 var bytes = peFixture();
1182 const optional = 68 + pe_coff_bytes;
1183 put32(
1184 &bytes,
1185 optional + 112 + pe_directory_base_relocation * 8,
1186 0x2000,
1187 );
1188 put32(
1189 &bytes,
1190 optional + 112 + pe_directory_base_relocation * 8 + 4,
1191 12,
1192 );
1193 put32(&bytes, 576, 0x2000);
1194 put32(&bytes, 580, 12);
1195 put16(&bytes, 584, pe_relocation_dir64 << 12);
1196 var policy = pePolicy();
1197 policy.pe_directories_allowed =
1198 @as(u16, 1) << pe_directory_base_relocation;
1199 policy.relocations_max = 2;
1200 var loads: [2]Load = undefined;
1201 var relocations: [1]Relocation = undefined;
1202 const report = try inspect(&bytes, policy, .{
1203 .loads = &loads,
1204 .relocations = &relocations,
1205 });
1206 try std.testing.expectEqual(@as(usize, 1), report.relocations.len);
1207 put32(&bytes, 576, 0x1000);
1208 try std.testing.expectError(
1209 error.RelocationTargetInvalid,
1210 inspect(&bytes, policy, .{
1211 .loads = &loads,
1212 .relocations = &relocations,
1213 }),
1214 );
1215 }