mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 18:38:55 +00:00
## Summary Implements authenticode signature stripping for Windows PE files when using `bun build --compile`, ensuring that generated executables can be properly signed with external tools after Bun embeds its data section. ## What Changed ### Core Implementation - **Authenticode stripping**: Removes digital signatures from PE files before adding the .bun section - **Safe memory access**: Replaced all `@alignCast` operations with safe unaligned access helpers to prevent crashes - **Hardened PE parsing**: Added comprehensive bounds checking and validation throughout - **PE checksum recalculation**: Properly updates checksums after modifications ### Key Features - Always strips authenticode signatures when using `--compile` for Windows (uses `.strip_always` mode) - Validates PE file structure according to PE/COFF specification - Handles overlapping memory regions safely during certificate removal - Clears `IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY` flag when stripping signatures - Ensures no unexpected overlay data remains after stripping ### Bug Fixes - Fixed memory corruption bug using `copyBackwards` for overlapping regions - Fixed checksum calculation skipping 6 bytes instead of 4 - Added integer overflow protection in payload size calculations - Fixed double alignment bug in `size_of_image` calculation ## Technical Details The implementation follows the Windows PE/COFF specification and includes: - `StripMode` enum to control when signatures are stripped (none/strip_if_signed/strip_always) - Safe unaligned memory access helpers (`viewAtConst`, `viewAtMut`) - Proper alignment helpers with overflow protection (`alignUpU32`, `alignUpUsize`) - Comprehensive error types for all failure cases ## Testing - Passes all existing PE tests in `test/regression/issue/pe-codesigning-integrity.test.ts` - Compiles successfully with `bun run zig:check-windows` - Properly integrated with StandaloneModuleGraph for Windows compilation ## Impact This ensures Windows users can: 1. Use `bun build --compile` to create standalone executables 2. Sign the resulting executables with their own certificates 3. Distribute properly signed Windows binaries Fixes issues where previously signed executables would have invalid signatures after Bun added its embedded data. --------- Co-authored-by: Claude Bot <claude-bot@bun.sh> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
747 lines
30 KiB
Zig
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.ArrayList(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.ArrayList(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;
|