Skip to content

Commit 3306a6b

Browse files
committed
NSBundle: fix localization lookup races
1 parent caa0816 commit 3306a6b

3 files changed

Lines changed: 181 additions & 0 deletions

File tree

ChangeLog

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
2026-03-12 Frederik Seiffert <frederik@algoriddim.com>
2+
3+
* Source/NSBundle.m:
4+
Hold self alive while resolving localized strings and guard
5+
_localizations teardown in -dealloc with _localizationsLock to avoid
6+
concurrent access races.
7+
8+
* Tests/base/NSBundle/localization_concurrency.m: New file.
9+
Add a multithreaded regression test for concurrent localization
10+
lookups while repeatedly creating and releasing NSBundle instances.
11+
112
2026-03-12 Richard Frith-Macdonald <rfm@gnu.org>
213

314
* Source/GSHTTPURLHandle.m:

Source/NSBundle.m

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2287,7 +2287,14 @@ - (void) dealloc
22872287
TEST_RELEASE(_frameworkVersion);
22882288
TEST_RELEASE(_bundleClasses);
22892289
TEST_RELEASE(_infoDict);
2290+
/*
2291+
* Synchronize teardown with -localizedStringForKey:value:table:
2292+
* to prevent releasing _localizations while another thread reads/writes it.
2293+
*/
2294+
GS_MUTEX_LOCK(_localizationsLock);
22902295
TEST_RELEASE(_localizations);
2296+
_localizations = nil;
2297+
GS_MUTEX_UNLOCK(_localizationsLock);
22912298
[super dealloc];
22922299
}
22932300

@@ -2864,6 +2871,13 @@ - (NSString *) localizedStringForKey: (NSString *)key
28642871
NSDictionary *parsedTable = nil;
28652872
BOOL shouldLoad = NO;
28662873

2874+
/*
2875+
* Keep the bundle alive for the full duration of this lookup.
2876+
* This avoids races with concurrent release/dealloc on callers
2877+
* that do not hold a strong reference to the bundle object.
2878+
*/
2879+
IF_NO_ARC([self retain];)
2880+
28672881
// Lazily create and populate the per-bundle localization cache.
28682882
GS_MUTEX_LOCK(_localizationsLock);
28692883
if (_localizations == nil)
@@ -2963,6 +2977,7 @@ - (NSString *) localizedStringForKey: (NSString *)key
29632977
newString = @"";
29642978
}
29652979

2980+
IF_NO_ARC([self release];)
29662981
return newString;
29672982
}
29682983

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
#import "Testing.h"
2+
#import <Foundation/NSAutoreleasePool.h>
3+
#import <Foundation/NSBundle.h>
4+
#import <Foundation/NSCondition.h>
5+
#import <Foundation/NSDate.h>
6+
#import <Foundation/NSFileManager.h>
7+
#import <Foundation/NSPathUtilities.h>
8+
#import <Foundation/NSThread.h>
9+
#import <Foundation/NSString.h>
10+
11+
static NSString *bundlePath = nil;
12+
static NSCondition *condition = nil;
13+
static int remainingThreads = 0;
14+
static int invalidResultCount = 0;
15+
static int localizedResultCount = 0;
16+
static int fallbackResultCount = 0;
17+
18+
static const int workerCount = 8;
19+
static const int iterationsPerWorker = 2500;
20+
21+
@interface LocalizationWorker: NSObject
22+
+ (void) run: (id)unused;
23+
@end
24+
25+
@implementation LocalizationWorker
26+
27+
+ (void) run: (id)unused
28+
{
29+
NSAutoreleasePool *arp = [NSAutoreleasePool new];
30+
int localInvalid = 0;
31+
int localLocalized = 0;
32+
int localFallback = 0;
33+
int i;
34+
35+
for (i = 0; i < iterationsPerWorker; i++)
36+
{
37+
NSBundle *bundle = [[NSBundle alloc] initWithPath: bundlePath];
38+
NSString *str;
39+
40+
if (nil == bundle)
41+
{
42+
localInvalid++;
43+
continue;
44+
}
45+
46+
str = [bundle localizedStringForKey: @"test"
47+
value: @"test"
48+
table: nil];
49+
if ([str isEqualToString: @"42"])
50+
{
51+
localLocalized++;
52+
}
53+
else if ([str isEqualToString: @"test"])
54+
{
55+
localFallback++;
56+
}
57+
else
58+
{
59+
localInvalid++;
60+
}
61+
62+
str = [bundle localizedStringForKey: @"test"
63+
value: @"test"
64+
table: @"Localizable.strings"];
65+
if ([str isEqualToString: @"42"])
66+
{
67+
localLocalized++;
68+
}
69+
else if ([str isEqualToString: @"test"])
70+
{
71+
localFallback++;
72+
}
73+
else
74+
{
75+
localInvalid++;
76+
}
77+
78+
[bundle release];
79+
}
80+
81+
[condition lock];
82+
invalidResultCount += localInvalid;
83+
localizedResultCount += localLocalized;
84+
fallbackResultCount += localFallback;
85+
remainingThreads--;
86+
[condition signal];
87+
[condition unlock];
88+
89+
[arp release];
90+
}
91+
92+
@end
93+
94+
int main()
95+
{
96+
NSAutoreleasePool *arp = [NSAutoreleasePool new];
97+
NSString *path;
98+
NSBundle *bundle;
99+
NSDate *timeout;
100+
int i;
101+
102+
START_SET("NSBundle localizedStringForKey concurrency")
103+
104+
path = [[[[[NSFileManager defaultManager] currentDirectoryPath]
105+
stringByStandardizingPath] stringByAppendingPathComponent: @"Resources"]
106+
stringByAppendingPathComponent: @"TestBundle.bundle"];
107+
bundle = [NSBundle bundleWithPath: path];
108+
if (nil == bundle)
109+
{
110+
SKIP("test bundle not available");
111+
}
112+
113+
bundlePath = [path copy];
114+
condition = [NSCondition new];
115+
remainingThreads = workerCount;
116+
invalidResultCount = 0;
117+
localizedResultCount = 0;
118+
fallbackResultCount = 0;
119+
120+
for (i = 0; i < workerCount; i++)
121+
{
122+
[NSThread detachNewThreadSelector: @selector(run:)
123+
toTarget: [LocalizationWorker class]
124+
withObject: nil];
125+
}
126+
127+
timeout = [NSDate dateWithTimeIntervalSinceNow: 60.0];
128+
[condition lock];
129+
while (remainingThreads > 0)
130+
{
131+
if (NO == [condition waitUntilDate: timeout])
132+
{
133+
break;
134+
}
135+
}
136+
PASS(remainingThreads == 0, "all worker threads completed");
137+
[condition unlock];
138+
139+
PASS(invalidResultCount == 0,
140+
"localizedStringForKey returned only expected values");
141+
PASS(localizedResultCount > 0, "localized lookups succeeded");
142+
PASS((localizedResultCount + fallbackResultCount)
143+
== (workerCount * iterationsPerWorker * 2),
144+
"all lookups accounted for");
145+
146+
[condition release];
147+
condition = nil;
148+
[bundlePath release];
149+
bundlePath = nil;
150+
151+
END_SET("NSBundle localizedStringForKey concurrency")
152+
153+
[arp release];
154+
return 0;
155+
}

0 commit comments

Comments
 (0)