authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-18 11:01:23-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-18 11:01:23-07:00
log9118b6d3e868db161ca3f330c0a8011527348dce
tree39478b4b9dd655ab0badd8e9faf6dbebaf029497
parenteadeaf228267be215a2e9486f16b876d703dcccc
signature Signed by PGP key B5690EEEBB952194

fix(mobile): synchronize native permission registry access (#12482)


5 files changed, 168 insertions(+), 4 deletions(-)

apps/mobile/scripts/fixtures/PermissionsServiceRegression.m created+55
......@@ -0,0 +1,55 @@
1@interface TestPermissionRequester : NSObject <EXPermissionsRequester>
2@end
3
4@implementation TestPermissionRequester
5+ (NSString *)permissionType { return NSStringFromClass(self); }
6- (NSDictionary *)getPermissions { return @{ @"status": @(EXPermissionStatusGranted) }; }
7- (void)requestPermissionsWithResolver:(EXPromiseResolveBlock)resolve rejecter:(EXPromiseRejectBlock)reject {
8 resolve([self getPermissions]);
9}
10@end
11
12typedef struct {
13 __unsafe_unretained EXPermissionsService *service;
14 __unsafe_unretained NSArray *requesters;
15 size_t offset;
16} Worker;
17
18static void *exerciseRegistry(void *context) {
19 Worker *worker = context;
20 for (size_t index = 0; index < 250; index++) {
21 @autoreleasepool {
22 id<EXPermissionsRequester> requester = worker->requesters[(index + worker->offset) % worker->requesters.count];
23 [worker->service registerRequesters:@[requester]];
24 id resolved = [worker->service getPermissionRequesterForType:[[requester class] permissionType]];
25 assert(resolved == requester);
26 NSDictionary *permissions = [worker->service getPermissionUsingRequesterClass:[requester class]];
27 assert([permissions[@"status"] isEqualToString:@"granted"]);
28 }
29 }
30 return NULL;
31}
32
33int main(void) {
34 @autoreleasepool {
35 EXPermissionsService *service = [EXPermissionsService new];
36 NSMutableArray *requesters = [NSMutableArray new];
37 for (int i = 0; i < 32; i++) {
38 NSString *name = [NSString stringWithFormat:@"TestPermission%d", i];
39 Class cls = objc_allocateClassPair([TestPermissionRequester class], name.UTF8String, 0);
40 objc_registerClassPair(cls);
41 [requesters addObject:[cls new]];
42 }
43 Worker workers[8];
44 pthread_t threads[8];
45 for (size_t index = 0; index < 8; index++) {
46 workers[index] = (Worker){ service, requesters, index };
47 assert(pthread_create(&threads[index], NULL, exerciseRegistry, &workers[index]) == 0);
48 }
49 for (size_t index = 0; index < 8; index++) {
50 assert(pthread_join(threads[index], NULL) == 0);
51 }
52 puts("passed");
53 }
54 return 0;
55}
apps/mobile/scripts/permissions-service.test.ts created+68
......@@ -0,0 +1,68 @@
1// @effect-diagnostics nodeBuiltinImport:off - Compiles the native dependency regression directly.
2import * as NodeChildProcess from "node:child_process";
3import * as NodeFS from "node:fs";
4import * as NodeModule from "node:module";
5import * as NodeOS from "node:os";
6import * as NodePath from "node:path";
7import { expect, it } from "vite-plus/test";
8
9// oxlint-disable-next-line t3code/no-global-process-runtime -- This test compiles against the host Foundation framework.
10it.skipIf(NodeOS.platform() !== "darwin")(
11 "registers and reads native permissions concurrently without corrupting the registry",
12 () => {
13 const directory = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-permissions-test-"));
14 try {
15 const require = NodeModule.createRequire(import.meta.url);
16 const core = NodePath.dirname(
17 NodeModule.createRequire(require.resolve("expo/package.json")).resolve(
18 "expo-modules-core/package.json",
19 ),
20 );
21 const read = (relativePath: string) =>
22 NodeFS.readFileSync(NodePath.join(core, relativePath), "utf8").replace(
23 /^#import .*$/gm,
24 "",
25 );
26 const source = NodePath.join(directory, "regression.m");
27 NodeFS.writeFileSync(
28 source,
29 [
30 "#import <Foundation/Foundation.h>",
31 "#import <objc/runtime.h>",
32 "#include <assert.h>",
33 "#include <pthread.h>",
34 "typedef void (^EXPromiseResolveBlock)(id result);",
35 "typedef void (^EXPromiseRejectBlock)(NSString *, NSString *, NSError *);",
36 "#define RCTLogWarn(...)",
37 read("ios/Interfaces/Permissions/EXPermissionsInterface.h"),
38 read("ios/Legacy/Services/Permissions/EXPermissionsService.h"),
39 read("ios/Legacy/Services/Permissions/EXPermissionsService.m"),
40 NodeFS.readFileSync(
41 new URL("./fixtures/PermissionsServiceRegression.m", import.meta.url),
42 "utf8",
43 ),
44 ].join("\n"),
45 );
46 const executable = NodePath.join(directory, "regression");
47 NodeChildProcess.execFileSync(
48 "clang",
49 [
50 "-fobjc-arc",
51 "-fblocks",
52 "-fsanitize=thread",
53 "-framework",
54 "Foundation",
55 source,
56 "-o",
57 executable,
58 ],
59 { encoding: "utf8", timeout: 30_000 },
60 );
61 expect(
62 NodeChildProcess.execFileSync(executable, { encoding: "utf8", timeout: 15_000 }).trim(),
63 ).toBe("passed");
64 } finally {
65 NodeFS.rmSync(directory, { recursive: true, force: true });
66 }
67 },
68);
patches/expo-modules-core@57.0.14.patch created+36
......@@ -0,0 +1,36 @@
1diff --git a/ios/Legacy/Services/Permissions/EXPermissionsService.m b/ios/Legacy/Services/Permissions/EXPermissionsService.m
2--- a/ios/Legacy/Services/Permissions/EXPermissionsService.m
3+++ b/ios/Legacy/Services/Permissions/EXPermissionsService.m
4@@ -32,8 +32,11 @@
5
6 - (void)registerRequesters:(NSArray<id<EXPermissionsRequester>> *)newRequesters {
7 for (id<EXPermissionsRequester> requester in newRequesters) {
8- [_requesters setObject:requester forKey:[[requester class] permissionType]];
9- [_requestersByClass setObject:requester forKey:[requester class]];
10+ NSString *permissionType = [[requester class] permissionType];
11+ @synchronized (self) {
12+ [_requesters setObject:requester forKey:permissionType];
13+ [_requestersByClass setObject:requester forKey:[requester class]];
14+ }
15 }
16 }
17
18@@ -162,12 +165,16 @@
19
20 - (id<EXPermissionsRequester>)getPermissionRequesterForType:(NSString *)type
21 {
22- return _requesters[type];
23+ @synchronized (self) {
24+ return _requesters[type];
25+ }
26 }
27
28 - (id<EXPermissionsRequester>)getPermissionRequesterForClass:(Class)requesterClass
29 {
30- return [_requestersByClass objectForKey:requesterClass];
31+ @synchronized (self) {
32+ return [_requestersByClass objectForKey:requesterClass];
33+ }
34 }
35
36 @end
pnpm-lock.yaml+6-4
......@@ -71,6 +71,7 @@ overrides:
7171 '@expo/dom-webview': 57.0.1
7272 '@expo/metro-config': 57.0.12
7373 expo-constants: 57.0.16
74 expo-modules-core: 57.0.14
7475 '@pierre/diffs>@shikijs/transformers': ^4.2.0
7576 '@tailwindcss/node': 4.3.3
7677 '@tailwindcss/oxide': 4.3.3
......@@ -101,6 +102,7 @@ patchedDependencies:
101102 dbus-next@0.10.2: cfff57561b0ee59b5addb3b2e6c6f20906e967507a530ab67e8db8108e520ba4
102103 effect@4.0.0-rc.115: 0dfc4bb8ebd80fb3e06b91ef61346f5259517ab0f2437644fe95ae531084b1f5
103104 expo-audio@57.0.4: fa9a3e0442ed395d4071bb406e08c3a471c9a84700bdfa0b9ad7ff144c96041a
105 expo-modules-core@57.0.14: 6be7092f8c6f03a57b75c797d98b5f19ed62d2dd205bfc4c8adf4cef45655d7e
104106 expo-sharing@57.0.17: 8d2e3b10eb3f52036a9a086800180ec6cebf3b75bccc5b1775117a7244d4ac45
105107 expo-widgets@57.0.15: 319a9ded5db49c5b5215c511a138b33f44c7ea2972eb418192e8d5342fe75ce6
106108 react-native-gesture-handler@2.32.0: 0579f8e4dad02bf3183d95b02620358412983c36f9bda7425dc8bcb9643b5ce2
......@@ -17412,7 +17414,7 @@ snapshots:
1741217414 - supports-color
1741317415 - typescript
1741417416
17415 expo-modules-core@57.0.14(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6):
17417 expo-modules-core@57.0.14(patch_hash=6be7092f8c6f03a57b75c797d98b5f19ed62d2dd205bfc4c8adf4cef45655d7e)(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6):
1741617418 dependencies:
1741717419 '@expo/expo-modules-macros-plugin': 0.6.1
1741817420 expo-modules-jsi: 57.0.6(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))
......@@ -17423,7 +17425,7 @@ snapshots:
1742317425 react-native-worklets: 0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6)
1742417426 optional: true
1742517427
17426 expo-modules-core@57.0.14(react-native-worklets@0.11.4(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3):
17428 expo-modules-core@57.0.14(patch_hash=6be7092f8c6f03a57b75c797d98b5f19ed62d2dd205bfc4c8adf4cef45655d7e)(react-native-worklets@0.11.4(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3):
1742717429 dependencies:
1742817430 '@expo/expo-modules-macros-plugin': 0.6.1
1742917431 expo-modules-jsi: 57.0.6(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))
......@@ -17605,7 +17607,7 @@ snapshots:
1760517607 expo-font: 57.0.2(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
1760617608 expo-keep-awake: 57.0.2(expo@57.0.18)(react@19.2.3)
1760717609 expo-modules-autolinking: 57.0.12(typescript@7.0.2)
17608 expo-modules-core: 57.0.14(react-native-worklets@0.11.4(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
17610 expo-modules-core: 57.0.14(patch_hash=6be7092f8c6f03a57b75c797d98b5f19ed62d2dd205bfc4c8adf4cef45655d7e)(react-native-worklets@0.11.4(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
1760917611 pretty-format: 29.7.0
1761017612 react: 19.2.3
1761117613 react-native: 0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
......@@ -17647,7 +17649,7 @@ snapshots:
1764717649 expo-font: 57.0.2(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6)
1764817650 expo-keep-awake: 57.0.2(expo@57.0.18)(react@19.2.6)
1764917651 expo-modules-autolinking: 57.0.12(typescript@6.0.3)
17650 expo-modules-core: 57.0.14(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6)
17652 expo-modules-core: 57.0.14(patch_hash=6be7092f8c6f03a57b75c797d98b5f19ed62d2dd205bfc4c8adf4cef45655d7e)(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6)
1765117653 pretty-format: 29.7.0
1765217654 react: 19.2.6
1765317655 react-native: 0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6)
pnpm-workspace.yaml+3
......@@ -143,6 +143,8 @@ overrides:
143143 "@expo/dom-webview": 57.0.1
144144 "@expo/metro-config": 57.0.12
145145 expo-constants: 57.0.16
146 # Keep fresh resolution on the version carrying the permissions registry fix.
147 expo-modules-core: 57.0.14
146148 "@pierre/diffs>@shikijs/transformers": ^4.2.0
147149 # Keep one copy of these native-binary packages. lightningcss ships 11 platform binaries per
148150 # version, and the workspaces' tailwindcss ^4 ranges drift into two 4.3.x copies of the family.
......@@ -187,6 +189,7 @@ patchedDependencies:
187189 dbus-next@0.10.2: patches/dbus-next@0.10.2.patch
188190 effect@4.0.0-rc.115: patches/effect@4.0.0-rc.115.patch
189191 expo-audio@57.0.4: patches/expo-audio@57.0.4.patch
192 expo-modules-core@57.0.14: patches/expo-modules-core@57.0.14.patch
190193 expo-sharing@57.0.17: patches/expo-sharing@57.0.17.patch
191194 expo-widgets@57.0.15: patches/expo-widgets@57.0.15.patch
192195 react-native-gesture-handler@2.32.0: patches/react-native-gesture-handler@2.32.0.patch