To better understand the situation I resorted to dynamic analisys of the following CoreServicesInternal functions using Ghidra, a software reverse engineering tool:
URLReplaceObject
CFURLCopySecurityEAs
TransferExtendedAttributes
copyExtendedAttribute
The following test case was considered:
Source file mode: 0444
Source file ACL: user id 501 allow write,append,delete
The temporary file, created by TextEdit, i.e. the destination file mode: 0644
srcUrlMode != dstUrlMode
Execution path
CFURLCopySecurityEAs(srcURL,dstURL,&local_8d0)
This function is called based on xpc_runtime_is_app_sandboxed() returning true and isSrcURLDirectory==isDstURLDirectory.
Internally, CFURLCopySecurityEAs():
- Opens both
srcURL and dstURL - Retrieves a list of
EAs from srcURL via flistxattr(2):
% sudo xattr /Volumes/myfs/nf.txt
com.apple.quarantine
com.apple.TextEncoding
com.apple.lastuseddate#PS
com.apple.system.Security
- Iterates over the list of
EAs returned - Copies any
EAs that match the com.apple.security. pattern, e.g.:
% kextstat 2>/dev/null | awk '$6~/com.apple.security./{print $6}'
com.apple.security.AppleImage4
com.apple.security.sandbox
com.apple.security.quarantine
com.apple.security.BootPolicy
The value returned is 1.
Next, the file security object associated with the source and destination URLs is obtained via CFURLCopyResourcePropertyForKey:
CFURLCopyResourcePropertyForKey(srcURL,NSURLFileSecurityKey,&srcFileSec,&error);
CFURLCopyResourcePropertyForKey(dstURL,NSURLFileSecurityKey,&dstFileSec,&error);
Next, a new file security object is created via CFFileSecurityCreate:
<FileSecurity 0xaf54acd10> [0xaf6a71980] {FILESEC_OWNER = (null), FILESEC_GROUP = (null), FILESEC_MODE = (null), FILESEC_UUID = (null), FILESEC_GRPUUID = (null), FILESEC_ACL = (null)}
Next, the file mode 33060==o100444 is extracted from srcFileSec via CFFileSecurityGetMode(srcFileSec,&srcUrlMode):
<FileSecurity 0xa3f5dee60> [0xa3fbf0a80] {FILESEC_OWNER = 501, FILESEC_GROUP = 20, FILESEC_MODE = 33060, FILESEC_UUID = 0000000
0-0000-0000-0000-000000000000, FILESEC_GRPUUID = 00000000-0000-0000-0000-000000000000, FILESEC_ACL = !#acl 1
user:ED27D762-80A1-496B-BD81-4AE8CE54CE82:developer:501:allow:write,delete,append
}
Next, the GID 20 (staff) of the owner of the source and the destination files is extracted from srcFileSec and dstFileSec respectively via CFFileSecurityGetGroup:
CFFileSecurityGetGroup(srcFileSec,(long)&srcgid)
CFFileSecurityGetGroup(dstFileSec,&dstgid)
Next, the mode of the source file is applied to the brand new file sec object via CFFileSecuritySetMode(newFileSec,srcUrlMode):
<FileSecurity 0xaf54acd10> [0xaf6a71980] {FILESEC_OWNER = (null), FILESEC_GROUP = (null), FILESEC_MODE = 33060, FILESEC_UUID =
(null), FILESEC_GRPUUID = (null), FILESEC_ACL = (null)}
Next, the ACL is extracted from the source file sec object via CFFileSecurityCopyAccessControlList(srcFileSec,&tmpACL):
<FileSecurity 0xa3f5dee60> [0xa3fbf0a80] {FILESEC_OWNER = 501, FILESEC_GROUP = 20, FILESEC_MODE = 33060, FILESEC_UUID = 0000000
0-0000-0000-0000-000000000000, FILESEC_GRPUUID = 00000000-0000-0000-0000-000000000000, FILESEC_ACL = !#acl 1
user:ED27D762-80A1-496B-BD81-4AE8CE54CE82:developer:501:allow:write,delete,append
}
(lldb) expr char* $tmpACL = (char*)acl_to_text(*(void**)0x16d35a138, (long*)0)
(lldb) p (char*)$tmpACL
(lldb) "!#acl 1\nuser:ED27D762-80A1-496B-BD81-4AE8CE54CE82:developer:501:allow:write,delete,append\n"
Next, the ACL extracted from the source file sec object is applied to the brand new file sec object created via CFFileSecuritySetAccessControlList(newFileSec,tmpACL):
<FileSecurity 0xaf54acd10> [0xaf6a71980] {FILESEC_OWNER = (null), FILESEC_GROUP = (null), FILESEC_MODE = 33060, FILESEC_UUID =
(null), FILESEC_GRPUUID = (null), FILESEC_ACL = !#acl 1
user:ED27D762-80A1-496B-BD81-4AE8CE54CE82:developer:501:allow:write,delete,append
}
Next, the destination file mode o100644 is extracted from the destination file sec object via CFFileSecurityGetMode(dstFileSec,&dstUrlMode):
<FileSecurity 0xa3f5def70> [0xa3fbf0200] {FILESEC_OWNER = 501, FILESEC_GROUP = 20, FILESEC_MODE = 33188 (o100644), FILESEC_UUID = (null),
FILESEC_GRPUUID = (null), FILESEC_ACL = (null)}
Next, the brand new file sec object is added to a dictionary:
CFDictionaryAddValue(dict,NSURLFileSecurityKey,newFileSec)
Next, TransferExtendedAttributes(alloc,srcURL,dstURL,&error) is called.
Internally, TransferExtendedAttributes:
- Obtains a list of
EAs via listxattr(2) - Calls
xattr_preserve_for_intent for each of the EAs iteratively passing XATTR_OPERATION_INTENT_SAVE. - Calls
copyExtendedAttribute to copy the ACL EA from the source to the destination file.
xattr_preserve_for_intent considers the following EAs worth saving:
com.apple.lastuseddate#PS
com.apple.system.Security
The EAs are saved by calling copyExtendedAttribute.
com.apple.lastuseddate#PS is saved successfully.
com.apple.system.Security fails to be saved.
Internally, copyExtendedAttribute:
- Calls
getxattr(dstFile, 'com.apple.system.Security', NULL, 0, 0, XATTR_NOFOLLOW) to obtain the size of the attribute. -1 is returned. - Calls
getxattr(srcFile, 'com.apple.system.Security', buffer, 512, 0, XATTR_NOFOLLOW) to obtain the ACL EA. -1 is returned with errno==1 EPERM.
TransferExtendedAttributes returns 0 to the caller and the errno in the error parameter.
URLReplaceObject proceeds down the error path and ends up setting a CFError to:
Error Domain=NSCocoaErrorDomain Code=513 "You don’t have permission to save the file “nf.txt.sb-4b9a8883-7jQtqz” in the folder
“myfs”." UserInfo={NSURL=file:///Volumes/myfs/nf.txt.sb-4b9a8883-7jQtqz, NSFilePath=/Volumes/myfs/nf.txt.sb-4b9a8883-7jQtqz, NS
UnderlyingError=0xaf76952c0 {Error Domain=NSPOSIXErrorDomain Code=1 "Operation not permitted"}}
URLReplaceObject returns 0x201==513 to the caller.
If you have any further guidance to offer, I would greatly appreciate it.