From 17b99798f4bb312c61a05fd276a47561bf9745f7 Mon Sep 17 00:00:00 2001 From: Martin Liska Date: Mon, 6 Apr 2026 10:04:44 +0200 Subject: [PATCH 01/21] include commands and file header into Text segment --- libwild/src/alignment.rs | 3 ++ libwild/src/args/macho.rs | 4 ++- libwild/src/macho.rs | 65 ++++++++++++++++++++----------------- libwild/src/macho_writer.rs | 23 +++++++++---- 4 files changed, 58 insertions(+), 37 deletions(-) diff --git a/libwild/src/alignment.rs b/libwild/src/alignment.rs index e30d8e948..72871118d 100644 --- a/libwild/src/alignment.rs +++ b/libwild/src/alignment.rs @@ -62,6 +62,9 @@ pub(crate) const NOTE_GNU_BUILD_ID: Alignment = Alignment { exponent: 2 }; // GNU_STACK.alignment pub(crate) const STACK_ALIGNMENT: Alignment = Alignment { exponent: 4 }; +// Mach-O specific +pub(crate) const MACHO_PAGE_ALIGNMENT: Alignment = Alignment { exponent: 14 }; + impl Alignment { pub(crate) fn new(raw: u64) -> Result { if !raw.is_power_of_two() { diff --git a/libwild/src/args/macho.rs b/libwild/src/args/macho.rs index fed1fe695..9fd601b98 100644 --- a/libwild/src/args/macho.rs +++ b/libwild/src/args/macho.rs @@ -2,6 +2,8 @@ #![allow(unused_variables)] #![allow(unused)] +use crate::alignment::Alignment; +use crate::alignment::MACHO_PAGE_ALIGNMENT; use crate::args::ArgumentParser; use crate::args::CommonArgs; use crate::args::FILES_PER_GROUP_ENV; @@ -92,7 +94,7 @@ impl platform::Args for MachOArgs { } fn loadable_segment_alignment(&self) -> crate::alignment::Alignment { - todo!() + MACHO_PAGE_ALIGNMENT } fn should_merge_sections(&self) -> bool { diff --git a/libwild/src/macho.rs b/libwild/src/macho.rs index 090f2799e..433f844e5 100644 --- a/libwild/src/macho.rs +++ b/libwild/src/macho.rs @@ -4,6 +4,8 @@ use crate::OutputKind; use crate::alignment; +use crate::alignment::Alignment; +use crate::alignment::MACHO_PAGE_ALIGNMENT; use crate::args::macho::MachOArgs; use crate::ensure; use crate::error; @@ -598,17 +600,15 @@ impl platform::NonAddressableIndexes for NonAddressableIndexes { } } +// TODO: update comment + #[derive(Debug, Copy, Clone, Default, PartialEq)] pub(crate) enum SegmentType { - Header, - // All load commands are grouped into the segment. - LoadCommands, - // Sections belonging to __TEXT segment. Text, - // Sections belonging to __DATA segment. - Data, - // Sections belonging to __DATA_CONST segment. - DataConst, + LoadCommands, + TextSections, + DataSections, + DataConstSections, #[default] Misc, } @@ -642,7 +642,7 @@ impl platform::ProgramSegmentDef for ProgramSegmentDef { } fn is_loadable(self) -> bool { - false + true } fn is_stack(self) -> bool { @@ -662,24 +662,29 @@ impl platform::ProgramSegmentDef for ProgramSegmentDef { section_info: &crate::output_section_id::SectionOutputInfo, section_id: crate::output_section_id::OutputSectionId, ) -> bool { - self.segment_type - == match section_id { - output_section_id::FILE_HEADER => SegmentType::Header, - output_section_id::PAGEZERO_SEGMENT - | output_section_id::TEXT_SEGMENT - | output_section_id::DATA_SEGMENT - | output_section_id::LINK_EDIT_SEGMENT - | output_section_id::ENTRY_POINT => SegmentType::LoadCommands, - output_section_id::TEXT | output_section_id::CSTRING => SegmentType::Text, - output_section_id::DATA => SegmentType::Data, - _ => SegmentType::Misc, - } + let mapped_segment = match section_id { + output_section_id::FILE_HEADER => SegmentType::Text, + output_section_id::PAGEZERO_SEGMENT + | output_section_id::TEXT_SEGMENT + | output_section_id::DATA_SEGMENT + | output_section_id::LINK_EDIT_SEGMENT + | output_section_id::ENTRY_POINT => SegmentType::LoadCommands, + output_section_id::TEXT | output_section_id::CSTRING => SegmentType::TextSections, + output_section_id::DATA => SegmentType::DataSections, + _ => SegmentType::Misc, + }; + + match (self.segment_type, mapped_segment) { + (SegmentType::Text, SegmentType::LoadCommands | SegmentType::TextSections) => true, + _ => self.segment_type == mapped_segment, + } } } pub(crate) struct BuiltInSectionDetails { pub(crate) kind: SectionKind<'static>, pub(crate) section_flags: SectionFlags, + pub(crate) min_alignment: Alignment, pub(crate) target_segment_type: Option, } @@ -688,6 +693,7 @@ impl platform::BuiltInSectionDetails for BuiltInSectionDetails {} const DEFAULT_DEFS: BuiltInSectionDetails = BuiltInSectionDetails { kind: SectionKind::Primary(SectionName(&[])), section_flags: SectionFlags::empty(), + min_alignment: alignment::MIN, target_segment_type: None, }; @@ -955,7 +961,7 @@ impl platform::Platform for MachO { flags: d.section_flags, }, kind: d.kind, - min_alignment: alignment::MIN, + min_alignment: d.min_alignment, location: None, secondary_order: None, }) @@ -1083,14 +1089,14 @@ impl platform::Platform for MachO { part_id::TEXT_SEGMENT, (size_of::() + size_of::() - * count_sections_for_segment_type(output_sections, SegmentType::Text)) + * count_sections_for_segment_type(output_sections, SegmentType::TextSections)) as u64, ); sizes.increment( part_id::DATA_SEGMENT, (size_of::() + size_of::() - * count_sections_for_segment_type(output_sections, SegmentType::Data)) + * count_sections_for_segment_type(output_sections, SegmentType::DataSections)) as u64, ); sizes.increment( @@ -1224,7 +1230,7 @@ const SECTION_DEFINITIONS: [BuiltInSectionDetails; NUM_BUILT_IN_SECTIONS] = { defs[output_section_id::FILE_HEADER.as_usize()] = BuiltInSectionDetails { kind: SectionKind::Primary(SectionName(b"FILE_HEADER")), - target_segment_type: Some(SegmentType::Header), + target_segment_type: Some(SegmentType::Text), ..DEFAULT_DEFS }; // Load commands @@ -1280,6 +1286,7 @@ const SECTION_DEFINITIONS: [BuiltInSectionDetails; NUM_BUILT_IN_SECTIONS] = { defs[output_section_id::DATA.as_usize()] = BuiltInSectionDetails { kind: SectionKind::Primary(SectionName(b"__data")), section_flags: SectionFlags::from_u32(macho::S_REGULAR), + min_alignment: MACHO_PAGE_ALIGNMENT, ..DEFAULT_DEFS }; @@ -1296,19 +1303,19 @@ const DEFAULT_SECTION_RULES: &[SectionRule<'static>] = &[ const PROGRAM_SEGMENT_DEFS: &[ProgramSegmentDef] = &[ ProgramSegmentDef { - segment_type: SegmentType::Header, + segment_type: SegmentType::Text, }, ProgramSegmentDef { segment_type: SegmentType::LoadCommands, }, ProgramSegmentDef { - segment_type: SegmentType::Text, + segment_type: SegmentType::TextSections, }, ProgramSegmentDef { - segment_type: SegmentType::Data, + segment_type: SegmentType::DataSections, }, ProgramSegmentDef { - segment_type: SegmentType::DataConst, + segment_type: SegmentType::DataConstSections, }, ProgramSegmentDef { segment_type: SegmentType::Misc, diff --git a/libwild/src/macho_writer.rs b/libwild/src/macho_writer.rs index b6a9122b3..013780ef9 100644 --- a/libwild/src/macho_writer.rs +++ b/libwild/src/macho_writer.rs @@ -201,14 +201,23 @@ fn write_segment_commands>( layout: &MachOLayout, buffers: &mut OutputSectionPartMap<&mut [u8]>, ) -> Result { - for (part_id, seg_name, segment_type) in [ - (part_id::TEXT_SEGMENT, SEG_TEXT, SegmentType::Text), - (part_id::DATA_SEGMENT, SEG_DATA, SegmentType::Data), + for (part_id, seg_name, segment_type, segment_sections_type) in [ + ( + part_id::TEXT_SEGMENT, + SEG_TEXT, + SegmentType::Text, + SegmentType::TextSections, + ), + ( + part_id::DATA_SEGMENT, + SEG_DATA, + SegmentType::DataSections, + SegmentType::DataSections, + ), ] { - let SegmentSectionsInfo { - segment_size, - segment_sections, - } = get_segment_sections(layout, segment_type); + // TODO: write comments + let segment_sections = get_segment_sections(layout, segment_sections_type).segment_sections; + let segment_size = get_segment_sections(layout, segment_type).segment_size; let (segment_cmd, sections) = split_segment_command_buffer(buffers.get_mut(part_id), segment_sections.len())?; From 50f2da57e7928b65132a6887b69f4cfbd4eb6888 Mon Sep 17 00:00:00 2001 From: Martin Liska Date: Mon, 6 Apr 2026 12:59:18 +0200 Subject: [PATCH 02/21] introduce align_load_segment_start --- libwild/src/elf.rs | 9 +++++++++ libwild/src/layout.rs | 8 +++++++- libwild/src/macho.rs | 16 +++++++++++++++- libwild/src/platform.rs | 9 +++++++++ 4 files changed, 40 insertions(+), 2 deletions(-) diff --git a/libwild/src/elf.rs b/libwild/src/elf.rs index d8ebb8766..864d6ffc6 100644 --- a/libwild/src/elf.rs +++ b/libwild/src/elf.rs @@ -1927,6 +1927,15 @@ impl platform::Platform for Elf { group_sizes.merge(&extra_sizes); total_sizes.merge(&extra_sizes); } + + fn align_load_segment_start( + _segment_def: Self::ProgramSegmentDef, + segment_alignment: Alignment, + file_offset: &mut usize, + mem_offset: &mut u64, + ) { + *mem_offset = segment_alignment.align_modulo(*file_offset as u64, *mem_offset); + } } impl<'data> platform::ObjectFile<'data> for File<'data> { diff --git a/libwild/src/layout.rs b/libwild/src/layout.rs index 0933cf0a7..7899d1d6f 100644 --- a/libwild/src/layout.rs +++ b/libwild/src/layout.rs @@ -4595,7 +4595,13 @@ fn layout_section_parts( file_offset = segment_alignment.align_modulo(mem_offset, file_offset as u64) as usize; } else { - mem_offset = segment_alignment.align_modulo(file_offset as u64, mem_offset); + let segment_def = *program_segments.segment_def(segment_id); + P::align_load_segment_start( + segment_def, + segment_alignment, + &mut file_offset, + &mut mem_offset, + ); } } } diff --git a/libwild/src/macho.rs b/libwild/src/macho.rs index 433f844e5..d2fe7af5f 100644 --- a/libwild/src/macho.rs +++ b/libwild/src/macho.rs @@ -1222,6 +1222,21 @@ impl platform::Platform for MachO { fn start_memory_address(output_kind: OutputKind) -> u64 { MACHO_START_MEM_ADDRESS } + + fn align_load_segment_start( + segment_def: ProgramSegmentDef, + segment_alignment: Alignment, + file_offset: &mut usize, + mem_offset: &mut u64, + ) { + if matches!( + segment_def.segment_type, + SegmentType::Text | SegmentType::DataSections | SegmentType::DataConstSections + ) { + *file_offset = segment_alignment.align_up(*file_offset as u64) as usize; + *mem_offset = segment_alignment.align_up(*mem_offset); + } + } } const SECTION_DEFINITIONS: [BuiltInSectionDetails; NUM_BUILT_IN_SECTIONS] = { @@ -1286,7 +1301,6 @@ const SECTION_DEFINITIONS: [BuiltInSectionDetails; NUM_BUILT_IN_SECTIONS] = { defs[output_section_id::DATA.as_usize()] = BuiltInSectionDetails { kind: SectionKind::Primary(SectionName(b"__data")), section_flags: SectionFlags::from_u32(macho::S_REGULAR), - min_alignment: MACHO_PAGE_ALIGNMENT, ..DEFAULT_DEFS }; diff --git a/libwild/src/platform.rs b/libwild/src/platform.rs index fe5697f9d..5646b9e2e 100644 --- a/libwild/src/platform.rs +++ b/libwild/src/platform.rs @@ -290,6 +290,15 @@ pub(crate) trait Platform: Copy + Send + Sync + Sized + std::fmt::Debug + 'stati /// Resolves a reference to the frame data section. fn frame_data_base_address(memory_offsets: &OutputSectionPartMap) -> u64; + /// Aligns the start of a load segment. Platforms may override this to coordinate file and + /// memory offsets when a segment boundary is introduced. + fn align_load_segment_start( + _segment_def: Self::ProgramSegmentDef, + segment_alignment: Alignment, + file_offset: &mut usize, + mem_offset: &mut u64, + ); + /// Called after GC phase has completed. Mostly useful for platform-specific logging. fn finalise_find_required_sections(groups: &[layout::GroupState]); From 506faec4d796f39cea5f85b9e61bfa65669c8071 Mon Sep 17 00:00:00 2001 From: Martin Liska Date: Mon, 6 Apr 2026 13:29:21 +0200 Subject: [PATCH 03/21] align segments to 16KiB --- libwild/src/macho.rs | 29 ++++++++++++++----- libwild/src/macho_writer.rs | 48 +++++++++++++++++--------------- libwild/src/output_section_id.rs | 2 +- 3 files changed, 49 insertions(+), 30 deletions(-) diff --git a/libwild/src/macho.rs b/libwild/src/macho.rs index d2fe7af5f..4fd593e6e 100644 --- a/libwild/src/macho.rs +++ b/libwild/src/macho.rs @@ -609,8 +609,10 @@ pub(crate) enum SegmentType { TextSections, DataSections, DataConstSections, + LinkeditSections, + // The other ELF-specific (or unused) parts/sections will be collected here. #[default] - Misc, + Unused, } impl platform::SegmentType for SegmentType {} @@ -671,7 +673,8 @@ impl platform::ProgramSegmentDef for ProgramSegmentDef { | output_section_id::ENTRY_POINT => SegmentType::LoadCommands, output_section_id::TEXT | output_section_id::CSTRING => SegmentType::TextSections, output_section_id::DATA => SegmentType::DataSections, - _ => SegmentType::Misc, + output_section_id::STRTAB => SegmentType::LinkeditSections, + _ => SegmentType::Unused, }; match (self.segment_type, mapped_segment) { @@ -1101,7 +1104,12 @@ impl platform::Platform for MachO { ); sizes.increment( part_id::LINK_EDIT_SEGMENT, - size_of::() as u64, + (size_of::() + + size_of::() + * count_sections_for_segment_type( + output_sections, + SegmentType::LinkeditSections, + )) as u64, ); sizes.increment(part_id::ENTRY_POINT, size_of::() as u64); } @@ -1158,7 +1166,9 @@ impl platform::Platform for MachO { common: &mut crate::layout::CommonGroupState, symbol_db: &crate::symbol_db::SymbolDb, ) { - // TODO + // Mach-O string tables start with an empty string at index 0. + // TODO: Just a filler for now. + common.allocate(part_id::STRTAB, 1); } fn finalise_prelude_layout<'data>( @@ -1208,13 +1218,14 @@ impl platform::Platform for MachO { builder.add_section(output_section_id::PAGEZERO_SEGMENT); builder.add_section(output_section_id::TEXT_SEGMENT); builder.add_section(output_section_id::DATA_SEGMENT); - builder.add_section(output_section_id::ENTRY_POINT); builder.add_section(output_section_id::LINK_EDIT_SEGMENT); + builder.add_section(output_section_id::ENTRY_POINT); // Content of the sections (e.g. __text, __data). builder.add_section(output_section_id::TEXT); builder.add_section(output_section_id::CSTRING); builder.add_section(output_section_id::DATA); // The rest (e.g. symbol table, string table). + builder.add_section(output_section_id::STRTAB); builder.build() } @@ -1231,7 +1242,10 @@ impl platform::Platform for MachO { ) { if matches!( segment_def.segment_type, - SegmentType::Text | SegmentType::DataSections | SegmentType::DataConstSections + SegmentType::Text + | SegmentType::DataSections + | SegmentType::DataConstSections + | SegmentType::LinkeditSections ) { *file_offset = segment_alignment.align_up(*file_offset as u64) as usize; *mem_offset = segment_alignment.align_up(*mem_offset); @@ -1278,6 +1292,7 @@ const SECTION_DEFINITIONS: [BuiltInSectionDetails; NUM_BUILT_IN_SECTIONS] = { }; defs[output_section_id::STRTAB.as_usize()] = BuiltInSectionDetails { kind: SectionKind::Primary(SectionName(secnames::STRTAB_SECTION_NAME)), + target_segment_type: Some(SegmentType::LinkeditSections), ..DEFAULT_DEFS }; // Multi-part generated sections @@ -1332,7 +1347,7 @@ const PROGRAM_SEGMENT_DEFS: &[ProgramSegmentDef] = &[ segment_type: SegmentType::DataConstSections, }, ProgramSegmentDef { - segment_type: SegmentType::Misc, + segment_type: SegmentType::LinkeditSections, }, ]; diff --git a/libwild/src/macho_writer.rs b/libwild/src/macho_writer.rs index 013780ef9..3396def92 100644 --- a/libwild/src/macho_writer.rs +++ b/libwild/src/macho_writer.rs @@ -2,6 +2,7 @@ #![allow(unused_variables)] #![allow(unused)] +use crate::alignment::MACHO_PAGE_ALIGNMENT; use crate::bail; use crate::error; use crate::error::Context; @@ -27,6 +28,7 @@ use crate::macho::SegmentSectionsInfo; use crate::macho::SegmentType; use crate::macho::get_segment_sections; use crate::output_section_id; +use crate::output_section_id::LINK_EDIT_SEGMENT; use crate::output_section_id::OrderEvent; use crate::output_section_id::OutputSectionId; use crate::output_section_id::SectionName; @@ -55,6 +57,7 @@ use object::macho::SEG_TEXT; use object::slice_from_bytes_mut; use rayon::iter::IntoParallelIterator; use rayon::iter::ParallelIterator; +use std::io::Write; use tracing::debug_span; use zerocopy::FromZeros; @@ -121,12 +124,6 @@ fn write_prelude<'data, A: Arch>( .map_err(|_| error!("Invalid PAGEZERO segment allocation"))? .0; write_pagezero_command::(pagezero_command); - - let linkedit_command: &mut SegmentCommand = - from_bytes_mut(buffers.get_mut(part_id::LINK_EDIT_SEGMENT)) - .map_err(|_| error!("Invalid LINKEDIT segment allocation"))? - .0; - write_linkedit_command::(linkedit_command); write_segment_commands::(layout, buffers)?; let entry_point_command: &mut EntryPointCommand = @@ -135,6 +132,9 @@ fn write_prelude<'data, A: Arch>( .0; write_entry_point_command::(layout, entry_point_command); + // TODO: remove + buffers.get_mut(part_id::STRTAB).write_all(b"x")?; + Ok(()) } @@ -169,20 +169,6 @@ fn write_pagezero_command>(command: &mut SegmentComman command.flags.set(LE, 0); } -fn write_linkedit_command>(command: &mut SegmentCommand) { - command.cmd.set(LE, LC_SEGMENT_64); - command.cmdsize.set(LE, size_of::() as u32); - command.segname[..SEG_LINKEDIT.len()].copy_from_slice(SEG_LINKEDIT.as_bytes()); - command.vmaddr.set(LE, 0); - command.vmsize.set(LE, 0); - command.fileoff.set(LE, 0); - command.filesize.set(LE, 0); - command.maxprot.set(LE, 0); - command.initprot.set(LE, 0); - command.nsects.set(LE, 0); - command.flags.set(LE, 0); -} - fn split_segment_command_buffer( bytes: &mut [u8], section_count: usize, @@ -214,6 +200,12 @@ fn write_segment_commands>( SegmentType::DataSections, SegmentType::DataSections, ), + ( + part_id::LINK_EDIT_SEGMENT, + SEG_LINKEDIT, + SegmentType::LinkeditSections, + SegmentType::LinkeditSections, + ), ] { // TODO: write comments let segment_sections = get_segment_sections(layout, segment_sections_type).segment_sections; @@ -236,10 +228,22 @@ fn write_segment_commands>( segment_cmd.segname[..seg_name.len()].copy_from_slice(seg_name.as_bytes()); segment_cmd.segname[seg_name.len()..].zero(); segment_cmd.vmaddr.set(LE, segment_size.mem_offset); - segment_cmd.vmsize.set(LE, segment_size.mem_size); + segment_cmd.vmsize.set( + LE, + segment_size + .mem_size + .next_multiple_of(MACHO_PAGE_ALIGNMENT.value()), + ); // TODO: should be likely offset relative to the place after the commands segment_cmd.fileoff.set(LE, segment_size.file_offset as u64); - segment_cmd.filesize.set(LE, segment_size.file_size as u64); + segment_cmd.filesize.set( + LE, + dbg!( + segment_size + .file_size + .next_multiple_of(MACHO_PAGE_ALIGNMENT.value() as usize) as u64 + ), + ); segment_cmd.maxprot.set(LE, prot_flags); segment_cmd.initprot.set(LE, prot_flags); segment_cmd.nsects.set(LE, segment_sections.len() as u32); diff --git a/libwild/src/output_section_id.rs b/libwild/src/output_section_id.rs index c622caaef..8fee5348d 100644 --- a/libwild/src/output_section_id.rs +++ b/libwild/src/output_section_id.rs @@ -104,9 +104,9 @@ pub(crate) const SYMTAB_SHNDX_GLOBAL: OutputSectionId = pub(crate) const PAGEZERO_SEGMENT: OutputSectionId = part_id::PAGEZERO_SEGMENT.output_section_id(); pub(crate) const TEXT_SEGMENT: OutputSectionId = part_id::TEXT_SEGMENT.output_section_id(); pub(crate) const DATA_SEGMENT: OutputSectionId = part_id::DATA_SEGMENT.output_section_id(); -pub(crate) const ENTRY_POINT: OutputSectionId = part_id::ENTRY_POINT.output_section_id(); pub(crate) const LINK_EDIT_SEGMENT: OutputSectionId = part_id::LINK_EDIT_SEGMENT.output_section_id(); +pub(crate) const ENTRY_POINT: OutputSectionId = part_id::ENTRY_POINT.output_section_id(); // Regular sections copied from the input objects. pub(crate) const RODATA: OutputSectionId = OutputSectionId::regular(0); From fc2e4fb41319f6dc9e9495563ca601be8eaf4f24 Mon Sep 17 00:00:00 2001 From: Martin Liska Date: Mon, 6 Apr 2026 13:32:54 +0200 Subject: [PATCH 04/21] fix LC_MAIN command emission --- libwild/src/macho_writer.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/libwild/src/macho_writer.rs b/libwild/src/macho_writer.rs index 3396def92..e537e907b 100644 --- a/libwild/src/macho_writer.rs +++ b/libwild/src/macho_writer.rs @@ -238,11 +238,9 @@ fn write_segment_commands>( segment_cmd.fileoff.set(LE, segment_size.file_offset as u64); segment_cmd.filesize.set( LE, - dbg!( - segment_size - .file_size - .next_multiple_of(MACHO_PAGE_ALIGNMENT.value() as usize) as u64 - ), + segment_size + .file_size + .next_multiple_of(MACHO_PAGE_ALIGNMENT.value() as usize) as u64, ); segment_cmd.maxprot.set(LE, prot_flags); segment_cmd.initprot.set(LE, prot_flags); @@ -346,7 +344,8 @@ fn write_entry_point_command>( layout: &MachOLayout, command: &mut EntryPointCommand, ) { - let SegmentSectionsInfo { segment_size, .. } = get_segment_sections(layout, SegmentType::Text); + let SegmentSectionsInfo { segment_size, .. } = + get_segment_sections(layout, SegmentType::TextSections); command.cmd.set(LE, LC_MAIN); command From 2d721e8d8ac151915e400564620f1e0401c1c7f6 Mon Sep 17 00:00:00 2001 From: Martin Liska Date: Mon, 6 Apr 2026 13:35:39 +0200 Subject: [PATCH 05/21] fill entire 1 page for LINKEDIT --- libwild/src/macho.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libwild/src/macho.rs b/libwild/src/macho.rs index 4fd593e6e..c8c01c355 100644 --- a/libwild/src/macho.rs +++ b/libwild/src/macho.rs @@ -1168,7 +1168,7 @@ impl platform::Platform for MachO { ) { // Mach-O string tables start with an empty string at index 0. // TODO: Just a filler for now. - common.allocate(part_id::STRTAB, 1); + common.allocate(part_id::STRTAB, MACHO_PAGE_ALIGNMENT.value()); } fn finalise_prelude_layout<'data>( From 1475abc6a32a434eeae305ab9ee36e9599f0e254 Mon Sep 17 00:00:00 2001 From: Martin Liska Date: Mon, 6 Apr 2026 13:49:01 +0200 Subject: [PATCH 06/21] include DYLINKER --- libwild/src/macho.rs | 19 +++++++++++++++++-- libwild/src/macho_writer.rs | 29 +++++++++++++++++++++++++++++ libwild/src/output_section_id.rs | 1 + libwild/src/part_id.rs | 3 ++- libwild/src/verification.rs | 1 + 5 files changed, 50 insertions(+), 3 deletions(-) diff --git a/libwild/src/macho.rs b/libwild/src/macho.rs index c8c01c355..8e2c0c352 100644 --- a/libwild/src/macho.rs +++ b/libwild/src/macho.rs @@ -51,7 +51,10 @@ const LE: Endianness = Endianness::Little; /// Mach-O uses a zero page for all 32bit addresses and thus we begin the memory /// offsets right after that (1GiB). -pub const MACHO_START_MEM_ADDRESS: u64 = 0x1_0000_0000; +pub(crate) const MACHO_START_MEM_ADDRESS: u64 = 0x1_0000_0000; + +/// A path to the default dynamic linker. +pub(crate) const DYLINKER_PATH: &str = "/usr/lib/dyld"; type SectionHeader = Section64; type SectionTable<'data> = &'data [Section64]; @@ -63,6 +66,7 @@ pub(crate) type FileHeader = object::macho::MachHeader64; pub(crate) type SegmentCommand = object::macho::SegmentCommand64; pub(crate) type SectionEntry = object::macho::Section64; pub(crate) type EntryPointCommand = object::macho::EntryPointCommand; +pub(crate) type DylinkerCommand = object::macho::DylinkerCommand; #[derive(derive_more::Debug)] pub(crate) struct File<'data> { @@ -670,7 +674,8 @@ impl platform::ProgramSegmentDef for ProgramSegmentDef { | output_section_id::TEXT_SEGMENT | output_section_id::DATA_SEGMENT | output_section_id::LINK_EDIT_SEGMENT - | output_section_id::ENTRY_POINT => SegmentType::LoadCommands, + | output_section_id::ENTRY_POINT + | output_section_id::DYLINKER => SegmentType::LoadCommands, output_section_id::TEXT | output_section_id::CSTRING => SegmentType::TextSections, output_section_id::DATA => SegmentType::DataSections, output_section_id::STRTAB => SegmentType::LinkeditSections, @@ -1112,6 +1117,10 @@ impl platform::Platform for MachO { )) as u64, ); sizes.increment(part_id::ENTRY_POINT, size_of::() as u64); + sizes.increment( + part_id::DYLINKER, + (size_of::() + (DYLINKER_PATH.len()).next_multiple_of(4)) as u64, + ); } fn finalise_sizes_for_symbol<'data>( @@ -1220,6 +1229,7 @@ impl platform::Platform for MachO { builder.add_section(output_section_id::DATA_SEGMENT); builder.add_section(output_section_id::LINK_EDIT_SEGMENT); builder.add_section(output_section_id::ENTRY_POINT); + builder.add_section(output_section_id::DYLINKER); // Content of the sections (e.g. __text, __data). builder.add_section(output_section_id::TEXT); builder.add_section(output_section_id::CSTRING); @@ -1290,6 +1300,11 @@ const SECTION_DEFINITIONS: [BuiltInSectionDetails; NUM_BUILT_IN_SECTIONS] = { target_segment_type: Some(SegmentType::LoadCommands), ..DEFAULT_DEFS }; + defs[output_section_id::DYLINKER.as_usize()] = BuiltInSectionDetails { + kind: SectionKind::Primary(SectionName(b"LC_LOAD_DYLINKER")), + target_segment_type: Some(SegmentType::LoadCommands), + ..DEFAULT_DEFS + }; defs[output_section_id::STRTAB.as_usize()] = BuiltInSectionDetails { kind: SectionKind::Primary(SectionName(secnames::STRTAB_SECTION_NAME)), target_segment_type: Some(SegmentType::LinkeditSections), diff --git a/libwild/src/macho_writer.rs b/libwild/src/macho_writer.rs index e537e907b..f732fc8f8 100644 --- a/libwild/src/macho_writer.rs +++ b/libwild/src/macho_writer.rs @@ -18,6 +18,8 @@ use crate::layout::ObjectLayout; use crate::layout::OutputRecordLayout; use crate::layout::PreludeLayout; use crate::layout::Section; +use crate::macho::DYLINKER_PATH; +use crate::macho::DylinkerCommand; use crate::macho::EntryPointCommand; use crate::macho::FileHeader; use crate::macho::MACHO_START_MEM_ADDRESS; @@ -46,6 +48,7 @@ use object::Endianness; use object::U32; use object::from_bytes_mut; use object::macho::CPU_TYPE_ARM64; +use object::macho::LC_LOAD_DYLINKER; use object::macho::LC_MAIN; use object::macho::LC_SEGMENT_64; use object::macho::MH_CIGAM_64; @@ -132,6 +135,11 @@ fn write_prelude<'data, A: Arch>( .0; write_entry_point_command::(layout, entry_point_command); + let (dylinker_command, dylinker_path_buffer): (&mut DylinkerCommand, &mut [u8]) = + from_bytes_mut(buffers.get_mut(part_id::DYLINKER)) + .map_err(|_| error!("Invalid DYLINKER command allocation"))?; + write_dylinker_command::(dylinker_command, dylinker_path_buffer); + // TODO: remove buffers.get_mut(part_id::STRTAB).write_all(b"x")?; @@ -354,3 +362,24 @@ fn write_entry_point_command>( command.entryoff.set(LE, segment_size.file_offset as u64); command.stacksize.set(LE, 0); } + +fn write_dylinker_command>( + command: &mut DylinkerCommand, + path_buffer: &mut [u8], +) { + command.cmd.set(LE, LC_LOAD_DYLINKER); + command.cmdsize.set( + LE, + (size_of::() + DYLINKER_PATH.len().next_multiple_of(4)) as u32, + ); + command + .name + .offset + .set(LE, size_of::() as u32); + + let path_buffer_len = DYLINKER_PATH.len() + 1; + + path_buffer[0..DYLINKER_PATH.len()].copy_from_slice(DYLINKER_PATH.as_bytes()); + // The string size is always a multiple of 4B. + path_buffer[DYLINKER_PATH.len()..].zero(); +} diff --git a/libwild/src/output_section_id.rs b/libwild/src/output_section_id.rs index 8fee5348d..547970dd4 100644 --- a/libwild/src/output_section_id.rs +++ b/libwild/src/output_section_id.rs @@ -107,6 +107,7 @@ pub(crate) const DATA_SEGMENT: OutputSectionId = part_id::DATA_SEGMENT.output_se pub(crate) const LINK_EDIT_SEGMENT: OutputSectionId = part_id::LINK_EDIT_SEGMENT.output_section_id(); pub(crate) const ENTRY_POINT: OutputSectionId = part_id::ENTRY_POINT.output_section_id(); +pub(crate) const DYLINKER: OutputSectionId = part_id::DYLINKER.output_section_id(); // Regular sections copied from the input objects. pub(crate) const RODATA: OutputSectionId = OutputSectionId::regular(0); diff --git a/libwild/src/part_id.rs b/libwild/src/part_id.rs index 53eccd01f..0f6a59e50 100644 --- a/libwild/src/part_id.rs +++ b/libwild/src/part_id.rs @@ -53,8 +53,9 @@ pub(crate) const TEXT_SEGMENT: PartId = PartId(32); pub(crate) const DATA_SEGMENT: PartId = PartId(33); pub(crate) const LINK_EDIT_SEGMENT: PartId = PartId(34); pub(crate) const ENTRY_POINT: PartId = PartId(35); +pub(crate) const DYLINKER: PartId = PartId(36); -pub(crate) const NUM_SINGLE_PART_SECTIONS: u32 = 36; +pub(crate) const NUM_SINGLE_PART_SECTIONS: u32 = 37; #[cfg(test)] pub(crate) const NUM_BUILT_IN_PARTS: usize = NUM_SINGLE_PART_SECTIONS as usize diff --git a/libwild/src/verification.rs b/libwild/src/verification.rs index f1631edb8..4499b2d65 100644 --- a/libwild/src/verification.rs +++ b/libwild/src/verification.rs @@ -117,6 +117,7 @@ pub(crate) fn clear_ignored(expected: &mut OutputSectionPartMap) { part_id::PAGEZERO_SEGMENT, part_id::LINK_EDIT_SEGMENT, part_id::ENTRY_POINT, + part_id::DYLINKER, ]; for part_id in IGNORED { From 62d07d732989bda2905aab95d3db6121a2fe5a9a Mon Sep 17 00:00:00 2001 From: Martin Liska Date: Mon, 6 Apr 2026 13:59:39 +0200 Subject: [PATCH 07/21] fix expected alignment --- libwild/src/macho.rs | 2 +- libwild/src/macho_writer.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libwild/src/macho.rs b/libwild/src/macho.rs index 8e2c0c352..df9363771 100644 --- a/libwild/src/macho.rs +++ b/libwild/src/macho.rs @@ -1119,7 +1119,7 @@ impl platform::Platform for MachO { sizes.increment(part_id::ENTRY_POINT, size_of::() as u64); sizes.increment( part_id::DYLINKER, - (size_of::() + (DYLINKER_PATH.len()).next_multiple_of(4)) as u64, + ((size_of::() + DYLINKER_PATH.len()).next_multiple_of(8)) as u64, ); } diff --git a/libwild/src/macho_writer.rs b/libwild/src/macho_writer.rs index f732fc8f8..dfc6fadbf 100644 --- a/libwild/src/macho_writer.rs +++ b/libwild/src/macho_writer.rs @@ -370,7 +370,7 @@ fn write_dylinker_command>( command.cmd.set(LE, LC_LOAD_DYLINKER); command.cmdsize.set( LE, - (size_of::() + DYLINKER_PATH.len().next_multiple_of(4)) as u32, + ((size_of::() + DYLINKER_PATH.len()).next_multiple_of(8)) as u32, ); command .name @@ -380,6 +380,6 @@ fn write_dylinker_command>( let path_buffer_len = DYLINKER_PATH.len() + 1; path_buffer[0..DYLINKER_PATH.len()].copy_from_slice(DYLINKER_PATH.as_bytes()); - // The string size is always a multiple of 4B. + // The string size is always a multiple of 8B. path_buffer[DYLINKER_PATH.len()..].zero(); } From df8239a4ca973a5af2fabf44e06b2babd867c8b5 Mon Sep 17 00:00:00 2001 From: Martin Liska Date: Mon, 6 Apr 2026 14:13:58 +0200 Subject: [PATCH 08/21] fix flags for __LINKEDIT segment --- libwild/src/macho.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/libwild/src/macho.rs b/libwild/src/macho.rs index df9363771..987e0bb8b 100644 --- a/libwild/src/macho.rs +++ b/libwild/src/macho.rs @@ -1293,6 +1293,7 @@ const SECTION_DEFINITIONS: [BuiltInSectionDetails; NUM_BUILT_IN_SECTIONS] = { defs[output_section_id::LINK_EDIT_SEGMENT.as_usize()] = BuiltInSectionDetails { kind: SectionKind::Primary(SectionName(SEG_LINKEDIT.as_bytes())), target_segment_type: Some(SegmentType::LoadCommands), + section_flags: SectionFlags::from_u32(macho::VM_PROT_READ), ..DEFAULT_DEFS }; defs[output_section_id::ENTRY_POINT.as_usize()] = BuiltInSectionDetails { From 09bfce8e6db50bc41a0bc432cc13abcd5aeed768 Mon Sep 17 00:00:00 2001 From: Martin Liska Date: Mon, 6 Apr 2026 14:41:55 +0200 Subject: [PATCH 09/21] allocate a space for CodeSignatureCommand --- libwild/src/macho.rs | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/libwild/src/macho.rs b/libwild/src/macho.rs index 987e0bb8b..98ad33bc3 100644 --- a/libwild/src/macho.rs +++ b/libwild/src/macho.rs @@ -67,6 +67,7 @@ pub(crate) type SegmentCommand = object::macho::SegmentCommand64; pub(crate) type SectionEntry = object::macho::Section64; pub(crate) type EntryPointCommand = object::macho::EntryPointCommand; pub(crate) type DylinkerCommand = object::macho::DylinkerCommand; +pub(crate) type CodeSignatureCommand = object::macho::LinkeditDataCommand; #[derive(derive_more::Debug)] pub(crate) struct File<'data> { @@ -1250,15 +1251,22 @@ impl platform::Platform for MachO { file_offset: &mut usize, mem_offset: &mut u64, ) { - if matches!( - segment_def.segment_type, + match segment_def.segment_type { SegmentType::Text - | SegmentType::DataSections - | SegmentType::DataConstSections - | SegmentType::LinkeditSections - ) { - *file_offset = segment_alignment.align_up(*file_offset as u64) as usize; - *mem_offset = segment_alignment.align_up(*mem_offset); + | SegmentType::DataSections + | SegmentType::DataConstSections + | SegmentType::LinkeditSections => { + *file_offset = segment_alignment.align_up(*file_offset as u64) as usize; + *mem_offset = segment_alignment.align_up(*mem_offset); + } + SegmentType::TextSections => { + // We allocate a placeholder space for the LinkeditDataCommand command (added by + // codesign tool) in order to preserve the offsets into __text and + // other sections in the __TEXT segment. + *file_offset += size_of::(); + *mem_offset += size_of::() as u64; + } + _ => {} } } } From 8278080f415e25529c3d45b37f6c8587217d9a7e Mon Sep 17 00:00:00 2001 From: Martin Liska Date: Mon, 6 Apr 2026 15:07:04 +0200 Subject: [PATCH 10/21] fillup extra flags in file header --- libwild/src/macho_writer.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/libwild/src/macho_writer.rs b/libwild/src/macho_writer.rs index dfc6fadbf..5315f0a64 100644 --- a/libwild/src/macho_writer.rs +++ b/libwild/src/macho_writer.rs @@ -47,6 +47,7 @@ use object::BigEndian; use object::Endianness; use object::U32; use object::from_bytes_mut; +use object::macho; use object::macho::CPU_TYPE_ARM64; use object::macho::LC_LOAD_DYLINKER; use object::macho::LC_MAIN; @@ -159,7 +160,10 @@ fn populate_file_header>( header.filetype = U32::new(LE, MH_EXECUTE); header.ncmds = U32::new(LE, load_commands_info.segment_sections.len() as u32); header.sizeofcmds = U32::new(LE, load_commands_info.segment_size.file_size as u32); - header.flags = U32::new(LE, 0); + header.flags = U32::new( + LE, + macho::MH_PIE | macho::MH_DYLDLINK | macho::MH_NOUNDEFS | macho::MH_TWOLEVEL, + ); header.reserved = U32::new(LE, 0); } From cffb8b8b3ae2285613a5321d8685dc8dcd6f2b81 Mon Sep 17 00:00:00 2001 From: Martin Liska Date: Mon, 6 Apr 2026 16:38:43 +0200 Subject: [PATCH 11/21] prototype for the chained fixup table --- libwild/src/macho.rs | 73 ++++++++++++++++++++++++++++++-- libwild/src/macho_writer.rs | 57 +++++++++++++++++++++++++ libwild/src/output_section_id.rs | 4 ++ libwild/src/part_id.rs | 4 +- libwild/src/verification.rs | 2 + 5 files changed, 136 insertions(+), 4 deletions(-) diff --git a/libwild/src/macho.rs b/libwild/src/macho.rs index 98ad33bc3..6180a6b8c 100644 --- a/libwild/src/macho.rs +++ b/libwild/src/macho.rs @@ -26,7 +26,9 @@ use crate::part_id; use crate::platform; use crate::symbol_db::Visibility; use linker_utils::elf::secnames; +use object::Endian; use object::Endianness; +use object::U32; use object::macho; use object::macho::N_ABS; use object::macho::N_EXT; @@ -55,6 +57,9 @@ pub(crate) const MACHO_START_MEM_ADDRESS: u64 = 0x1_0000_0000; /// A path to the default dynamic linker. pub(crate) const DYLINKER_PATH: &str = "/usr/lib/dyld"; +pub(crate) const DEFAULT_SEGMENT_COUNT: usize = 3; +pub(crate) const CHAINED_FIXUP_TABLE_SIZE: u64 = + (size_of::() + size_of::() * (DEFAULT_SEGMENT_COUNT + 1 + 1)) as u64; type SectionHeader = Section64; type SectionTable<'data> = &'data [Section64]; @@ -68,6 +73,45 @@ pub(crate) type SectionEntry = object::macho::Section64; pub(crate) type EntryPointCommand = object::macho::EntryPointCommand; pub(crate) type DylinkerCommand = object::macho::DylinkerCommand; pub(crate) type CodeSignatureCommand = object::macho::LinkeditDataCommand; +pub(crate) type DyldChainedFixupsCommand = object::macho::LinkeditDataCommand; +pub(crate) type ChainedFixupsHeader = DyldChainedFixupsHeader; + +// TODO: move to object crate + +// values for dyld_chained_fixups_header.imports_format +#[repr(C)] +enum DyldChainedFixupsImporstFormat { + DYLD_CHAINED_IMPORT = 1, + DYLD_CHAINED_IMPORT_ADDEND = 2, + DYLD_CHAINED_IMPORT_ADDEND64 = 3, +} + +// header of the LC_DYLD_CHAINED_FIXUPS payload +#[repr(C)] +pub(crate) struct DyldChainedFixupsHeader { + // 0 + fixups_version: U32, + // offset of dyld_chained_starts_in_image in chain_data + starts_offset: U32, + // offset of imports table in chain_data + imports_offset: U32, + // offset of symbol strings in chain_data + symbols_offset: U32, + // number of imported symbol names + imports_count: U32, + // DYLD_CHAINED_IMPORT* + imports_format: U32, + // 0 => uncompressed, 1 => zlib compressed + symbols_format: U32, +} + +// This struct is embedded in LC_DYLD_CHAINED_FIXUPS payload +// struct dyld_chained_starts_in_image +// { +// uint32_t seg_count; +// uint32_t seg_info_offset[1]; // each entry is offset into this struct for that segment +// // followed by pool of dyld_chain_starts_in_segment data +// }; #[derive(derive_more::Debug)] pub(crate) struct File<'data> { @@ -676,10 +720,13 @@ impl platform::ProgramSegmentDef for ProgramSegmentDef { | output_section_id::DATA_SEGMENT | output_section_id::LINK_EDIT_SEGMENT | output_section_id::ENTRY_POINT - | output_section_id::DYLINKER => SegmentType::LoadCommands, + | output_section_id::DYLINKER + | output_section_id::DYLD_CHAINED_FIXUPS => SegmentType::LoadCommands, output_section_id::TEXT | output_section_id::CSTRING => SegmentType::TextSections, output_section_id::DATA => SegmentType::DataSections, - output_section_id::STRTAB => SegmentType::LinkeditSections, + output_section_id::CHAINED_FIXUP_TABLE | output_section_id::STRTAB => { + SegmentType::LinkeditSections + } _ => SegmentType::Unused, }; @@ -1122,6 +1169,10 @@ impl platform::Platform for MachO { part_id::DYLINKER, ((size_of::() + DYLINKER_PATH.len()).next_multiple_of(8)) as u64, ); + sizes.increment( + part_id::DYLD_CHAINED_FIXUPS, + size_of::() as u64, + ); } fn finalise_sizes_for_symbol<'data>( @@ -1176,9 +1227,13 @@ impl platform::Platform for MachO { common: &mut crate::layout::CommonGroupState, symbol_db: &crate::symbol_db::SymbolDb, ) { + common.allocate(part_id::CHAINED_FIXUP_TABLE, CHAINED_FIXUP_TABLE_SIZE); // Mach-O string tables start with an empty string at index 0. // TODO: Just a filler for now. - common.allocate(part_id::STRTAB, MACHO_PAGE_ALIGNMENT.value()); + common.allocate( + part_id::STRTAB, + MACHO_PAGE_ALIGNMENT.value() - CHAINED_FIXUP_TABLE_SIZE, + ); } fn finalise_prelude_layout<'data>( @@ -1231,12 +1286,14 @@ impl platform::Platform for MachO { builder.add_section(output_section_id::LINK_EDIT_SEGMENT); builder.add_section(output_section_id::ENTRY_POINT); builder.add_section(output_section_id::DYLINKER); + builder.add_section(output_section_id::DYLD_CHAINED_FIXUPS); // Content of the sections (e.g. __text, __data). builder.add_section(output_section_id::TEXT); builder.add_section(output_section_id::CSTRING); builder.add_section(output_section_id::DATA); // The rest (e.g. symbol table, string table). builder.add_section(output_section_id::STRTAB); + builder.add_section(output_section_id::CHAINED_FIXUP_TABLE); builder.build() } @@ -1314,6 +1371,16 @@ const SECTION_DEFINITIONS: [BuiltInSectionDetails; NUM_BUILT_IN_SECTIONS] = { target_segment_type: Some(SegmentType::LoadCommands), ..DEFAULT_DEFS }; + defs[output_section_id::DYLD_CHAINED_FIXUPS.as_usize()] = BuiltInSectionDetails { + kind: SectionKind::Primary(SectionName(b"LC_DYLD_CHAINED_FIXUPS")), + target_segment_type: Some(SegmentType::LoadCommands), + ..DEFAULT_DEFS + }; + defs[output_section_id::CHAINED_FIXUP_TABLE.as_usize()] = BuiltInSectionDetails { + kind: SectionKind::Primary(SectionName(b"__chain_table")), + target_segment_type: Some(SegmentType::LinkeditSections), + ..DEFAULT_DEFS + }; defs[output_section_id::STRTAB.as_usize()] = BuiltInSectionDetails { kind: SectionKind::Primary(SectionName(secnames::STRTAB_SECTION_NAME)), target_segment_type: Some(SegmentType::LinkeditSections), diff --git a/libwild/src/macho_writer.rs b/libwild/src/macho_writer.rs index 5315f0a64..3a817df48 100644 --- a/libwild/src/macho_writer.rs +++ b/libwild/src/macho_writer.rs @@ -19,6 +19,7 @@ use crate::layout::OutputRecordLayout; use crate::layout::PreludeLayout; use crate::layout::Section; use crate::macho::DYLINKER_PATH; +use crate::macho::DyldChainedFixupsCommand; use crate::macho::DylinkerCommand; use crate::macho::EntryPointCommand; use crate::macho::FileHeader; @@ -44,11 +45,13 @@ use crate::resolution::SectionSlot; use crate::timing_phase; use crate::verbose_timing_phase; use object::BigEndian; +use object::Endian; use object::Endianness; use object::U32; use object::from_bytes_mut; use object::macho; use object::macho::CPU_TYPE_ARM64; +use object::macho::LC_DYLD_CHAINED_FIXUPS; use object::macho::LC_LOAD_DYLINKER; use object::macho::LC_MAIN; use object::macho::LC_SEGMENT_64; @@ -66,6 +69,7 @@ use tracing::debug_span; use zerocopy::FromZeros; const LE: Endianness = Endianness::Little; +const DYLD_CHAINED_IMPORT: u32 = 1; type MachOLayout<'data> = Layout<'data, MachO>; @@ -141,6 +145,14 @@ fn write_prelude<'data, A: Arch>( .map_err(|_| error!("Invalid DYLINKER command allocation"))?; write_dylinker_command::(dylinker_command, dylinker_path_buffer); + let chained_fixups_command: &mut DyldChainedFixupsCommand = + from_bytes_mut(buffers.get_mut(part_id::DYLD_CHAINED_FIXUPS)) + .map_err(|_| error!("Invalid DYLD_CHAINED_FIXUPS command allocation"))? + .0; + write_dyld_chained_fixups_command::(layout, chained_fixups_command); + + write_chained_fixup_table::(buffers.get_mut(part_id::CHAINED_FIXUP_TABLE))?; + // TODO: remove buffers.get_mut(part_id::STRTAB).write_all(b"x")?; @@ -387,3 +399,48 @@ fn write_dylinker_command>( // The string size is always a multiple of 8B. path_buffer[DYLINKER_PATH.len()..].zero(); } + +fn write_dyld_chained_fixups_command>( + layout: &MachOLayout, + command: &mut DyldChainedFixupsCommand, +) { + let chained_fixup_table = layout + .section_layouts + .get(output_section_id::CHAINED_FIXUP_TABLE); + + command.cmd.set(LE, LC_DYLD_CHAINED_FIXUPS); + command + .cmdsize + .set(LE, size_of::() as u32); + command + .dataoff + .set(LE, chained_fixup_table.file_offset as u32); + command + .datasize + .set(LE, chained_fixup_table.file_size as u32); +} + +fn write_chained_fixup_table>(out: &mut [u8]) -> Result { + // TODO: check length + + out.fill(0); + put_u32(out, 0x00, 0); + put_u32(out, 0x04, 32); + put_u32(out, 0x08, 48); + put_u32(out, 0x0c, 48); + put_u32(out, 0x10, 0); + put_u32(out, 0x14, DYLD_CHAINED_IMPORT); + put_u32(out, 0x18, 0); + put_u32(out, 0x1c, 0); + + put_u32(out, 0x20, 3); + put_u32(out, 0x24, 0); + put_u32(out, 0x28, 0); + put_u32(out, 0x2c, 0); + + Ok(()) +} + +fn put_u32(out: &mut [u8], offset: usize, value: u32) { + out[offset..offset + size_of::()].copy_from_slice(&value.to_le_bytes()); +} diff --git a/libwild/src/output_section_id.rs b/libwild/src/output_section_id.rs index 547970dd4..343e3b5d9 100644 --- a/libwild/src/output_section_id.rs +++ b/libwild/src/output_section_id.rs @@ -108,6 +108,10 @@ pub(crate) const LINK_EDIT_SEGMENT: OutputSectionId = part_id::LINK_EDIT_SEGMENT.output_section_id(); pub(crate) const ENTRY_POINT: OutputSectionId = part_id::ENTRY_POINT.output_section_id(); pub(crate) const DYLINKER: OutputSectionId = part_id::DYLINKER.output_section_id(); +pub(crate) const DYLD_CHAINED_FIXUPS: OutputSectionId = + part_id::DYLD_CHAINED_FIXUPS.output_section_id(); +pub(crate) const CHAINED_FIXUP_TABLE: OutputSectionId = + part_id::CHAINED_FIXUP_TABLE.output_section_id(); // Regular sections copied from the input objects. pub(crate) const RODATA: OutputSectionId = OutputSectionId::regular(0); diff --git a/libwild/src/part_id.rs b/libwild/src/part_id.rs index 0f6a59e50..450885f71 100644 --- a/libwild/src/part_id.rs +++ b/libwild/src/part_id.rs @@ -54,8 +54,10 @@ pub(crate) const DATA_SEGMENT: PartId = PartId(33); pub(crate) const LINK_EDIT_SEGMENT: PartId = PartId(34); pub(crate) const ENTRY_POINT: PartId = PartId(35); pub(crate) const DYLINKER: PartId = PartId(36); +pub(crate) const DYLD_CHAINED_FIXUPS: PartId = PartId(37); +pub(crate) const CHAINED_FIXUP_TABLE: PartId = PartId(38); -pub(crate) const NUM_SINGLE_PART_SECTIONS: u32 = 37; +pub(crate) const NUM_SINGLE_PART_SECTIONS: u32 = 39; #[cfg(test)] pub(crate) const NUM_BUILT_IN_PARTS: usize = NUM_SINGLE_PART_SECTIONS as usize diff --git a/libwild/src/verification.rs b/libwild/src/verification.rs index 4499b2d65..20cc3447a 100644 --- a/libwild/src/verification.rs +++ b/libwild/src/verification.rs @@ -118,6 +118,8 @@ pub(crate) fn clear_ignored(expected: &mut OutputSectionPartMap) { part_id::LINK_EDIT_SEGMENT, part_id::ENTRY_POINT, part_id::DYLINKER, + part_id::DYLD_CHAINED_FIXUPS, + part_id::CHAINED_FIXUP_TABLE, ]; for part_id in IGNORED { From 0b93697f43a08837b6c87258d5a087bec02b341e Mon Sep 17 00:00:00 2001 From: Martin Liska Date: Mon, 6 Apr 2026 16:42:51 +0200 Subject: [PATCH 12/21] refactor --- libwild/src/macho.rs | 24 +++++++----- libwild/src/macho_writer.rs | 73 ++++++++++++++++++++++++++----------- 2 files changed, 66 insertions(+), 31 deletions(-) diff --git a/libwild/src/macho.rs b/libwild/src/macho.rs index 6180a6b8c..1c78a9786 100644 --- a/libwild/src/macho.rs +++ b/libwild/src/macho.rs @@ -79,32 +79,38 @@ pub(crate) type ChainedFixupsHeader = DyldChainedFixupsHeader; // TODO: move to object crate // values for dyld_chained_fixups_header.imports_format -#[repr(C)] -enum DyldChainedFixupsImporstFormat { +#[allow(non_camel_case_types)] +#[repr(u32)] +pub(crate) enum DyldChainedFixupsImporstFormat { DYLD_CHAINED_IMPORT = 1, DYLD_CHAINED_IMPORT_ADDEND = 2, DYLD_CHAINED_IMPORT_ADDEND64 = 3, } // header of the LC_DYLD_CHAINED_FIXUPS payload +#[derive(Clone, Copy)] #[repr(C)] pub(crate) struct DyldChainedFixupsHeader { // 0 - fixups_version: U32, + pub(crate) fixups_version: U32, // offset of dyld_chained_starts_in_image in chain_data - starts_offset: U32, + pub(crate) starts_offset: U32, // offset of imports table in chain_data - imports_offset: U32, + pub(crate) imports_offset: U32, // offset of symbol strings in chain_data - symbols_offset: U32, + pub(crate) symbols_offset: U32, // number of imported symbol names - imports_count: U32, + pub(crate) imports_count: U32, // DYLD_CHAINED_IMPORT* - imports_format: U32, + pub(crate) imports_format: U32, // 0 => uncompressed, 1 => zlib compressed - symbols_format: U32, + pub(crate) symbols_format: U32, } +// Safety: +// `DyldChainedFixupsHeader` is repr(C), contains only `U32` fields, and has no padding. +unsafe impl object::Pod for DyldChainedFixupsHeader {} + // This struct is embedded in LC_DYLD_CHAINED_FIXUPS payload // struct dyld_chained_starts_in_image // { diff --git a/libwild/src/macho_writer.rs b/libwild/src/macho_writer.rs index 3a817df48..1a58db63d 100644 --- a/libwild/src/macho_writer.rs +++ b/libwild/src/macho_writer.rs @@ -18,8 +18,11 @@ use crate::layout::ObjectLayout; use crate::layout::OutputRecordLayout; use crate::layout::PreludeLayout; use crate::layout::Section; +use crate::macho::ChainedFixupsHeader; +use crate::macho::DEFAULT_SEGMENT_COUNT; use crate::macho::DYLINKER_PATH; use crate::macho::DyldChainedFixupsCommand; +use crate::macho::DyldChainedFixupsImporstFormat; use crate::macho::DylinkerCommand; use crate::macho::EntryPointCommand; use crate::macho::FileHeader; @@ -69,8 +72,6 @@ use tracing::debug_span; use zerocopy::FromZeros; const LE: Endianness = Endianness::Little; -const DYLD_CHAINED_IMPORT: u32 = 1; - type MachOLayout<'data> = Layout<'data, MachO>; pub(crate) fn write<'data, A: Arch>( @@ -151,7 +152,24 @@ fn write_prelude<'data, A: Arch>( .0; write_dyld_chained_fixups_command::(layout, chained_fixups_command); - write_chained_fixup_table::(buffers.get_mut(part_id::CHAINED_FIXUP_TABLE))?; + let chained_fixup_table = buffers.get_mut(part_id::CHAINED_FIXUP_TABLE); + chained_fixup_table.fill(0); + let starts_len = size_of::() * (DEFAULT_SEGMENT_COUNT + 1); + let min_len = size_of::() + starts_len; + if chained_fixup_table.len() < min_len { + bail!( + "CHAINED_FIXUP_TABLE allocation too small. Need at least {} bytes, got {}", + min_len, + chained_fixup_table.len() + ); + } + let (chained_fixups_header, rest): (&mut ChainedFixupsHeader, &mut [u8]) = + from_bytes_mut(chained_fixup_table) + .map_err(|_| error!("Invalid chained fixups header allocation"))?; + let (starts_in_image, _) = + slice_from_bytes_mut::>(rest, DEFAULT_SEGMENT_COUNT + 1) + .map_err(|_| error!("Invalid chained fixups starts allocation"))?; + write_chained_fixup_table::(chained_fixups_header, starts_in_image)?; // TODO: remove buffers.get_mut(part_id::STRTAB).write_all(b"x")?; @@ -420,27 +438,38 @@ fn write_dyld_chained_fixups_command>( .set(LE, chained_fixup_table.file_size as u32); } -fn write_chained_fixup_table>(out: &mut [u8]) -> Result { - // TODO: check length +fn write_chained_fixup_table>( + header: &mut ChainedFixupsHeader, + starts_in_image: &mut [U32], +) -> Result { + let starts_len = size_of::() * (DEFAULT_SEGMENT_COUNT + 1); + if starts_in_image.len() != DEFAULT_SEGMENT_COUNT + 1 { + bail!( + "Invalid chained fixups starts allocation. Expected {} entries, got {}", + DEFAULT_SEGMENT_COUNT + 1, + starts_in_image.len() + ); + } - out.fill(0); - put_u32(out, 0x00, 0); - put_u32(out, 0x04, 32); - put_u32(out, 0x08, 48); - put_u32(out, 0x0c, 48); - put_u32(out, 0x10, 0); - put_u32(out, 0x14, DYLD_CHAINED_IMPORT); - put_u32(out, 0x18, 0); - put_u32(out, 0x1c, 0); + header.fixups_version.set(LE, 0); + header + .starts_offset + .set(LE, size_of::() as u32); + header + .imports_offset + .set(LE, (size_of::() + starts_len) as u32); + header + .symbols_offset + .set(LE, (size_of::() + starts_len) as u32); + header.imports_count.set(LE, 0); + header.imports_format.set( + LE, + DyldChainedFixupsImporstFormat::DYLD_CHAINED_IMPORT as u32, + ); + header.symbols_format.set(LE, 0); - put_u32(out, 0x20, 3); - put_u32(out, 0x24, 0); - put_u32(out, 0x28, 0); - put_u32(out, 0x2c, 0); + starts_in_image[0].set(LE, DEFAULT_SEGMENT_COUNT as u32); + starts_in_image[1..].fill(U32::new(LE, 0)); Ok(()) } - -fn put_u32(out: &mut [u8], offset: usize, value: u32) { - out[offset..offset + size_of::()].copy_from_slice(&value.to_le_bytes()); -} From da9760aa562180d3821d60923d7744d39f8bf17c Mon Sep 17 00:00:00 2001 From: Martin Liska Date: Mon, 6 Apr 2026 16:49:41 +0200 Subject: [PATCH 13/21] do not emit sections for __LINKEDIT segment --- libwild/src/macho.rs | 9 ++---- libwild/src/macho_writer.rs | 61 +++++++++++++++++++++---------------- 2 files changed, 37 insertions(+), 33 deletions(-) diff --git a/libwild/src/macho.rs b/libwild/src/macho.rs index 1c78a9786..d6ee0095f 100644 --- a/libwild/src/macho.rs +++ b/libwild/src/macho.rs @@ -57,7 +57,7 @@ pub(crate) const MACHO_START_MEM_ADDRESS: u64 = 0x1_0000_0000; /// A path to the default dynamic linker. pub(crate) const DYLINKER_PATH: &str = "/usr/lib/dyld"; -pub(crate) const DEFAULT_SEGMENT_COUNT: usize = 3; +pub(crate) const DEFAULT_SEGMENT_COUNT: usize = 4; pub(crate) const CHAINED_FIXUP_TABLE_SIZE: u64 = (size_of::() + size_of::() * (DEFAULT_SEGMENT_COUNT + 1 + 1)) as u64; @@ -1163,12 +1163,7 @@ impl platform::Platform for MachO { ); sizes.increment( part_id::LINK_EDIT_SEGMENT, - (size_of::() - + size_of::() - * count_sections_for_segment_type( - output_sections, - SegmentType::LinkeditSections, - )) as u64, + size_of::() as u64, ); sizes.increment(part_id::ENTRY_POINT, size_of::() as u64); sizes.increment( diff --git a/libwild/src/macho_writer.rs b/libwild/src/macho_writer.rs index 1a58db63d..e92b89aec 100644 --- a/libwild/src/macho_writer.rs +++ b/libwild/src/macho_writer.rs @@ -252,10 +252,15 @@ fn write_segment_commands>( // TODO: write comments let segment_sections = get_segment_sections(layout, segment_sections_type).segment_sections; let segment_size = get_segment_sections(layout, segment_type).segment_size; + + let section_count = if segment_sections_type == SegmentType::LinkeditSections { + 0 + } else { + segment_sections.len() + }; let (segment_cmd, sections) = - split_segment_command_buffer(buffers.get_mut(part_id), segment_sections.len())?; + split_segment_command_buffer(buffers.get_mut(part_id), section_count)?; - debug_assert_eq!(sections.len(), segment_sections.len()); let prot_flags = layout .output_sections .section_flags(part_id.output_section_id()) @@ -264,8 +269,7 @@ fn write_segment_commands>( segment_cmd.cmd.set(LE, LC_SEGMENT_64); segment_cmd.cmdsize.set( LE, - (size_of::() + size_of::() * segment_sections.len()) - as u32, + (size_of::() + size_of::() * section_count) as u32, ); segment_cmd.segname[..seg_name.len()].copy_from_slice(seg_name.as_bytes()); segment_cmd.segname[seg_name.len()..].zero(); @@ -289,28 +293,33 @@ fn write_segment_commands>( segment_cmd.nsects.set(LE, segment_sections.len() as u32); segment_cmd.flags.set(LE, 0); - for (section, (size, section_name, section_flags)) in - sections.iter_mut().zip(segment_sections) - { - let section_name = section_name - .ok_or_else(|| error!("section name must be known"))? - .0; - - section.segname[..seg_name.len()].copy_from_slice(seg_name.as_bytes()); - section.segname[seg_name.len()..].zero(); - section.sectname[..section_name.len()].copy_from_slice(section_name); - section.sectname[section_name.len()..].zero(); - section.addr.set(LE, size.mem_offset); - section.size.set(LE, size.mem_size); - section.offset.set(LE, size.file_offset as u32); - // TODO - section.align.set(LE, 0); - section.reloff.set(LE, 0); - section.nreloc.set(LE, 0); - section.flags.set(LE, section_flags.raw()); - section.reserved1.set(LE, 0); - section.reserved2.set(LE, 0); - section.reserved3.set(LE, 0); + if segment_sections_type == SegmentType::LinkeditSections { + segment_cmd.nsects.set(LE, 0); + } else { + segment_cmd.nsects.set(LE, segment_sections.len() as u32); + for (section, (size, section_name, section_flags)) in + sections.iter_mut().zip(segment_sections) + { + let section_name = section_name + .ok_or_else(|| error!("section name must be known"))? + .0; + + section.segname[..seg_name.len()].copy_from_slice(seg_name.as_bytes()); + section.segname[seg_name.len()..].zero(); + section.sectname[..section_name.len()].copy_from_slice(section_name); + section.sectname[section_name.len()..].zero(); + section.addr.set(LE, size.mem_offset); + section.size.set(LE, size.mem_size); + section.offset.set(LE, size.file_offset as u32); + // TODO + section.align.set(LE, 0); + section.reloff.set(LE, 0); + section.nreloc.set(LE, 0); + section.flags.set(LE, section_flags.raw()); + section.reserved1.set(LE, 0); + section.reserved2.set(LE, 0); + section.reserved3.set(LE, 0); + } } } Ok(()) From 21bb5ad7874947df7353ddccc73f4367154d48e3 Mon Sep 17 00:00:00 2001 From: Martin Liska Date: Mon, 6 Apr 2026 16:50:26 +0200 Subject: [PATCH 14/21] remove covered TODO --- libwild/src/macho_writer.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/libwild/src/macho_writer.rs b/libwild/src/macho_writer.rs index e92b89aec..df17bcdc3 100644 --- a/libwild/src/macho_writer.rs +++ b/libwild/src/macho_writer.rs @@ -280,7 +280,6 @@ fn write_segment_commands>( .mem_size .next_multiple_of(MACHO_PAGE_ALIGNMENT.value()), ); - // TODO: should be likely offset relative to the place after the commands segment_cmd.fileoff.set(LE, segment_size.file_offset as u64); segment_cmd.filesize.set( LE, From f8cb7ceed4a53553fe7c4d28e96de16c35881cbe Mon Sep 17 00:00:00 2001 From: Martin Liska Date: Mon, 6 Apr 2026 16:52:30 +0200 Subject: [PATCH 15/21] fix ELF test-case --- libwild/src/output_section_part_map.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libwild/src/output_section_part_map.rs b/libwild/src/output_section_part_map.rs index 30a6615b2..783c21691 100644 --- a/libwild/src/output_section_part_map.rs +++ b/libwild/src/output_section_part_map.rs @@ -256,6 +256,10 @@ fn test_merge_parts() { output_section_id::CSTRING, output_section_id::ENTRY_POINT, output_section_id::LINK_EDIT_SEGMENT, + output_section_id::ENTRY_POINT, + output_section_id::DYLINKER, + output_section_id::DYLD_CHAINED_FIXUPS, + output_section_id::CHAINED_FIXUP_TABLE, ]; let mut sum_of_sums = 0; sum_of_1s.for_each(|section_id, sum| { From 2f51efb4b4e3be7c26af412b4b44f4552d9e2a78 Mon Sep 17 00:00:00 2001 From: Martin Liska Date: Mon, 6 Apr 2026 17:02:27 +0200 Subject: [PATCH 16/21] rename dummy sections in the __LINKEDIT segment --- libwild/src/macho.rs | 9 ++++----- libwild/src/macho_writer.rs | 2 ++ 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/libwild/src/macho.rs b/libwild/src/macho.rs index d6ee0095f..52680ab68 100644 --- a/libwild/src/macho.rs +++ b/libwild/src/macho.rs @@ -25,7 +25,6 @@ use crate::output_section_id::SectionOutputInfo; use crate::part_id; use crate::platform; use crate::symbol_db::Visibility; -use linker_utils::elf::secnames; use object::Endian; use object::Endianness; use object::U32; @@ -1229,8 +1228,8 @@ impl platform::Platform for MachO { symbol_db: &crate::symbol_db::SymbolDb, ) { common.allocate(part_id::CHAINED_FIXUP_TABLE, CHAINED_FIXUP_TABLE_SIZE); - // Mach-O string tables start with an empty string at index 0. - // TODO: Just a filler for now. + // TODO: Just a filler for now that will ensure the __LINKEDIT takes 16KiB - find a better + // solution. common.allocate( part_id::STRTAB, MACHO_PAGE_ALIGNMENT.value() - CHAINED_FIXUP_TABLE_SIZE, @@ -1378,12 +1377,12 @@ const SECTION_DEFINITIONS: [BuiltInSectionDetails; NUM_BUILT_IN_SECTIONS] = { ..DEFAULT_DEFS }; defs[output_section_id::CHAINED_FIXUP_TABLE.as_usize()] = BuiltInSectionDetails { - kind: SectionKind::Primary(SectionName(b"__chain_table")), + kind: SectionKind::Primary(SectionName(b"DYLD_CHAINED_FIXUPS_TABLE")), target_segment_type: Some(SegmentType::LinkeditSections), ..DEFAULT_DEFS }; defs[output_section_id::STRTAB.as_usize()] = BuiltInSectionDetails { - kind: SectionKind::Primary(SectionName(secnames::STRTAB_SECTION_NAME)), + kind: SectionKind::Primary(SectionName(b"STRING_TABLE")), target_segment_type: Some(SegmentType::LinkeditSections), ..DEFAULT_DEFS }; diff --git a/libwild/src/macho_writer.rs b/libwild/src/macho_writer.rs index df17bcdc3..4826f4896 100644 --- a/libwild/src/macho_writer.rs +++ b/libwild/src/macho_writer.rs @@ -292,6 +292,8 @@ fn write_segment_commands>( segment_cmd.nsects.set(LE, segment_sections.len() as u32); segment_cmd.flags.set(LE, 0); + // The sections in __LINKEDIT are actually hidden and must be hidden (not exposed in the + // SEGMENT). if segment_sections_type == SegmentType::LinkeditSections { segment_cmd.nsects.set(LE, 0); } else { From 1107ec3077b35f26f97148f14bf6d7bc30ce238c Mon Sep 17 00:00:00 2001 From: Martin Liska Date: Mon, 6 Apr 2026 17:06:32 +0200 Subject: [PATCH 17/21] clarify comment --- libwild/src/macho.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libwild/src/macho.rs b/libwild/src/macho.rs index 52680ab68..a538f95f2 100644 --- a/libwild/src/macho.rs +++ b/libwild/src/macho.rs @@ -1317,8 +1317,8 @@ impl platform::Platform for MachO { *mem_offset = segment_alignment.align_up(*mem_offset); } SegmentType::TextSections => { - // We allocate a placeholder space for the LinkeditDataCommand command (added by - // codesign tool) in order to preserve the offsets into __text and + // TODO: A placeholder space for the LinkeditDataCommand command is allocated + // (added by codesign tool) in order to preserve the offsets into __text and // other sections in the __TEXT segment. *file_offset += size_of::(); *mem_offset += size_of::() as u64; From 439df5be9574b0c1f741671869d35dbab86905aa Mon Sep 17 00:00:00 2001 From: Martin Liska Date: Mon, 6 Apr 2026 19:13:18 +0200 Subject: [PATCH 18/21] document LC_DYLD_CHAINED_FIXUPS --- libwild/MachO.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/libwild/MachO.md b/libwild/MachO.md index 5e06e8355..c962a36d9 100644 --- a/libwild/MachO.md +++ b/libwild/MachO.md @@ -196,9 +196,17 @@ Pretty straightforward to implement, replaces a legacy `LC_DYLD_INFO(_ONLY)` com ## `LC_DYLD_CHAINED_FIXUPS` command -TODO: explain better +This is roughly analogous to dynamic relocations in ELF. The format is made up of three parts: -Good documentation here: https://github.com/qyang-nj/llios/blob/main/dynamic_linking/chained_fixups.md. +- a table of imported symbols, including the symbol name and the referenced dylib +- a string table used by the import table +- a per-segment chain of fixups, where each entry records the location to patch and an index into the import table that identifies the target + +Good documentation is available here: https://github.com/qyang-nj/llios/blob/main/dynamic_linking/chained_fixups.md. +The relevant data structures are also defined in Apple's `mach-o/fixup-chains.h` header +and mirrored here: https://github.com/qyang-nj/llios/blob/d204d56ff0533c1fae115b77e7554d2e6f4bc4aa/apple_open_source/dyld/include/mach-o/fixup-chains.h. + +Ideally, support for these structures should be added to the `object` crate. ## benchmarks: LLD vs. system linker From f82fc4cdcb8c71160af95b34eb7757cd2e3349fa Mon Sep 17 00:00:00 2001 From: Martin Liska Date: Mon, 6 Apr 2026 19:22:45 +0200 Subject: [PATCH 19/21] add note about the LC_CODE_SIGNATURE --- libwild/MachO.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libwild/MachO.md b/libwild/MachO.md index c962a36d9..a162ff731 100644 --- a/libwild/MachO.md +++ b/libwild/MachO.md @@ -180,7 +180,9 @@ Contents of __unwind_info section: ## `LC_CODE_SIGNATURE` command Code signature is mandatory and cannot run a final binary without it. Can be manually created for a produced binary: `codesign -s - -f a.out`. -A linker can skip emitting the signature by using: `-Wl,-no_adhoc_codesign`. +A linker can skip emitting the signature by using: `-Wl,-no_adhoc_codesign`. One drawback of invoking `codesign` externally is that you +must reserve space for the additional load command in advance; otherwise, offsets in segments such as `__TEXT` will shift. + It's basically an array of SHA-256 hashes, one for each page of the file - similar to how we emit build-id. There's existing LLVM implementation of the format we can use: https://github.com/llvm/llvm-project/blob/36e495dd903cea000f6c4f51954554c22f39d7da/lld/MachO/SyntheticSections.cpp#L1622-L1662 From 0994fd6c0d160ed7007e982b6ce68168cfdd73de Mon Sep 17 00:00:00 2001 From: Martin Liska Date: Tue, 7 Apr 2026 07:32:22 +0200 Subject: [PATCH 20/21] replace DYLINKER with the existing INTERP --- libwild/src/macho.rs | 8 ++++---- libwild/src/macho_writer.rs | 4 ++-- libwild/src/output_section_id.rs | 1 - libwild/src/output_section_part_map.rs | 1 - libwild/src/part_id.rs | 7 +++---- libwild/src/verification.rs | 1 - 6 files changed, 9 insertions(+), 13 deletions(-) diff --git a/libwild/src/macho.rs b/libwild/src/macho.rs index a538f95f2..98e6d80b5 100644 --- a/libwild/src/macho.rs +++ b/libwild/src/macho.rs @@ -725,7 +725,7 @@ impl platform::ProgramSegmentDef for ProgramSegmentDef { | output_section_id::DATA_SEGMENT | output_section_id::LINK_EDIT_SEGMENT | output_section_id::ENTRY_POINT - | output_section_id::DYLINKER + | output_section_id::INTERP | output_section_id::DYLD_CHAINED_FIXUPS => SegmentType::LoadCommands, output_section_id::TEXT | output_section_id::CSTRING => SegmentType::TextSections, output_section_id::DATA => SegmentType::DataSections, @@ -1166,7 +1166,7 @@ impl platform::Platform for MachO { ); sizes.increment(part_id::ENTRY_POINT, size_of::() as u64); sizes.increment( - part_id::DYLINKER, + part_id::INTERP, ((size_of::() + DYLINKER_PATH.len()).next_multiple_of(8)) as u64, ); sizes.increment( @@ -1285,7 +1285,7 @@ impl platform::Platform for MachO { builder.add_section(output_section_id::DATA_SEGMENT); builder.add_section(output_section_id::LINK_EDIT_SEGMENT); builder.add_section(output_section_id::ENTRY_POINT); - builder.add_section(output_section_id::DYLINKER); + builder.add_section(output_section_id::INTERP); // DYLINKER builder.add_section(output_section_id::DYLD_CHAINED_FIXUPS); // Content of the sections (e.g. __text, __data). builder.add_section(output_section_id::TEXT); @@ -1366,7 +1366,7 @@ const SECTION_DEFINITIONS: [BuiltInSectionDetails; NUM_BUILT_IN_SECTIONS] = { target_segment_type: Some(SegmentType::LoadCommands), ..DEFAULT_DEFS }; - defs[output_section_id::DYLINKER.as_usize()] = BuiltInSectionDetails { + defs[output_section_id::INTERP.as_usize()] = BuiltInSectionDetails { kind: SectionKind::Primary(SectionName(b"LC_LOAD_DYLINKER")), target_segment_type: Some(SegmentType::LoadCommands), ..DEFAULT_DEFS diff --git a/libwild/src/macho_writer.rs b/libwild/src/macho_writer.rs index 4826f4896..b525a0560 100644 --- a/libwild/src/macho_writer.rs +++ b/libwild/src/macho_writer.rs @@ -142,8 +142,8 @@ fn write_prelude<'data, A: Arch>( write_entry_point_command::(layout, entry_point_command); let (dylinker_command, dylinker_path_buffer): (&mut DylinkerCommand, &mut [u8]) = - from_bytes_mut(buffers.get_mut(part_id::DYLINKER)) - .map_err(|_| error!("Invalid DYLINKER command allocation"))?; + from_bytes_mut(buffers.get_mut(part_id::INTERP)) + .map_err(|_| error!("Invalid INTERP command allocation"))?; write_dylinker_command::(dylinker_command, dylinker_path_buffer); let chained_fixups_command: &mut DyldChainedFixupsCommand = diff --git a/libwild/src/output_section_id.rs b/libwild/src/output_section_id.rs index 343e3b5d9..7d1e6730e 100644 --- a/libwild/src/output_section_id.rs +++ b/libwild/src/output_section_id.rs @@ -107,7 +107,6 @@ pub(crate) const DATA_SEGMENT: OutputSectionId = part_id::DATA_SEGMENT.output_se pub(crate) const LINK_EDIT_SEGMENT: OutputSectionId = part_id::LINK_EDIT_SEGMENT.output_section_id(); pub(crate) const ENTRY_POINT: OutputSectionId = part_id::ENTRY_POINT.output_section_id(); -pub(crate) const DYLINKER: OutputSectionId = part_id::DYLINKER.output_section_id(); pub(crate) const DYLD_CHAINED_FIXUPS: OutputSectionId = part_id::DYLD_CHAINED_FIXUPS.output_section_id(); pub(crate) const CHAINED_FIXUP_TABLE: OutputSectionId = diff --git a/libwild/src/output_section_part_map.rs b/libwild/src/output_section_part_map.rs index 783c21691..72b4f124f 100644 --- a/libwild/src/output_section_part_map.rs +++ b/libwild/src/output_section_part_map.rs @@ -257,7 +257,6 @@ fn test_merge_parts() { output_section_id::ENTRY_POINT, output_section_id::LINK_EDIT_SEGMENT, output_section_id::ENTRY_POINT, - output_section_id::DYLINKER, output_section_id::DYLD_CHAINED_FIXUPS, output_section_id::CHAINED_FIXUP_TABLE, ]; diff --git a/libwild/src/part_id.rs b/libwild/src/part_id.rs index 450885f71..a4a32e5e4 100644 --- a/libwild/src/part_id.rs +++ b/libwild/src/part_id.rs @@ -53,11 +53,10 @@ pub(crate) const TEXT_SEGMENT: PartId = PartId(32); pub(crate) const DATA_SEGMENT: PartId = PartId(33); pub(crate) const LINK_EDIT_SEGMENT: PartId = PartId(34); pub(crate) const ENTRY_POINT: PartId = PartId(35); -pub(crate) const DYLINKER: PartId = PartId(36); -pub(crate) const DYLD_CHAINED_FIXUPS: PartId = PartId(37); -pub(crate) const CHAINED_FIXUP_TABLE: PartId = PartId(38); +pub(crate) const DYLD_CHAINED_FIXUPS: PartId = PartId(36); +pub(crate) const CHAINED_FIXUP_TABLE: PartId = PartId(37); -pub(crate) const NUM_SINGLE_PART_SECTIONS: u32 = 39; +pub(crate) const NUM_SINGLE_PART_SECTIONS: u32 = 38; #[cfg(test)] pub(crate) const NUM_BUILT_IN_PARTS: usize = NUM_SINGLE_PART_SECTIONS as usize diff --git a/libwild/src/verification.rs b/libwild/src/verification.rs index 20cc3447a..27d03d997 100644 --- a/libwild/src/verification.rs +++ b/libwild/src/verification.rs @@ -117,7 +117,6 @@ pub(crate) fn clear_ignored(expected: &mut OutputSectionPartMap) { part_id::PAGEZERO_SEGMENT, part_id::LINK_EDIT_SEGMENT, part_id::ENTRY_POINT, - part_id::DYLINKER, part_id::DYLD_CHAINED_FIXUPS, part_id::CHAINED_FIXUP_TABLE, ]; From 89faadc85e0c06889d7cec07c52c92211077c757 Mon Sep 17 00:00:00 2001 From: Martin Liska Date: Tue, 7 Apr 2026 07:35:54 +0200 Subject: [PATCH 21/21] Add comment --- libwild/src/layout.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/libwild/src/layout.rs b/libwild/src/layout.rs index 7899d1d6f..2fa1e1f84 100644 --- a/libwild/src/layout.rs +++ b/libwild/src/layout.rs @@ -4591,6 +4591,7 @@ fn layout_section_parts( .copied() .unwrap_or_else(|| args.loadable_segment_alignment()); if let Some(location) = pending_location.take() { + // The OrderEvent::SetLocation is ELF-specific only. mem_offset = location.address; file_offset = segment_alignment.align_modulo(mem_offset, file_offset as u64) as usize;