Skip to main content

macho_cpp/
strict_vtable.rs

1//! Strict, bounded Itanium virtual-table and VTT leaf records.
2
3use serde::{Deserialize, Deserializer, Serialize};
4
5use crate::strict_rtti::{
6    StrictPointerObservation, StrictRttiConservation, StrictRttiGap, StrictRttiLimits,
7    StrictRttiObservation, StrictRttiObservationKind, StrictRttiOutcome,
8};
9use crate::{Error, Result};
10
11#[path = "strict_vtable_decoder.rs"]
12mod decoder;
13pub use decoder::{decode_strict_vtables, decode_strict_vtables_from_source};
14
15const HARD_RECORDS: u64 = 4_000_000;
16const HARD_WORDS: u64 = 64_000_000;
17const HARD_EVIDENCE_BYTES: u64 = 1 << 31;
18const HARD_INPUT_BYTES: u64 = 1 << 34;
19const HARD_SYMBOLS: u64 = 16_000_000;
20
21/// Structural limits for one strict Itanium virtual-table decode.
22#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
23#[serde(deny_unknown_fields)]
24pub struct StrictVtableLimits {
25    /// Maximum selected thin-image byte length.
26    pub max_input_bytes: u64,
27    /// Maximum symbols admitted before candidate discovery.
28    pub max_symbols: u64,
29    /// Maximum `_ZTV`, `_ZTC`, and `_ZTT` candidates.
30    pub max_records: u64,
31    /// Maximum pointer-width words across all defined candidates.
32    pub max_words: u64,
33    /// Maximum total bytes covered by observations.
34    pub max_evidence_bytes: u64,
35}
36
37impl Default for StrictVtableLimits {
38    fn default() -> Self {
39        Self {
40            max_input_bytes: 1 << 30,
41            max_symbols: 4_000_000,
42            max_records: 1_000_000,
43            max_words: 8_000_000,
44            max_evidence_bytes: 1 << 29,
45        }
46    }
47}
48
49impl StrictVtableLimits {
50    /// Reject zero limits and values above implementation hard maxima.
51    pub fn validate(self) -> Result<()> {
52        for (value, hard, name) in [
53            (self.max_input_bytes, HARD_INPUT_BYTES, "vtable input-byte"),
54            (self.max_symbols, HARD_SYMBOLS, "vtable symbol"),
55            (self.max_records, HARD_RECORDS, "vtable record"),
56            (self.max_words, HARD_WORDS, "vtable word"),
57            (
58                self.max_evidence_bytes,
59                HARD_EVIDENCE_BYTES,
60                "vtable evidence-byte",
61            ),
62        ] {
63            if value == 0 || value > hard {
64                return Err(Error::format(format!(
65                    "{name} limit is zero or exceeds its hard maximum"
66                )));
67            }
68        }
69        Ok(())
70    }
71
72    pub(crate) fn reader_limits(self) -> StrictRttiLimits {
73        StrictRttiLimits {
74            max_input_bytes: self.max_input_bytes,
75            max_symbols: self.max_symbols,
76            max_records: self.max_records,
77            max_evidence_bytes: self.max_evidence_bytes,
78            ..StrictRttiLimits::default()
79        }
80    }
81}
82
83/// ABI role of one special-name symbol.
84#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
85#[serde(rename_all = "snake_case")]
86pub enum ItaniumVtableSymbolKind {
87    /// Complete-object virtual table group (`_ZTV`).
88    CompleteGroup,
89    /// Construction virtual table group (`_ZTC`).
90    ConstructionGroup,
91    /// Virtual table table (`_ZTT`).
92    Vtt,
93}
94
95/// Authority used to bound a Mach-O symbol that has no encoded size.
96#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
97#[serde(rename_all = "snake_case")]
98pub enum ItaniumVtableExtentSource {
99    /// The next defined symbol in the same section.
100    NextDefinedSymbol,
101    /// The end of the containing file-backed section.
102    SectionEnd,
103}
104
105/// Best leaf-level role for a pre-address-point offset component.
106#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
107#[serde(rename_all = "snake_case")]
108pub enum ItaniumVtableOffsetRole {
109    /// The leaf image cannot distinguish vcall from vbase without graph context.
110    VcallOrVbase,
111}
112
113/// Structural authority that located one address-point header.
114#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
115#[serde(rename_all = "snake_case")]
116pub enum ItaniumVtableAddressPointSource {
117    /// The typeinfo word resolves to an exact `_ZTI` symbol.
118    TypeinfoSymbol,
119    /// No `_ZTI` target exists and a null typeinfo word follows symbol word zero.
120    NullTypeinfoAtSymbolStart,
121}
122
123/// One encoded thunk adjustment.
124#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
125#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
126pub enum ItaniumThunkAdjustment {
127    /// Fixed adjustment to a non-virtual base.
128    NonVirtual {
129        /// Signed byte displacement.
130        offset: i64,
131    },
132    /// Fixed adjustment followed by a vtable-loaded virtual adjustment.
133    Virtual {
134        /// Signed displacement to the nearest virtual base.
135        offset: i64,
136        /// Signed byte displacement of the vcall/vbase offset entry.
137        virtual_offset: i64,
138    },
139}
140
141/// Function-slot semantic role proven by its exact target symbol.
142#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
143#[serde(rename_all = "snake_case")]
144pub enum ItaniumVtableSlotRole {
145    /// Ordinary function or an exact function symbol without a narrower role.
146    Function,
147    /// `__cxa_pure_virtual`.
148    PureVirtual,
149    /// `__cxa_deleted_virtual`.
150    DeletedVirtual,
151    /// D0 deleting destructor.
152    DeletingDestructor,
153    /// D1 complete-object destructor.
154    CompleteDestructor,
155    /// D2 base-object destructor.
156    BaseDestructor,
157    /// `Th` non-virtual override thunk.
158    NonVirtualThunk,
159    /// `Tv` virtual override thunk.
160    VirtualThunk,
161    /// `Tc` covariant-return thunk.
162    CovariantThunk,
163    /// Null virtual entry.
164    Null,
165    /// A resolved target with no exact symbol role.
166    Unknown,
167}
168
169/// One word before an address-point header.
170#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
171#[serde(deny_unknown_fields)]
172pub struct ItaniumVtableOffsetRecord {
173    /// Absolute word ordinal in the containing symbol.
174    pub word_ordinal: u64,
175    /// Observation containing the pointer-width signed integer.
176    pub observation_ordinal: u64,
177    /// Raw unsigned storage value.
178    pub raw_value: u64,
179    /// Sign-extended ABI value.
180    pub signed_value: i64,
181    /// Leaf-level offset role.
182    pub role: ItaniumVtableOffsetRole,
183}
184
185/// One function-entry candidate after an address point.
186#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
187#[serde(deny_unknown_fields)]
188pub struct ItaniumVtableSlotRecord {
189    /// Zero-based slot ordinal within this address point.
190    pub ordinal: u64,
191    /// Absolute word ordinal in the containing symbol.
192    pub word_ordinal: u64,
193    /// Full pointer and fixup provenance.
194    pub pointer: StrictPointerObservation,
195    /// Exact target symbol when one is present.
196    pub target_symbol: Option<String>,
197    /// Function, destructor, thunk, pure/deleted, null, or unknown role.
198    pub role: ItaniumVtableSlotRole,
199    /// `this` adjustment encoded by a thunk name.
200    pub this_adjustment: Option<ItaniumThunkAdjustment>,
201    /// Return adjustment encoded by a covariant thunk name.
202    pub return_adjustment: Option<ItaniumThunkAdjustment>,
203}
204
205/// One ABI address point and its structural header.
206#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
207#[serde(deny_unknown_fields)]
208pub struct ItaniumVtableAddressPointRecord {
209    /// Zero-based address-point ordinal in the group.
210    pub ordinal: u64,
211    /// Address-point virtual address, immediately after the typeinfo pointer.
212    pub va: u64,
213    /// Structural authority used to locate this header.
214    pub source: ItaniumVtableAddressPointSource,
215    /// Word ordinal of the offset-to-top field.
216    pub offset_to_top_word: u64,
217    /// Offset components preceding the header when their ownership is exact.
218    pub prefix_offsets: Vec<ItaniumVtableOffsetRecord>,
219    /// Observation containing offset-to-top.
220    pub offset_to_top_observation_ordinal: u64,
221    /// Signed offset from this subobject to the complete object.
222    pub offset_to_top: i64,
223    /// Typeinfo pointer, including a distinct null state for `-fno-rtti`.
224    pub typeinfo: StrictPointerObservation,
225    /// Slots owned unambiguously by this address point.
226    pub slots: Vec<ItaniumVtableSlotRecord>,
227}
228
229/// Words between address points whose slot/offset ownership requires graph context.
230#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
231#[serde(deny_unknown_fields)]
232pub struct ItaniumVtableAmbiguousWordRecord {
233    /// Absolute word ordinal in the containing symbol.
234    pub word_ordinal: u64,
235    /// Observation containing the raw word.
236    pub observation_ordinal: u64,
237    /// Raw unsigned storage value.
238    pub raw_value: u64,
239}
240
241/// Strict complete or construction virtual-table group.
242#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
243#[serde(deny_unknown_fields)]
244pub struct ItaniumVtableGroupRecord {
245    /// Exact `_ZTV` or `_ZTC` symbol spelling.
246    pub symbol: String,
247    /// Complete-object or construction-table kind.
248    pub kind: ItaniumVtableSymbolKind,
249    /// Symbol virtual address.
250    pub va: u64,
251    /// Symbol thin-image file offset.
252    pub file_offset: u64,
253    /// Exact bounded extent in bytes.
254    pub byte_length: u64,
255    /// Mach-O authority used for the extent.
256    pub extent_source: ItaniumVtableExtentSource,
257    /// Pointer width in bytes; relative-vtable encodings are rejected.
258    pub pointer_width: u8,
259    /// Structurally proven address points.
260    pub address_points: Vec<ItaniumVtableAddressPointRecord>,
261    /// Conserved inter-address-point words whose role is not leaf-decidable.
262    pub ambiguous_words: Vec<ItaniumVtableAmbiguousWordRecord>,
263    /// Whether the definition is weak.
264    pub weak_definition: bool,
265}
266
267/// One pointer entry in a VTT array.
268#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
269#[serde(deny_unknown_fields)]
270pub struct ItaniumVttEntryRecord {
271    /// Zero-based VTT ordinal. Nested VTT roles are assigned by graph context.
272    pub ordinal: u64,
273    /// Pointer to an address point in a complete or construction table.
274    pub address_point: StrictPointerObservation,
275}
276
277/// Strict VTT array.
278#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
279#[serde(deny_unknown_fields)]
280pub struct ItaniumVttRecord {
281    /// Exact `_ZTT` symbol spelling.
282    pub symbol: String,
283    /// Symbol virtual address.
284    pub va: u64,
285    /// Symbol thin-image file offset.
286    pub file_offset: u64,
287    /// Exact bounded extent in bytes.
288    pub byte_length: u64,
289    /// Mach-O authority used for the extent.
290    pub extent_source: ItaniumVtableExtentSource,
291    /// Entries in ABI array order.
292    pub entries: Vec<ItaniumVttEntryRecord>,
293    /// Whether the definition is weak.
294    pub weak_definition: bool,
295}
296
297/// One included strict virtual-table record.
298#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
299#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
300pub enum StrictVtableRecord {
301    /// Complete or construction vtable group.
302    Group {
303        /// Decoded group.
304        record: Box<ItaniumVtableGroupRecord>,
305    },
306    /// VTT array.
307    Vtt {
308        /// Decoded VTT.
309        record: Box<ItaniumVttRecord>,
310    },
311    /// Undefined special-name symbol retained rather than dropped.
312    External {
313        /// Special-name family.
314        symbol_kind: ItaniumVtableSymbolKind,
315        /// Exact symbol spelling.
316        symbol: String,
317        /// Mach-O library ordinal.
318        library_ordinal: i32,
319        /// Weak-reference bit.
320        weak: bool,
321    },
322}
323
324/// Closed strict vtable/VTT decode batch.
325#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
326#[serde(deny_unknown_fields)]
327pub struct StrictVtableBatch {
328    /// Terminal outcome.
329    pub outcome: StrictRttiOutcome,
330    /// Included records in canonical symbol/address order.
331    pub records: Vec<StrictVtableRecord>,
332    /// Exact byte observations in decode order.
333    pub observations: Vec<StrictRttiObservation>,
334    /// Typed failures.
335    pub gaps: Vec<StrictRttiGap>,
336    /// Candidate conservation ledger.
337    pub conservation: StrictRttiConservation,
338}
339
340impl StrictVtableBatch {
341    pub(crate) fn validate(&self, limits: StrictVtableLimits) -> Result<()> {
342        limits.validate()?;
343        self.conservation.validate()?;
344        if self.observations.iter().enumerate().any(|(index, value)| {
345            value.ordinal != u64::try_from(index).unwrap_or(u64::MAX) || value.length == 0
346        }) {
347            return Err(Error::format(
348                "strict vtable observations are not canonical",
349            ));
350        }
351        let evidence = self.observations.iter().try_fold(0_u64, |total, value| {
352            total
353                .checked_add(value.length)
354                .ok_or_else(|| Error::format("strict vtable evidence overflows"))
355        })?;
356        if evidence > limits.max_evidence_bytes
357            || u64::try_from(self.records.len()).ok() != Some(self.conservation.included)
358        {
359            return Err(Error::format(
360                "strict vtable counts exceed limits or disagree",
361            ));
362        }
363        let expected_gaps = self
364            .conservation
365            .unknown
366            .checked_add(u64::from(self.conservation.excluded > 0))
367            .and_then(|value| {
368                value.checked_add(u64::from(
369                    self.outcome == StrictRttiOutcome::Rejected && self.conservation.attempted == 0,
370                ))
371            })
372            .ok_or_else(|| Error::format("strict vtable gap count overflows"))?;
373        if u64::try_from(self.gaps.len()).ok() != Some(expected_gaps) {
374            return Err(Error::format(
375                "strict vtable gaps do not reconstruct conservation",
376            ));
377        }
378        let mut prior: Option<(String, u64)> = None;
379        let mut words = 0_u64;
380        for record in &self.records {
381            let (symbol, va, record_words) = match record {
382                StrictVtableRecord::Group { record } => {
383                    self.validate_group(record)?;
384                    (
385                        &record.symbol,
386                        record.va,
387                        record.byte_length / u64::from(record.pointer_width),
388                    )
389                }
390                StrictVtableRecord::Vtt { record } => {
391                    self.validate_vtt(record)?;
392                    (
393                        &record.symbol,
394                        record.va,
395                        u64::try_from(record.entries.len()).unwrap_or(u64::MAX),
396                    )
397                }
398                StrictVtableRecord::External {
399                    symbol_kind,
400                    symbol,
401                    ..
402                } => {
403                    if decoder::classify_symbol(symbol) != Some(*symbol_kind) {
404                        return Err(Error::format("strict external vtable symbol is invalid"));
405                    }
406                    (symbol, 0, 0)
407                }
408            };
409            let key = (symbol.clone(), va);
410            if prior.as_ref().is_some_and(|value| value >= &key) {
411                return Err(Error::format(
412                    "strict vtable records are not canonical and unique",
413                ));
414            }
415            prior = Some(key);
416            words = words
417                .checked_add(record_words)
418                .ok_or_else(|| Error::format("strict vtable word count overflows"))?;
419        }
420        if words > limits.max_words {
421            return Err(Error::format("strict vtable word limit exceeded"));
422        }
423        match self.outcome {
424            StrictRttiOutcome::Absent
425                if self.conservation.attempted == 0 && self.gaps.is_empty() => {}
426            StrictRttiOutcome::Complete
427                if self.conservation.attempted > 0
428                    && self.gaps.is_empty()
429                    && self.conservation.included == self.conservation.attempted => {}
430            StrictRttiOutcome::Rejected if !self.gaps.is_empty() => {}
431            _ => {
432                return Err(Error::format(
433                    "strict vtable outcome and conservation disagree",
434                ));
435            }
436        }
437        Ok(())
438    }
439
440    fn validate_group(&self, record: &ItaniumVtableGroupRecord) -> Result<()> {
441        if !matches!(
442            record.kind,
443            ItaniumVtableSymbolKind::CompleteGroup | ItaniumVtableSymbolKind::ConstructionGroup
444        ) || decoder::classify_symbol(&record.symbol) != Some(record.kind)
445            || !matches!(record.pointer_width, 4 | 8)
446            || record.byte_length == 0
447            || record.byte_length % u64::from(record.pointer_width) != 0
448            || record.address_points.is_empty()
449        {
450            return Err(Error::format("strict vtable group shape is invalid"));
451        }
452        let mut used = std::collections::BTreeSet::new();
453        for (index, point) in record.address_points.iter().enumerate() {
454            if point.ordinal != index as u64
455                || point.va
456                    != record.va + (point.offset_to_top_word + 2) * u64::from(record.pointer_width)
457            {
458                return Err(Error::format(
459                    "strict vtable address point is not canonical",
460                ));
461            }
462            let source_matches = match point.source {
463                ItaniumVtableAddressPointSource::TypeinfoSymbol => !matches!(
464                    point.typeinfo.target,
465                    crate::strict_rtti::StrictPointerTarget::Null
466                ),
467                ItaniumVtableAddressPointSource::NullTypeinfoAtSymbolStart => {
468                    index == 0
469                        && point.offset_to_top_word == 0
470                        && matches!(
471                            point.typeinfo.target,
472                            crate::strict_rtti::StrictPointerTarget::Null
473                        )
474                }
475            };
476            if !source_matches {
477                return Err(Error::format(
478                    "strict vtable address-point source disagrees with its header",
479                ));
480            }
481            self.validate_integer(
482                &record.symbol,
483                point.offset_to_top_observation_ordinal,
484                record.pointer_width,
485                &mut used,
486            )?;
487            self.validate_pointer(&record.symbol, &point.typeinfo, &mut used)?;
488            for offset in &point.prefix_offsets {
489                self.validate_integer(
490                    &record.symbol,
491                    offset.observation_ordinal,
492                    record.pointer_width,
493                    &mut used,
494                )?;
495                if offset.signed_value != sign_extend(offset.raw_value, record.pointer_width) {
496                    return Err(Error::format(
497                        "strict vtable offset sign extension disagrees",
498                    ));
499                }
500            }
501            for (slot_index, slot) in point.slots.iter().enumerate() {
502                if slot.ordinal != slot_index as u64 {
503                    return Err(Error::format("strict vtable slots are not canonical"));
504                }
505                self.validate_pointer(&record.symbol, &slot.pointer, &mut used)?;
506                decoder::validate_slot_semantics(slot)?;
507            }
508        }
509        for word in &record.ambiguous_words {
510            self.validate_integer(
511                &record.symbol,
512                word.observation_ordinal,
513                record.pointer_width,
514                &mut used,
515            )?;
516        }
517        let expected = record.byte_length / u64::from(record.pointer_width);
518        if u64::try_from(used.len()).ok() != Some(expected) {
519            return Err(Error::format(
520                "strict vtable group does not conserve every word exactly once",
521            ));
522        }
523        Ok(())
524    }
525
526    fn validate_vtt(&self, record: &ItaniumVttRecord) -> Result<()> {
527        if decoder::classify_symbol(&record.symbol) != Some(ItaniumVtableSymbolKind::Vtt)
528            || record.byte_length == 0
529            || record.entries.is_empty()
530        {
531            return Err(Error::format("strict VTT shape is invalid"));
532        }
533        let mut used = std::collections::BTreeSet::new();
534        for (index, entry) in record.entries.iter().enumerate() {
535            if entry.ordinal != index as u64 {
536                return Err(Error::format("strict VTT entries are not canonical"));
537            }
538            self.validate_pointer(&record.symbol, &entry.address_point, &mut used)?;
539        }
540        if record.byte_length
541            != record.entries.len() as u64 * u64::from(record.entries[0].address_point.width)
542        {
543            return Err(Error::format("strict VTT extent differs from its entries"));
544        }
545        Ok(())
546    }
547
548    fn validate_pointer(
549        &self,
550        symbol: &str,
551        pointer: &StrictPointerObservation,
552        used: &mut std::collections::BTreeSet<u64>,
553    ) -> Result<()> {
554        let observation = self
555            .observations
556            .get(pointer.observation_ordinal as usize)
557            .ok_or_else(|| Error::format("strict vtable pointer observation is absent"))?;
558        if observation.symbol != symbol
559            || observation.kind != StrictRttiObservationKind::Pointer
560            || observation.length != u64::from(pointer.width)
561            || !used.insert(pointer.observation_ordinal)
562        {
563            return Err(Error::format(
564                "strict vtable pointer observation disagrees or aliases",
565            ));
566        }
567        Ok(())
568    }
569
570    fn validate_integer(
571        &self,
572        symbol: &str,
573        ordinal: u64,
574        width: u8,
575        used: &mut std::collections::BTreeSet<u64>,
576    ) -> Result<()> {
577        let observation = self
578            .observations
579            .get(ordinal as usize)
580            .ok_or_else(|| Error::format("strict vtable integer observation is absent"))?;
581        if observation.symbol != symbol
582            || observation.kind != StrictRttiObservationKind::Integer
583            || observation.length != u64::from(width)
584            || !used.insert(ordinal)
585        {
586            return Err(Error::format(
587                "strict vtable integer observation disagrees or aliases",
588            ));
589        }
590        Ok(())
591    }
592}
593
594pub(crate) fn sign_extend(value: u64, width: u8) -> i64 {
595    if width == 4 {
596        i64::from(value as u32 as i32)
597    } else {
598        value as i64
599    }
600}
601
602#[derive(Deserialize)]
603#[serde(deny_unknown_fields)]
604struct StrictVtableBatchWire {
605    outcome: StrictRttiOutcome,
606    records: Vec<StrictVtableRecord>,
607    observations: Vec<StrictRttiObservation>,
608    gaps: Vec<StrictRttiGap>,
609    conservation: StrictRttiConservation,
610}
611
612impl<'de> Deserialize<'de> for StrictVtableBatch {
613    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
614    where
615        D: Deserializer<'de>,
616    {
617        let wire = StrictVtableBatchWire::deserialize(deserializer)?;
618        let value = Self {
619            outcome: wire.outcome,
620            records: wire.records,
621            observations: wire.observations,
622            gaps: wire.gaps,
623            conservation: wire.conservation,
624        };
625        value
626            .validate(StrictVtableLimits::default())
627            .map_err(serde::de::Error::custom)?;
628        Ok(value)
629    }
630}