Objective-C chained offsets past PAGEZERO resolve to the image (#298)

Objective-C pointers whose chained offset lands in file-empty __PAGEZERO now reach the image-base fallback, so the classes are found again.

Fixes #296
This commit is contained in:
Oliver Mee
2026-11-02 15:44:02 +08:00
committed by GitHub
parent 10fb3cfc8d
commit 2a9bfe628e
3 changed files with 41 additions and 4 deletions
+39 -4
View File
@@ -423,20 +423,22 @@ fn decode_ptr(b: &Binary, raw: u64) -> Option<u64> {
if raw == 0 {
return None;
}
if b.is_mapped(raw) {
// __PAGEZERO is mapped but has no file bytes. A small chained target
// offset inside it must still reach the image-base fallback below.
if b.va_to_offset(raw).is_some() {
return Some(raw);
}
if raw >> 63 != 0 {
// authenticated rebase: low 32 bits are an offset from the image base
let t = b.image_base.wrapping_add(raw & 0xffff_ffff);
return b.is_mapped(t).then_some(t);
return b.va_to_offset(t).is_some().then_some(t);
}
let t36 = raw & 0xf_ffff_ffff;
if b.is_mapped(t36) {
if b.va_to_offset(t36).is_some() {
return Some(t36);
}
let t = b.image_base.wrapping_add(t36);
b.is_mapped(t).then_some(t)
b.va_to_offset(t).is_some().then_some(t)
}
fn section_words(data: &[u8], b: &Binary, names: &[&str]) -> Vec<u64> {
@@ -633,6 +635,39 @@ mod tests {
assert!(meta.classes[0].methods.is_empty());
}
#[test]
fn objc_chained_offsets_do_not_resolve_into_pagezero() {
let mut data = tk::macho64();
let b = crate::parse_binary(&data).unwrap();
let list = b.sections.iter().find(|s| s.name == "__objc_classlist").unwrap().addr;
let class = b.read_ptr(&data, list).unwrap();
let raw_ro = b.read_ptr(&data, class + 32).unwrap();
for (slot, target) in [(list, class), (class + 32, raw_ro)] {
let off = b.va_to_offset(slot).unwrap() as usize;
let encoded = (1u64 << 52) | (target - b.image_base);
data[off..off + 8].copy_from_slice(&encoded.to_le_bytes());
}
assert!(b.is_mapped(class - b.image_base));
assert!(b.va_to_offset(class - b.image_base).is_none());
let meta = objc_metadata(&data, &b);
assert_eq!(meta.classes.len(), 1, "{meta:?}");
assert_eq!(meta.classes[0].name, tk::OBJC_CLASS);
assert_eq!(meta.classes[0].methods[0].selector, tk::OBJC_SELECTOR);
assert_eq!(meta.classes[0].methods[0].imp, Some(tk::MACHO_BASE + tk::MACHO_TEXT_OFF + tk::x64::HIDDEN));
let fat = tk::macho_fat(&[data]);
let fat_binary = crate::parse_binary(&fat).unwrap();
let fat_meta = objc_metadata(&fat, &fat_binary);
assert_eq!(fat_meta.classes.len(), 1, "{fat_meta:?}");
assert_eq!(fat_meta.classes[0].methods[0].selector, tk::OBJC_SELECTOR);
assert_eq!(decode_ptr(&b, class), Some(class));
let offset = class - b.image_base;
assert_eq!(decode_ptr(&b, offset), Some(class));
assert_eq!(decode_ptr(&b, (1u64 << 63) | offset), Some(class));
assert_eq!(decode_ptr(&b, u64::MAX), None);
assert_eq!(decode_ptr(&b, b.image_base + 0x0fff_ffff), None);
assert_eq!(decode_ptr(&b, 0), None);
}
#[test]
fn fat_slices_and_parse() {
let thin = tk::macho64();
+1
View File
@@ -141,6 +141,7 @@ No `deny.toml` exists. The survey found no GPL, AGPL or SSPL crates, and only MP
### R0.5 Small correctness items from the survey
Owner: open · Status: done (branch chore/r0.5-cleanups)
- [x] Decode Objective-C chained pointer offsets past file-empty PAGEZERO mappings (#296).
- [x] Checked arithmetic on file offsets in `elf.rs`: `va_to_off` adds a segment's file offset to an
address delta unchecked, so a `PT_LOAD` at offset `u64::MAX` overflows, found by a lint scan
of `crates/formats` (#43)
+1
View File
@@ -8,6 +8,7 @@ workspace clippy, layers, fmt and the secret scan of all history were clean. Chi
| Area | State | Verified by |
|---|---|---|
| Mach-O Objective-C pointers | Chained offsets mapped only into file-empty `__PAGEZERO` reach the image-base fallback; pointer candidates must be backed by file bytes | Synthetic thin and fat Mach-O regression, plain-pointer and authenticated-rebase controls, null and unmapped-pointer checks. Real binaries were not checked for this correction |
| Chinese UI | Simplified (`zh-hans`) and Traditional (`zh-hant`) Chinese in Settings → General → Language; System chooses a supported OS language, otherwise English. Both catalogs cover UI controls and tool titles; translated titles are searchable. Raw tool data, AI text, logs, CLI/MCP and the separate browser viewer keep their original text. See [localisation](localization.md). | Catalog integrity and locale-resolution tests; `cargo xtask i18n-coverage`; isolated UI language-switch test. Platform visual checks are recorded separately. |
| Engine | 298 engine tools (314 with the AI and setup layers') across native, managed, Java, Android, Apple, JS/Electron, web, firmware, packets, databases, game archives and assets; see [tools.md](tools.md) and [tool-status.md](tool-status.md). A saved result can be narrowed without running again (`core_evidence_browse` keeps the rows matching given field values, cut to chosen fields) | A functional audit that ran every tool on real inputs on Windows, plus 600+ unit and integration tests |
| Single-file apps and game files | `core_packed_files_list` and `core_packed_files_unpack` read Bun and Node.js single-file programs, .NET single-file apps, PyInstaller and Godot packs through one seam (`formats::packed`); `formats_bytecode_show` reads Python bytecode, `game_unity_code_names_show` Unity IL2CPP metadata, `formats_ai_weights_show` GGUF and safetensors headers. `app_folder_explain` says what an install folder is made with and what to run next | Unit tests with damaged files. Real files: a 279 MB Bun program, Node blobs, PyInstaller programs (Python 3.9, 3.14), .NET 8 single-file apps (macOS, Linux, Windows, compressed), Godot 3.6, 4.3 and 4.5 packs, 214 Python modules against CPython's own reader, a GGUF and a safetensors file, about 25 installed Mac apps. IL2CPP on synthetic files only |