Skip to main content

macho_cpp/
strict_rtti.rs

1//! Strict, bounded Itanium C++ RTTI leaf records.
2
3use serde::{Deserialize, Deserializer, Serialize};
4
5use crate::{Error, Result};
6
7#[path = "strict_rtti_decoder.rs"]
8pub(crate) mod decoder;
9pub use decoder::{decode_strict_rtti, decode_strict_rtti_from_source};
10
11const HARD_RECORDS: u64 = 4_000_000;
12const HARD_SYMBOLS: u64 = 16_000_000;
13const HARD_BASES: u64 = 8_000_000;
14const HARD_NAME_BYTES: u64 = 65_536;
15const HARD_EVIDENCE_BYTES: u64 = 1 << 31;
16const HARD_INPUT_BYTES: u64 = 1 << 34;
17
18/// Structural limits for one strict Itanium RTTI decode.
19#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
20#[serde(deny_unknown_fields)]
21pub struct StrictRttiLimits {
22    /// Maximum selected thin-image byte length.
23    pub max_input_bytes: u64,
24    /// Maximum symbols admitted before RTTI candidate discovery.
25    pub max_symbols: u64,
26    /// Maximum defined and external `_ZTI` records admitted from the symbol table.
27    pub max_records: u64,
28    /// Maximum direct bases admitted across all VMI records.
29    pub max_bases: u64,
30    /// Maximum bytes, excluding the terminator, in one type-name object.
31    pub max_name_bytes: u64,
32    /// Maximum total bytes covered by retained observations.
33    pub max_evidence_bytes: u64,
34}
35
36impl Default for StrictRttiLimits {
37    fn default() -> Self {
38        Self {
39            max_input_bytes: 1 << 30,
40            max_symbols: 4_000_000,
41            max_records: 1_000_000,
42            max_bases: 2_000_000,
43            max_name_bytes: 4_096,
44            max_evidence_bytes: 1 << 29,
45        }
46    }
47}
48
49impl StrictRttiLimits {
50    /// Reject zero limits and values above the implementation hard maxima.
51    pub fn validate(self) -> Result<()> {
52        for (value, hard, name) in [
53            (self.max_input_bytes, HARD_INPUT_BYTES, "RTTI input-byte"),
54            (self.max_symbols, HARD_SYMBOLS, "RTTI symbol"),
55            (self.max_records, HARD_RECORDS, "RTTI record"),
56            (self.max_bases, HARD_BASES, "RTTI base"),
57            (self.max_name_bytes, HARD_NAME_BYTES, "RTTI name-byte"),
58            (
59                self.max_evidence_bytes,
60                HARD_EVIDENCE_BYTES,
61                "RTTI evidence-byte",
62            ),
63        ] {
64            if value == 0 || value > hard {
65                return Err(Error::format(format!(
66                    "{name} limit is zero or exceeds its hard maximum"
67                )));
68            }
69        }
70        Ok(())
71    }
72}
73
74/// Terminal state of a strict RTTI batch.
75#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
76#[serde(rename_all = "snake_case")]
77pub enum StrictRttiOutcome {
78    /// No defined or external Itanium typeinfo symbols exist.
79    Absent,
80    /// Every candidate was decoded and conserved.
81    Complete,
82    /// At least one candidate or required field failed strict decoding.
83    Rejected,
84}
85
86/// Exact conservation ledger for symbol-table RTTI candidates.
87#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
88#[serde(deny_unknown_fields)]
89pub struct StrictRttiConservation {
90    /// Total `_ZTI` candidates attempted.
91    pub attempted: u64,
92    /// Candidates represented by complete records.
93    pub included: u64,
94    /// Candidates retained as typed gaps.
95    pub unknown: u64,
96    /// Candidates excluded by a structural budget before decoding.
97    pub excluded: u64,
98}
99
100impl StrictRttiConservation {
101    pub(crate) fn validate(self) -> Result<()> {
102        let balanced = self
103            .included
104            .checked_add(self.unknown)
105            .and_then(|value| value.checked_add(self.excluded));
106        if balanced != Some(self.attempted) {
107            return Err(Error::format("strict RTTI conservation does not balance"));
108        }
109        Ok(())
110    }
111}
112
113/// Closed strict-decoder gap registry.
114#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
115#[serde(rename_all = "snake_case")]
116pub enum StrictRttiGapCode {
117    /// A structural count or evidence budget was exceeded.
118    StructuralLimitExceeded,
119    /// A required pointer could not be decoded or resolved.
120    PointerUnresolved,
121    /// A required record field was malformed or out of bounds.
122    RecordMalformed,
123    /// The runtime typeinfo implementation family is not admitted.
124    FamilyUnsupported,
125    /// A type-name object was unterminated, oversized, or not UTF-8.
126    TypeNameInvalid,
127}
128
129/// One failed candidate or batch-wide structural rejection.
130#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
131#[serde(deny_unknown_fields)]
132pub struct StrictRttiGap {
133    /// Candidate symbol, or `None` for a batch-wide limit failure.
134    pub symbol: Option<String>,
135    /// Exact field or registry subject that failed.
136    pub field: String,
137    /// Stable machine-readable failure code.
138    pub code: StrictRttiGapCode,
139    /// Source error code when one exists.
140    pub source_code: Option<String>,
141}
142
143/// Kind of byte range retained as strict evidence.
144#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
145#[serde(rename_all = "snake_case")]
146pub enum StrictRttiObservationKind {
147    /// Pointer-sized field.
148    Pointer,
149    /// Fixed-width integer field.
150    Integer,
151    /// NUL-terminated type-name bytes including the terminator.
152    TypeName,
153}
154
155/// One exact file-backed observation.
156#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
157#[serde(deny_unknown_fields)]
158pub struct StrictRttiObservation {
159    /// Canonical zero-based ordinal in this batch.
160    pub ordinal: u64,
161    /// Candidate symbol that owns the field.
162    pub symbol: String,
163    /// ABI field name.
164    pub field: String,
165    /// Virtual address of the observed bytes.
166    pub va: u64,
167    /// Thin-image file offset of the observed bytes.
168    pub file_offset: u64,
169    /// Exact byte length.
170    pub length: u64,
171    /// Observation interpretation.
172    pub kind: StrictRttiObservationKind,
173}
174
175/// Pointer encoding or fixup mechanism used by one field.
176#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
177#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
178pub enum StrictPointerEncoding {
179    /// Ordinary pointer bytes with no covering fixup record.
180    Direct,
181    /// Chained rebase.
182    ChainedRebase,
183    /// Chained import bind.
184    ChainedBind {
185        /// Import addend retained from the chained-import table.
186        addend: i64,
187        /// Whether the import is weak.
188        weak: bool,
189    },
190    /// Legacy dyld rebase opcode.
191    LegacyRebase,
192    /// Legacy dyld bind opcode.
193    LegacyBind {
194        /// Bind addend.
195        addend: i64,
196        /// Whether the bind is weak.
197        weak: bool,
198        /// Whether the bind is lazy.
199        lazy: bool,
200    },
201}
202
203/// Pointer-authentication state retained independently from target resolution.
204#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
205#[serde(tag = "state", rename_all = "snake_case", deny_unknown_fields)]
206pub enum StrictPointerAuthentication {
207    /// The pointer format is not authenticated.
208    NotApplicable,
209    /// The chained fixup carries authenticated-pointer metadata.
210    Authenticated {
211        /// ABI key selector.
212        key: u8,
213        /// Diversity discriminator.
214        diversity: u16,
215        /// Whether the storage address participates in diversification.
216        address_diversity: bool,
217    },
218}
219
220/// Resolved target of one pointer-valued RTTI field.
221#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
222#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
223pub enum StrictPointerTarget {
224    /// Null pointer.
225    Null,
226    /// Address within the selected image.
227    Local {
228        /// Unslid virtual address.
229        va: u64,
230    },
231    /// Imported symbol outside the selected image.
232    External {
233        /// Exact symbol spelling.
234        symbol: String,
235        /// Mach-O library ordinal.
236        library_ordinal: i32,
237    },
238}
239
240/// Full pointer observation including raw storage and resolution provenance.
241#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
242#[serde(deny_unknown_fields)]
243pub struct StrictPointerObservation {
244    /// Ordinal of the corresponding byte observation.
245    pub observation_ordinal: u64,
246    /// Raw pointer-sized value as stored in the file.
247    pub raw_value: u64,
248    /// Pointer width in bytes.
249    pub width: u8,
250    /// Encoding or fixup mechanism.
251    pub encoding: StrictPointerEncoding,
252    /// Pointer-authentication state.
253    pub authentication: StrictPointerAuthentication,
254    /// Resolved local, external, or null target.
255    pub target: StrictPointerTarget,
256}
257
258/// Complete admitted Itanium `type_info` implementation family.
259#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
260#[serde(rename_all = "snake_case")]
261pub enum ItaniumTypeInfoFamily {
262    /// `__fundamental_type_info`.
263    Fundamental,
264    /// `__array_type_info`.
265    Array,
266    /// `__function_type_info`.
267    Function,
268    /// `__enum_type_info`.
269    Enum,
270    /// `__class_type_info`.
271    Class,
272    /// `__si_class_type_info`.
273    SingleInheritanceClass,
274    /// `__vmi_class_type_info`.
275    VirtualMultipleInheritanceClass,
276    /// `__pointer_type_info`.
277    Pointer,
278    /// `__pointer_to_member_type_info`.
279    PointerToMember,
280    /// Qualified pbase variant retained from its flags.
281    Qualified,
282}
283
284/// One direct base entry in ABI declaration order.
285#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
286#[serde(deny_unknown_fields)]
287pub struct ItaniumBaseRecord {
288    /// Zero-based direct-base ordinal.
289    pub ordinal: u64,
290    /// Exact base typeinfo pointer.
291    pub typeinfo: StrictPointerObservation,
292    /// Raw `offset_flags` word.
293    pub offset_flags: u64,
294    /// Signed non-virtual offset or signed vbase-slot displacement.
295    pub signed_offset: i64,
296    /// Virtual-base flag.
297    pub is_virtual: bool,
298    /// Public-base flag.
299    pub is_public: bool,
300}
301
302/// Pointee and optional member-owner links for pbase families.
303#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
304#[serde(deny_unknown_fields)]
305pub struct ItaniumPointeeRecord {
306    /// Raw ABI qualifier/incomplete flags.
307    pub flags: u32,
308    /// Pointee typeinfo pointer.
309    pub pointee: StrictPointerObservation,
310    /// Containing-class typeinfo for pointer-to-member records.
311    pub member_of: Option<StrictPointerObservation>,
312}
313
314/// Strict file-backed typeinfo object.
315#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
316#[serde(deny_unknown_fields)]
317pub struct ItaniumTypeInfoRecord {
318    /// Exact `_ZTI` symbol spelling.
319    pub symbol: String,
320    /// Typeinfo object virtual address.
321    pub va: u64,
322    /// Typeinfo object thin-image file offset.
323    pub file_offset: u64,
324    /// Runtime implementation family selected from the vptr target.
325    pub family: ItaniumTypeInfoFamily,
326    /// Exact encoded type-name bytes interpreted as UTF-8.
327    pub type_name: String,
328    /// Darwin's high-bit type-name tag marks this RTTI identity non-unique.
329    pub type_name_non_unique: bool,
330    /// Runtime-family vptr observation.
331    pub runtime_vtable: StrictPointerObservation,
332    /// Type-name pointer observation.
333    pub type_name_pointer: StrictPointerObservation,
334    /// VMI class flags, zero for non-VMI records.
335    pub class_flags: u32,
336    /// Direct bases in ABI order.
337    pub bases: Vec<ItaniumBaseRecord>,
338    /// Pointee details for pointer families.
339    pub pointee: Option<ItaniumPointeeRecord>,
340    /// Whether the defining symbol is weak.
341    pub weak_definition: bool,
342}
343
344/// One included strict RTTI record.
345#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
346#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
347pub enum StrictRttiRecord {
348    /// Complete file-backed typeinfo object.
349    TypeInfo {
350        /// Decoded object.
351        record: Box<ItaniumTypeInfoRecord>,
352    },
353    /// Undefined external `_ZTI` symbol retained rather than dropped.
354    ExternalTypeInfo {
355        /// Exact symbol spelling.
356        symbol: String,
357        /// Library ordinal from `n_desc`.
358        library_ordinal: i32,
359        /// Weak-reference bit.
360        weak: bool,
361    },
362}
363
364/// Closed strict RTTI decode batch.
365#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
366#[serde(deny_unknown_fields)]
367pub struct StrictRttiBatch {
368    /// Terminal batch outcome.
369    pub outcome: StrictRttiOutcome,
370    /// Included records in canonical symbol/address order.
371    pub records: Vec<StrictRttiRecord>,
372    /// Exact byte-range evidence in decode order.
373    pub observations: Vec<StrictRttiObservation>,
374    /// Typed failures; nonempty exactly when `outcome` is rejected.
375    pub gaps: Vec<StrictRttiGap>,
376    /// Candidate conservation ledger.
377    pub conservation: StrictRttiConservation,
378}
379
380impl StrictRttiBatch {
381    fn validate(&self, limits: StrictRttiLimits) -> Result<()> {
382        limits.validate()?;
383        self.conservation.validate()?;
384        if self.observations.iter().enumerate().any(|(index, value)| {
385            value.ordinal != u64::try_from(index).unwrap_or(u64::MAX) || value.length == 0
386        }) {
387            return Err(Error::format(
388                "strict RTTI observations are not canonical and nonempty",
389            ));
390        }
391        let evidence_bytes = self.observations.iter().try_fold(0_u64, |total, value| {
392            total
393                .checked_add(value.length)
394                .ok_or_else(|| Error::format("strict RTTI evidence bytes overflow"))
395        })?;
396        if evidence_bytes > limits.max_evidence_bytes {
397            return Err(Error::format("strict RTTI evidence exceeds its limit"));
398        }
399        if u64::try_from(self.records.len()).ok() != Some(self.conservation.included) {
400            return Err(Error::format(
401                "strict RTTI included count differs from its record count",
402            ));
403        }
404        let preflight_gap = u64::from(
405            self.outcome == StrictRttiOutcome::Rejected && self.conservation.attempted == 0,
406        );
407        let expected_gaps = self
408            .conservation
409            .unknown
410            .checked_add(u64::from(self.conservation.excluded > 0))
411            .and_then(|value| value.checked_add(preflight_gap))
412            .ok_or_else(|| Error::format("strict RTTI expected gap count overflows"))?;
413        if u64::try_from(self.gaps.len()).ok() != Some(expected_gaps)
414            || self
415                .gaps
416                .iter()
417                .any(|gap| gap.field.is_empty() || gap.field.chars().any(char::is_control))
418        {
419            return Err(Error::format(
420                "strict RTTI gaps do not reconstruct unknown and excluded candidates",
421            ));
422        }
423        let mut prior_key: Option<(String, u64)> = None;
424        let mut base_count = 0_u64;
425        for record in &self.records {
426            let key = match record {
427                StrictRttiRecord::TypeInfo { record } => {
428                    self.validate_typeinfo(record, limits, &mut base_count)?;
429                    (record.symbol.clone(), record.va)
430                }
431                StrictRttiRecord::ExternalTypeInfo { symbol, .. } => {
432                    if symbol.is_empty() || !decoder::is_typeinfo_symbol(symbol) {
433                        return Err(Error::format(
434                            "strict external RTTI record has an invalid symbol",
435                        ));
436                    }
437                    (symbol.clone(), 0)
438                }
439            };
440            if prior_key.as_ref().is_some_and(|prior| prior >= &key) {
441                return Err(Error::format(
442                    "strict RTTI records are not canonical and unique",
443                ));
444            }
445            prior_key = Some(key);
446        }
447        match self.outcome {
448            StrictRttiOutcome::Absent
449                if self.conservation.attempted == 0
450                    && self.records.is_empty()
451                    && self.gaps.is_empty() => {}
452            StrictRttiOutcome::Complete
453                if self.conservation.attempted > 0
454                    && self.conservation.included == self.conservation.attempted
455                    && self.gaps.is_empty() => {}
456            StrictRttiOutcome::Rejected if !self.gaps.is_empty() => {}
457            _ => {
458                return Err(Error::format(
459                    "strict RTTI outcome, gaps, and conservation disagree",
460                ));
461            }
462        }
463        Ok(())
464    }
465
466    fn validate_typeinfo(
467        &self,
468        record: &ItaniumTypeInfoRecord,
469        limits: StrictRttiLimits,
470        base_count: &mut u64,
471    ) -> Result<()> {
472        if record.symbol.is_empty()
473            || !decoder::is_typeinfo_symbol(&record.symbol)
474            || record.type_name.is_empty()
475            || u64::try_from(record.type_name.len())
476                .ok()
477                .is_none_or(|length| length > limits.max_name_bytes)
478        {
479            return Err(Error::format(
480                "strict RTTI record identity or name is invalid",
481            ));
482        }
483        self.validate_pointer(&record.symbol, &record.runtime_vtable)?;
484        self.validate_pointer(&record.symbol, &record.type_name_pointer)?;
485        *base_count = base_count
486            .checked_add(
487                u64::try_from(record.bases.len())
488                    .map_err(|_| Error::format("strict RTTI base count exceeds UInt64"))?,
489            )
490            .ok_or_else(|| Error::format("strict RTTI base count overflows"))?;
491        if *base_count > limits.max_bases
492            || record
493                .bases
494                .iter()
495                .enumerate()
496                .any(|(index, base)| base.ordinal != index as u64)
497        {
498            return Err(Error::format(
499                "strict RTTI bases exceed limits or are not canonical",
500            ));
501        }
502        for base in &record.bases {
503            self.validate_pointer(&record.symbol, &base.typeinfo)?;
504            if base.signed_offset != (base.offset_flags as i64) >> 8
505                || base.is_virtual != (base.offset_flags & 1 != 0)
506                || base.is_public != (base.offset_flags & 2 != 0)
507            {
508                return Err(Error::format(
509                    "strict RTTI base semantics disagree with offset_flags",
510                ));
511            }
512        }
513        if let Some(pointee) = &record.pointee {
514            self.validate_pointer(&record.symbol, &pointee.pointee)?;
515            if let Some(member_of) = &pointee.member_of {
516                self.validate_pointer(&record.symbol, member_of)?;
517            }
518        }
519        let shape_matches = match record.family {
520            ItaniumTypeInfoFamily::SingleInheritanceClass => {
521                record.bases.len() == 1 && record.pointee.is_none() && record.class_flags == 0
522            }
523            ItaniumTypeInfoFamily::VirtualMultipleInheritanceClass => record.pointee.is_none(),
524            ItaniumTypeInfoFamily::Pointer => {
525                record
526                    .pointee
527                    .as_ref()
528                    .is_some_and(|pointee| pointee.member_of.is_none())
529                    && record.bases.is_empty()
530                    && record.class_flags == 0
531            }
532            ItaniumTypeInfoFamily::PointerToMember => {
533                record
534                    .pointee
535                    .as_ref()
536                    .is_some_and(|pointee| pointee.member_of.is_some())
537                    && record.bases.is_empty()
538                    && record.class_flags == 0
539            }
540            _ => record.bases.is_empty() && record.pointee.is_none() && record.class_flags == 0,
541        };
542        if !shape_matches {
543            return Err(Error::format(
544                "strict RTTI family disagrees with its family-specific fields",
545            ));
546        }
547        Ok(())
548    }
549
550    fn validate_pointer(&self, symbol: &str, pointer: &StrictPointerObservation) -> Result<()> {
551        let observation = usize::try_from(pointer.observation_ordinal)
552            .ok()
553            .and_then(|index| self.observations.get(index))
554            .ok_or_else(|| Error::format("strict RTTI pointer observation is absent"))?;
555        if observation.symbol != symbol
556            || observation.kind != StrictRttiObservationKind::Pointer
557            || observation.length != u64::from(pointer.width)
558            || !matches!(pointer.width, 4 | 8)
559        {
560            return Err(Error::format(
561                "strict RTTI pointer disagrees with its byte observation",
562            ));
563        }
564        Ok(())
565    }
566}
567
568#[derive(Deserialize)]
569#[serde(deny_unknown_fields)]
570struct StrictRttiBatchWire {
571    outcome: StrictRttiOutcome,
572    records: Vec<StrictRttiRecord>,
573    observations: Vec<StrictRttiObservation>,
574    gaps: Vec<StrictRttiGap>,
575    conservation: StrictRttiConservation,
576}
577
578impl<'de> Deserialize<'de> for StrictRttiBatch {
579    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
580    where
581        D: Deserializer<'de>,
582    {
583        let wire = StrictRttiBatchWire::deserialize(deserializer)?;
584        let value = Self {
585            outcome: wire.outcome,
586            records: wire.records,
587            observations: wire.observations,
588            gaps: wire.gaps,
589            conservation: wire.conservation,
590        };
591        value
592            .validate(StrictRttiLimits::default())
593            .map_err(serde::de::Error::custom)?;
594        Ok(value)
595    }
596}