diff --git a/.github/workflows/track.yml b/.github/workflows/track.yml new file mode 100644 index 0000000..0bf6e5f --- /dev/null +++ b/.github/workflows/track.yml @@ -0,0 +1,57 @@ +name: Track + +on: + schedule: + # Monthly, matching GitHub's own cadence for rebuilding the hosted + # macOS runner images (actions/runner-images: "we typically deploy + # weekly updates to the software on the runner images", so a monthly + # run is never re-checking a build it already checked, with margin). + - cron: "17 6 1 * *" + workflow_dispatch: {} + +permissions: + contents: write + +jobs: + track: + strategy: + fail-fast: false + matrix: + runner: [macos-26, macos-15, xcode-27] + runs-on: ${{ matrix.runner }} + steps: + - uses: actions/checkout@v4 + + - name: Run the LAContext counter race repro + working-directory: LocalAuthentication/newInstanceId-newCommandId-race + run: | + clang -O2 -fobjc-arc -framework Foundation -framework LocalAuthentication repro.m -o repro + { + date -u +%Y-%m-%dT%H:%M:%SZ + sw_vers + ./repro direct 1 16000000 + for i in 1 2 3; do ./repro direct 16 1000000; done + for i in 1 2 3; do ./repro new 8 3000; done + date -u +%Y-%m-%dT%H:%M:%SZ + } | tee output.txt + + - name: Record the result + working-directory: LocalAuthentication/newInstanceId-newCommandId-race + run: | + xcode_version=$(xcodebuild -version | head -1 | awk '{print $2}') + ../../scripts/make-result.sh "gh-actions-${{ matrix.runner }}" output.txt results "$xcode_version" + rm output.txt + + - name: Commit the result if it changed + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add LocalAuthentication/newInstanceId-newCommandId-race/results + git diff --cached --quiet && exit 0 + git commit -m "Track newInstanceId/newCommandId race on ${{ matrix.runner }}" + for attempt in 1 2 3 4 5; do + git fetch origin main + git rebase origin/main && git push origin HEAD:main && break + git rebase --abort 2>/dev/null || true + sleep $((attempt * 10)) + done diff --git a/LocalAuthentication/newInstanceId-newCommandId-race/README.md b/LocalAuthentication/newInstanceId-newCommandId-race/README.md new file mode 100644 index 0000000..8761cef --- /dev/null +++ b/LocalAuthentication/newInstanceId-newCommandId-race/README.md @@ -0,0 +1,36 @@ +# `+[LAContext newInstanceId]` / `+[LAContext newCommandId]` counter race + +Apple Feedback: FB25157108 (filed, open as of 2026-10-12, Apple has not replied). + +## What this tests + +`LAContext` construction and most of its methods take an ID from one of two process-wide counters (`+newInstanceId`, `+newCommandId`). On the macOS 26.6.2 binary, both counters are a plain load, add, and store, with no atomic instruction and no lock. `-[LARight init]` creates an `LAContext` internally, so it goes through the same counters. + +Two threads that each create and use their own `LAContext` can race on these counters and end up with the same instance or command ID. Apple's own threading guidance says an object that is not documented thread-safe can still be used from several threads as long as each thread sticks to its own instance, so this race is reachable without doing anything unusual. + +See `madsmtm/objc2#864` for where this was found and for the `objc2` Rust bindings this motivated. + +## Running it + +```sh +clang -O2 -fobjc-arc -framework Foundation -framework LocalAuthentication repro.m -o repro +./repro new 8 3000 # public API: 8 threads each create 3000 LAContext objects +./repro direct 16 1000000 # 16 threads call +newInstanceId directly, no XPC round trip in the way +./repro direct 1 16000000 # single-threaded control +``` + +`direct` calls `+newInstanceId` through a private selector, used only to measure how far the counter moved; it does not touch anything not already reachable through `new`. `new` only goes through public API. + +## Reading a result + +Each line reports `expected N increments, counter advanced M, lost N-M`. `lost` greater than zero on `new` means two or more `LAContext` objects, created through nothing but public API, ended up sharing an instance ID. `direct` isolates the counter from the XPC round trip each real construction makes, so it loses a much larger fraction, which is expected and not itself news; `new` losing anything is the result that matters. + +The single-threaded control (`direct 1 …`) should always show `lost 0`. If it doesn't, something about the run itself is suspect, not the framework. + +## Platforms + +Reproduces on macOS. Not meaningfully testable on iOS/watchOS Simulator or in a way that differs from macOS, since this is pure Foundation/Objective-C runtime logic with no biometric-hardware dependency; a Simulator result would be redundant with the macOS one rather than additional evidence, so none is recorded here. + +## Results + +See `results/`. One file per platform and exact OS build. diff --git a/LocalAuthentication/newInstanceId-newCommandId-race/repro.m b/LocalAuthentication/newInstanceId-newCommandId-race/repro.m new file mode 100644 index 0000000..bed5db7 --- /dev/null +++ b/LocalAuthentication/newInstanceId-newCommandId-race/repro.m @@ -0,0 +1,49 @@ +// clang -O2 -fobjc-arc -framework Foundation -framework LocalAuthentication counters.m -o counters +// ./counters new 8 3000 threads create LAContext objects through public API +// ./counters direct 16 1000000 threads call +newInstanceId directly +#import +#import +#import +#import +#import + +static BOOL direct; +static int per_thread; +static atomic_int go; + +// Private selector, used only to measure how far the counter moved. +static unsigned instance_id(void) { + return ((unsigned (*)(id, SEL))objc_msgSend)([LAContext class], NSSelectorFromString(@"newInstanceId")); +} + +static void *worker(void *arg) { + (void)arg; + while (!atomic_load(&go)) {} + for (int i = 0; i < per_thread; i++) { + if (direct) { + (void)instance_id(); + } else { + @autoreleasepool { (void)[LAContext new]; } + } + } + return NULL; +} + +int main(int argc, char **argv) { + if (argc < 4) { + fprintf(stderr, "usage: %s new|direct threads iterations\n", argv[0]); + return 2; + } + direct = strcmp(argv[1], "direct") == 0; + int threads = atoi(argv[2]); + per_thread = atoi(argv[3]); + pthread_t t[64]; + unsigned before = instance_id(); + for (int i = 0; i < threads; i++) pthread_create(&t[i], NULL, worker, NULL); + atomic_store(&go, 1); + for (int i = 0; i < threads; i++) pthread_join(t[i], NULL); + unsigned advanced = instance_id() - before - 1; + unsigned expected = (unsigned)(threads * per_thread); + printf("%s: expected %u increments, counter advanced %u, lost %u\n", argv[1], expected, advanced, expected - advanced); + return 0; +} diff --git a/LocalAuthentication/newInstanceId-newCommandId-race/results/macos-host-apple-silicon-25G83.json b/LocalAuthentication/newInstanceId-newCommandId-race/results/macos-host-apple-silicon-25G83.json new file mode 100644 index 0000000..eda1f48 --- /dev/null +++ b/LocalAuthentication/newInstanceId-newCommandId-race/results/macos-host-apple-silicon-25G83.json @@ -0,0 +1,55 @@ +{ + "platform": "macos-host-apple-silicon", + "os": { + "product_version": "26.6.2", + "build": "25G83" + }, + "xcode_version": "27.0", + "command": "clang -O2 -fobjc-arc -framework Foundation -framework LocalAuthentication repro.m -o repro", + "runs": [ + { + "mode": "direct", + "expected": 16000000, + "advanced": 16000000, + "lost": 0 + }, + { + "mode": "direct", + "expected": 16000000, + "advanced": 1564740, + "lost": 14435260 + }, + { + "mode": "direct", + "expected": 16000000, + "advanced": 1639281, + "lost": 14360719 + }, + { + "mode": "direct", + "expected": 16000000, + "advanced": 1571119, + "lost": 14428881 + }, + { + "mode": "new", + "expected": 24000, + "advanced": 23999, + "lost": 1 + }, + { + "mode": "new", + "expected": 24000, + "advanced": 23999, + "lost": 1 + }, + { + "mode": "new", + "expected": 24000, + "advanced": 24000, + "lost": 0 + } + ], + "verdict": "reproduced", + "raw_output": "2026-10-12T15:40:30Z\nProductName:\t\tmacOS\nProductVersion:\t\t26.6.2\nBuildVersion:\t\t25G83\ndirect: expected 16000000 increments, counter advanced 16000000, lost 0\ndirect: expected 16000000 increments, counter advanced 1564740, lost 14435260\ndirect: expected 16000000 increments, counter advanced 1639281, lost 14360719\ndirect: expected 16000000 increments, counter advanced 1571119, lost 14428881\nnew: expected 24000 increments, counter advanced 23999, lost 1\nnew: expected 24000 increments, counter advanced 23999, lost 1\nnew: expected 24000 increments, counter advanced 24000, lost 0\n2026-10-12T15:57:06Z\n" +} diff --git a/LocalAuthentication/newInstanceId-newCommandId-race/results/macos-vm-tart-tahoe-base-25G83.json b/LocalAuthentication/newInstanceId-newCommandId-race/results/macos-vm-tart-tahoe-base-25G83.json new file mode 100644 index 0000000..400bea2 --- /dev/null +++ b/LocalAuthentication/newInstanceId-newCommandId-race/results/macos-vm-tart-tahoe-base-25G83.json @@ -0,0 +1,97 @@ +{ + "platform": "macos-vm-tart-tahoe-base", + "os": { + "product_version": "26.6.2", + "build": "25G83" + }, + "xcode_version": "27.0", + "command": "clang -O2 -fobjc-arc -framework Foundation -framework LocalAuthentication repro.m -o repro", + "runs": [ + { + "mode": "direct", + "expected": 16000000, + "advanced": 16000000, + "lost": 0 + }, + { + "mode": "direct", + "expected": 16000000, + "advanced": 4397651, + "lost": 11602349 + }, + { + "mode": "direct", + "expected": 16000000, + "advanced": 4398613, + "lost": 11601387 + }, + { + "mode": "direct", + "expected": 16000000, + "advanced": 4433406, + "lost": 11566594 + }, + { + "mode": "new", + "expected": 24000, + "advanced": 24000, + "lost": 0 + }, + { + "mode": "new", + "expected": 24000, + "advanced": 23998, + "lost": 2 + }, + { + "mode": "new", + "expected": 24000, + "advanced": 23999, + "lost": 1 + }, + { + "mode": "new", + "expected": 24000, + "advanced": 24000, + "lost": 0 + }, + { + "mode": "new", + "expected": 24000, + "advanced": 24000, + "lost": 0 + }, + { + "mode": "new", + "expected": 24000, + "advanced": 23999, + "lost": 1 + }, + { + "mode": "new", + "expected": 24000, + "advanced": 23997, + "lost": 3 + }, + { + "mode": "new", + "expected": 24000, + "advanced": 24000, + "lost": 0 + }, + { + "mode": "new", + "expected": 24000, + "advanced": 24000, + "lost": 0 + }, + { + "mode": "new", + "expected": 24000, + "advanced": 24000, + "lost": 0 + } + ], + "verdict": "reproduced", + "raw_output": "Mon Oct 12 07:50:13 UTC 2026\nProductName:\t\tmacOS\nProductVersion:\t\t26.6.2\nBuildVersion:\t\t25G83\ndirect: expected 16000000 increments, counter advanced 16000000, lost 0\ndirect: expected 16000000 increments, counter advanced 4397651, lost 11602349\ndirect: expected 16000000 increments, counter advanced 4398613, lost 11601387\ndirect: expected 16000000 increments, counter advanced 4433406, lost 11566594\nnew: expected 24000 increments, counter advanced 24000, lost 0\nnew: expected 24000 increments, counter advanced 23998, lost 2\nnew: expected 24000 increments, counter advanced 23999, lost 1\nnew: expected 24000 increments, counter advanced 24000, lost 0\nnew: expected 24000 increments, counter advanced 24000, lost 0\nnew: expected 24000 increments, counter advanced 23999, lost 1\nnew: expected 24000 increments, counter advanced 23997, lost 3\nnew: expected 24000 increments, counter advanced 24000, lost 0\nnew: expected 24000 increments, counter advanced 24000, lost 0\nnew: expected 24000 increments, counter advanced 24000, lost 0\nMon Oct 12 09:05:53 UTC 2026\n" +} diff --git a/README.md b/README.md index 8738ce4..5384127 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,7 @@ # apple-sdk-repros -Runnable reproductions of soundness defects in Apple SDK frameworks, rerun on each new macOS release to track Apple's fixes + +[![Track](https://github.com/LucaCappelletti94/apple-sdk-repros/actions/workflows/track.yml/badge.svg)](https://github.com/LucaCappelletti94/apple-sdk-repros/actions/workflows/track.yml) + +Runnable reproductions of soundness defects in Apple SDK frameworks, rerun on each new macOS release to track Apple's fixes. + +One directory per method or bug, each with a standalone `repro.m`, a `README.md` naming the Apple Feedback ID and what the repro proves, and `results/` holding one dated record per platform and exact OS build. `.github/workflows/track.yml` reruns every tracked repro monthly on the macOS majors still receiving Apple security patches and commits a fresh result when one changes. `scripts/run-in-tart.sh` runs the same repros against macOS majors GitHub's hosted runners don't offer, using [tart](https://tart.run/).