iOS app running in "Mac (Designed for iPad)" mode fails to write files — NSDocumentDirectory and NSLibraryDirectory are read-only

Summary

When an iOS binary is run on Apple Silicon Mac in “Mac (Designed for iPad)” compatibility mode, FIOSPlatformFile::ConvertToPlatformPath() and FIOSPlatformMisc::GamePersistentDownloadDir() resolve write paths to

NSDocumentDirectory and NSLibraryDirectory, which are read-only in this sandbox configuration. All file write attempts to those directories fail with EPERM (errno=1).

Steps to Reproduce

1. Build an iOS target (iphoneos SDK, arm64) for any Unreal project

2. Install and run it on an Apple Silicon Mac via iOS App Installer (the “Mac (Designed for iPad)” path)

3. Attempt any file write through UE’s IFileManager or FPlatformFileManager (e.g. config, save games, persistent download directory)

Expected Result

Write succeeds. On a physical iOS device, NSDocumentDirectory is writable.

Actual Result

Write fails with Operation not permitted (errno=1). Under Mac-iPad compatibility mode, macOS maps NSDocumentDirectory and NSLibraryDirectory to a read-only location inside the .app bundle container. Only

NSApplicationSupportDirectory is writable.

Root Cause

Two sites in the iOS platform layer are affected:

1. IOSPlatformFile.cpp — ConvertToPlatformPath(): The bForWrite branch unconditionally resolves to NSDocumentDirectory (public) or NSLibraryDirectory (private). On Mac-iPad, both are read-only. Additionally,

paths that callers have already resolved to an absolute container path (e.g. via NSSearchPathForDirectoriesInDomains) are re-processed and mangled by the ReplaceInline(“../”) strip logic.

2. IOSPlatformMisc.cpp — GamePersistentDownloadDir(): Returns a relative path (e.g. ../../../MyProject/PersistentDownloadDir) regardless of platform mode. Callers that use this path directly via POSIX mkdir

(bypassing IOSPlatformFile) attempt to create directories relative to the read-only .app bundle location.

Confirmed via runtime logging that all write attempts return errno=1 until redirected to

NSApplicationSupportDirectory.

Fix

Detect Mac-iPad mode via [[NSProcessInfo processInfo] isiOSAppOnMac] (available iOS 14+ / macOS 11+):

- In ConvertToPlatformPath(): (a) return already-absolute container paths unchanged, and (b) redirect write paths to NSApplicationSupportDirectory instead of NSDocumentDirectory/NSLibraryDirectory.

- In GamePersistentDownloadDir(): redirect the base path to NSApplicationSupportDirectory and return the resulting absolute path (not the relative path) so POSIX callers also write to the correct location.

Affected Versions

Verified on UE 5.x (tested against UE 5.7 / SDK iphoneos26.1). The issue is present in any UE version that supports iOS deployment without explicit Mac-iPad handling.

Platform

macOS 15.x (Sequoia), Apple Silicon (arm64), iOS app in “Mac (Designed for iPad)” mode

Workaround

None available without engine changes. The issue manifests as silent failure (EPERM) on any persistent storage write, breaking save games, config, download caches, and any plugin that uses FPlatformFileManager.

Attachments

Proposed fix (two files):

- Engine/Source/Runtime/Core/Private/IOS/IOSPlatformFile.cpp

- Engine/Source/Runtime/Core/Private/IOS/IOSPlatformMisc.cpp

[Attachment Removed]

Steps to Reproduce
1. Build an iOS target (iphoneos SDK, arm64) for an Unreal project

2. Install and run it on an Apple Silicon Mac via iOS App Installer (the “Mac (Designed for iPad)” path)

3. Attempt any file write through UE’s IFileManager or FPlatformFileManager (e.g. config, save games, persistent download directory)

I can provide a repro project if needed but it’s a bit tricky to set it up for iOS.

[Attachment Removed]

Hi Guy,

That is odd behavior you are describing. I suspect the issue may be caused by how the app is being distributed/installed internally as iPad app on Mac should run under the same sandbox as they do on device.

Running a template of an iOS app on Mac on Vanilla 5.7 locally, I am observing that both save files, creation of directories both via IFileManager and POSIX APIs (after path conversion via ConvertToPlatformPath) appearsto function as intended (running from Xcode/UnrealEd or Packaged output). The files and directories are created as expected on the the specific App’s sandbox under the resolved NSDocumentDirectory which ends up on Mac to be at ~/Library/Containers/<UUID>/Data/Documents/… outside of the app bundle and in the Containers subdirectory of the user home directory where other apps’ sandboxes are present as well.

IFileManager::Get().MakeDirectory( *( FString( FPlatformMisc::GamePersistentDownloadDir() ) / TEXT("PersistentSubDir") ) );yields

ls /Users/stephane.jacoby/Library/Containers/28ED0A9E-046B-4A58-B4EF-80F253863565/Data/Documents/TopDown57CPP/PersistentDownloadDir
BackgroundHttp		PersistentSubDir

for example.

Writing to the app Sandbox is a requirement on both the iOS and Mac App Stores. Is this occurring with a specific build config? Is the app signed provisioned for Development, AdHoc or Distribution? Do you observe the same behaviour if you just launch the development packaged app on the device where it was built rather than putting it through the iOS App Installer? What directory does NSDocumentDirectory resolve to when the app is run on Mac under the error conditions?

Best regards.

[Attachment Removed]

Hi Stéphane,

We have logs showing the failure. Before our fix, ConvertToPlatformPath was not resolving the path in our distributed builds, leaving a relative path (../../../[AppName]/PersistentDownloadDir/…) that the

sandbox rejects with EPERM:

Error code 1 for path ‘../../../[AppName]’: Operation not permitted

After our fix, the path resolves correctly to the absolute container path under NSApplicationSupportDirectory:

~/Library/Containers/<UUID>/Data/Library/Application Support/[AppName]/PersistentDownloadDir/…

This is why your template test passed — running from Xcode sets the working directory inside the container, so the relative path happened to resolve. Our CI-built, AdHoc-distributed builds do not have that

working directory set, so the relative path escapes the sandbox entirely.

We’ve since confirmed this in additional testing — the fix resolves the issue and we were able to get through login, patching, and into gameplay.

To answer your other questions: builds are produced by our CI system and distributed internally via AdHoc provisioning, not installed directly from Xcode.

We reproduced the failure in a DebugGame build distributed via AdHoc; we have not yet tested a Shipping config specifically, but the fix applies unconditionally across all configurations.

Best,

Guy

[Attachment Removed]

Hi Guy,

The intended behaviour of GamePersistentDownloadDir is to create the directory internally but return a UE relative path for use across IFileManager APIs. The fix you’ve attached alters this behaviour to return an absolute IOS path and later guards against that change in ConvertToPlatformPath. A preferable usage pattern would be to leave internal engine behaviour unchanged and convert your paths to platform paths at the application level prior to direct use with POSIX APIs. This can be achieved as follows:

FString absPathForPOSIX = IFileManager::Get().ConvertToAbsolutePathForExternalAppForWrite( *( FString( FPlatformMisc::GamePersistentDownloadDir() ) / TEXT("PersistentSubDir2") ) );
	mkdir( TCHAR_TO_UTF8(*absPathForPOSIX), S_IRWXU );

As such, we would not consider integration of the changes that were originally attached and would suggest keeping the file system behaviour consistent across iOS and iPad (Designed for Mac).

Best regards.

[Attachment Removed]