FileManager.copyItem Permission Bug in iOS 18.1.1 with iCloud Shared Folders

Hi there,

I've encountered a file permission bug in iOS 18.1.1 when using FileManager.default.copyItem(at:to:) to copy files from an iCloud shared folder to the app sandbox. This issue occurs under the following conditions:

  • The source file resides in an iCloud shared folder.
  • The iCloud shared folder is owned by another iCloud user and shared with read-only permissions.
  • The app copies the file to its sandbox using the copyItem(at:to:) method.

Observed Behavior:

After copying the file to the app sandbox, the original file's read-only permissions are propagated to the copied file. This results in an inability to delete the copied file from the app sandbox, with the following error message:

NSCocoaErrorDomain, Code 513: "The file couldn’t be removed because you don’t have permission to access it."

Steps to Reproduce:

  • Access a shared iCloud folder owned by another user with read-only permissions.
  • Copy a file from this folder to the app sandbox using FileManager.default.copyItem(at:to:).
  • Attempt to delete the copied file within the app sandbox.

Workaround: Until this issue is resolved, the bug can be avoided by initializing the UIDocumentPickerViewController with the asCopy: true parameter:

UIDocumentPickerViewController(forOpeningContentTypes: contentTypes, asCopy: true)

This ensures that the copied file does not inherit the original permissions from the shared source file.

Example Project: To reproduce the issue and observe the error, I’ve created a sample project hosted on GitHub: https://github.com/giomurru/FileDeletePermissionBug

This project provides step-by-step instructions for testing and reproducing the bug.

Environment:

iOS/iPadOS Version: 18.1.1 Devices: [iPhone 15, iPad 9th Gen] Development Tool: Xcode 16.1

I hope this helps Apple engineers and other developers experiencing the same issue. Feedback or additional insights would be appreciated.

Giovanni

Answered by DTS Engineer in 817516022

I've encountered a file permission bug in iOS 18.1.1 when using FileManager.default.copyItem(at:to:) to copy files from an iCloud shared folder to the app sandbox.

This isn't a bug. By design, whenever possible, NSFileManager attempts to preserve the permissions of the files it copies and that's what's happened here. However, what you've overlooked here is the permissions don't have have to STAY that way. You have write access to the containing directory (otherwise, you wouldn't have been able to copy the item at all). You can change the permission to whatever you want with setAttributes(_:ofItemAtPath:) or with chmod().

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

I’d like to add that I tested the same code on an iPhone X running iOS 16.7.10, and the bug does not occur on that version.

I've encountered a file permission bug in iOS 18.1.1 when using FileManager.default.copyItem(at:to:) to copy files from an iCloud shared folder to the app sandbox.

This isn't a bug. By design, whenever possible, NSFileManager attempts to preserve the permissions of the files it copies and that's what's happened here. However, what you've overlooked here is the permissions don't have have to STAY that way. You have write access to the containing directory (otherwise, you wouldn't have been able to copy the item at all). You can change the permission to whatever you want with setAttributes(_:ofItemAtPath:) or with chmod().

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

I apologize for contradicting you, but I believe there might be a bug. When I use the same code FileManager.default.copyItem(at:to:) with iOS 16.7.10 to copy a file into the app sandbox, the app can successfully delete the copied file.

After further testing, I discovered that the issue may not be related to file permissions. When importing files using UIDocumentPickerViewController (regardless of whether the asCopy flag is set to true or false), the files are assigned 600 permissions. This should be sufficient since the app is the owner of the file, granting it both read and write access, including the ability to delete the file.

However, in iOS 18.1.1, an issue occurs when the app copies a file imported using UIDocumentPickerViewController with the asCopy flag set to false. When such a file is copied into the app sandbox, the app is unable to delete the file or modify its attributes. Strangely, the group and owner IDs of the file appear identical to those of files imported with the asCopy flag set to true that the app is able to delete.

To provide more context, I have created a branch on the GitHub repository that logs the file attributes: https://github.com/giomurru/FileDeletePermissionBug/tree/permissions-log.

This inconsistency in behavior is perplexing and suggests there may be an underlying issue beyond permissions or ownership. I believe this warrants further investigation.

Have a nice day,

Giovanni

I’m gonna let Kevin respond more completely but I wanted to address this:

600 permissions … granting it both read and write access, including the ability to delete the file.

That’s wrong. From the perspective of BSD permissions, you need write permission on the parent directory to delete a file. Consider:

% mkdir test
% touch test/all.txt
% touch test/none.txt
% chmod 666 test/all.txt 
% chmod 000 test/none.txt 
% chmod 555 test
% rm test/all.txt 
rm: test/all.txt: Permission denied
% rm test/none.txt  
rm: test/none.txt: Permission denied
% chmod 777 test 
% rm test/all.txt  
% rm test/none.txt 
% 

Keep in mind that BSD permissions aren’t the only thing in play here; see On File System Permissions.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

You’re absolutely correct that write permissions on the parent directory are required to delete a file, and your example clearly demonstrates this. I’d like to add to the discussion with another perspective on folder permissions and their impact on file operations.

If the directory where I’m copying a file has permissions set to 555, it wouldn’t be possible to copy the file into it because write permissions are not allowed. Here’s an example to illustrate:

mkdir test
chmod 555 test
touch test/file.txt

When you run this, you’ll encounter the error:

touch: test/file.txt: Permission denied

This behavior reinforces your point about needing proper permissions on the parent directory to modify or delete files within it. Similarly, copying or creating new files also requires write access.

In my case, I’m working with the temporary directory provided by FileManager.default.temporaryDirectory, which has more permissive settings 755. This avoids the issues seen with 555 permissions.

I’ll definitely take a closer look at the "On File System Permissions" post that you linked. Thank you for pointing it out and for your detailed explanation!

I apologize for contradicting you, but I believe there might be a bug. When I use the same code FileManager.default.copyItem(at:to:) with iOS 16.7.10 to copy a file into the app sandbox, the app can successfully delete the copied file.

SO, expanding a bit on my previous answer, there are actually two different issues that make the actual behavior here somewhat unpredictable.

First off, in the simplest case, this is a classic example of a "dueling bug" issue, where whatever behavior you pick will end up generating a new set of bugs about how the system is broken. That is (in simplified terms):

  1. In iOS 16, we change existing permissions to ensure files are writeable by the receiving app. We then get bugs complaining about how the system has destroyed the permission configuration the user carefully configured. We fix that..

  2. And now you've filed a bug saying that we're providing files that can't be modified.

There isn't really any "solution" here, we just have to pick one of those choices and stick with it to provide a stable solution. I'd argue that preserving permission is the better of the two options (it can be difficult to try and sync up the permission of two hierarchies), but the point is our only option is to "pick one" and stick with it.

Second, the actual behavior here is more complicated than it looks. Off the top of my head:

  • The UNIX subsystem has it's own architecture (which I won't try and summarize) that can alter what a process actually write to disk.

  • The system intentionally ignores the permissions of external volumes, assuming that they won't apply to the current system.

  • iOS doesn't really support user/owner/group permissions, even though that's part of most of the file systems it's interacting with.

The point here is that it can be very easy to set up a test that looks like it's showing one behavior, when what's actually going on is something else.

Finally, that last point leads back to here:

When such a file is copied into the app sandbox, the app is unable to delete the file or modify its attributes. Strangely, the group and owner IDs of the file appear identical to those of files imported with the asCopy flag set to true that the app is able to delete.

On iOS at least, this isn't actually possible. That is, while you can set a file/directory to read-only, you can't does that in a way that you can't also "undo". What actually cause something like this:

When such a file is copied into the app sandbox, the app is unable to delete the file or modify its attributes.

...is that the directory it was copied into/with was ALSO set as read-only. Changing the directories permissions will then allow you to modify the file. This might need to be done recursively (the directory is read-only, so change it first, etc.) but because there's only one "user", there isn't anyway for you to "get" a file that you can't modify the permissions of, assuming you modify it's parent first.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

After updating to iOS 18.2, the issue I’ve been experiencing remains unresolved. To ensure the issue wasn’t related to the temporary folder permissions, I took the following steps:

  • Created a custom temporary folder inside the app’s existing temporary folder.
  • Used the UIDocumentPickerViewController to import a file and copied it into the newly created custom temporary folder.

Unfortunately, this approach did not resolve the problem. Even after successfully copying the file into the custom folder, I am still unable to delete it.

You have to understand that when I import a file using the UIDocumentPickerController, I expect the provided API to enable me to successfully copy the returned file into the app sandbox in such a way that I am then able to delete it.

This operation appears to work correctly.

do {
  let securityScoped = sourceItemURL.startAccessingSecurityScopedResource()
  try FileManager.default.copyItem(at: sourceItemURL, to: temporaryItemURL)
  print("INFO: Source item copied to temporary directory successfully")
  currentItemURL = temporaryItemURL
  if securityScoped {
    sourceItemURL.stopAccessingSecurityScopedResource()
  }
} catch {
  print("ERROR: Could not copy source item to temporary directory: \(error)")
  return
}

Naturally, as the owner of the newly copied file, I also expect to be able to delete it without any issues. However this is not happening, because when I do this:

guard let currentItemURL = currentItemURL else {
    return
}
do {
    try FileManager.default.removeItem(at: currentItemURL)
    self.currentItemURL = nil
} catch {
    errorDeletingItem(error: error as NSError, temporaryItemURL: currentItemURL)
    return
}

This is the error I get.

ERROR: “IMG_6897.png” couldn’t be removed because you don’t have permission to access it.

Error code: 513

Error domain: NSCocoaErrorDomain

Error userInfo: ["NSUserStringVariant": <__NSSingleObjectArrayI 0x301efc3c0>( Remove ) , "NSUnderlyingError": Error Domain=NSPOSIXErrorDomain ?> Code=1 "Operation not permitted", "NSFilePath": /private/var/mobile/Containers/Data/Application/EB178B53-1BBA-47FF-9DFD-AA4F0419940C/tmp/MyTemporaryFolder/IMG_6897.png, "NSURL": file:///private/var/mobile/Containers/Data/Application/EB178B53-1BBA-47FF-9DFD-AA4F0419940C/tmp/MyTemporaryFolder/IMG_6897.png]

Please show me the correct way to copy a file (which belongs to an iCloud readonly shared folder) opened with the document picker initialized as UIDocumentPickerViewController(forOpeningContentTypes: [.movie, .image], asCopy: false), so that I can subsequently delete it.

Giovanni

After updating to iOS 18.2, the issue I’ve been experiencing remains unresolved. To ensure the issue wasn’t related to the temporary folder permissions, I took the following steps:

I was able to get a bit of extra time today, so I downloaded and tested your sample app and I was unable to replicate your issue. There was no issue deleting files, both standard files and files that were specifically shared as view only. I don't know what's different about your configuration, but I believe the issue is specific to whatever data you're working with.

Please show me the correct way to copy a file (which belongs to an iCloud readonly shared folder) opened with the document picker initialized as UIDocumentPickerViewController(forOpeningContentTypes: [.movie, .image], asCopy: false), so that I can subsequently delete it.

Please post back the full contents of the dictionary returned by "attributesOfItem(atPath:)" when called:

  • On "MyTemporaryFolder" after it's created but before the copy occurs.

  • On "MyTemporaryFolder" after the copy has occurred.

  • On the source item you're copying.

  • On the new item you've created by copying.

Post that data back here and I'll see if I can determine what's going on.

__
Kevin Elliott
DTS Engineer

The shared folder must be created by another iCloud user. You can ask someone to share an iCloud folder with you and then test again. The folder must be shared as View Only.

Thank you.

Giovanni

Accepted Answer

The shared folder must be created by another iCloud user. You can ask someone to share an iCloud folder with you and then test again. The folder must be shared as View Only.

Yes, though I shared and individual file instead of a directory. That apparently made a difference and I've replicated the issue.

In any case, if you print out the files attributes, you'll find that the files permissions are fine (384 -> 0600)* but the immutable bit is set:

*Or whatever the permissions of the object happen to be.

Attr: [
...
__C.NSFileAttributeKey(_rawValue: NSFilePosixPermissions): 511, 
...
__C.NSFileAttributeKey(_rawValue: NSFileImmutable): 1,
]

NSFileImmutable corresponds to the combination of "UF_IMMUTABLE" and "SF_IMMUTABLE" described in "man 2 chflags".

In any case, clearing the immutable flag allows the file to be deleted. In code form, that makes your deleteFile:

    @objc func deleteFile(_ sender: UIButton) {
        guard let currentItemURL = currentItemURL else {
            return
        }
        do {
            print("INFO: deleting file \(currentItemURL.lastPathComponent)")
            var attributes = try FileManager.default.attributesOfItem(atPath: currentItemURL.path)
            NSLog("Initial Attrs: %@", attributes.description)
            
            #if true
                let newPerms = [   FileAttributeKey.immutable : false as NSNumber]
                try FileManager.default.setAttributes(newPerms, ofItemAtPath: currentItemURL.path)
                NSLog("Attr Set Success")
                attributes = try FileManager.default.attributesOfItem(atPath: currentItemURL.path)
                NSLog("Modified Attrs: %@", attributes.description)
            #endif

            try FileManager.default.removeItem(at: currentItemURL)
            print("INFO: file \(currentItemURL.lastPathComponent) deleted successfully")
            self.currentItemURL = nil
        } catch {
            errorDeletingItem(error: error as NSError, temporaryItemURL: currentItemURL)
            
            
            return
        }
	}
	

Finally, jumping back to what I said earlier:

Yes, though I shared and individual file instead of a directory. That apparently made a difference

I haven't tracked down exactly why the change was made, but I think the difference here was made to find a better compromise between protecting from modification and destroying broader context/metadata. In the single files case, permissions don't actually matter as the system can simply set the file as read-only and be "done". However, when sharing a more complex hierarchy (particular a very large one), the permission configuration itself can also be meaningful (for example, marking objects executable), so I think the system tries to preserve it by maintaining the permission configuration and then using UF_IMMUTABLE to protect* the file.

*Note that "protect" isn't quite the right word here. The issues here isn't about protecting the data itself- that is, clearing that flag and then writing to that file doesn't mean that the other users file will change. It's about communicating to the broader system (particularly on macOS) the "meaning"/behavior of a given file so that the rest of the system handles it "sensibly".

You can see this dynamic at work fairly clearly on macOS. You can clear the flag through the Terminal:

chflags nouimmutable <file path>

...and even modify that file using the Terminal (once you've cleared the flag), but that doesn't mean your changes will actually "work". iCloud Drive still "knows" that this is a read-only file and resets to the "original" shared file before it allows other apps to open it. In other words, it's there so that other apps realize the system isn't going to let the file be modified, not to act as the primary protection system for the file.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Applying this immediately after copying the file resolves the issue:

var attributes = try FileManager.default.attributesOfItem(atPath: temporaryItemURL.path)
attributes[FileAttributeKey.immutable] = false
try FileManager.default.setAttributes(attributes, ofItemAtPath: temporaryItemURL.path)

That said, it might be better to preserve the original attributes and only modify them right before deleting the file, as you demonstrated. This approach would also address the issue for files that were copied previously.

Thank you for your help, Kevin!

FileManager.copyItem Permission Bug in iOS 18.1.1 with iCloud Shared Folders
 
 
Q