SO, the question here is how/why an ACL was set on the file but the xattr was not created.
The ACL was set on the file with chmod(1):
% chmod +a 'developer allow write,append,delete' /Volumes/myfs/nf.txt
In any case, the real key point is here:
Next, the ACL extracted from the source file sec object is applied to the brand-new file sec object created via CFFileSecuritySetAccessControlList(newFileSec,tmpACL):
At the point this succeeded, the file should have acquired a "com.apple.system.Security" attribute, since that's where your file system should be storing the ACL.
Why didn't it?
Actually, it did.
The output I supplied was meant to demonstrate it, 0xaf54acd10 being the new file sec object:
<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
}
I guess I should've made it explicitly clear that CFFileSecuritySetAccessControlList(newFileSec,tmpACL) did succeed.
That matters because the reason copyExtendedAttribute did this first:
Calls getxattr(dstFile, 'com.apple.system.Security', NULL, 0, 0, XATTR_NOFOLLOW) to obtain the size of the attribute. -1 is returned.
...is because IF an xattr had been returned there, as it should have been, copyExtendedAttribute would have then skipped this step:
Calls getxattr(srcFile, 'com.apple.system.Security', buffer, 512, 0, XATTR_NOFOLLOW) to obtain the ACL EA. -1 is returned with errno==1 EPERM.
...and simply continued normally without any error.
Your statement about copyExtendedAttribute skipping these getxattr(2) calls isn't accurate. I'll expand on that later.
Both getxattr(2) calls fail with EPERM because com.apple.system.Security is a protected EA and the TextEdit process is running with regular privileges and it doesn't have FILESEC_ACCESS_ENTITLEMENT, getxattr(2):
if (xattr_protected(attrname) &&
(error = xattr_entitlement_check(attrname, ctx, false)) != 0) {
goto out;
}
I dynamically analysed these functions, URLReplaceObject,CFURLCopySecurityEAs,TransferExtendedAttributes,copyExtendedAttribute, for APFS to see which code paths are taken on a successful run.
The file resides on APFS:
% df -h apfs.txt
Filesystem Size Used Avail Capacity iused ifree %iused Mounted on
/dev/disk4s5 367Gi 105Gi 241Gi 31% 1.0M 2.5G 0% /System/Volumes/Data
And uses the following POSIX and ACL permissions:
% ls -le apfs.txt
-r--r--r--+ 1 developer staff 6 Sep 28 16:20 apfs.txt
0: user:developer allow write,delete,append
The ACL permissions on apfs.txt were created with chmod(1):
% chmod +a 'developer allow write,append.delete' apfs.txt
I found that the code behaves as I had observed with my filesystem, i.e.:
A new file sec object is created.
The ACL is obtained from the source file's file sec object.
The ACL is stored in the new file sec object.
The new file sec object is stored in a dictionary CFDictionaryAddValue(dict,NSURLFileSecurityKey,newFileSec) for further use.
TransferExtendedAttributes(alloc,srcURL,dstURL,&error) gets called for APFS as well.
Because the file had no EAs created by TextEdit yet, later on com.apple.TextEncoding gets added for this file by TextEdit, but that has no bearing on the ACL situation.
TransferExtendedAttributes calls listxattr(2) which returns zero.
TransferExtendedAttributes returns two values to the caller:
The status code returned is 1, probably a Boolean value and
The CFError parameter is set to NULL on success.
This is where the behaviour differs between my filesystem and APFS.
My filesystem returns 0 in the status code and a non-null CFError.
On TransferExtendedAttributes returning success for APFS, the actual call that writes the new file sec object out to disk is:
CFURLSetResourcePropertiesForKeys(dstURL,dict,&error):
dstURL: file:///Users/developer/Library/Containers/com.apple.TextEdit/Data/tmp/TemporaryItems/NSIRD_TextEdit_EfokSZ/apfs.txt
dict: {
NSURLCreationDateKey = "2026-09-28 13:03:50 +0000";
NSURLFileSecurityKey = "<FileSecurity 0xc289c1f50> [0xc28aec680] {FILESEC_OWNER = (null), FILESEC_GROUP = (null), FILESEC_M
ODE = 33060, FILESEC_UUID = (null), FILESEC_GRPUUID = (null), FILESEC_ACL = !#acl 1\nuser:ED27D762-80A1-496B-BD81-4AE8CE54CE82:
developer:501:allow:write,delete,append\n}";
}
And this is it.
To see if this would result in the same code path being taken for my filesystem, I modified the status code and the CFError object returned by TransferExtendedAttributes dynamically to 1 and NULL respectively while debugging with Ghidra and lldb, and had CFURLSetResourcePropertiesForKeys(dstURL,dict,&error) called with the file contents being successfully written out to disk.
To the best of my knowledge, this brings us back to my earlier assumption, which has now been confirmed by these findings, that TextEdit and the like aren't permitted to query com.apple.system.Security.
The only way to have TextEdit interoperate with ACLs is to have them stored as file metadata instead of as an extended attribute.
Topic:
App & System Services
SubTopic:
Core OS
Tags: