macho-cpp 0.5.6

C++ RTTI, vtable, and ABI inference for Mach-O images
Documentation
#![cfg(feature = "itanium-rtti")]

use macho_cpp::{
    ItaniumTypeInfoFamily, StrictPointerTarget, StrictRttiGapCode, StrictRttiLimits,
    StrictRttiOutcome, StrictRttiRecord, decode_strict_rtti_from_source,
};
use macho_test_support::SymbolFixture;

const IMAGE: u64 = 0x1_0000_0000;
const DATA_OFFSET: usize = 0x100;
const SYMBOL_OFFSET: usize = 0x140;

fn write_u64(bytes: &mut [u8], offset: usize, value: u64) {
    bytes[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
}

fn set_symbol_value(bytes: &mut [u8], index: usize, value: u64) {
    write_u64(bytes, SYMBOL_OFFSET + index * 16 + 8, value);
}

fn single_inheritance_fixture() -> Vec<u8> {
    let mut bytes = macho_test_support::thin64_x86_64_with_data_symbols(&[
        SymbolFixture {
            name: "__ZTI7Derived",
            external: true,
            defined: true,
        },
        SymbolFixture {
            name: "__ZTI4Base",
            external: true,
            defined: true,
        },
        SymbolFixture {
            name: "__ZTVN10__cxxabiv120__si_class_type_infoE",
            external: false,
            defined: true,
        },
        SymbolFixture {
            name: "__ZTVN10__cxxabiv117__class_type_infoE",
            external: false,
            defined: true,
        },
    ]);
    let derived = IMAGE + 0x100;
    let base = IMAGE + 0x118;
    let derived_name = IMAGE + 0x128;
    let base_name = IMAGE + 0x131;
    let si_vtable = IMAGE + 0x138;
    let class_vtable = IMAGE + 0x13c;

    write_u64(&mut bytes, DATA_OFFSET, si_vtable);
    write_u64(&mut bytes, DATA_OFFSET + 8, derived_name);
    write_u64(&mut bytes, DATA_OFFSET + 16, base);
    write_u64(&mut bytes, DATA_OFFSET + 24, class_vtable);
    write_u64(&mut bytes, DATA_OFFSET + 32, base_name);
    bytes[DATA_OFFSET + 40..DATA_OFFSET + 49].copy_from_slice(b"7Derived\0");
    bytes[DATA_OFFSET + 49..DATA_OFFSET + 55].copy_from_slice(b"4Base\0");

    set_symbol_value(&mut bytes, 0, derived);
    set_symbol_value(&mut bytes, 1, base);
    set_symbol_value(&mut bytes, 2, si_vtable);
    set_symbol_value(&mut bytes, 3, class_vtable);
    bytes
}

fn vmi_fixture() -> Vec<u8> {
    let mut bytes = macho_test_support::thin64_x86_64_with_data_symbols(&[
        SymbolFixture {
            name: "__ZTI7Diamond",
            external: true,
            defined: true,
        },
        SymbolFixture {
            name: "__ZTVN10__cxxabiv121__vmi_class_type_infoE",
            external: false,
            defined: true,
        },
    ]);
    let record = IMAGE + 0x100;
    let base = IMAGE + 0x138;
    let name = IMAGE + 0x130;
    let runtime_vtable = IMAGE + 0x13c;
    write_u64(&mut bytes, DATA_OFFSET, runtime_vtable);
    write_u64(&mut bytes, DATA_OFFSET + 8, name);
    bytes[DATA_OFFSET + 16..DATA_OFFSET + 20].copy_from_slice(&2u32.to_le_bytes());
    bytes[DATA_OFFSET + 20..DATA_OFFSET + 24].copy_from_slice(&1u32.to_le_bytes());
    write_u64(&mut bytes, DATA_OFFSET + 24, base);
    let offset_flags = ((-24_i64 << 8) as u64) | 3;
    write_u64(&mut bytes, DATA_OFFSET + 32, offset_flags);
    bytes[DATA_OFFSET + 48..DATA_OFFSET + 57].copy_from_slice(b"7Diamond\0");
    set_symbol_value(&mut bytes, 0, record);
    set_symbol_value(&mut bytes, 1, runtime_vtable);
    bytes
}

fn pointer_to_member_fixture() -> Vec<u8> {
    let mut bytes = macho_test_support::thin64_x86_64_with_data_symbols(&[
        SymbolFixture {
            name: "__ZTIM7DerivedFvvE",
            external: true,
            defined: true,
        },
        SymbolFixture {
            name: "__ZTVN10__cxxabiv129__pointer_to_member_type_infoE",
            external: false,
            defined: true,
        },
        SymbolFixture {
            name: "__ZTSFvvE",
            external: false,
            defined: true,
        },
        SymbolFixture {
            name: "__ZTS7Derived",
            external: false,
            defined: true,
        },
    ]);
    let record = IMAGE + 0x100;
    let name = IMAGE + 0x128;
    let pointee = IMAGE + 0x134;
    let member_of = IMAGE + 0x138;
    let runtime_vtable = IMAGE + 0x13c;
    write_u64(&mut bytes, DATA_OFFSET, runtime_vtable);
    write_u64(&mut bytes, DATA_OFFSET + 8, name);
    bytes[DATA_OFFSET + 16..DATA_OFFSET + 20].copy_from_slice(&3u32.to_le_bytes());
    write_u64(&mut bytes, DATA_OFFSET + 24, pointee);
    write_u64(&mut bytes, DATA_OFFSET + 32, member_of);
    bytes[DATA_OFFSET + 40..DATA_OFFSET + 54].copy_from_slice(b"M7DerivedFvvE\0");
    set_symbol_value(&mut bytes, 0, record);
    set_symbol_value(&mut bytes, 1, runtime_vtable);
    set_symbol_value(&mut bytes, 2, pointee);
    set_symbol_value(&mut bytes, 3, member_of);
    bytes
}

#[test]
fn strict_itanium_leaf_conserves_exact_single_inheritance_records() {
    let bytes = single_inheritance_fixture();
    let batch = decode_strict_rtti_from_source(&bytes, StrictRttiLimits::default())
        .expect("strict RTTI fixture decodes");
    assert_eq!(batch.outcome, StrictRttiOutcome::Complete);
    assert_eq!(batch.conservation.attempted, 2);
    assert_eq!(batch.conservation.included, 2);
    assert_eq!(batch.conservation.unknown, 0);
    assert!(batch.gaps.is_empty());
    assert!(
        batch
            .observations
            .iter()
            .enumerate()
            .all(|(index, value)| value.ordinal == index as u64)
    );

    let derived = batch
        .records
        .iter()
        .find_map(|record| match record {
            StrictRttiRecord::TypeInfo { record } if record.symbol == "__ZTI7Derived" => {
                Some(record)
            }
            _ => None,
        })
        .expect("derived record");
    assert_eq!(
        derived.family,
        ItaniumTypeInfoFamily::SingleInheritanceClass
    );
    assert_eq!(derived.type_name, "7Derived");
    assert_eq!(derived.bases.len(), 1);
    assert_eq!(derived.bases[0].ordinal, 0);
    assert!(derived.bases[0].is_public);
    assert!(!derived.bases[0].is_virtual);
    assert_eq!(
        derived.bases[0].typeinfo.target,
        StrictPointerTarget::Local { va: IMAGE + 0x118 }
    );

    let json = serde_json::to_value(&batch).expect("strict batch JSON");
    let mut hostile = json;
    hostile["future"] = serde_json::json!(true);
    assert!(serde_json::from_value::<macho_cpp::StrictRttiBatch>(hostile).is_err());

    let mut forged = serde_json::to_value(&batch).expect("strict batch JSON");
    forged["records"][1]["record"]["runtime_vtable"]["observation_ordinal"] =
        serde_json::json!(999);
    assert!(serde_json::from_value::<macho_cpp::StrictRttiBatch>(forged).is_err());
}

#[test]
fn strict_itanium_leaf_rejects_limits_without_usable_truncation() {
    let bytes = single_inheritance_fixture();
    let input = decode_strict_rtti_from_source(
        &bytes,
        StrictRttiLimits {
            max_input_bytes: 1,
            ..StrictRttiLimits::default()
        },
    )
    .expect("input limit rejection is a typed batch");
    assert_eq!(input.outcome, StrictRttiOutcome::Rejected);
    assert_eq!(input.conservation.attempted, 0);
    assert_eq!(input.gaps[0].field, "input_bytes");

    let batch = decode_strict_rtti_from_source(
        &bytes,
        StrictRttiLimits {
            max_records: 1,
            ..StrictRttiLimits::default()
        },
    )
    .expect("limit rejection is a typed batch");
    assert_eq!(batch.outcome, StrictRttiOutcome::Rejected);
    assert!(batch.records.is_empty());
    assert!(batch.observations.is_empty());
    assert_eq!(batch.conservation.attempted, 2);
    assert_eq!(batch.conservation.excluded, 2);
    assert_eq!(
        batch.gaps[0].code,
        StrictRttiGapCode::StructuralLimitExceeded
    );

    let evidence = decode_strict_rtti_from_source(
        &bytes,
        StrictRttiLimits {
            max_evidence_bytes: 8,
            ..StrictRttiLimits::default()
        },
    )
    .expect("evidence limit rejection is a typed batch");
    assert_eq!(evidence.outcome, StrictRttiOutcome::Rejected);
    assert!(
        evidence
            .gaps
            .iter()
            .all(|gap| gap.code == StrictRttiGapCode::StructuralLimitExceeded)
    );
    assert_eq!(
        evidence
            .observations
            .iter()
            .map(|value| value.length)
            .sum::<u64>(),
        8
    );
}

#[test]
fn strict_itanium_leaf_rejects_unmapped_base_and_oversized_names() {
    let mut bad_base = single_inheritance_fixture();
    write_u64(&mut bad_base, DATA_OFFSET + 16, 0xdead_beef);
    let batch = decode_strict_rtti_from_source(&bad_base, StrictRttiLimits::default())
        .expect("malformed base is a typed batch");
    assert_eq!(batch.outcome, StrictRttiOutcome::Rejected);
    assert!(batch.gaps.iter().any(|gap| {
        gap.symbol.as_deref() == Some("__ZTI7Derived")
            && gap.code == StrictRttiGapCode::PointerUnresolved
    }));
    assert_eq!(
        batch.conservation.included + batch.conservation.unknown,
        batch.conservation.attempted
    );

    let names = decode_strict_rtti_from_source(
        &single_inheritance_fixture(),
        StrictRttiLimits {
            max_name_bytes: 1,
            ..StrictRttiLimits::default()
        },
    )
    .expect("oversized names are typed gaps");
    assert_eq!(names.outcome, StrictRttiOutcome::Rejected);
    assert!(
        names
            .gaps
            .iter()
            .all(|gap| gap.code == StrictRttiGapCode::TypeNameInvalid)
    );
}

#[test]
fn strict_itanium_leaf_distinguishes_absent_and_external_typeinfo() {
    let absent = macho_test_support::thin64_x86_64_with_symbols(&[SymbolFixture {
        name: "_main",
        external: true,
        defined: true,
    }]);
    let batch = decode_strict_rtti_from_source(&absent, StrictRttiLimits::default())
        .expect("no RTTI is absence");
    assert_eq!(batch.outcome, StrictRttiOutcome::Absent);

    let external = macho_test_support::thin64_x86_64_with_symbols(&[SymbolFixture {
        name: "__ZTI8External",
        external: true,
        defined: false,
    }]);
    let batch = decode_strict_rtti_from_source(&external, StrictRttiLimits::default())
        .expect("external RTTI is conserved");
    assert_eq!(batch.outcome, StrictRttiOutcome::Complete);
    assert!(matches!(
        &batch.records[0],
        StrictRttiRecord::ExternalTypeInfo { symbol, .. } if symbol == "__ZTI8External"
    ));
}

#[test]
fn strict_itanium_leaf_preserves_vmi_flags_order_and_signed_offsets() {
    let batch = decode_strict_rtti_from_source(&vmi_fixture(), StrictRttiLimits::default())
        .expect("VMI fixture decodes");
    assert_eq!(batch.outcome, StrictRttiOutcome::Complete);
    let record = match &batch.records[0] {
        StrictRttiRecord::TypeInfo { record } => record,
        other => panic!("unexpected VMI record: {other:?}"),
    };
    assert_eq!(
        record.family,
        ItaniumTypeInfoFamily::VirtualMultipleInheritanceClass
    );
    assert_eq!(record.class_flags, 2);
    assert_eq!(record.bases.len(), 1);
    assert_eq!(record.bases[0].ordinal, 0);
    assert_eq!(record.bases[0].signed_offset, -24);
    assert!(record.bases[0].is_virtual);
    assert!(record.bases[0].is_public);
}

#[test]
fn strict_itanium_leaf_preserves_pointer_to_member_qualifiers_and_owner() {
    let batch =
        decode_strict_rtti_from_source(&pointer_to_member_fixture(), StrictRttiLimits::default())
            .expect("pointer-to-member fixture decodes");
    assert_eq!(batch.outcome, StrictRttiOutcome::Complete);
    let record = match &batch.records[0] {
        StrictRttiRecord::TypeInfo { record } => record,
        other => panic!("unexpected pointer-to-member record: {other:?}"),
    };
    assert_eq!(record.family, ItaniumTypeInfoFamily::PointerToMember);
    let pointee = record.pointee.as_ref().expect("pbase details");
    assert_eq!(pointee.flags, 3);
    assert_eq!(
        pointee.pointee.target,
        StrictPointerTarget::Local { va: IMAGE + 0x134 }
    );
    assert_eq!(
        pointee.member_of.as_ref().expect("member owner").target,
        StrictPointerTarget::Local { va: IMAGE + 0x138 }
    );
}

#[test]
fn strict_itanium_leaf_decodes_real_darwin_tagged_and_coalesced_rtti() {
    let mut saw_non_unique_name = false;
    for bytes in [
        include_bytes!("fixtures/arm64-darwin-tagged-rtti.dylib").as_slice(),
        include_bytes!("fixtures/x86_64-darwin-tagged-rtti.dylib").as_slice(),
    ] {
        let batch = decode_strict_rtti_from_source(bytes, StrictRttiLimits::default())
            .expect("real Darwin RTTI decodes");
        assert_eq!(
            batch.outcome,
            StrictRttiOutcome::Complete,
            "{:#?}",
            batch.gaps
        );
        assert!(batch.records.len() >= 8);
        saw_non_unique_name |= batch.records.iter().any(|record| {
            matches!(
                record,
                StrictRttiRecord::TypeInfo { record } if record.type_name_non_unique
            )
        });
        assert!(batch.records.iter().any(|record| matches!(
            record,
            StrictRttiRecord::TypeInfo { record }
                if record.family == ItaniumTypeInfoFamily::PointerToMember
        )));
    }
    assert!(saw_non_unique_name);
}