Monday, March 23, 2009

chmod, ACL and symbolic links

Let's say you want to set an ACL on a symbolic link. So you try the -h option of chmod that is documented to change the mode of the link itself rather than the file that the link points to.

  $ ln -s aFileThatDoesNotExist myLink
$ /bin/chmod -h +a "everyone allow delete" myLink
chmod: Failed to set ACL on file 'myLink': No such file or directory

So, it seems myLink was followed even though we passed the -h option. Let's try with a link that points to an existing file:

  $ touch aFileThatExist
$ ln -fs aFileThatExist myLink
$ /bin/chmod -h +a "everyone allow delete" myLink
$ ls -le myLink aFileThatExist
-rw-r--r--+ 1 cluthi staff 0 5 mar 18:11 aFileThatExist
0: group:everyone allow delete
lrwxr-xr-x 1 cluthi staff 14 5 mar 18:12 myLink -> aFileThatExist

Our hypothesis is confirmed, the link was followed. Let's investigate and have a look at chmod source code (file_cmds-188 on darwinsource).

chmod.c:125 variable hflag is set to true if the -h argument is passed
chmod.c:388 function modify_file_acl() is called, the hflag is not passed to this function

Doh, lazy boys! Let's patch it and add a follow argument to the modify_file_acl() function:
int modify_file_acl(unsigned int optflags, const char *path, acl_t modifier, int position, int inheritance_level, int follow);
and call it with modify_file_acl(..., !hflag)

From acl_set(3) man: The acl_set_link_np() function acts on a symlink rather than its target, if the target of the path is a symlink. Perfect, that's what we need: we must call acl_set_link_np instead of acl_set_file in order not to follow the link.

chmod_acl.c:807 acl_set_file is called: (0 != acl_set_file(path, ACL_TYPE_EXTENDED, oacl))
Replace with (0 != (follow ? acl_set_file(path, ACL_TYPE_EXTENDED, oacl) : acl_set_link_np(path, ACL_TYPE_EXTENDED, oacl)))

With this small modifications, the -h option should be respected. Rebuild chmod and try again:

  $ ./chmod -h +a "everyone allow delete" myLink
chmod: Failed to set ACL on file 'myLink': Operation not supported
$ ln -fs aFileThatDoesNotExist myLink
$ ./chmod -h +a "everyone allow delete" myLink
chmod: Failed to set ACL on file 'myLink': Operation not supported

Now, whether the link points to an existing file or not, we get the Operation not supported error. That's better diagnostic, but not exactly what we expected :-( So, why is it not supported? Let's dig a bit more. acl_set_link_np implementation is found at Libc-498.1.5/posix1e/acl_file.c:175 and is:

  return(acl_set_file1(path, acl_type, acl, 0));

The last argument (follow) passed to acl_set_file1 is 0 and the first lines of acl_set_file1 implementation reads:

  if (follow == 0) {
/* XXX this requires some thought - can links have ACLs? */
errno = ENOTSUP;
return(-1);
}

We have the explanation of the Operation not supported error we got earlier. Note that the comment is not mine, it is actually in the libc source code!

Does it mean it is impossible to set an ACL on a symlink? Does it mean our only option is to file a bug asking some Apple engineer to think harder if links can have ACLs? Fortunately no. There is a third function in the acl_set(3) API: acl_set_fd, which acts on a file descriptor. Hopefully, getting the file descriptor of a symlink is as simple as open(path, O_SYMLINK).

That's it. With this patch, you'll be able to run ./chmod -h +a "everyone allow delete" myLink and have the ACL to be set on the symlink!

If you need to set ACLs on symlinks on a daily basis, I suggest you do not overwrite /bin/chmod but install the patched version of chmod in /usr/local/bin. Well, if you read that post till there, you probably know that already.

This bug has been reported to Apple and is known as radar #6264303 which is a duplicate of radar #5684438.

3 comments:

Philip said...

I thought I needed to enable ACL clearing or symbolic links, so that resulted in additional changes to chmod_acl.c, as below. Compiles but I didn't test it:

@@ -706,24 +706,12 @@
if (filesec_set_property(fsec, FILESEC_ACL,
_FILESEC_REMOVE_ACL) != 0)
err(1, "filesec_set_property() failed");
- if (follow) {
- if (chmodx_np(path, fsec) != 0) {
- if (!fflag)
- warn("Failed to clear ACL on file %s", path);
- retval = 1;
- } else
- retval = 0;
- } else {
- int fd = open(path, O_SYMLINK);
- if (fd != -1) {
- if (fchmodx_np(fd, fsec) != 0) {
- if (!fflag)
- warn("Failed to clear ACL on file %s", path);
- retval = 1;
- } else
- retval = 0;
- }
- }
+ if (chmodx_np(path, fsec) != 0) {
+ if (!fflag)
+ warn("Failed to clear ACL on file %s", path);
+ retval = 1;
+ } else
+ retval = 0;
filesec_free(fsec);
return (retval);
}

SF said...

Well done, thanks! You saved a Netatalk dev some serious digging! ;)

Simon Strandgaard said...

Thank you Cédric for uncovering this. I have integrated acl_set_fd_np() in my AnalyzeCopy project, that exercises various Mac applications.

See code here:
http://github.com/neoneye/AnalyzeCopy/blob/master/utils/ac_mklink_acl.c