
Debug Mobile App Sandbox & Security Policy Crashes
Mobile app sandbox violations and security policy enforcement failures represent one of the most frustrating — and often misdiagnosed — categories of production crashes. Unlike a null pointer dereference that points you straight to a line of code, sandbox crashes manifest as cryptic SIGABRT signals, opaque avc: denied logcat entries, or iOS Data Protection errors that only surface when the device is locked. These crashes are fundamentally different: they aren't bugs in your business logic — they're your app colliding with the operating system's security architecture. In this guide, we'll dissect the most common sandbox and security policy crash patterns across iOS and Android, walk through real debugging workflows for each, and show you how to instrument your app so these violations never reach production undetected.
Every modern mobile OS enforces strict per-app isolation boundaries. On iOS, the App Sandbox restricts filesystem access to a per-app container, mandates entitlement declarations for capabilities like network access or the camera, and layers Data Protection on top so files become inaccessible when the device locks. On Android, SELinux (Security-Enhanced Linux) enforces mandatory access control policies at the kernel level, and Android 10's scoped storage overhaul fundamentally changed how apps interact with the filesystem. When your app steps outside these boundaries — whether by accessing a file outside its container, calling a restricted API without declaring the right entitlement, or touching a Data-Protected file while the device is locked — the OS terminates your process. The crash report you get rarely explains what security policy was violated. Understanding the signals is the hard part.
iOS App Sandbox Violations
The iOS sandbox is built on a set of layered protections. At the base is the container directory: every app gets its own slice of the filesystem under /var/mobile/Containers/Data/Application/<UUID>/, and any attempt to read or write outside this container triggers a sandbox denial followed by SIGABRT. This most commonly happens when developers use hardcoded paths or interact with shared resources incorrectly.
Consider a scenario where your app tries to write a log file to a shared directory. On a simulator or a jailbroken device, this works fine. In production, the sandbox kills your process:
// DANGEROUS: Writing outside the app container
let sharedPath = "/var/mobile/Documents/app_debug.log"
try "log data".write(toFile: sharedPath, atomically: true, encoding: .utf8)
// → SIGABRT: Sandbox: deny(1) file-write-create /var/mobile/Documents/app_debug.logThe fix is always to use Apple's sandbox-compliant APIs. On iOS, FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) returns a path inside your container. For shared data between apps from the same developer, App Groups provide a container accessible to all members:
// SAFE: App Group shared container
let sharedURL = FileManager.default.containerURL(
forSecurityApplicationGroupIdentifier: "group.com.yourcompany.app"
)A subtler failure mode involves App Sandbox entitlements. Apple requires you to declare capabilities like incoming network connections, camera access, or location services as entitlements in your provisioning profile — not just as Info.plist usage description strings. If your app uses a capability that isn't in the entitlements, the sandbox denies it silently in some cases and crashes in others. The classic symptom: a feature works on the Simulator because the macOS kernel doesn't enforce iOS sandbox rules, but crashes on a physical device with a terse EXC_CRASH (SIGABRT) and no obvious stack trace pointing to your code.
<!-- Required in your .entitlements file -->
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>Data Protection File Access Crashes
Data Protection is Apple's file-level encryption system that ties file accessibility to the device lock state. Files can be created with protection levels like .completeUntilFirstUserAuthentication, .complete (only accessible when unlocked), or .none. The crash pattern here is insidious: your app works perfectly during development because you're actively using the device, but background tasks — push notification handlers, background fetch, HealthKit queries — crash because they attempt to read a Data-Protected file while the device is locked.
// This crashes in the background if the device is locked
// and the file was created with .complete protection
let url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
.appendingPathComponent("config.json")
let data = try Data(contentsOf: url)
// → SIGABRT: Could not read from file, protection level prevents accessTo debug this, check the protection level of files your background code touches. You can inspect a file's protection class using NSFileProtectionKey:
let attrs = try FileManager.default.attributesOfItem(atPath: path)
if let protection = attrs[.protectionKey] as? FileProtectionType {
print("Protection level: \(protection.rawValue)")
}For files that background processes need to access, use .completeUntilFirstUserAuthentication — it decrypts after first unlock and stays accessible even when the device re-locks. Reserve .complete only for truly sensitive data that should never be accessible in the background.
Keychain Access Control Failures
The iOS Keychain is protected by its own access control flags (kSecAttrAccessible), and the most common crash trigger is attempting Keychain operations with .kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly or similar restrictive flags in a background context. The Keychain returns errSecInteractionNotAllowed (-25308) when the device state doesn't match the access requirements — and if your code doesn't handle this gracefully, it escalates to a crash:
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: "authToken",
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
kSecReturnData as String: true
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
// status == errSecInteractionNotAllowed when device is locked
// Crash if we force-unwrap result without checking statusAlways check the OSStatus return value from Keychain APIs. For background-accessible credentials, use kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly:
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnlyAndroid SELinux Denials and AVC Violations
On Android, SELinux operates at the kernel level, enforcing mandatory access control policies defined by the sepolicy files shipped in each Android build. When your app's process attempts an operation the policy forbids — accessing a restricted file, binding to a privileged port, executing a system call — SELinux generates an "AVC denied" (Access Vector Cache) log entry and blocks the operation. For native code, this often triggers a SIGSEGV or SIGABRT because the system call fails in a way the code doesn't handle.
The signature of an SELinux crash in logcat looks like this:
avc: denied { read write } for pid=12345 comm="com.yourapp" name="config" dev="mmcblk0p25" ino=67890 scontext=u:r:untrusted_app:s0:c512,c768 tcontext=u:object_r:system_data_file:s0 tclass=file permissive=0This says: your app (untrusted_app domain) tried to read/write a file labeled system_data_file, and SELinux denied it. The key fields are scontext (source context — your app's security label), tcontext (target context — the resource's label), and tclass (the type of resource).
To pull AVC denials programmatically:
adb logcat -b all | grep "avc: denied"Or filter for your specific package:
adb logcat -b all | grep "avc: denied" | grep "com.yourapp"Scoped Storage Crashes (Android 10+)
Android 10 introduced scoped storage, which changed the default behavior so apps can only access their own app-specific directory and shared media collections through the MediaStore API — not by raw filesystem paths. Legacy apps targeting API 28 or below could opt out with requestLegacyExternalStorage, but this flag is ignored on Android 11+. The result: file I/O that worked for years suddenly throws FileNotFoundException or SecurityException.
The canonical crash looks like this:
// BROKEN on Android 11+: Scoped storage blocks direct access
File file = new File(Environment.getExternalStorageDirectory(), "downloads/data.json");
FileInputStream fis = new FileInputStream(file);
// → java.io.FileNotFoundException: /storage/emulated/0/downloads/data.json:
// open failed: EACCES (Permission denied)The migration path uses MediaStore or the Storage Access Framework (SAF). For app-private files, use context.getExternalFilesDir():
// SAFE: App-specific external storage
File appDir = context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS);
File file = new File(appDir, "data.json");For shared media, use MediaStore:
ContentValues values = new ContentValues();
values.put(MediaStore.Downloads.DISPLAY_NAME, "data.json");
values.put(MediaStore.Downloads.MIME_TYPE, "application/json");
Uri uri = context.getContentResolver()
.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values);FileProvider Permission Crashes
Android's FileProvider is the sanctioned way to share files between apps, replacing the deprecated file:// URIs with content:// URIs. The most frequent crash here is a SecurityException thrown when the receiving app lacks the grant URI permission — or when the FileProvider XML configuration doesn't include the right paths:
<!-- res/xml/file_paths.xml -->
<paths>
<cache-path name="shared_images" path="images/" />
<files-path name="shared_docs" path="docs/" />
</paths>If your code generates a URI for a path not covered by file_paths.xml, the provider throws an IllegalArgumentException:
// Crash if "exports" directory isn't in file_paths.xml
File file = new File(context.getFilesDir(), "exports/report.pdf");
Uri uri = FileProvider.getUriForFile(context,
"com.yourapp.fileprovider", file);
// → IllegalArgumentException: Failed to find configured rootAlways align your file_paths.xml with every directory you share. Use android:grantUriPermissions="true" on the provider in your manifest:
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.yourapp.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>Debugging Workflow for Security Policy Crashes
When you encounter a crash that smells like a security policy violation — non-deterministic, device-state-dependent, no obvious code path failure — follow this triage sequence:
1. Identify the crash signal. On iOS, look for SIGABRT with "Sandbox" or "deny" in the crash log. On Android, search logcat for avc: denied entries correlated to your crash timestamp. Both Apple's crash log documentation and Android's logcat reference provide detailed parsing guidance.
2. Reproduce with security auditing enabled. On Android, temporarily set SELinux to permissive mode on a rooted test device (setenforce 0) to log denials without killing the process — this lets you see all violations at once. On iOS, use the log stream command to capture sandbox violations in real time:
log stream --predicate 'subsystem == "com.apple.sandbox"' --level debug3. Map the violation to the API. An avc: denied for tclass=file with tcontext=media_rw_data_file points to scoped storage. An iOS sandbox deny(1) file-write-create means your path is outside the container. Don't guess — use the security context labels as your map.
4. Check entitlement and manifest declarations. For iOS, verify your .entitlements file matches every restricted capability your app uses. For Android, review your AndroidManifest.xml permissions and <provider> configurations against the Android permissions reference.
5. Test across device states. Data Protection crashes only happen with locked devices. SELinux policies differ across OEMs — Samsung, Xiaomi, and OnePlus all ship customized sepolicy files. Test your sandbox-sensitive code paths on locked devices and across at least three major OEM builds.
Proactive Prevention with Bugspulse
Catching sandbox violations before they hit production requires observability that understands security-layer signals. Bugspulse captures the full crash context — including iOS sandbox denial messages and Android AVC denied log entries — and correlates them with user sessions so you can see exactly what sequence of actions triggered the policy violation. Instead of grep'ing through raw device logs after a user complaint, you get the violation surfaced as a first-class crash event with the security context labels preserved.
This is especially critical for apps that already rely on our platform for debugging related crash categories — for example, our guide on debugging mobile data storage and file system crashes covers the filesystem side of access failures, and combining that knowledge with sandbox-aware monitoring closes the last blind spot in your crash detection pipeline.
Sandbox and security policy violations are a distinct category of mobile crash — they emerge from the OS security layer, not from application logic. They're sensitive to device lock state, OEM SELinux policy customizations, and entitlement configuration drift. By understanding the signals each platform emits and instrumenting your app to capture security-layer context, you can eliminate one of the most stubborn sources of production instability.
Ready to catch sandbox violations before your users do? Start your free BugsPulse trial and get full visibility into every crash — including the security-layer signals other crash reporters miss.