Skip to content

Mark reviewed LocalAuthentication methods safe - #864

Open
LucaCappelletti94 wants to merge 1 commit into
madsmtm:mainfrom
LucaCappelletti94:upstream/safe-lacontext-constructor
Open

LucaCappelletti94 wants to merge 1 commit into
madsmtm:mainfrom
LucaCappelletti94:upstream/safe-lacontext-constructor

Conversation

@LucaCappelletti94

@LucaCappelletti94 LucaCappelletti94 commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Enables the blanket safety defaults for LocalAuthentication and keeps an explicit unsafe opt-out only where there is a concrete reason, with that reason written as a comment in translation-config.toml. The per-instance LAContext and LARight accessors become safe, since neither class is Send.

The constructors of LAContext and LARight stay unsafe. On macOS 26.6.2 they increment process-wide instance and command counters with a plain load, add and store, so concurrent construction is a data race. canEvaluatePolicy:error:, biometryType, domainState and LARight.state stay unsafe because they touch shared state of the same kind, as do the completion-handler methods whose error pointers can be null. Behaviour on other OS releases is unverified.

The regenerated bindings are in the generated fork.

@madsmtm madsmtm left a comment •

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR! I think I'd prefer blanket:

unsafe-default-safety.documentation-is-reviewed = true
unsafe-default-safety.bounds-checked-internally = true

With opt-outs for the things that aren't safe?

LAContext.biometryType, LAContext.domainState and LARight.state also stay unsafe because the framework updates their backing state from its own queue without a lock

I suspect these are safe because LAContext isn't Sendable? Unless the queue (unsure which queue you're talking about?) runs on another thread?

@madsmtm madsmtm added enhancement New feature or request A-framework Affects the framework crates and the translator for them labels Oct 1, 2026
@LucaCappelletti94

Copy link
Copy Markdown
Contributor Author

Ok to go for blanket safety defaults with explicit opt-outs.

My proposed opt-outs are speculations informed by public decompilations of the iOS 18.2 and 26.1 binaries, I am not certain about how well they generalize across versions and variants.

In those snapshots, LAClient creates the la_client queue, assigns its XPC connection to it, and dispatches asynchronous evaluation completions onto a global queue (queue, XPC connection, completion).

The decompilations appear to show a process-wide _cachedBiometryType read by LAContext.biometryType and replaced by domainState fetches and evaluation completions without visible synchronization (getter and fetch, completion). If those accesses are not synchronized elsewhere, two threads owning separate LAContext instances could race on that cache despite LAContext lacking Send. The PR also marks canEvaluatePolicy:error: safe, although its evaluation error path can replace the cache, so I suspect it should remain unsafe, which I had not noticed before and I had previously marked as safe.

LARight.state appears to read _state directly while an authorization completion writes it through setState: (writer, getter), which raises a similar concern. Apple marks LAEnvironmentState and LAEnvironmentMechanism NS_SWIFT_SENDABLE at the class level, while LAContext and LARight lack that marker. The LAContext reply block is marked separately.

It seems to me that using these methods unwisely could lead to very odd behaviours. I would keep them unsafe under the blanket defaults.

@LucaCappelletti94
LucaCappelletti94 marked this pull request as draft October 1, 2026 20:17
@LucaCappelletti94

Copy link
Copy Markdown
Contributor Author

I am spending a little while trying to prove unsafety by provoking crashes or misbehaviour, so we can be surer of the choice made.

@LucaCappelletti94
LucaCappelletti94 force-pushed the upstream/safe-lacontext-constructor branch from 68f3ff6 to f9a5cd6 Compare October 1, 2026 21:48
@LucaCappelletti94

Copy link
Copy Markdown
Contributor Author

Now the translator emits configured # Safety text for ordinary methods and synthesized property accessors, and the framework uses blanket safety defaults with explicit unsafe opt-outs.

I tested that on macOS 26.6.2 constructing LAContext and LARight really does update process-wide counters without synchronization, so they really should be kept unsafe in my opinion.

I audited all 53 added safety contracts against the Xcode 27 headers, generated selectors, and available runtime evidence. The corrections cover nullable callback errors, deprecated properties, direct initialization, and concurrency statements that lacked support.

I was only wondering whether to include permalinks to the lines of the decompiled code in the docs to show the source of the claims. Currently I omitted them, what do you think?

Double checked all of the comments with Astra.

@LucaCappelletti94
LucaCappelletti94 marked this pull request as ready for review October 1, 2026 22:04
@LucaCappelletti94
LucaCappelletti94 force-pushed the upstream/safe-lacontext-constructor branch 2 times, most recently from 57fdb9e to 895a245 Compare October 4, 2026 12:46

@madsmtm madsmtm left a comment •

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for investigating further.

I tested that on macOS 26.6.2 constructing LAContext and LARight really does update process-wide counters without synchronization, so they really should be kept unsafe in my opinion.

Hmm, are you sure it's not just done atomically?

If it really is true that LAContext.new could race with other such calls, that sounds like a bug in Apple's framework?

Is it possible for you to reproduce the racing behaviour somehow?

I was only wondering whether to include permalinks to the lines of the decompiled code in the docs to show the source of the claims. Currently I omitted them, what do you think?

I'd prefer to omit that, since it's not really an autoritative source.

Comment thread crates/header-translator/src/config.rs Outdated
#[serde(rename = "unsafe")]
#[serde(default)]
pub unsafe_: Option<bool>,
#[serde(rename = "unsafe-reason")]

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is valuable, but I'd rather have it done separately (because there's a bunch of existing places where such documentation could be useful).

Would you be up for filing a PR doing that across all the translation-config.tomls? Regardless, for this PR I'd be fine with having the safety docs as comments in just the toml.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you do get around to making a PR for it, I think I would rather have it like class.LAContext.methods.new.unsafe = "xyz", where it automatically detects string vs boolean, and treats a string as unsafe + safety reason.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved the reasons to TOML comments. I'll do the unsafe = "reason" follow-up once this lands if I get the time.

@LucaCappelletti94

Copy link
Copy Markdown
Contributor Author

Hmm, are you sure it's not just done atomically?

The decompiled code I linked does not show any sort of thread-awareness, but I did not red team it, I could try to see whether I can make it crash by assuming it is safe.

If it really is true that LAContext.new could race with other such calls, that sounds like a bug in Apple's framework?

Wouldn't be the first, and that is going in the top of the list of stuff to fuzz now.
In the last few days it has really been a madness of bug reporting & fixing.

Ok on the other notes.

@LucaCappelletti94

Copy link
Copy Markdown
Contributor Author

Reproduced the counter race directly this time around.

+[LAContext newInstanceId] and +[LAContext newCommandId] disassemble to a plain adrp/ldr/add/str sequence on a process-global address, no atomic instruction, no lock. Calling that selector from 16 threads at a million iterations each on macOS 26.6.2 advanced the counter by only 1,201,097 instead of 16,000,000, so it is not done atomically.

I could not make (LAContext.new / LARight.new) analogously race, but I believe it is because each construction also does an XPC round trip to the LA daemon, which happens to space the threads out past the tiny racy window. The primitive is still very much unsound, it is just more unlikely to misbehave.

biometryType and domainState read a process-wide cached NSNumber pointer without retaining it first, while the writer stores a new value and releases the old one right after, both unguarded. That I suppose could be a potential use-after-free, worse than a plain race.

Also found https://developer.apple.com/forums/thread/710327 which suggests to me somebody slammed into this unsafety.

@LucaCappelletti94
LucaCappelletti94 force-pushed the upstream/safe-lacontext-constructor branch from 895a245 to 3153537 Compare October 5, 2026 19:06
@LucaCappelletti94 LucaCappelletti94 changed the title Mark plain LocalAuthentication accessors and constructors safe Mark reviewed LocalAuthentication methods safe Oct 5, 2026
@LucaCappelletti94
LucaCappelletti94 force-pushed the upstream/safe-lacontext-constructor branch from 3153537 to 36f6ae3 Compare October 5, 2026 19:26
@LucaCappelletti94

Copy link
Copy Markdown
Contributor Author

LAContext and LARight accessors are now safe since neither class is Send, and only methods touching process-wide state or callbacks stay unsafe.

Three jobs were cancelled for lack of a GitHub runner. Could you rerun them?

@madsmtm madsmtm left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ugh, annoying, thanks for reproducing it!

I'm not really happy with this (which is due to no fault of yours), I feel like it doesn't really help to mark these methods as unsafe (since you wouldn't be able to soundly use them in a library, the only sound usage is in an application where you state "I don't use this from any other thread"). Ideally we'd find some way to ensure that these APIs are used from a single thread, but I don't think there is such a way (it's the same situation as with std::env::set_var, we can fix it Rust-only with a Mutex, but that doesn't help with C libraries using these).


Could I get you to file a FB with Apple through their Feedback Assistant? This is also a bug affecting Obj-C and Swift as you found, and Apple should either make the accesses here atomic, mark the APIs as @MainActor, or in some other way give it actor isolation.

Without a reply from Apple, I'm a bit hesitant to mark methods safe, since IIUC there's nothing stopping Apple from changing e.g. LADomainStateBiometry.biometryType to read the process-wide biometry type instead in a future macOS version? And then we'd have to mark that method as unsafe again.

Does that make sense? Sorry for the big asks, but I'm not too familiar with LocalAuthentication, and I'd rather be conservative here.

Comment on lines +59 to +72
# Completion result and error pointers may be null.
class.LARightStore.methods."rightForIdentifier:completion:".unsafe = true
class.LARightStore.methods."saveRight:identifier:completion:".unsafe = true
class.LARightStore.methods."saveRight:identifier:secret:completion:".unsafe = true
class.LARightStore.methods."removeRight:completion:".unsafe = true
class.LARightStore.methods."removeRightForIdentifier:completion:".unsafe = true
class.LARightStore.methods."removeAllRightsWithCompletion:".unsafe = true
class.LAPrivateKey.methods."signData:secKeyAlgorithm:completion:".unsafe = true
class.LAPrivateKey.methods."decryptData:secKeyAlgorithm:completion:".unsafe = true
class.LAPrivateKey.methods."exchangeKeysWithPublicKey:secKeyAlgorithm:secKeyParameters:completion:".unsafe = true
class.LAPublicKey.methods."exportBytesWithCompletion:".unsafe = true
class.LAPublicKey.methods."encryptData:secKeyAlgorithm:completion:".unsafe = true
class.LAPublicKey.methods."verifyData:signature:secKeyAlgorithm:completion:".unsafe = true
class.LASecret.methods."loadDataWithCompletion:".unsafe = true

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not a soundness problem, since to actually use the pointers, you have to write unsafe { err.as_ref() } or similar in the completion handler. So I think these can be safe.

@LucaCappelletti94
LucaCappelletti94 force-pushed the upstream/safe-lacontext-constructor branch 2 times, most recently from 95e3292 to 7582001 Compare October 12, 2026 10:50
@LucaCappelletti94

Copy link
Copy Markdown
Contributor Author

I'm not really happy with this (which is due to no fault of yours), I feel like it doesn't really help to mark these methods as unsafe (since you wouldn't be able to soundly use them in a library, the only sound usage is in an application where you state "I don't use this from any other thread"). Ideally we'd find some way to ensure that these APIs are used from a single thread, but I don't think there is such a way (it's the same situation as with std::env::set_var, we can fix it Rust-only with a Mutex, but that doesn't help with C libraries using these).

Yeah these are not "unsafeties" as much as just plain bad design, as "unsafe" does not mean race-y and buggy. In the context of https://github.com/open-source-cooperative/apple-native-keyring-store I am handling these APIs with the necessary care (if the user does not directly call the underlying APIs also elsewhere, which would immediately defeat the purpose)

Could I get you to file a FB with Apple through their Feedback Assistant? This is also a bug affecting Obj-C and Swift as you found, and Apple should either make the accesses here atomic, mark the APIs as @MainActor, or in some other way give it actor isolation.

Yeah, I went Hail Mary and started putting reproductions for each of the API crashes in https://github.com/LucaCappelletti94/apple-sdk-repros alongside the FB where I filed them already. It is rather unfortunate that Apple keeps all FBs private, I get it for CVEs but these are just bugs I believe.

Without a reply from Apple, I'm a bit hesitant to mark methods safe, since IIUC there's nothing stopping Apple from changing e.g. LADomainStateBiometry.biometryType to read the process-wide biometry type instead in a future macOS version? And then we'd have to mark that method as unsafe again.

True, and there is no harness we could use to prove safety. When I started with this PR work I honestly expected this to be somewhat more straightforward and while it kindof is for certain methods, there is no guarantee that between versions the same safety guarantee holds.

The only thing I can think of would be, in the brave new LLM world, to add some automation that detects safety/unsafety, but I would only ever take the result of an LLM interpretation of a method's safety as a suggestion.

Given that these two PRs only aimed at bringing better Rust clarify to the bindings, and make it clear which ones can be considered safe and not, I would suggest to pivot from trying to add the safe/unsafe marker and add documentation of bugginess in some way.

@madsmtm

madsmtm commented Oct 14, 2026

Copy link
Copy Markdown
Owner

Yeah, I went Hail Mary and started putting reproductions for each of the API crashes in https://github.com/LucaCappelletti94/apple-sdk-repros alongside the FB where I filed them already.

Thanks a lot!

I would suggest to pivot from trying to add the safe/unsafe marker and add documentation of bugginess in some way.

Yeah, that might be better. Let's wait a while though (probably at least a month if I know Apple Feedback right) and see whether they decide to want to fix it or not.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-framework Affects the framework crates and the translator for them enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants