Files
bun.sh/src/pe.zig
pfg 05d0475c6c Update to zig 0.15.2 (#24204)
Fixes ENG-21287

Build times, from `bun run build && echo '//' >> src/main.zig && time
bun run build`

|Platform|0.14.1|0.15.2|Speedup|
|-|-|-|-|
|macos debug asan|126.90s|106.27s|1.19x|
|macos debug noasan|60.62s|50.85s|1.19x|
|linux debug asan|292.77s|241.45s|1.21x|
|linux debug noasan|146.58s|130.94s|1.12x|
|linux debug use_llvm=false|n/a|78.27s|1.87x|
|windows debug asan|177.13s|142.55s|1.24x|

Runtime performance:

- next build memory usage may have gone up by 5%. Otherwise seems the
same. Some code with writers may have gotten slower, especially one
instance of a counting writer and a few instances of unbuffered writers
that now have vtable overhead.
- File size reduced by 800kb (from 100.2mb to 99.4mb)

Improvements:

- `@export` hack is no longer needed for watch
- native x86_64 backend for linux builds faster. to use it, set use_llvm
false and no_link_obj false. also set `ASAN_OPTIONS=detect_leaks=0`
otherwise it will spam the output with tens of thousands of lines of
debug info errors. may need to use the zig lldb fork for debugging.
- zig test-obj, which we will be able to use for zig unit tests

Still an issue:

- false 'dependency loop' errors remain in watch mode
- watch mode crashes observed

Follow-up:

- [ ] search `comptime Writer: type` and `comptime W: type` and remove
- [ ] remove format_mode in our zig fork
- [ ] remove deprecated.zig autoFormatLabelFallback
- [ ] remove deprecated.zig autoFormatLabel
- [ ] remove deprecated.BufferedWriter and BufferedReader
- [ ] remove override_no_export_cpp_apis as it is no longer needed
- [ ] css Parser(W) -> Parser, and remove all the comptime writer: type
params
- [ ] remove deprecated writer fully

Files that add lines:

```
649     src/deprecated.zig
167     scripts/pack-codegen-for-zig-team.ts
54      scripts/cleartrace-impl.js
46      scripts/cleartrace.ts
43      src/windows.zig
18      src/fs.zig
17      src/bun.js/ConsoleObject.zig
16      src/output.zig
12      src/bun.js/test/debug.zig
12      src/bun.js/node/node_fs.zig
8       src/env_loader.zig
7       src/css/printer.zig
7       src/cli/init_command.zig
7       src/bun.js/node.zig
6       src/string/escapeRegExp.zig
6       src/install/PnpmMatcher.zig
5       src/bun.js/webcore/Blob.zig
4       src/crash_handler.zig
4       src/bun.zig
3       src/install/lockfile/bun.lock.zig
3       src/cli/update_interactive_command.zig
3       src/cli/pack_command.zig
3       build.zig
2       src/Progress.zig
2       src/install/lockfile/lockfile_json_stringify_for_debugging.zig
2       src/css/small_list.zig
2       src/bun.js/webcore/prompt.zig
1       test/internal/ban-words.test.ts
1       test/internal/ban-limits.json
1       src/watcher/WatcherTrace.zig
1       src/transpiler.zig
1       src/shell/builtin/cp.zig
1       src/js_printer.zig
1       src/io/PipeReader.zig
1       src/install/bin.zig
1       src/css/selectors/selector.zig
1       src/cli/run_command.zig
1       src/bun.js/RuntimeTranspilerStore.zig
1       src/bun.js/bindings/JSRef.zig
1       src/bake/DevServer.zig
```

Files that remove lines:

```
-1      src/test/recover.zig
-1      src/sql/postgres/SocketMonitor.zig
-1      src/sql/mysql/MySQLRequestQueue.zig
-1      src/sourcemap/CodeCoverage.zig
-1      src/css/values/color_js.zig
-1      src/compile_target.zig
-1      src/bundler/linker_context/convertStmtsForChunk.zig
-1      src/bundler/bundle_v2.zig
-1      src/bun.js/webcore/blob/read_file.zig
-1      src/ast/base.zig
-2      src/sql/postgres/protocol/ArrayList.zig
-2      src/shell/builtin/mkdir.zig
-2      src/install/PackageManager/patchPackage.zig
-2      src/install/PackageManager/PackageManagerDirectories.zig
-2      src/fmt.zig
-2      src/css/declaration.zig
-2      src/css/css_parser.zig
-2      src/collections/baby_list.zig
-2      src/bun.js/bindings/ZigStackFrame.zig
-2      src/ast/E.zig
-3      src/StandaloneModuleGraph.zig
-3      src/deps/picohttp.zig
-3      src/deps/libuv.zig
-3      src/btjs.zig
-4      src/threading/Futex.zig
-4      src/shell/builtin/touch.zig
-4      src/meta.zig
-4      src/install/lockfile.zig
-4      src/css/selectors/parser.zig
-5      src/shell/interpreter.zig
-5      src/css/error.zig
-5      src/bun.js/web_worker.zig
-5      src/bun.js.zig
-6      src/cli/test_command.zig
-6      src/bun.js/VirtualMachine.zig
-6      src/bun.js/uuid.zig
-6      src/bun.js/bindings/JSValue.zig
-9      src/bun.js/test/pretty_format.zig
-9      src/bun.js/api/BunObject.zig
-14     src/install/install_binding.zig
-14     src/fd.zig
-14     src/bun.js/node/path.zig
-14     scripts/pack-codegen-for-zig-team.sh
-17     src/bun.js/test/diff_format.zig
```

`git diff --numstat origin/main...HEAD | awk '{ print ($1-$2)"\t"$3 }' |
sort -rn`

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Dylan Conway <dylan.conway567@gmail.com>
Co-authored-by: Meghan Denny <meghan@bun.com>
Co-authored-by: tayor.fish <contact@taylor.fish>
2025-11-10 14:38:26 -08:00

747 lines
30 KiB
Zig

// Windows PE sections use standard file alignment (typically 512 bytes)
// No special 16KB alignment needed like macOS code signing
// New error types for PE manipulation
pub const Error = error{
OutOfBounds,
BadAlignment,
Overflow,
InvalidPEFile,
InvalidDOSSignature,
InvalidPESignature,
UnsupportedPEFormat,
InsufficientHeaderSpace,
TooManySections,
SectionExists,
InputIsSigned,
InvalidSecurityDirectory,
SecurityDirInsideImage,
UnexpectedOverlayPresent,
InvalidSectionData,
BunSectionNotFound,
InvalidBunSection,
InsufficientSpace,
SizeOfImageMismatch,
};
// Enums for strip modes and options
pub const StripMode = enum { none, strip_if_signed, strip_always };
pub const StripOpts = struct {
require_overlay: bool = true,
recompute_checksum: bool = true,
};
/// Windows PE Binary manipulation for codesigning standalone executables
pub const PEFile = struct {
data: std.array_list.Managed(u8),
allocator: Allocator,
// Store offsets instead of pointers to avoid invalidation after resize
dos_header_offset: usize,
pe_header_offset: usize,
optional_header_offset: usize,
section_headers_offset: usize,
num_sections: u16,
// Cached values from init
first_raw: u32,
last_file_end: u32,
last_va_end: u32,
const DOSHeader = extern struct {
e_magic: u16, // Magic number
e_cblp: u16, // Bytes on last page of file
e_cp: u16, // Pages in file
e_crlc: u16, // Relocations
e_cparhdr: u16, // Size of header in paragraphs
e_minalloc: u16, // Minimum extra paragraphs needed
e_maxalloc: u16, // Maximum extra paragraphs needed
e_ss: u16, // Initial relative SS value
e_sp: u16, // Initial SP value
e_csum: u16, // Checksum
e_ip: u16, // Initial IP value
e_cs: u16, // Initial relative CS value
e_lfarlc: u16, // Address of relocation table
e_ovno: u16, // Overlay number
e_res: [4]u16, // Reserved words
e_oemid: u16, // OEM identifier (for e_oeminfo)
e_oeminfo: u16, // OEM information; e_oemid specific
e_res2: [10]u16, // Reserved words
e_lfanew: u32, // File address of new exe header
};
const PEHeader = extern struct {
signature: u32, // PE signature
machine: u16, // Machine type
number_of_sections: u16, // Number of sections
time_date_stamp: u32, // Time/date stamp
pointer_to_symbol_table: u32, // Pointer to symbol table
number_of_symbols: u32, // Number of symbols
size_of_optional_header: u16, // Size of optional header
characteristics: u16, // Characteristics
};
const OptionalHeader64 = extern struct {
magic: u16, // Magic number
major_linker_version: u8, // Major linker version
minor_linker_version: u8, // Minor linker version
size_of_code: u32, // Size of code
size_of_initialized_data: u32, // Size of initialized data
size_of_uninitialized_data: u32, // Size of uninitialized data
address_of_entry_point: u32, // Address of entry point
base_of_code: u32, // Base of code
image_base: u64, // Image base
section_alignment: u32, // Section alignment
file_alignment: u32, // File alignment
major_operating_system_version: u16, // Major OS version
minor_operating_system_version: u16, // Minor OS version
major_image_version: u16, // Major image version
minor_image_version: u16, // Minor image version
major_subsystem_version: u16, // Major subsystem version
minor_subsystem_version: u16, // Minor subsystem version
win32_version_value: u32, // Win32 version value
size_of_image: u32, // Size of image
size_of_headers: u32, // Size of headers
checksum: u32, // Checksum
subsystem: u16, // Subsystem
dll_characteristics: u16, // DLL characteristics
size_of_stack_reserve: u64, // Size of stack reserve
size_of_stack_commit: u64, // Size of stack commit
size_of_heap_reserve: u64, // Size of heap reserve
size_of_heap_commit: u64, // Size of heap commit
loader_flags: u32, // Loader flags
number_of_rva_and_sizes: u32, // Number of RVA and sizes
data_directories: [16]DataDirectory, // Data directories
};
const DataDirectory = extern struct {
virtual_address: u32,
size: u32,
};
const SectionHeader = extern struct {
name: [8]u8, // Section name
virtual_size: u32, // Virtual size
virtual_address: u32, // Virtual address
size_of_raw_data: u32, // Size of raw data
pointer_to_raw_data: u32, // Pointer to raw data
pointer_to_relocations: u32, // Pointer to relocations
pointer_to_line_numbers: u32, // Pointer to line numbers
number_of_relocations: u16, // Number of relocations
number_of_line_numbers: u16, // Number of line numbers
characteristics: u32, // Characteristics
};
const PE_SIGNATURE = 0x00004550; // "PE\0\0"
const DOS_SIGNATURE = 0x5A4D; // "MZ"
const OPTIONAL_HEADER_MAGIC_64 = 0x020B;
// Section characteristics
const IMAGE_SCN_CNT_CODE = 0x00000020;
const IMAGE_SCN_CNT_INITIALIZED_DATA = 0x00000040;
const IMAGE_SCN_MEM_READ = 0x40000000;
const IMAGE_SCN_MEM_WRITE = 0x80000000;
const IMAGE_SCN_MEM_EXECUTE = 0x20000000;
// Directory indices and DLL characteristics
const IMAGE_DIRECTORY_ENTRY_SECURITY: usize = 4;
const IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY: u16 = 0x0080;
// Section name constant for exact comparison
const BUN_SECTION_NAME = [_]u8{ '.', 'b', 'u', 'n', 0, 0, 0, 0 };
// Safe access helpers for unaligned views
fn viewAtConst(comptime T: type, buf: []const u8, off: usize) !*align(1) const T {
if (off + @sizeOf(T) > buf.len) return error.OutOfBounds;
return @ptrCast(buf[off .. off + @sizeOf(T)].ptr);
}
fn viewAtMut(comptime T: type, buf: []u8, off: usize) !*align(1) T {
if (off + @sizeOf(T) > buf.len) return error.OutOfBounds;
return @ptrCast(buf[off .. off + @sizeOf(T)].ptr);
}
fn isPow2(x: u32) bool {
return x != 0 and (x & (x - 1)) == 0;
}
fn alignUpU32(v: u32, a: u32) !u32 {
if (a == 0) return v;
if (!isPow2(a)) return error.BadAlignment;
const add = a - 1;
if (v > std.math.maxInt(u32) - add) return error.Overflow;
return (v + add) & ~add;
}
fn alignUpUsize(v: usize, a: usize) !usize {
if (a == 0) return v;
if ((a & (a - 1)) != 0) return error.BadAlignment;
const add = a - 1;
if (v > std.math.maxInt(usize) - add) return error.Overflow;
return (v + add) & ~add;
}
// Helper methods to safely access headers using unaligned pointers
fn getDosHeader(self: *const PEFile) !*align(1) const DOSHeader {
return viewAtConst(DOSHeader, self.data.items, self.dos_header_offset);
}
fn getDosHeaderMut(self: *PEFile) !*align(1) DOSHeader {
return viewAtMut(DOSHeader, self.data.items, self.dos_header_offset);
}
fn getPEHeader(self: *const PEFile) !*align(1) const PEHeader {
return viewAtConst(PEHeader, self.data.items, self.pe_header_offset);
}
fn getPEHeaderMut(self: *PEFile) !*align(1) PEHeader {
return viewAtMut(PEHeader, self.data.items, self.pe_header_offset);
}
fn getOptionalHeader(self: *const PEFile) !*align(1) const OptionalHeader64 {
return viewAtConst(OptionalHeader64, self.data.items, self.optional_header_offset);
}
fn getOptionalHeaderMut(self: *PEFile) !*align(1) OptionalHeader64 {
return viewAtMut(OptionalHeader64, self.data.items, self.optional_header_offset);
}
fn getSectionHeaders(self: *const PEFile) ![]align(1) const SectionHeader {
const start = self.section_headers_offset;
const size = @sizeOf(SectionHeader) * self.num_sections;
if (start + size > self.data.items.len) return error.OutOfBounds;
const ptr: [*]align(1) const SectionHeader = @ptrCast(self.data.items[start..].ptr);
return ptr[0..self.num_sections];
}
fn getSectionHeadersMut(self: *PEFile) ![]align(1) SectionHeader {
const start = self.section_headers_offset;
const size = @sizeOf(SectionHeader) * self.num_sections;
if (start + size > self.data.items.len) return error.OutOfBounds;
const ptr: [*]align(1) SectionHeader = @ptrCast(self.data.items[start..].ptr);
return ptr[0..self.num_sections];
}
pub fn init(allocator: Allocator, pe_data: []const u8) !*PEFile {
// 1. Reserve capacity as before
var data = try std.array_list.Managed(u8).initCapacity(allocator, pe_data.len + 64 * 1024);
try data.appendSlice(pe_data);
const self = try allocator.create(PEFile);
errdefer allocator.destroy(self);
// 2. Validate DOS header
if (data.items.len < @sizeOf(DOSHeader)) {
return error.InvalidPEFile;
}
const dos_header = try viewAtConst(DOSHeader, data.items, 0);
if (dos_header.e_magic != DOS_SIGNATURE) {
return error.InvalidDOSSignature;
}
// Bound e_lfanew against file size, not 0x1000
if (dos_header.e_lfanew < @sizeOf(DOSHeader)) {
return error.InvalidPEFile;
}
if (dos_header.e_lfanew > data.items.len -| @sizeOf(PEHeader)) {
return error.InvalidPEFile;
}
// 3. Read PE header via viewAtMut
const pe_off = dos_header.e_lfanew;
const pe_header = try viewAtMut(PEHeader, data.items, pe_off);
if (pe_header.signature != PE_SIGNATURE) {
return error.InvalidPESignature;
}
// 4. Compute optional_header_offset
const optional_header_offset = pe_off + @sizeOf(PEHeader);
if (data.items.len < optional_header_offset + pe_header.size_of_optional_header) {
return error.InvalidPEFile;
}
if (pe_header.size_of_optional_header < @sizeOf(OptionalHeader64)) {
return error.InvalidPEFile;
}
// 5. Read optional header
const optional_header = try viewAtMut(OptionalHeader64, data.items, optional_header_offset);
if (optional_header.magic != OPTIONAL_HEADER_MAGIC_64) {
return error.UnsupportedPEFormat;
}
// Validate file_alignment and section_alignment
if (!isPow2(optional_header.file_alignment) or !isPow2(optional_header.section_alignment)) {
return error.BadAlignment;
}
// If section_alignment < 4096, then file_alignment == section_alignment
if (optional_header.section_alignment < 4096) {
if (optional_header.file_alignment != optional_header.section_alignment) {
return error.InvalidPEFile;
}
}
// 6. Compute section_headers_offset
const section_headers_offset = optional_header_offset + pe_header.size_of_optional_header;
const num_sections = pe_header.number_of_sections;
if (num_sections > 96) { // PE limit
return error.TooManySections;
}
const section_headers_size = @sizeOf(SectionHeader) * num_sections;
if (data.items.len < section_headers_offset + section_headers_size) {
return error.InvalidPEFile;
}
// 7. Precompute first_raw, last_file_end, last_va_end
var first_raw: u32 = @intCast(data.items.len);
var last_file_end: u32 = 0;
var last_va_end: u32 = 0;
if (num_sections > 0) {
const sections_ptr: [*]align(1) const SectionHeader = @ptrCast(data.items[section_headers_offset..].ptr);
const sections = sections_ptr[0..num_sections];
for (sections) |section| {
if (section.size_of_raw_data > 0) {
if (section.pointer_to_raw_data < first_raw) {
first_raw = section.pointer_to_raw_data;
}
const file_end = section.pointer_to_raw_data + section.size_of_raw_data;
if (file_end > last_file_end) {
last_file_end = file_end;
}
}
// Use effective virtual size (max of virtual_size and size_of_raw_data)
const vs_effective = @max(section.virtual_size, section.size_of_raw_data);
const va_end = section.virtual_address + (try alignUpU32(vs_effective, optional_header.section_alignment));
if (va_end > last_va_end) {
last_va_end = va_end;
}
}
}
self.* = .{
.data = data,
.allocator = allocator,
.dos_header_offset = 0,
.pe_header_offset = pe_off,
.optional_header_offset = optional_header_offset,
.section_headers_offset = section_headers_offset,
.num_sections = num_sections,
.first_raw = first_raw,
.last_file_end = last_file_end,
.last_va_end = last_va_end,
};
return self;
}
pub fn deinit(self: *PEFile) void {
self.data.deinit();
self.allocator.destroy(self);
}
/// Strip Authenticode signatures from the PE file
pub fn stripAuthenticode(self: *PEFile, opts: StripOpts) !void {
const data = self.data.items;
const opt = try viewAtMut(OptionalHeader64, data, self.optional_header_offset);
// Read Security directory (index 4)
const dd_ptr: *align(1) DataDirectory = &opt.data_directories[IMAGE_DIRECTORY_ENTRY_SECURITY];
const sec_off_u32 = dd_ptr.virtual_address; // file offset (not RVA)
const sec_size_u32 = dd_ptr.size;
if (sec_off_u32 == 0 or sec_size_u32 == 0) return; // nothing to strip
// Compute last_file_end from sections (reuse cached or recompute)
var last_raw_end: u32 = 0;
const sections = try self.getSectionHeaders();
for (sections) |s| {
const end = s.pointer_to_raw_data + s.size_of_raw_data;
if (end > last_raw_end) last_raw_end = end;
}
const file_len = data.len;
const sec_off = @as(usize, sec_off_u32);
const sec_size = @as(usize, sec_size_u32);
if (sec_off >= file_len or sec_size == 0) return error.InvalidSecurityDirectory;
if (opts.require_overlay and sec_off < @as(usize, last_raw_end))
return error.SecurityDirInsideImage;
// Remove certificate plus 8-byte padding at tail
const end_raw = try alignUpUsize(sec_off + sec_size, 8);
if (end_raw > file_len) return error.InvalidSecurityDirectory;
if (end_raw == file_len) {
try self.data.resize(sec_off);
} else {
const tail_len = file_len - end_raw;
// Use copyBackwards for potentially overlapping memory regions
std.mem.copyBackwards(u8, self.data.items[sec_off .. sec_off + tail_len], self.data.items[end_raw..file_len]);
try self.data.resize(sec_off + tail_len);
}
// Re-get pointers after resize
const opt_after = try self.getOptionalHeaderMut();
const dd_after: *align(1) DataDirectory = &opt_after.data_directories[IMAGE_DIRECTORY_ENTRY_SECURITY];
// Zero Security directory entry
dd_after.virtual_address = 0;
dd_after.size = 0;
// Clear FORCE_INTEGRITY bit if set
if ((opt_after.dll_characteristics & IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY) != 0)
opt_after.dll_characteristics &= ~IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY;
// Recompute checksum (recommended)
if (opts.recompute_checksum) try self.recomputePEChecksum();
// After strip, ensure no remaining overlay beyond last section
const after_strip_len = self.data.items.len;
if (@as(usize, last_raw_end) < after_strip_len)
return error.UnexpectedOverlayPresent;
}
/// Recompute PE checksum according to Windows spec
fn recomputePEChecksum(self: *PEFile) !void {
const data = self.data.items;
const checksum_off = self.optional_header_offset + @offsetOf(OptionalHeader64, "checksum");
// Zero checksum field before summing
@memset(self.data.items[checksum_off .. checksum_off + 4], 0);
var sum: u64 = 0;
var i: usize = 0;
// Sum 16-bit words
while (i + 1 < data.len) : (i += 2) {
const w: u16 = @as(u16, data[i]) | (@as(u16, data[i + 1]) << 8);
sum += w;
sum = (sum & 0xffff) + (sum >> 16); // fold periodically
}
// Odd trailing byte
if ((data.len & 1) != 0) {
sum += data[data.len - 1];
}
// Final folds + add length
sum = (sum & 0xffff) + (sum >> 16);
sum = (sum & 0xffff) + (sum >> 16);
sum += @as(u64, @intCast(data.len));
sum = (sum & 0xffff) + (sum >> 16);
const final_sum: u32 = @intCast((sum & 0xffff) + (sum >> 16));
const opt = try self.getOptionalHeaderMut();
opt.checksum = final_sum;
}
/// Add a new section to the PE file for storing Bun module data
pub fn addBunSection(self: *PEFile, data_to_embed: []const u8, strip: StripMode) !void {
// 1. Optional strip (before any addition)
if (strip == .strip_always) {
try self.stripAuthenticode(.{ .require_overlay = true, .recompute_checksum = true });
} else if (strip == .strip_if_signed) {
// Read Security directory to check if signed
const opt = try self.getOptionalHeader();
const dd = opt.data_directories[IMAGE_DIRECTORY_ENTRY_SECURITY];
if (dd.virtual_address != 0 or dd.size != 0) {
try self.stripAuthenticode(.{ .require_overlay = true, .recompute_checksum = true });
}
}
// 2. Re-read PE/Optional (pointers may have moved due to resize in strip)
const opt = try self.getOptionalHeaderMut();
// 3. Duplicate .bun guard - compare all 8 bytes exactly
const section_headers = try self.getSectionHeaders();
for (section_headers) |section| {
if (std.mem.eql(u8, section.name[0..8], &BUN_SECTION_NAME)) {
return error.SectionExists;
}
}
// Check if we can add another section
if (self.num_sections >= 96) { // PE limit
return error.TooManySections;
}
// 4. Compute header slack requirement
const new_headers_end = self.section_headers_offset + @sizeOf(SectionHeader) * (self.num_sections + 1);
const new_size_of_headers = try alignUpU32(@intCast(new_headers_end), opt.file_alignment);
// Determine first_raw (min PointerToRawData among sections with raw data, else data.len)
var first_raw: u32 = @intCast(self.data.items.len);
for (section_headers) |section| {
if (section.size_of_raw_data > 0) {
if (section.pointer_to_raw_data < first_raw) {
first_raw = section.pointer_to_raw_data;
}
}
}
// Require new_size_of_headers <= first_raw
if (new_size_of_headers > first_raw) {
return error.InsufficientHeaderSpace;
}
// 5. Placement calculations
// Recompute last_file_end and last_va_end after strip
var last_file_end: u32 = 0;
var last_va_end: u32 = 0;
for (section_headers) |section| {
const file_end = section.pointer_to_raw_data + section.size_of_raw_data;
if (file_end > last_file_end) {
last_file_end = file_end;
}
// Use effective virtual size (max of virtual_size and size_of_raw_data)
const vs_effective = @max(section.virtual_size, section.size_of_raw_data);
const va_end = section.virtual_address + (try alignUpU32(vs_effective, opt.section_alignment));
if (va_end > last_va_end) {
last_va_end = va_end;
}
}
// Check for overflow before adding 4
if (data_to_embed.len > std.math.maxInt(u32) - 4) {
return error.Overflow;
}
const payload_len = @as(u32, @intCast(data_to_embed.len + 4)); // 4 for LE length prefix
const raw_size = try alignUpU32(payload_len, opt.file_alignment);
const new_va = try alignUpU32(last_va_end, opt.section_alignment);
const new_raw = try alignUpU32(last_file_end, opt.file_alignment);
// 6. Resize & zero only the new section area
const new_file_size = @as(usize, new_raw) + @as(usize, raw_size);
try self.data.resize(new_file_size);
@memset(self.data.items[@intCast(new_raw)..new_file_size], 0);
// 7. Write the new SectionHeader by byte copy
const sh = SectionHeader{
.name = [_]u8{ '.', 'b', 'u', 'n', 0, 0, 0, 0 },
.virtual_size = payload_len,
.virtual_address = new_va,
.size_of_raw_data = raw_size,
.pointer_to_raw_data = new_raw,
.pointer_to_relocations = 0,
.pointer_to_line_numbers = 0,
.number_of_relocations = 0,
.number_of_line_numbers = 0,
.characteristics = IMAGE_SCN_CNT_INITIALIZED_DATA | IMAGE_SCN_MEM_READ,
};
const new_sh_off = self.section_headers_offset + @sizeOf(SectionHeader) * self.num_sections;
// Bounds check against first_raw (not file length)
if (new_sh_off + @sizeOf(SectionHeader) > first_raw) {
return error.InsufficientHeaderSpace;
}
std.mem.copyForwards(u8, self.data.items[new_sh_off .. new_sh_off + @sizeOf(SectionHeader)], std.mem.asBytes(&sh));
// 8. Write payload
// At data[new_raw ..]: write LE length prefix then data
std.mem.writeInt(u32, self.data.items[new_raw..][0..4], @intCast(data_to_embed.len), .little);
@memcpy(self.data.items[new_raw + 4 ..][0..data_to_embed.len], data_to_embed);
// 9. Update headers
// Get fresh pointers after resize
const pe_after = try self.getPEHeaderMut();
pe_after.number_of_sections += 1;
self.num_sections += 1;
const opt_after = try self.getOptionalHeaderMut();
// If opt.size_of_headers < new_size_of_headers
if (opt_after.size_of_headers < new_size_of_headers) {
opt_after.size_of_headers = new_size_of_headers;
}
// Calculate size_of_image: aligned end of last section
const section_va_end = new_va + sh.virtual_size;
opt_after.size_of_image = try alignUpU32(section_va_end, opt_after.section_alignment);
// Security directory must be zero (signature invalidated by change)
const dd_ptr: *align(1) DataDirectory = &opt_after.data_directories[IMAGE_DIRECTORY_ENTRY_SECURITY];
if (dd_ptr.virtual_address != 0 or dd_ptr.size != 0) {
dd_ptr.virtual_address = 0;
dd_ptr.size = 0;
}
// Do not touch size_of_initialized_data (leave as is)
// 10. Recompute checksum (recommended)
try self.recomputePEChecksum();
}
/// Find the .bun section and return its data
pub fn getBunSectionData(self: *const PEFile) ![]const u8 {
const section_headers = try self.getSectionHeaders();
for (section_headers) |section| {
if (std.mem.eql(u8, section.name[0..8], &BUN_SECTION_NAME)) {
if (section.size_of_raw_data < @sizeOf(u32)) {
return error.InvalidBunSection;
}
// Bounds check
if (section.pointer_to_raw_data >= self.data.items.len or
section.pointer_to_raw_data + section.size_of_raw_data > self.data.items.len)
{
return error.InvalidBunSection;
}
const section_data = self.data.items[section.pointer_to_raw_data..][0..section.size_of_raw_data];
const data_size = std.mem.readInt(u32, section_data[0..4], .little);
if (data_size + @sizeOf(u32) > section.size_of_raw_data) {
return error.InvalidBunSection;
}
return section_data[4..][0..data_size];
}
}
return error.BunSectionNotFound;
}
/// Get the length of the Bun section data
pub fn getBunSectionLength(self: *const PEFile) !u32 {
const section_headers = try self.getSectionHeaders();
for (section_headers) |section| {
if (std.mem.eql(u8, section.name[0..8], &BUN_SECTION_NAME)) {
if (section.size_of_raw_data < @sizeOf(u32)) {
return error.InvalidBunSection;
}
// Bounds check
if (section.pointer_to_raw_data >= self.data.items.len or
section.pointer_to_raw_data + @sizeOf(u32) > self.data.items.len)
{
return error.InvalidBunSection;
}
const section_data = self.data.items[section.pointer_to_raw_data..];
return std.mem.readInt(u32, section_data[0..4], .little);
}
}
return error.BunSectionNotFound;
}
/// Write the modified PE file
pub fn write(self: *const PEFile, writer: anytype) !void {
try writer.writeAll(self.data.items);
}
/// Validate the PE file structure
pub fn validate(self: *const PEFile) !void {
// Check DOS & PE signatures
const dos_header = try self.getDosHeader();
if (dos_header.e_magic != DOS_SIGNATURE) {
return error.InvalidDOSSignature;
}
const pe_header = try self.getPEHeader();
if (pe_header.signature != PE_SIGNATURE) {
return error.InvalidPESignature;
}
// Check optional header magic is 0x20B (64-bit)
const optional_header = try self.getOptionalHeader();
if (optional_header.magic != OPTIONAL_HEADER_MAGIC_64) {
return error.UnsupportedPEFormat;
}
// Validate file_alignment, section_alignment sanity
if (!isPow2(optional_header.file_alignment) or !isPow2(optional_header.section_alignment)) {
return error.BadAlignment;
}
// Relational rule
if (optional_header.section_alignment < 4096) {
if (optional_header.file_alignment != optional_header.section_alignment) {
return error.InvalidPEFile;
}
}
// Section headers region fits within size_of_headers and file
const section_headers_end = self.section_headers_offset + @sizeOf(SectionHeader) * self.num_sections;
if (section_headers_end > optional_header.size_of_headers or
section_headers_end > self.data.items.len)
{
return error.InvalidPEFile;
}
// Validate each section
const section_headers = try self.getSectionHeaders();
var max_va_end: u32 = 0;
for (section_headers, 0..) |section, i| {
// If size_of_raw_data > 0, validate raw data bounds
if (section.size_of_raw_data > 0) {
if (section.pointer_to_raw_data < optional_header.size_of_headers or
section.pointer_to_raw_data + section.size_of_raw_data > self.data.items.len)
{
return error.InvalidSectionData;
}
// Check for overlaps with other sections using correct interval test
for (section_headers[i + 1 ..]) |other| {
if (other.size_of_raw_data > 0) {
const section_start = section.pointer_to_raw_data;
const section_end = section_start + section.size_of_raw_data;
const other_start = other.pointer_to_raw_data;
const other_end = other_start + other.size_of_raw_data;
// Standard overlap test: max(start) < min(end)
if (@max(section_start, other_start) < @min(section_end, other_end)) {
return error.InvalidPEFile; // Section raw ranges overlap
}
}
}
}
// Track max virtual address end using effective virtual size
const vs_effective = @max(section.virtual_size, section.size_of_raw_data);
const va_end = section.virtual_address + (try alignUpU32(vs_effective, optional_header.section_alignment));
if (va_end > max_va_end) {
max_va_end = va_end;
}
}
// Verify size_of_image equals alignUp(max(VA + alignUp(VS, SA)), SA)
const expected_size_of_image = try alignUpU32(max_va_end, optional_header.section_alignment);
if (optional_header.size_of_image != expected_size_of_image) {
return error.SizeOfImageMismatch;
}
// Security directory should be 0,0 post-change (if we modified it)
// (This is optional validation, not critical)
// If checksum recomputed, field should be non-zero
// (Unless we intentionally write zero, which is allowed)
}
};
/// Utilities for PE file detection and validation
pub const utils = struct {
pub fn isPE(data: []const u8) bool {
if (data.len < @sizeOf(PEFile.DOSHeader)) return false;
const dos: *align(1) const PEFile.DOSHeader = @ptrCast(data.ptr);
if (dos.e_magic != PEFile.DOS_SIGNATURE) return false;
const off = dos.e_lfanew;
if (off < @sizeOf(PEFile.DOSHeader) or off > data.len -| @sizeOf(PEFile.PEHeader)) return false;
const pe: *align(1) const PEFile.PEHeader = @ptrCast(data[off..].ptr);
return pe.signature == PEFile.PE_SIGNATURE;
}
};
/// Windows-specific external interface for accessing embedded Bun data
/// This matches the macOS interface but for PE files
pub const BUN_COMPILED_SECTION_NAME = ".bun";
/// External C interface declarations - these are implemented in C++ bindings
/// The C++ code uses Windows PE APIs to directly access the .bun section
/// from the current process memory without loading the entire executable
extern "C" fn Bun__getStandaloneModuleGraphPELength() u32;
extern "C" fn Bun__getStandaloneModuleGraphPEData() ?[*]u8;
const bun = @import("bun");
const std = @import("std");
const mem = std.mem;
const Allocator = mem.Allocator;