Skip to content

Commit f9a5cd6

Browse files
Review LocalAuthentication safety and document caller obligations
Enable blanket safety defaults on reviewed methods. Keep context and right operations unsafe where framework-internal concurrency can access shared state, and emit selector-specific safety documentation on generated methods and property accessors. On macOS 26.6.2, context and right construction can update unsynchronized process-wide counters. Callback error pointers can be null, and direct persisted-right initialization is unsupported. Behavior on other Apple OS releases remains unverified.
1 parent b113b04 commit f9a5cd6

5 files changed

Lines changed: 145 additions & 19 deletions

File tree

‎crates/header-translator/src/config.rs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -822,6 +822,9 @@ pub struct MethodData {
822822
#[serde(rename = "unsafe")]
823823
#[serde(default)]
824824
pub unsafe_: Option<bool>,
825+
#[serde(rename = "unsafe-reason")]
826+
#[serde(default)]
827+
pub unsafe_reason: Option<String>,
825828
#[serde(default)]
826829
#[serde(deserialize_with = "deserialize_argument_overrides")]
827830
pub arguments: HashMap<usize, TypeOverride>,
@@ -857,6 +860,7 @@ impl MethodData {
857860

858861
Self {
859862
unsafe_,
863+
unsafe_reason: self.unsafe_reason.or(superclass.unsafe_reason),
860864
renamed: self.renamed.or(superclass.renamed),
861865
skipped: self.skipped || superclass.skipped,
862866
arguments,

‎crates/header-translator/src/method.rs‎

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,28 @@ impl Method {
427427
}
428428
}
429429

430+
fn add_safety_documentation(
431+
documentation: &mut Documentation,
432+
safety: &SafetyProperty,
433+
unsafe_: Option<bool>,
434+
unsafe_reason: Option<String>,
435+
) {
436+
if unsafe_ == Some(false) {
437+
return;
438+
}
439+
440+
let inferred_safety = safety.to_safety_comment();
441+
if inferred_safety.is_some() || unsafe_reason.is_some() {
442+
documentation.add("# Safety");
443+
}
444+
if let Some(reason) = inferred_safety {
445+
documentation.add(reason);
446+
}
447+
if let Some(reason) = unsafe_reason {
448+
documentation.add(reason);
449+
}
450+
}
451+
430452
pub(crate) fn parse_method(
431453
entity: Entity<'_>,
432454
data: MethodData,
@@ -668,12 +690,12 @@ impl Method {
668690

669691
let mut documentation = Documentation::from_entity(&entity, context);
670692

671-
if let Some(safety) = safety.to_safety_comment() {
672-
if data.unsafe_ != Some(false) {
673-
documentation.add("# Safety");
674-
documentation.add(safety);
675-
}
676-
}
693+
Self::add_safety_documentation(
694+
&mut documentation,
695+
&safety,
696+
data.unsafe_,
697+
data.unsafe_reason,
698+
);
677699

678700
Some((
679701
modifiers.designated_initializer,
@@ -844,12 +866,12 @@ impl Method {
844866
safety.is_safe() && default_safety.automatically_safe
845867
};
846868

847-
if let Some(safety) = safety.to_safety_comment() {
848-
if getter_data.unsafe_ != Some(false) {
849-
documentation.add("# Safety");
850-
documentation.add(safety);
851-
}
852-
}
869+
Self::add_safety_documentation(
870+
&mut documentation,
871+
&safety,
872+
getter_data.unsafe_,
873+
getter_data.unsafe_reason,
874+
);
853875

854876
let fn_name = if let Some(renamed) = &getter_data.renamed {
855877
renamed.clone()
@@ -968,12 +990,12 @@ impl Method {
968990
_ => {}
969991
}
970992

971-
if let Some(safety) = safety.to_safety_comment() {
972-
if setter_data.unsafe_ != Some(false) {
973-
documentation.add("# Safety");
974-
documentation.add(safety);
975-
}
976-
}
993+
Self::add_safety_documentation(
994+
&mut documentation,
995+
&safety,
996+
setter_data.unsafe_,
997+
setter_data.unsafe_reason,
998+
);
977999

9781000
Some(Method {
9791001
selector,

‎crates/objc2/src/topics/FRAMEWORKS_CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
1717
- `CoreText` / `objc2-core-text`.
1818
- `ScreenCaptureKit` / `objc2-screen-capture-kit`.
1919
- `Network` / `objc2-network`.
20+
- `LocalAuthentication` / `objc2-local-authentication`.
2021
* Added a few missing APIs that uses arrays.
2122
* Added `CGBitmapInfo::new`.
2223
* Added `Default` implementations for enumerations, bitflags and structs.

‎framework-crates/objc2-local-authentication/translation-config.toml‎

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,102 @@ enum.LAPolicy.use-value = true
1515
enum.LACredentialType.use-value = true
1616
enum.LAError.use-value = true
1717
enum.LAAccessControlOperation.use-value = true
18+
19+
unsafe-default-safety.documentation-is-reviewed = true
20+
unsafe-default-safety.bounds-checked-internally = true
21+
22+
class.LAContext.unsafe = true
23+
class.LAContext.methods.init.unsafe-reason = "The caller must serialize construction with all concurrent `LocalAuthentication` operations in the process, including asynchronous completions, because it can update shared counters."
24+
class.LAContext.methods.new.unsafe-reason = "The caller must serialize construction with all concurrent `LocalAuthentication` operations in the process, including asynchronous completions, because it can update shared counters."
25+
26+
class.LAContext.methods."canEvaluatePolicy:error:".unsafe = true
27+
class.LAContext.methods."canEvaluatePolicy:error:".unsafe-reason = "The caller must serialize this call with all concurrent `LocalAuthentication` operations in the process, including `LAContext` and `LARight` construction and asynchronous completions."
28+
class.LAContext.methods.invalidate.unsafe-reason = "The caller must synchronize mutable state shared with the evaluation completion that invalidation may trigger on a framework-private queue."
29+
class.LAContext.methods."setCredential:type:".unsafe-reason = "The caller must synchronize accesses to this context's credential state with other operations that can read or change it."
30+
class.LAContext.methods."isCredentialSet:".unsafe-reason = "The caller must synchronize accesses to this context's credential state with other operations that can read or change it."
31+
class.LAContext.methods.biometryType.unsafe = true
32+
class.LAContext.methods.domainState.unsafe = true
33+
class.LAContext.methods.evaluatedPolicyDomainState.unsafe = true
34+
class.LAContext.methods.biometryType.unsafe-reason = "The caller must prevent concurrent `LocalAuthentication` operations in the process from accessing biometry state, including operations on other contexts and asynchronous completions."
35+
class.LAContext.methods.domainState.unsafe-reason = "The caller must prevent concurrent `LocalAuthentication` operations in the process from accessing biometry state, including operations on other contexts and asynchronous completions."
36+
class.LAContext.methods.evaluatedPolicyDomainState.unsafe-reason = "The caller must prevent concurrent `LocalAuthentication` operations in the process from accessing biometry state, including operations on other contexts and asynchronous completions."
37+
38+
class.LAContext.methods.maxBiometryFailures.unsafe = true
39+
class.LAContext.methods."setMaxBiometryFailures:".unsafe = true
40+
class.LAContext.methods.maxBiometryFailures.unsafe-reason = "The caller must synchronize reads and writes of this deprecated non-atomic property across threads."
41+
class.LAContext.methods."setMaxBiometryFailures:".unsafe-reason = "The caller must synchronize reads and writes of this deprecated non-atomic property across threads."
42+
class.LAContext.methods.localizedFallbackTitle.unsafe-reason = "The caller must serialize access to this non-atomic property with concurrent reads, writes, and pending evaluations on this context."
43+
class.LAContext.methods."setLocalizedFallbackTitle:".unsafe-reason = "The caller must serialize access to this non-atomic property with concurrent reads, writes, and pending evaluations on this context."
44+
class.LAContext.methods.localizedCancelTitle.unsafe-reason = "The caller must serialize access to this non-atomic property with concurrent reads, writes, and pending evaluations on this context."
45+
class.LAContext.methods."setLocalizedCancelTitle:".unsafe-reason = "The caller must serialize access to this non-atomic property with concurrent reads, writes, and pending evaluations on this context."
46+
class.LAContext.methods.touchIDAuthenticationAllowableReuseDuration.unsafe-reason = "The caller must serialize access to this non-atomic property with concurrent reads, writes, and pending evaluations on this context."
47+
class.LAContext.methods."setTouchIDAuthenticationAllowableReuseDuration:".unsafe-reason = "The caller must serialize access to this non-atomic property with concurrent reads, writes, and pending evaluations on this context."
48+
class.LAContext.methods.localizedReason.unsafe-reason = "The caller must serialize access to this non-atomic property with concurrent reads, writes, and pending evaluations on this context."
49+
class.LAContext.methods."setLocalizedReason:".unsafe-reason = "The caller must serialize access to this non-atomic property with concurrent reads, writes, and pending evaluations on this context."
50+
class.LAContext.methods.interactionNotAllowed.unsafe-reason = "The caller must serialize access to this non-atomic property with concurrent reads, writes, and pending evaluations on this context."
51+
class.LAContext.methods."setInteractionNotAllowed:".unsafe-reason = "The caller must serialize access to this non-atomic property with concurrent reads, writes, and pending evaluations on this context."
52+
53+
class.LAContext.methods."evaluatePolicy:localizedReason:reply:".unsafe = true
54+
class.LAContext.methods."evaluatePolicy:localizedReason:reply:".unsafe-reason = "The caller must serialize the evaluation and its completion with concurrent `LocalAuthentication` operations in the process, including context and right construction, and treat callback error pointers as nullable borrowed values."
55+
class.LAContext.methods."evaluateAccessControl:operation:localizedReason:reply:".unsafe = true
56+
class.LAContext.methods."evaluateAccessControl:operation:localizedReason:reply:".unsafe-reason = "The caller must serialize the evaluation and its completion with concurrent `LocalAuthentication` operations in the process, including context and right construction, and treat callback error pointers as nullable borrowed values."
57+
class.LARight.methods."authorizeWithLocalizedReason:completion:".unsafe = true
58+
class.LARight.methods."authorizeWithLocalizedReason:completion:".unsafe-reason = "The caller must serialize this operation and its completion with concurrent `LocalAuthentication` operations, including accesses to this right's state, and treat callback error pointers as nullable borrowed values."
59+
class.LARight.methods."checkCanAuthorizeWithCompletion:".unsafe = true
60+
class.LARight.methods."checkCanAuthorizeWithCompletion:".unsafe-reason = "The caller must serialize this operation and its completion with concurrent `LocalAuthentication` operations, including accesses to this right's state, and treat callback error pointers as nullable borrowed values."
61+
class.LARight.methods."deauthorizeWithCompletion:".unsafe = true
62+
class.LARight.methods."deauthorizeWithCompletion:".unsafe-reason = "The caller must serialize this operation and its completion with concurrent `LocalAuthentication` operations, including accesses to this right's state."
63+
class.LARightStore.methods."rightForIdentifier:completion:".unsafe = true
64+
class.LARightStore.methods."rightForIdentifier:completion:".unsafe-reason = "The caller must treat the completion's result pointers as nullable borrowed values and synchronize mutable state accessed by the sendable completion across threads."
65+
class.LARightStore.methods."saveRight:identifier:completion:".unsafe = true
66+
class.LARightStore.methods."saveRight:identifier:completion:".unsafe-reason = "The caller must treat the completion's result pointers as nullable borrowed values and synchronize mutable state accessed by the sendable completion across threads."
67+
class.LARightStore.methods."saveRight:identifier:secret:completion:".unsafe = true
68+
class.LARightStore.methods."saveRight:identifier:secret:completion:".unsafe-reason = "The caller must treat the completion's result pointers as nullable borrowed values and synchronize mutable state accessed by the sendable completion across threads."
69+
class.LARightStore.methods."removeRight:completion:".unsafe = true
70+
class.LARightStore.methods."removeRight:completion:".unsafe-reason = "The caller must treat the completion's error pointer as a nullable borrowed value and synchronize mutable state accessed by the sendable completion across threads."
71+
class.LARightStore.methods."removeRightForIdentifier:completion:".unsafe = true
72+
class.LARightStore.methods."removeRightForIdentifier:completion:".unsafe-reason = "The caller must treat the completion's error pointer as a nullable borrowed value and synchronize mutable state accessed by the sendable completion across threads."
73+
class.LARightStore.methods."removeAllRightsWithCompletion:".unsafe = true
74+
class.LARightStore.methods."removeAllRightsWithCompletion:".unsafe-reason = "The caller must treat the completion's error pointer as a nullable borrowed value and synchronize mutable state accessed by the sendable completion across threads."
75+
class.LAPrivateKey.methods."signData:secKeyAlgorithm:completion:".unsafe = true
76+
class.LAPrivateKey.methods."signData:secKeyAlgorithm:completion:".unsafe-reason = "The caller must synchronize mutable state accessed by the sendable completion across threads and treat callback result pointers as nullable borrowed values."
77+
class.LAPrivateKey.methods."decryptData:secKeyAlgorithm:completion:".unsafe = true
78+
class.LAPrivateKey.methods."decryptData:secKeyAlgorithm:completion:".unsafe-reason = "The caller must synchronize mutable state accessed by the sendable completion across threads and treat callback result pointers as nullable borrowed values."
79+
class.LAPrivateKey.methods."exchangeKeysWithPublicKey:secKeyAlgorithm:secKeyParameters:completion:".unsafe = true
80+
class.LAPrivateKey.methods."exchangeKeysWithPublicKey:secKeyAlgorithm:secKeyParameters:completion:".unsafe-reason = "The caller must synchronize mutable state accessed by the sendable completion across threads and treat callback result pointers as nullable borrowed values."
81+
class.LAPublicKey.methods."exportBytesWithCompletion:".unsafe = true
82+
class.LAPublicKey.methods."exportBytesWithCompletion:".unsafe-reason = "The caller must synchronize mutable state accessed by the sendable completion across threads and treat callback result pointers as nullable borrowed values."
83+
class.LAPublicKey.methods."encryptData:secKeyAlgorithm:completion:".unsafe = true
84+
class.LAPublicKey.methods."encryptData:secKeyAlgorithm:completion:".unsafe-reason = "The caller must synchronize mutable state accessed by the sendable completion across threads and treat callback result pointers as nullable borrowed values."
85+
class.LAPublicKey.methods."verifyData:signature:secKeyAlgorithm:completion:".unsafe = true
86+
class.LAPublicKey.methods."verifyData:signature:secKeyAlgorithm:completion:".unsafe-reason = "The caller must synchronize mutable state accessed by the sendable completion across threads and treat callback error pointers as nullable borrowed values."
87+
class.LASecret.methods."loadDataWithCompletion:".unsafe = true
88+
class.LASecret.methods."loadDataWithCompletion:".unsafe-reason = "The caller must synchronize mutable state accessed by the sendable completion across threads and treat callback result pointers as nullable borrowed values."
89+
90+
class.LAEnvironment.methods."addObserver:".unsafe = true
91+
class.LAEnvironment.methods."addObserver:".unsafe-reason = "The caller must keep the weakly held observer alive and synchronize its state with callbacks invoked on framework-private queues."
92+
class.LAEnvironment.methods."removeObserver:".unsafe = true
93+
class.LAEnvironment.methods."removeObserver:".unsafe-reason = "The caller must keep the observer alive during removal and synchronize mutable state shared with its callback on the framework-private queue."
94+
protocol.LAEnvironmentObserver.methods."environment:stateDidChangeFromOldState:".unsafe = true
95+
protocol.LAEnvironmentObserver.methods."environment:stateDidChangeFromOldState:".unsafe-reason = "Implementors must synchronize observer state with other calls because this callback can run on a framework-private queue."
96+
97+
class.LAAuthenticationRequirement.methods.init.unsafe = true
98+
class.LAAuthenticationRequirement.methods.init.unsafe-reason = "Callers must use documented factory methods because this initializer has no specified behavior for creating a usable requirement."
99+
class.LAAuthenticationRequirement.methods.new.unsafe = true
100+
class.LAAuthenticationRequirement.methods.new.unsafe-reason = "Callers must use documented factory methods because this constructor has no specified behavior for creating a usable requirement."
101+
class.LABiometryFallbackRequirement.methods.init.unsafe = true
102+
class.LABiometryFallbackRequirement.methods.init.unsafe-reason = "Callers must use documented factory methods because this initializer has no specified behavior for creating a usable requirement."
103+
class.LABiometryFallbackRequirement.methods.new.unsafe = true
104+
class.LABiometryFallbackRequirement.methods.new.unsafe-reason = "Callers must use documented factory methods because this constructor has no specified behavior for creating a usable requirement."
105+
class.LAPersistedRight.methods."initWithRequirement:".unsafe = true
106+
class.LAPersistedRight.methods."initWithRequirement:".unsafe-reason = "This inherited initializer must not be called on `LAPersistedRight` because instances are only obtained through `LARightStore`."
107+
108+
class.LARight.unsafe = true
109+
class.LARight.methods.init.unsafe-reason = "The caller must serialize construction with all concurrent `LocalAuthentication` operations in the process, including asynchronous completions, because it can construct an `LAContext`."
110+
class.LARight.methods."initWithRequirement:".unsafe-reason = "The caller must serialize construction with all concurrent `LocalAuthentication` operations in the process, including asynchronous completions, because it can construct an `LAContext`."
111+
class.LARight.methods.new.unsafe-reason = "The caller must serialize construction with all concurrent `LocalAuthentication` operations in the process, including asynchronous completions, because it can construct an `LAContext`."
112+
113+
class.LARight.methods.state.unsafe = true
114+
class.LARight.methods.state.unsafe-reason = "The caller must ensure no concurrent operation can update this right's state while reading it, including framework-managed authorization completions."
115+
class.LARight.methods.tag.unsafe-reason = "The caller must serialize reads and writes of this right's non-atomic `tag` property across threads."
116+
class.LARight.methods."setTag:".unsafe-reason = "The caller must serialize reads and writes of this right's non-atomic `tag` property across threads."

0 commit comments

Comments
 (0)