macho_core/model/validate/
mod.rs1pub mod diagnostics;
3
4pub use diagnostics::{Diagnostic, DiagnosticCode, Severity, Span};
5
6use crate::format::constants::VmProtection;
7use crate::model::header::Bitness;
8use crate::model::load_command::LoadCommand;
9use crate::model::macho_file::MachoFile;
10use crate::model::names::SegmentName;
11
12const HEADER_COMMAND_COUNT_CODE: &str = "parse.validation.header_command_count";
13const HEADER_COMMAND_SIZE_CODE: &str = "parse.validation.header_command_size";
14const SEGMENT_BOUNDS_CODE: &str = "parse.validation.segment_bounds";
15const SECTION_BOUNDS_CODE: &str = "parse.validation.section_bounds";
16const DUPLICATE_SEGMENT_CODE: &str = "parse.validation.duplicate_segment";
17const SEGMENT_OVERLAP_CODE: &str = "parse.validation.segment_overlap";
18const PAGEZERO_FILE_SIZE_CODE: &str = "parse.validation.pagezero_file_size";
19const PROTECTION_MISMATCH_CODE: &str = "parse.validation.protection_mismatch";
20const SYMBOL_TABLE_BOUNDS_CODE: &str = "parse.validation.symbol_table_bounds";
21const STRING_TABLE_BOUNDS_CODE: &str = "parse.validation.string_table_bounds";
22
23pub fn validate(macho: &MachoFile<'_>) -> Vec<Diagnostic> {
25 let mut diags = Vec::new();
26
27 check_header_consistency(macho, &mut diags);
28 check_segment_bounds(macho, &mut diags);
29 check_segment_vm_overlap(macho, &mut diags);
30 check_pagezero(macho, &mut diags);
31 check_symtab_bounds(macho, &mut diags);
32 check_protections(macho, &mut diags);
33
34 diags
35}
36
37fn check_header_consistency(macho: &MachoFile<'_>, diags: &mut Vec<Diagnostic>) {
38 let expected = macho.header().ncmds as usize;
39 let actual = macho.load_commands().len();
40 if expected != actual {
41 diags.push(Diagnostic::error(
42 HEADER_COMMAND_COUNT_CODE,
43 format!("header ncmds={expected} but parsed {actual} load commands"),
44 ));
45 }
46
47 let expected_size = macho.header().sizeofcmds;
48 let actual_size: u32 = macho.load_commands().iter().map(|lc| lc.raw_size).sum();
49 if expected_size != actual_size {
50 diags.push(Diagnostic::error(
51 HEADER_COMMAND_SIZE_CODE,
52 format!(
53 "header sizeofcmds={expected_size:#x} but load commands sum to {actual_size:#x}"
54 ),
55 ));
56 }
57}
58
59fn check_segment_bounds(macho: &MachoFile<'_>, diags: &mut Vec<Diagnostic>) {
60 let file_size = macho.file_size() as u64;
61
62 for seg in macho.segments() {
63 let seg_end = seg.file_offset.0.saturating_add(seg.file_size);
64 if seg.file_size > 0 && seg_end > file_size {
65 diags.push(Diagnostic::error(
66 SEGMENT_BOUNDS_CODE,
67 format!(
68 "segment {} file range {:#x}..{:#x} extends beyond file size {:#x}",
69 seg.name, seg.file_offset.0, seg_end, file_size
70 ),
71 ));
72 }
73
74 for sect in &seg.sections {
75 if sect.section_type.is_zerofill() || sect.size == 0 {
76 continue;
77 }
78 let sect_end = sect.offset.0.saturating_add(sect.size);
79 if sect.offset.0 < seg.file_offset.0 || sect_end > seg_end {
80 diags.push(Diagnostic::warning(
81 SECTION_BOUNDS_CODE,
82 format!(
83 "section {},{} file range {:#x}..{:#x} outside segment {} range {:#x}..{:#x}",
84 sect.segment_name, sect.section_name,
85 sect.offset.0, sect_end,
86 seg.name, seg.file_offset.0, seg_end
87 ),
88 ));
89 }
90 }
91 }
92
93 let mut seen_names = std::collections::HashSet::new();
95 for seg in macho.segments() {
96 let name_str = seg.name.as_str_lossy().into_owned();
97 if !name_str.is_empty() && !seen_names.insert(name_str.clone()) {
98 diags.push(Diagnostic::warning(
99 DUPLICATE_SEGMENT_CODE,
100 format!("duplicate segment name: {name_str}"),
101 ));
102 }
103 }
104}
105
106fn check_segment_vm_overlap(macho: &MachoFile<'_>, diags: &mut Vec<Diagnostic>) {
107 let segments = macho.segments();
108 for (i, a) in segments.iter().enumerate() {
109 if a.vm_size == 0 {
110 continue;
111 }
112 let a_end = a.vm_addr.0.saturating_add(a.vm_size);
113 for b in &segments[i + 1..] {
114 if b.vm_size == 0 {
115 continue;
116 }
117 let b_end = b.vm_addr.0.saturating_add(b.vm_size);
118 let overlaps = a.vm_addr.0 < b_end && b.vm_addr.0 < a_end;
119 if overlaps {
120 diags.push(Diagnostic::error(
121 SEGMENT_OVERLAP_CODE,
122 format!(
123 "segments {} and {} have overlapping VM ranges: \
124 {:#x}..{:#x} vs {:#x}..{:#x}",
125 a.name, b.name, a.vm_addr.0, a_end, b.vm_addr.0, b_end
126 ),
127 ));
128 }
129 }
130 }
131}
132
133fn check_pagezero(macho: &MachoFile<'_>, diags: &mut Vec<Diagnostic>) {
134 for seg in macho.segments() {
135 if seg.name == SegmentName::PAGEZERO && seg.file_size != 0 {
136 diags.push(Diagnostic::error(
137 PAGEZERO_FILE_SIZE_CODE,
138 format!(
139 "__PAGEZERO has non-zero file_size {:#x} (must be 0)",
140 seg.file_size
141 ),
142 ));
143 }
144 }
145}
146
147fn check_protections(macho: &MachoFile<'_>, diags: &mut Vec<Diagnostic>) {
148 for seg in macho.segments() {
149 let init = seg.init_prot;
151 let max = seg.max_prot;
152 if init.bits() & !max.bits() != 0 {
153 diags.push(Diagnostic::warning(
154 PROTECTION_MISMATCH_CODE,
155 format!(
156 "segment {} has initprot ({}) with bits not in maxprot ({})",
157 seg.name,
158 format_prot(init),
159 format_prot(max),
160 ),
161 ));
162 }
163 }
164}
165
166fn check_symtab_bounds(macho: &MachoFile<'_>, diags: &mut Vec<Diagnostic>) {
167 let file_size = macho.file_size() as u64;
168 let nlist_size: u64 = match macho.bitness() {
169 Bitness::Bits64 => 16,
170 Bitness::Bits32 => 12,
171 };
172
173 for lc in macho.load_commands() {
174 if let LoadCommand::Symtab(ref st) = lc.kind {
175 if let Some(sym_end) = (st.nsyms as u64)
177 .checked_mul(nlist_size)
178 .and_then(|sz| (st.sym_offset as u64).checked_add(sz))
179 {
180 if sym_end > file_size {
181 diags.push(Diagnostic::error(
182 SYMBOL_TABLE_BOUNDS_CODE,
183 format!(
184 "LC_SYMTAB symbol table {:#x}..{:#x} extends beyond file size {:#x}",
185 st.sym_offset, sym_end, file_size
186 ),
187 ));
188 }
189 } else {
190 diags.push(Diagnostic::error(
191 SYMBOL_TABLE_BOUNDS_CODE,
192 "LC_SYMTAB symbol table offset+size overflows".to_string(),
193 ));
194 }
195
196 if let Some(str_end) = (st.str_offset as u64).checked_add(st.str_size as u64) {
198 if str_end > file_size {
199 diags.push(Diagnostic::error(
200 STRING_TABLE_BOUNDS_CODE,
201 format!(
202 "LC_SYMTAB string table {:#x}..{:#x} extends beyond file size {:#x}",
203 st.str_offset, str_end, file_size
204 ),
205 ));
206 }
207 } else {
208 diags.push(Diagnostic::error(
209 STRING_TABLE_BOUNDS_CODE,
210 "LC_SYMTAB string table offset+size overflows".to_string(),
211 ));
212 }
213 }
214 }
215}
216
217fn format_prot(prot: VmProtection) -> String {
218 prot.rwx_string()
219}