Skip to content

fix: required-reason cross-check reads symbols, not strings - #20

Open
adamXbot wants to merge 1 commit into
mainfrom
fix/required-reason-crosscheck
Open

fix: required-reason cross-check reads symbols, not strings#20
adamXbot wants to merge 1 commit into
mainfrom
fix/required-reason-crosscheck

Conversation

@adamXbot

Copy link
Copy Markdown
Collaborator

Root cause

The privacy-manifest cross-check compared the manifest's NSPrivacyAccessedAPITypes against BinaryStringScanner.foundFrameworkSymbols — ASCII strings scraped from the binary — using an 11-entry lookup table. Three separate defects:

  1. The table omitted the libc symbols Apple actually rejects on. stat, fstat, lstat, fstatat, getattrlist, statfs, mach_absolute_time and friends are the core of Apple's documented required-reason symbol lists; most were missing, so binaries using them were reported as clean.
  2. A deliberately wrong mapping: the table mapped CMPedometer (a CoreMotion class, not a required-reason API at all) to the UserDefaults category, annotated "close enough for cross-check". Any app embedding the string CMPedometer was flagged as using UserDefaults.
  3. First-slice-only symbol reading: MachOInspector.importedSymbols parsed LC_SYMTAB of only the first slice of a fat binary, so evidence present only in the other slice (including x86_64-specific spellings like _stat$INODE64) was invisible.

String matching itself is also the wrong mechanism: the presence of an ASCII string like creationDate in a binary is not evidence the API is called, and its absence is not evidence it isn't.

What changed

  • MachOInspector.importedSymbols now enumerates every arch slice and returns the de-duplicated union of undefined-external symbols across all slices.
  • New Analysis/RequiredReasonAPIs.swift: Apple's required-reason vocabulary as data — all five NSPrivacyAccessedAPICategory* values, their approved reason codes with restrictions, and their symbol lists with exact nlist spellings (_stat, _stat$INODE64, _OBJC_CLASS_$_NSUserDefaults, …), keyed to Apple's documentation. The old 11-entry table and its CMPedometer entry are deleted.
  • PrivacyManifestReader.crossCheck now takes importedSymbols: Set<String> and matches exactly on nlist spellings — never substring-based. A symbol in two categories (the getattrlist family is in both FileTimestamp and DiskSpace) counts as evidence for each.
  • Call sites (StaticAnalyzer, StaticAnalysisView) feed MachOInspector.importedSymbols(of:) instead of a string-scan result.
  • The Xcode app target compiles Core sources by explicit file reference, so RequiredReasonAPIs.swift is also registered in project.pbxproj.

What findings mean now

  • "Used but undeclared" now means: the binary imports a symbol on Apple's required-reason list (readable even on encrypted App Store binaries — __LINKEDIT is not encrypted). Expect more of these findings, and expect them to name real symbols.
  • "Declared but unused" now means: no symbol on that category's list is imported by any slice. Note the evidence is class-level for APIs reached only via objc_msgSend (e.g. ProcessInfo.systemUptime_OBJC_CLASS_$_NSProcessInfo).
  • Because importedSymbols now unions all slices, BinaryCapabilityAnalyzer (which consumes the same call) may report more capabilities on universal binaries than a first-slice read did — correct behaviour, and it may shift risk scores. The 9 ManifestTrackingMismatchTests were watched for fallout: all 9 pass unchanged.

Cache compatibility

Conservative path taken: no Codable shape change, no cache version bump needed. StaticReport persists PrivacyManifest (unchanged, byte-compatible encoding) and derived Finding strings; PrivacyManifestCrossCheck itself is computed on demand and never persisted. Cached reports remain loadable; their stored findings reflect the old detection until re-analysis (the cache key already rotates on auditor version and binary mtime).

Flagged, deliberately not done here

User-facing copy and the knowledge-base article for privacy-manifest were left untouched (the existing copy says "symbol references", which is accurate for the new mechanism, but a fuller KB pass — e.g. explaining nlist evidence and reason codes — is an open decision for the maintainer).

Verification

  • Baseline (main): swift test325 executed, 4 skipped, 0 failures
  • After: swift test325 executed, 4 skipped, 0 failures (identical; no test-by-test delta)
  • swift test --filter ManifestTrackingMismatchTests9 executed, 0 failures
  • Xcode app target: xcodebuild -project privacycommand/privacycommand.xcodeproj -scheme privacycommand -configuration Debug -destination 'platform=macOS' buildBUILD SUCCEEDED

🤖 Generated with Claude Code

PrivacyManifestReader.crossCheck now matches the manifest's declared
NSPrivacyAccessedAPITypes against the binary's undefined-external
symbols (MachOInspector.importedSymbols), using a full RequiredReasonAPIs
vocabulary table with exact nlist spellings for all five documented
categories. MachOInspector.importedSymbols now parses LC_SYMTAB in every
arch slice of a universal binary and returns the de-duplicated union.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant