Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions .github/workflows/track.yml
Original file line number Diff line number Diff line change
@@ -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
36 changes: 36 additions & 0 deletions LocalAuthentication/newInstanceId-newCommandId-race/README.md
Original file line number Diff line number Diff line change
@@ -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.
49 changes: 49 additions & 0 deletions LocalAuthentication/newInstanceId-newCommandId-race/repro.m
Original file line number Diff line number Diff line change
@@ -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 <Foundation/Foundation.h>
#import <LocalAuthentication/LocalAuthentication.h>
#import <objc/message.h>
#import <pthread.h>
#import <stdatomic.h>

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;
}
Original file line number Diff line number Diff line change
@@ -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"
}
Original file line number Diff line number Diff line change
@@ -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"
}
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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/).