From 32ce9a3890591c7a4a208a0c8d57495fd55c0cc3 Mon Sep 17 00:00:00 2001 From: jarred-sumner-bot Date: Tue, 15 Jul 2025 22:31:54 -0700 Subject: [PATCH] Add Windows PE codesigning support for standalone executables (#21091) Co-authored-by: Claude Bot Co-authored-by: Claude Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Jarred Sumner --- cmake/sources/ZigSources.txt | 1 + src/StandaloneModuleGraph.zig | 68 +++ src/bun.js/bindings/c-bindings.cpp | 51 +++ src/bun.zig | 1 + src/pe.zig | 405 ++++++++++++++++++ .../issue/pe-codesigning-integrity.test.ts | 376 ++++++++++++++++ 6 files changed, 902 insertions(+) create mode 100644 src/pe.zig create mode 100644 test/regression/issue/pe-codesigning-integrity.test.ts diff --git a/cmake/sources/ZigSources.txt b/cmake/sources/ZigSources.txt index 5ba0303655..a1051de723 100644 --- a/cmake/sources/ZigSources.txt +++ b/cmake/sources/ZigSources.txt @@ -652,6 +652,7 @@ src/paths.zig src/paths/EnvPath.zig src/paths/path_buffer_pool.zig src/paths/Path.zig +src/pe.zig src/perf.zig src/pool.zig src/Progress.zig diff --git a/src/StandaloneModuleGraph.zig b/src/StandaloneModuleGraph.zig index 18ac9f62e0..d965d779d0 100644 --- a/src/StandaloneModuleGraph.zig +++ b/src/StandaloneModuleGraph.zig @@ -13,6 +13,7 @@ const SourceMap = bun.sourcemap; const StringPointer = bun.StringPointer; const macho = bun.macho; +const pe = bun.pe; const w = std.os.windows; pub const StandaloneModuleGraph = struct { @@ -137,6 +138,19 @@ pub const StandaloneModuleGraph = struct { } }; + const PE = struct { + pub extern "C" fn Bun__getStandaloneModuleGraphPELength() u32; + pub extern "C" fn Bun__getStandaloneModuleGraphPEData() ?[*]u8; + + pub fn getData() ?[]const u8 { + const length = Bun__getStandaloneModuleGraphPELength(); + if (length == 0) return null; + + const data_ptr = Bun__getStandaloneModuleGraphPEData() orelse return null; + return data_ptr[0..length]; + } + }; + pub const File = struct { name: []const u8 = "", loader: bun.options.Loader, @@ -682,6 +696,44 @@ pub const StandaloneModuleGraph = struct { } return cloned_executable_fd; }, + .windows => { + const input_result = bun.sys.File.readToEnd(.{ .handle = cloned_executable_fd }, bun.default_allocator); + if (input_result.err) |err| { + Output.prettyErrorln("Error reading standalone module graph: {}", .{err}); + cleanup(zname, cloned_executable_fd); + Global.exit(1); + } + var pe_file = bun.pe.PEFile.init(bun.default_allocator, input_result.bytes.items) catch |err| { + Output.prettyErrorln("Error initializing PE file: {}", .{err}); + cleanup(zname, cloned_executable_fd); + Global.exit(1); + }; + defer pe_file.deinit(); + pe_file.addBunSection(bytes) catch |err| { + Output.prettyErrorln("Error adding Bun section to PE file: {}", .{err}); + cleanup(zname, cloned_executable_fd); + Global.exit(1); + }; + input_result.bytes.deinit(); + + switch (Syscall.setFileOffset(cloned_executable_fd, 0)) { + .err => |err| { + Output.prettyErrorln("Error seeking to start of temporary file: {}", .{err}); + cleanup(zname, cloned_executable_fd); + Global.exit(1); + }, + else => {}, + } + + var file = bun.sys.File{ .handle = cloned_executable_fd }; + const writer = file.writer(); + pe_file.write(writer) catch |err| { + Output.prettyErrorln("Error writing PE file: {}", .{err}); + cleanup(zname, cloned_executable_fd); + Global.exit(1); + }; + return cloned_executable_fd; + }, else => { var total_byte_count: usize = undefined; if (Environment.isWindows) { @@ -888,6 +940,22 @@ pub const StandaloneModuleGraph = struct { return try StandaloneModuleGraph.fromBytes(allocator, @constCast(macho_bytes), offsets); } + if (comptime Environment.isWindows) { + const pe_bytes = PE.getData() orelse return null; + if (pe_bytes.len < @sizeOf(Offsets) + trailer.len) { + Output.debugWarn("bun standalone module graph is too small to be valid", .{}); + return null; + } + const pe_bytes_slice = pe_bytes[pe_bytes.len - @sizeOf(Offsets) - trailer.len ..]; + const trailer_bytes = pe_bytes[pe_bytes.len - trailer.len ..][0..trailer.len]; + if (!bun.strings.eqlComptime(trailer_bytes, trailer)) { + Output.debugWarn("bun standalone module graph has invalid trailer", .{}); + return null; + } + const offsets = std.mem.bytesAsValue(Offsets, pe_bytes_slice).*; + return try StandaloneModuleGraph.fromBytes(allocator, @constCast(pe_bytes), offsets); + } + // Do not invoke libuv here. const self_exe = openSelf() catch return null; defer self_exe.close(); diff --git a/src/bun.js/bindings/c-bindings.cpp b/src/bun.js/bindings/c-bindings.cpp index 5b0f257490..3f7c50467a 100644 --- a/src/bun.js/bindings/c-bindings.cpp +++ b/src/bun.js/bindings/c-bindings.cpp @@ -921,4 +921,55 @@ extern "C" uint32_t* Bun__getStandaloneModuleGraphMachoLength() { return &BUN_COMPILED.size; } + +#elif defined(_WIN32) +// Windows PE section handling +#include +#include + +static uint32_t* pe_section_size = nullptr; +static uint8_t* pe_section_data = nullptr; + +// Helper function to find and map the .bun section +static bool initializePESection() +{ + if (pe_section_size != nullptr) return true; + + HMODULE hModule = GetModuleHandleA(NULL); + if (!hModule) return false; + + PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)hModule; + if (dosHeader->e_magic != IMAGE_DOS_SIGNATURE) return false; + + PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)((BYTE*)hModule + dosHeader->e_lfanew); + if (ntHeaders->Signature != IMAGE_NT_SIGNATURE) return false; + + PIMAGE_SECTION_HEADER sectionHeader = IMAGE_FIRST_SECTION(ntHeaders); + + for (int i = 0; i < ntHeaders->FileHeader.NumberOfSections; i++) { + if (strncmp((char*)sectionHeader->Name, ".bun", 4) == 0) { + // Found the .bun section + BYTE* sectionData = (BYTE*)hModule + sectionHeader->VirtualAddress; + pe_section_size = (uint32_t*)sectionData; + pe_section_data = sectionData + sizeof(uint32_t); + return true; + } + sectionHeader++; + } + + return false; +} + +extern "C" uint32_t Bun__getStandaloneModuleGraphPELength() +{ + if (!initializePESection()) return 0; + return pe_section_size ? *pe_section_size : 0; +} + +extern "C" uint8_t* Bun__getStandaloneModuleGraphPEData() +{ + if (!initializePESection()) return nullptr; + return pe_section_data; +} + #endif diff --git a/src/bun.zig b/src/bun.zig index be32d13b87..4efde87b06 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -3750,6 +3750,7 @@ pub fn freeSensitive(allocator: std.mem.Allocator, slice: anytype) void { pub const server = @import("./bun.js/api/server.zig"); pub const macho = @import("./macho.zig"); +pub const pe = @import("./pe.zig"); pub const valkey = @import("./valkey/index.zig"); pub const highway = @import("./highway.zig"); diff --git a/src/pe.zig b/src/pe.zig new file mode 100644 index 0000000000..3ed48dd315 --- /dev/null +++ b/src/pe.zig @@ -0,0 +1,405 @@ +const std = @import("std"); +const mem = std.mem; +const Allocator = mem.Allocator; +const bun = @import("bun"); +const strings = bun.strings; + +// Windows PE sections use standard file alignment (typically 512 bytes) +// No special 16KB alignment needed like macOS code signing + +/// 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, + + 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; + + // Helper methods to safely access headers + fn getDosHeader(self: *const PEFile) *DOSHeader { + return @ptrCast(@alignCast(self.data.items.ptr + self.dos_header_offset)); + } + + fn getPEHeader(self: *const PEFile) *PEHeader { + return @ptrCast(@alignCast(self.data.items.ptr + self.pe_header_offset)); + } + + fn getOptionalHeader(self: *const PEFile) *OptionalHeader64 { + return @ptrCast(@alignCast(self.data.items.ptr + self.optional_header_offset)); + } + + fn getSectionHeaders(self: *const PEFile) []SectionHeader { + return @as([*]SectionHeader, @ptrCast(@alignCast(self.data.items.ptr + self.section_headers_offset)))[0..self.num_sections]; + } + + pub fn init(allocator: Allocator, pe_data: []const u8) !*PEFile { + // Reserve some extra space for adding sections, but no need for 16KB alignment + 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); + + // Parse DOS header + if (data.items.len < @sizeOf(DOSHeader)) { + return error.InvalidPEFile; + } + + const dos_header: *const DOSHeader = @ptrCast(@alignCast(data.items.ptr)); + if (dos_header.e_magic != DOS_SIGNATURE) { + return error.InvalidDOSSignature; + } + + // Validate e_lfanew offset (should be reasonable) + if (dos_header.e_lfanew < @sizeOf(DOSHeader) or dos_header.e_lfanew > 0x1000) { + return error.InvalidPEFile; + } + + // Calculate offsets + const pe_header_offset = dos_header.e_lfanew; + const optional_header_offset = pe_header_offset + @sizeOf(PEHeader); + + // Parse PE header + if (data.items.len < pe_header_offset + @sizeOf(PEHeader)) { + return error.InvalidPEFile; + } + + const pe_header: *const PEHeader = @ptrCast(@alignCast(data.items.ptr + pe_header_offset)); + if (pe_header.signature != PE_SIGNATURE) { + return error.InvalidPESignature; + } + + // Parse optional header + if (data.items.len < optional_header_offset + @sizeOf(OptionalHeader64)) { + return error.InvalidPEFile; + } + + const optional_header: *const OptionalHeader64 = @ptrCast(@alignCast(data.items.ptr + optional_header_offset)); + if (optional_header.magic != OPTIONAL_HEADER_MAGIC_64) { + return error.UnsupportedPEFormat; + } + + // Parse section headers + const section_headers_offset = optional_header_offset + pe_header.size_of_optional_header; + const section_headers_size = @sizeOf(SectionHeader) * pe_header.number_of_sections; + if (data.items.len < section_headers_offset + section_headers_size) { + return error.InvalidPEFile; + } + + // Check if we have space for at least one more section header (for future addition) + const max_sections_space = section_headers_offset + @sizeOf(SectionHeader) * 96; // PE max sections + if (data.items.len < max_sections_space) { + // Not enough space to add sections - we'll need to handle this in addBunSection + } + + self.* = .{ + .data = data, + .allocator = allocator, + .dos_header_offset = 0, + .pe_header_offset = pe_header_offset, + .optional_header_offset = optional_header_offset, + .section_headers_offset = section_headers_offset, + .num_sections = pe_header.number_of_sections, + }; + + return self; + } + + pub fn deinit(self: *PEFile) void { + self.data.deinit(); + self.allocator.destroy(self); + } + + /// Add a new section to the PE file for storing Bun module data + pub fn addBunSection(self: *PEFile, data_to_embed: []const u8) !void { + const section_name = ".bun\x00\x00\x00\x00"; + const optional_header = self.getOptionalHeader(); + const aligned_size = alignSize(@intCast(data_to_embed.len + @sizeOf(u32)), optional_header.file_alignment); + + // Check if we can add another section + if (self.num_sections >= 95) { // PE limit is 96 sections + return error.TooManySections; + } + + // Find the last section to determine where to place the new one + var last_section_end: u32 = 0; + var last_virtual_end: u32 = 0; + + const section_headers = self.getSectionHeaders(); + for (section_headers) |section| { + const section_file_end = section.pointer_to_raw_data + section.size_of_raw_data; + const section_virtual_end = section.virtual_address + alignSize(section.virtual_size, optional_header.section_alignment); + + if (section_file_end > last_section_end) { + last_section_end = section_file_end; + } + if (section_virtual_end > last_virtual_end) { + last_virtual_end = section_virtual_end; + } + } + + // Create new section header + const new_section = SectionHeader{ + .name = section_name.*, + .virtual_size = @intCast(data_to_embed.len + @sizeOf(u32)), + .virtual_address = alignSize(last_virtual_end, optional_header.section_alignment), + .size_of_raw_data = aligned_size, + .pointer_to_raw_data = alignSize(last_section_end, optional_header.file_alignment), + .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, + }; + + // Resize data to accommodate new section + const new_data_size = new_section.pointer_to_raw_data + new_section.size_of_raw_data; + try self.data.resize(new_data_size); + + // Zero out the new section data + @memset(self.data.items[last_section_end..new_data_size], 0); + + // Write the section header - use our stored offset + const new_section_offset = self.section_headers_offset + @sizeOf(SectionHeader) * self.num_sections; + + // Check bounds before writing + if (new_section_offset + @sizeOf(SectionHeader) > self.data.items.len) { + return error.InsufficientSpace; + } + + const new_section_ptr: *SectionHeader = @ptrCast(@alignCast(self.data.items.ptr + new_section_offset)); + new_section_ptr.* = new_section; + + // Write the data with size header + const data_offset = new_section.pointer_to_raw_data; + std.mem.writeInt(u32, self.data.items[data_offset..][0..4], @intCast(data_to_embed.len), .little); + @memcpy(self.data.items[data_offset + 4 ..][0..data_to_embed.len], data_to_embed); + + // Update PE header - get fresh pointer after resize + const pe_header = self.getPEHeader(); + pe_header.number_of_sections += 1; + self.num_sections += 1; + + // Update optional header - get fresh pointer after resize + const updated_optional_header = self.getOptionalHeader(); + updated_optional_header.size_of_image = alignSize(new_section.virtual_address + new_section.virtual_size, updated_optional_header.section_alignment); + updated_optional_header.size_of_initialized_data += new_section.size_of_raw_data; + } + + /// Find the .bun section and return its data + pub fn getBunSectionData(self: *const PEFile) ![]const u8 { + const section_headers = self.getSectionHeaders(); + for (section_headers) |section| { + if (strings.eqlComptime(section.name[0..4], ".bun")) { + 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 = self.getSectionHeaders(); + for (section_headers) |section| { + if (strings.eqlComptime(section.name[0..4], ".bun")) { + 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 header + const dos_header = self.getDosHeader(); + if (dos_header.e_magic != DOS_SIGNATURE) { + return error.InvalidDOSSignature; + } + + // Check PE header + const pe_header = self.getPEHeader(); + if (pe_header.signature != PE_SIGNATURE) { + return error.InvalidPESignature; + } + + // Check optional header + const optional_header = self.getOptionalHeader(); + if (optional_header.magic != OPTIONAL_HEADER_MAGIC_64) { + return error.UnsupportedPEFormat; + } + + // Validate section headers + const section_headers = self.getSectionHeaders(); + for (section_headers) |section| { + if (section.pointer_to_raw_data + section.size_of_raw_data > self.data.items.len) { + return error.InvalidSectionData; + } + } + } +}; + +/// Align size to the nearest multiple of alignment +fn alignSize(size: u32, alignment: u32) u32 { + if (alignment == 0) return size; + // Check for overflow + if (size > std.math.maxInt(u32) - alignment + 1) return std.math.maxInt(u32); + return (size + alignment - 1) & ~(alignment - 1); +} + +/// 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_header: *const PEFile.DOSHeader = @ptrCast(@alignCast(data.ptr)); + if (dos_header.e_magic != PEFile.DOS_SIGNATURE) return false; + + if (data.len < dos_header.e_lfanew + @sizeOf(PEFile.PEHeader)) return false; + + const pe_header: *const PEFile.PEHeader = @ptrCast(@alignCast(data.ptr + dos_header.e_lfanew)); + return pe_header.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; diff --git a/test/regression/issue/pe-codesigning-integrity.test.ts b/test/regression/issue/pe-codesigning-integrity.test.ts new file mode 100644 index 0000000000..ffd6305365 --- /dev/null +++ b/test/regression/issue/pe-codesigning-integrity.test.ts @@ -0,0 +1,376 @@ +import { afterAll, beforeAll, describe, expect, it } from "bun:test"; +import { readFileSync, unlinkSync } from "fs"; +import { bunEnv, bunExe, isWindows, tempDirWithFiles } from "harness"; +import { join } from "path"; + +describe.if(isWindows)("PE codesigning integrity", () => { + let tempDir: string; + + beforeAll(() => { + tempDir = tempDirWithFiles("pe-codesigning", {}); + }); + + afterAll(() => { + // Cleanup any test executables + try { + unlinkSync(join(tempDir, "test-pe-simple.exe")); + unlinkSync(join(tempDir, "test-pe-large.exe")); + } catch {} + }); + + // PE file parsing utilities using DataView + class PEParser { + private view: DataView; + private buffer: ArrayBuffer; + + constructor(data: Uint8Array) { + this.buffer = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) as ArrayBuffer; + this.view = new DataView(this.buffer); + } + + // Parse DOS header + parseDOSHeader() { + const dosSignature = this.view.getUint16(0, true); // "MZ" = 0x5A4D + const e_lfanew = this.view.getUint32(60, true); // Offset to PE header + + return { + signature: dosSignature, + e_lfanew, + isValid: dosSignature === 0x5a4d && e_lfanew > 0 && e_lfanew < 0x1000, + }; + } + + // Parse PE header + parsePEHeader(offset: number) { + const peSignature = this.view.getUint32(offset, true); // "PE\0\0" = 0x00004550 + const machine = this.view.getUint16(offset + 4, true); + const numberOfSections = this.view.getUint16(offset + 6, true); + const sizeOfOptionalHeader = this.view.getUint16(offset + 20, true); + + return { + signature: peSignature, + machine, + numberOfSections, + sizeOfOptionalHeader, + isValid: peSignature === 0x00004550 && numberOfSections > 0, + }; + } + + // Parse optional header (PE32+) + parseOptionalHeader(offset: number) { + const magic = this.view.getUint16(offset, true); // 0x020B for PE32+ + const sizeOfImage = this.view.getUint32(offset + 56, true); + const fileAlignment = this.view.getUint32(offset + 36, true); + const sectionAlignment = this.view.getUint32(offset + 32, true); + + return { + magic, + sizeOfImage, + fileAlignment, + sectionAlignment, + isValid: magic === 0x020b, + }; + } + + // Parse section headers + parseSectionHeaders(offset: number, count: number) { + const sections: { + name: string; + virtualSize: number; + virtualAddress: number; + sizeOfRawData: number; + pointerToRawData: number; + characteristics: number; + isValid: boolean; + }[] = []; + + for (let i = 0; i < count; i++) { + const sectionOffset = offset + i * 40; // Each section header is 40 bytes + + // Read section name (8 bytes) + const nameBytes = new Uint8Array(this.buffer, sectionOffset, 8); + const name = new TextDecoder().decode(nameBytes).replace(/\0/g, ""); + + const virtualSize = this.view.getUint32(sectionOffset + 8, true); + const virtualAddress = this.view.getUint32(sectionOffset + 12, true); + const sizeOfRawData = this.view.getUint32(sectionOffset + 16, true); + const pointerToRawData = this.view.getUint32(sectionOffset + 20, true); + const characteristics = this.view.getUint32(sectionOffset + 36, true); + + sections.push({ + name, + virtualSize, + virtualAddress, + sizeOfRawData, + pointerToRawData, + characteristics, + isValid: sizeOfRawData > 0 && pointerToRawData > 0, + }); + } + + return sections; + } + + // Find and validate .bun section + findBunSection(sections: any[]) { + const bunSection = sections.find(s => s.name === ".bun"); + if (!bunSection) return null; + + // Read the .bun section data + const sectionData = new Uint8Array(this.buffer, bunSection.pointerToRawData, bunSection.sizeOfRawData); + + // First 4 bytes should be the data size + const dataSize = new DataView(sectionData.buffer, bunSection.pointerToRawData).getUint32(0, true); + + // Validate the size is reasonable - it should match or be close to virtual size + if (dataSize > bunSection.sizeOfRawData || dataSize === 0) { + throw new Error(`Invalid .bun section: data size ${dataSize} vs section size ${bunSection.sizeOfRawData}`); + } + + // The virtual size should match the data size (plus some alignment) + if (dataSize > bunSection.virtualSize + 16) { + // Allow some padding + throw new Error(`Invalid .bun section: data size ${dataSize} exceeds virtual size ${bunSection.virtualSize}`); + } + + // Extract the actual embedded data (skip the 4-byte size header) + const embeddedData = sectionData.slice(4, 4 + dataSize); + + return { + section: bunSection, + dataSize, + embeddedData, + isValid: dataSize > 0 && dataSize <= bunSection.virtualSize, + }; + } + + // Full PE validation + validatePE() { + const dos = this.parseDOSHeader(); + if (!dos.isValid) throw new Error("Invalid DOS header"); + + const pe = this.parsePEHeader(dos.e_lfanew); + if (!pe.isValid) throw new Error("Invalid PE header"); + + const optionalHeaderOffset = dos.e_lfanew + 24; // PE header is 24 bytes + const optional = this.parseOptionalHeader(optionalHeaderOffset); + if (!optional.isValid) throw new Error("Invalid optional header"); + + const sectionsOffset = optionalHeaderOffset + pe.sizeOfOptionalHeader; + const sections = this.parseSectionHeaders(sectionsOffset, pe.numberOfSections); + + const bunSection = this.findBunSection(sections); + if (!bunSection) throw new Error(".bun section not found"); + + return { + dos, + pe, + optional, + sections, + bunSection, + }; + } + } + + it("should create valid PE executable with .bun section", async () => { + const testContent = ` +console.log("Hello from PE codesigning test!"); +console.log("Testing PE file integrity with DataView"); + +const data = { + message: "PE integrity test", + timestamp: ${Date.now()}, + randomData: "x".repeat(100) +}; + +console.log("Test data:", JSON.stringify(data)); + `.trim(); + + // Write test file + const testFile = join(tempDir, "test-pe-simple.js"); + await Bun.write(testFile, testContent); + + // Compile to Windows PE executable + const result = Bun.spawn({ + cmd: [bunExe(), "build", "--compile", testFile], + env: bunEnv, + cwd: tempDir, + }); + + await result.exited; + expect(result.exitCode).toBe(0); + + // Read the generated PE file + const exePath = join(tempDir, "test-pe-simple.exe"); + const peData = readFileSync(exePath); + + // Parse and validate PE structure + const parser = new PEParser(peData); + const validation = parser.validatePE(); + + // Validate DOS header + expect(validation.dos.signature).toBe(0x5a4d); // "MZ" + expect(validation.dos.e_lfanew).toBeGreaterThan(0); + expect(validation.dos.e_lfanew).toBeLessThan(0x1000); + + // Validate PE header + expect(validation.pe.signature).toBe(0x00004550); // "PE\0\0" + expect(validation.pe.machine).toBe(0x8664); // x64 + expect(validation.pe.numberOfSections).toBeGreaterThan(0); + + // Validate optional header + expect(validation.optional.magic).toBe(0x020b); // PE32+ + expect(validation.optional.fileAlignment).toBeGreaterThan(0); + expect(validation.optional.sectionAlignment).toBeGreaterThan(0); + + // Validate sections exist + expect(validation.sections.length).toBeGreaterThan(0); + expect(validation.sections.every(s => s.isValid)).toBe(true); + + // Validate .bun section + expect(validation.bunSection).not.toBeNull(); + expect(validation.bunSection!.isValid).toBe(true); + expect(validation.bunSection!.dataSize).toBeGreaterThan(0); + + // Validate embedded data contains our test content + // The embedded data is in StandaloneModuleGraph format, which includes: + // - Virtual path (B:/~BUN/root/filename) + // - JavaScript source code + // - Binary metadata and trailer + const embeddedText = new TextDecoder().decode(validation.bunSection!.embeddedData); + expect(embeddedText).toContain("B:/~BUN/root/"); // Windows virtual path + expect(embeddedText).toContain("Hello from PE codesigning test!"); + expect(embeddedText).toContain("PE integrity test"); + expect(embeddedText).toContain("---- Bun! ----"); // Trailer signature + }); + + it("should handle large embedded data correctly", async () => { + // Create a larger test file to verify handling of bigger data + const largeContent = ` +console.log("Large PE test"); + +// Generate some substantial content +const largeData = { + message: "Large data test", + content: "${"x".repeat(5000)}", // 5KB of data + array: ${JSON.stringify(Array.from({ length: 100 }, (_, i) => `item-${i}`))}, + timestamp: ${Date.now()} +}; + +console.log("Large data length:", JSON.stringify(largeData).length); + `.trim(); + + const testFile = join(tempDir, "test-pe-large.js"); + await Bun.write(testFile, largeContent); + + const result = Bun.spawn({ + cmd: [bunExe(), "build", "--compile", testFile], + env: bunEnv, + cwd: tempDir, + }); + + await result.exited; + expect(result.exitCode).toBe(0); + + // Read and validate the larger PE file + const exePath = join(tempDir, "test-pe-large.exe"); + const peData = readFileSync(exePath); + + const parser = new PEParser(peData); + const validation = parser.validatePE(); + + // Basic PE validation + expect(validation.dos.isValid).toBe(true); + expect(validation.pe.isValid).toBe(true); + expect(validation.optional.isValid).toBe(true); + + // .bun section should contain the larger data + expect(validation.bunSection).not.toBeNull(); + expect(validation.bunSection!.dataSize).toBeGreaterThan(1000); // Should be substantial + + const embeddedText = new TextDecoder().decode(validation.bunSection!.embeddedData); + expect(embeddedText).toContain("B:/~BUN/root/"); // Virtual path + expect(embeddedText).toContain("Large PE test"); + expect(embeddedText).toContain("Large data test"); + expect(embeddedText).toContain("---- Bun! ----"); // Trailer + }); + + it("should align sections properly", async () => { + const testFile = join(tempDir, "test-pe-alignment.js"); + await Bun.write(testFile, 'console.log("Alignment test");'); + + const result = Bun.spawn({ + cmd: [bunExe(), "build", "--compile", testFile], + env: bunEnv, + cwd: tempDir, + }); + + await result.exited; + expect(result.exitCode).toBe(0); + + const exePath = join(tempDir, "test-pe-alignment.exe"); + const peData = readFileSync(exePath); + + const parser = new PEParser(peData); + const validation = parser.validatePE(); + + // Check that sections are properly aligned + const fileAlignment = validation.optional.fileAlignment; + const sectionAlignment = validation.optional.sectionAlignment; + + for (const section of validation.sections) { + // File offset should be aligned to file alignment + expect(section.pointerToRawData % fileAlignment).toBe(0); + + // Virtual address should be aligned to section alignment + expect(section.virtualAddress % sectionAlignment).toBe(0); + } + + // .bun section should also be properly aligned + const bunSection = validation.bunSection!.section; + expect(bunSection.pointerToRawData % fileAlignment).toBe(0); + expect(bunSection.virtualAddress % sectionAlignment).toBe(0); + + // Cleanup + unlinkSync(testFile); + unlinkSync(exePath); + }); + + it("should have correct section characteristics", async () => { + const testFile = join(tempDir, "test-pe-characteristics.js"); + await Bun.write(testFile, 'console.log("Characteristics test");'); + + const result = Bun.spawn({ + cmd: [bunExe(), "build", "--compile", testFile], + env: bunEnv, + cwd: tempDir, + }); + + await result.exited; + expect(result.exitCode).toBe(0); + + const exePath = join(tempDir, "test-pe-characteristics.exe"); + const peData = readFileSync(exePath); + + const parser = new PEParser(peData); + const validation = parser.validatePE(); + + // Find .bun section and check its characteristics + const bunSection = validation.bunSection!.section; + + // .bun section should have IMAGE_SCN_CNT_INITIALIZED_DATA | IMAGE_SCN_MEM_READ + const IMAGE_SCN_CNT_INITIALIZED_DATA = 0x00000040; + const IMAGE_SCN_MEM_READ = 0x40000000; + const expectedCharacteristics = IMAGE_SCN_CNT_INITIALIZED_DATA | IMAGE_SCN_MEM_READ; + + expect(bunSection.characteristics & expectedCharacteristics).toBe(expectedCharacteristics); + + // Should NOT have execute permissions + const IMAGE_SCN_MEM_EXECUTE = 0x20000000; + expect(bunSection.characteristics & IMAGE_SCN_MEM_EXECUTE).toBe(0); + + // Cleanup + unlinkSync(testFile); + unlinkSync(exePath); + }); +});