Problem summary
I have a macOS helper app that is launched from a sandboxed main app. The helper:
- has
com.apple.security.app-sandbox = true
andcom.apple.security.inherit = true
in its entitlements, - is signed and embedded inside the main app bundle (placed next to the main executable in
Contents/MacOS
), - reports
entitlement_check = 1
(code signature contains sandbox entitlement, implemented via SecStaticCode… check), sandbox_check(getpid(), NULL, 0)
returns1
(runtime sandbox enforcement present),APP_SANDBOX_CONTAINER_ID
environment variable is not present (0).
Despite that, Cocoa APIs return non-container home paths:
NSHomeDirectory()
returns/Users/<me>/
(the real home).[[NSFileManager defaultManager] URLsForDirectory:inDomains:]
andURLForDirectory:inDomain:appropriateForURL:create:error:
return paths rooted at/Users/<me>/
(not under~/Library/Containers/<app_id>/Data/...
) — i.e. they look like non-sandboxed locations.
However, one important exception: URLForDirectory:...
for NSItemReplacementDirectory
(temporary/replacement items) does return a path under the helper's container (example: ~/Library/Containers/<app_id>/Data/tmp/TemporaryItems/NSIRD_<helper_name>_hfc1bZ
).
This proves the sandbox is active for some FileManager APIs, yet standard directory lookups (Application Support, Documents, Caches, and NSHomeDirectory()
) are not being redirected to the container.
What I expect
The helper (which inherits the sandbox and is clearly sandboxed) should get container-scoped paths from Cocoa’s FileManager APIs (Application Support, Documents, Caches), i.e. paths under the helper’s container: /Users/<me>/Library/Containers/<app_id>/Data/...
.
What I tried / diagnostics already gathered
Entitlements & code signature
codesign -d --entitlements :- /path/to/Helper.app/Contents/MacOS/Helper
# shows com.apple.security.app-sandbox = true and com.apple.security.inherit = true
Runtime checks (Objective-C++ inside helper):
extern "C" int sandbox_check(pid_t pid, const char *op, int flags);
NSLog(@"entitlement_check = %d", entitlement_check()); // SecStaticCode check
NSLog(@"env_variable_check = %d", (getenv("APP_SANDBOX_CONTAINER_ID") != NULL));
NSLog(@"runtime_sandbox_check = %d", sandbox_check(getpid(), nullptr, 0));
NSLog(@"NSHomeDirectory = %s", NSHomeDirectory());
NSArray *urls = [[NSFileManager defaultManager]
URLsForDirectory:NSApplicationSupportDirectory
inDomains:NSUserDomainMask];
NSLog(@"URLsForDirectory: %@", urls);
Observed output:
entitlement_check = 1
env_variable_check = 0
runtime_sandbox_check = 1
NSHomeDirectory = /Users/<me>
URLsForDirectory: ( "file:///Users/<me>/Library/Application%20Support/..." )
Temporary/replacement directory (evidence sandbox active for some APIs):
NSURL *tmpReplacement = [[NSFileManager defaultManager]
URLForDirectory:NSItemReplacementDirectory
inDomain:NSUserDomainMask
appropriateForURL:nil
create:YES
error:&err];
NSLog(@"NSItemReplacementDirectory: %@", tmpReplacement.path);
Observed output (example):
/Users/<me>/Library/Containers/<app_id>/Data/tmp/TemporaryItems/NSIRD_<helper_name>_hfc1bZ
Other facts
- Calls to
NSHomeDirectory()
andURLsForDirectory:
are made aftermain()
to avoid "too early" initialization problems. - Helper is placed in
Contents/MacOS
(notContents/Library/LoginItems
). - Helper is a non-GUI helper binary launched by the main app (not an XPC service).
- macOS version: Sequoia 15.6
Questions
-
Why do
NSHomeDirectory()
andURLsForDirectory:
return the real/Users/<me>/...
paths in a helper process that is clearly sandboxed (entitlement + runtime enforcement), whileNSItemReplacementDirectory
returns a container-scoped temporary path? -
Is this behavior related to how the helper is packaged or launched (e.g., placement in
Contents/MacOS
vsContents/Library/LoginItems
, or whether it is launched withposix_spawn
/fork+exec
vs other APIs)? -
Are there additional entitlements or packaging rules required for a helper that inherits sandbox to have Cocoa directory APIs redirected to the container (for Application Support, Documents, Caches)?
*Thanks in advance — I can add any requested logs
I finally solved it! It is environment variables that determine the behavior.
The helper app was designed to accept some environment variables given by caller, but in our latest developement implementation, only those environment variables are given, without those from electron app.
After I added them back, those APIs work as expected.
Thanks for your helpful assistance!