Skip to main content

macho_core/model/validate/
mod.rs

1/// The diagnostics module.
2pub 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
23/// Performs validate.
24pub 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    // Duplicate segment names
94    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        // initprot bits should be a subset of maxprot bits
150        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            // Check symbol table bounds
176            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            // Check string table bounds
197            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}