Use futex-based synchronization on Apple platforms by joboet · Pull Request #122408 · rust-lang/rust (original) (raw)
Hey again. I got a friend (thanks a lot @extrawurst!) who has a live app in the Apple App Store to submit a few modified version of said app, as a way to probe whether they could pass App Store's opaque review process.
Note that test 1-4 was done sometime around March IIRC, while test 5 was done ~a week ago when we picked this up again, so it is possible that the App Store changed how things work in the meantime.
Theories
There are roughly three ways that a check like this could work:
- String-level check for these symbols.
- Inspect the binary using
objdumpor similar. - Execute the binary and do a runtime check.
Note that these are not mutually exclusive; the App Store may do one thing for some symbols, and another for other symbols. And they may do different things for e.g. compiled objects vs. Python files.
Also note that the check could be either an allowlist or a denylist.
Tests
Test 1
Using dlsym to look up the symbols (same as what this PR does).
Code
use core::ffi::{c_char, c_void}; use core::ptr::null_mut;
extern "C" { fn dlsym(handle: *mut c_void, symbol: *const c_char) -> *mut c_void; }
fn main() { unsafe { let _ = dlsym(null_mut(), c"__ulock_wait2".as_ptr()); let _ = dlsym(null_mut(), c"__ulock_wait".as_ptr()); let _ = dlsym(null_mut(), c"__ulock_wake".as_ptr()); }
// Rest of normal application calling `UIApplicationMain`.}
Result: Succeeded!
Test 2
Directly linking __ulock_wait2, __ulock_wait and __ulock_wake.
Code
use core::ffi::{c_int, c_void}; use core::ptr::null_mut;
extern "C" { fn __ulock_wait2(operation: u32, addr: *mut c_void, value: u64, timeout: u64, value2: u64) -> c_int; fn __ulock_wait(operation: u32, addr: *mut c_void, value: u64, timeout: u32) -> c_int; fn __ulock_wake(operation: u32, addr: *mut c_void, wake_value: u64) -> c_int; }
fn main() { if std::hint::black_box(false) { __ulock_wait(0, null_mut(), 0, 0); __ulock_wait2(0, null_mut(), 0, 0, 0); __ulock_wake(0, null_mut(), 0); }
// Rest of normal application calling `UIApplicationMain`.}
Result: Succeeded!
Test 3
Directly linking other private symbols from libSystem.dylib.
Code
use core::ffi::{c_char, c_int, c_uint}; use core::ptr::null_mut;
extern "C" { fn __openat(fd: c_int, path: *const c_char, oflag: c_int, mode: u16) -> c_int; fn _get_cpu_capabilities() -> u64; fn _kernelrpc_mach_vm_allocate(target: c_uint, address: *mut u32, size: u64, flags: c_int) -> c_int; fn _dispatch_get_main_queue_port_4CF() -> c_uint; fn dyld_dynamic_interpose(mh: *const c_void, dyld_interpose_tuple: *const c_void, count: isize); }
fn main() { if std::hint::black_box(false) { let _ = __openat(0, null_mut(), 0, 0); let _ = _get_cpu_capabilities(); let _ = _kernelrpc_mach_vm_allocate(0, null_mut(), 0, 0); let _ = _dispatch_get_main_queue_port_4CF(); let _ = dyld_dynamic_interpose(null_mut(), null_mut(), 0); }
// Rest of normal application calling `UIApplicationMain`.}
Result: Failed with:
[Application Loader Error Output]: [ContentDelivery.Uploader.REDACTED] Validation failed (409) The app references non-public symbols in Payload/REDACTED.app/REDACTED: __dispatch_get_main_queue_port_4CF, _dyld_dynamic_interpose
Test 4
Directly linking private symbols from test 3 that didn't seem to fail.
Code
use core::ffi::c_int; use core::ptr::null_mut;
extern "C" { fn __openat(fd: c_int, path: *const c_char, oflag: c_int, mode: u16) -> c_int; fn _get_cpu_capabilities() -> u64; fn _kernelrpc_mach_vm_allocate(target: c_uint, address: *mut u32, size: u64, flags: c_int) -> c_int; }
fn main() { if std::hint::black_box(false) { let _ = __openat(0, null_mut(), 0, 0); let _ = _get_cpu_capabilities(); let _ = _kernelrpc_mach_vm_allocate(0, null_mut(), 0, 0); }
// Rest of normal application calling `UIApplicationMain`.}
Result: Succeeded!
Test 5
Dynamically linking known private symbols from libSystem.dylib.
Code
use core::ffi::{c_char, c_void}; use core::ptr::null_mut;
extern "C" { fn dlsym(handle: *mut c_void, symbol: *const c_char) -> *mut c_void; }
fn main() { unsafe { let _ = dlsym(null_mut(), c"_dispatch_get_main_queue_port_4CF".as_ptr()); let _ = dlsym(null_mut(), c"dyld_dynamic_interpose".as_ptr()); }
// Rest of normal application calling `UIApplicationMain`.}
Result: Succeeded!
Analysis
It seems to me that the App Store's checks around private-ness (at least for functions in libSystem.dylib) may take more of a denylist than an allowlist approach, and that __ulock_wait2, __ulock_wait and __ulock_wake happen to not be denied. I'm basing this on test 3 failing, which shows that they do detect usage of some private APIs like _dispatch_get_main_queue_port_4CF, while test 2 and 4 succeeds, and I can't imagine they'd want to explicitly allowlist e.g. _kernelrpc_mach_vm_allocate which is also very private API.
Additionally, based on test 5, it seems like the days of dlsym failing is over (which they did do in the past as @BlackHoleFox noted), and that the App Store at least nowadays inspects the binary for linkage instead of just searching for denied strings. Test 5 also shows that the App Store doesn't do runtime checking of denylisted APIs.
All of this really is probing a black box function though. Possible failure modes:
- The App Store's checks were loosened between March and September, we'd have to re-run test 3 to be sure.
- The App Store's checks are non-deterministic (e.g. only 10% of applications with the symbols are flagged).
- Fewer restrictions on iOS than tvOS/watchOS/visionOS.
- Something specific to my friend's app (geolocation-based? Maybe the checks are less strict in the EU?).
- Future changes to the checks may cause this to fail in the future.
Conclusion
I believe the risk is sufficiently low that it should be safe for us to move forwards with this.
Given the results above test, we could probably use weak linking for the __ulock* symbols, but to be safe, I'd prefer we keep using dlsym for those. I don't think we have to obfuscate the string though.
I realized just now that I can also just try to ask Apple what they think, I have filed FB20671640 for that, will post a response here if I get one (but I think it's safe for us to move forwards with this without waiting for a response).