Skip to content

Instantly share code, notes, and snippets.

@creachadair
Last active June 25, 2026 15:59
Show Gist options
  • Select an option

  • Save creachadair/02f16fd651e0d48bc86a6c6c60b904ea to your computer and use it in GitHub Desktop.

Select an option

Save creachadair/02f16fd651e0d48bc86a6c6c60b904ea to your computer and use it in GitHub Desktop.
A Curious Permission Preservation Pitfall in rsync

A Curious Permission Preservation Pitfall in rsync

When using rsync to back up or archive data, you typically want to preserve file metadata such as owner and group ID, permissions, and modification times, in addition to the file content. You might run with --archive (-a) or using the --times (-t), --perms (-P), --owner (-o) and --group (-g) flags, for example.

In particular, preserving permissions naturally means that if a file is read-only in the source (e.g., mode 0444 or -r--r--r--), then its mirror in the destination will also be read-only. This is working as intended, and does not ordinarily cause any problems because (by default) when rsync updates file contents, it does so by creating a (writable) temp file with the new content, then replacing the target file (and updating the file metadata) once it is complete.

The --inplace flag overrides the default behaviour, causing rsync to update existing target files directly rather than via a temporary. This can be useful when dealing with large files, where you don't want to make a complete copy of the file with changes (either for capacity reasons, or to avoid wear-loading on an SSD).

For obvious reasons, --inplace interacts poorly with --perms for read-only files: The initial sync works fine, because there is no file present, so the target is created and its permissions set. On update, however, rsync will be unable to open the target file for writing, because of its existing preserved permissions. There does not appear to be a way to ask rsync to mark the target file writable temporarily to do the update. (That is probably just as well, that could lead to other issues).

You might not notice this immediately, since files marked read-only are often ones you do not expect to change, such as content-addressed data like the Go module cache or a Git object store. However, even if the file content does not change, rsync uses timestamps to check whether an update is needed, and if the source file's mtime changes (say, because you cloned a fresh working copy of a Git repository) it will attempt to apply an update.

In these cases (where the file contents don't actually change) you can avoid the conflict by setting the --checksum flag. That, however, generates a lot of disk activity on both sides of the transfer, as rsync will have to fully read the source and target files to compute the checksums.

Mitigation & Complication

The simplest mitigation for this problem is not to use --inplace together with --perms.

But I encountered this problem in the context of using rsync into a filesystem mounted via FUSE. For historical reasons, macOS will sometimes create specially-named files on non-native filesystems (including FUSE mounts), to represent auxiliary data like HFS file forks ("AppleDouble" format), security policies, and extended attributes.

These files are created using the base name of the file they're connected to, prefixed with ._ (period underscore). The leading dot prevents them from showing up by default in file listings, and the underscore presumably helps avert conflict with other dot files users are likely to have around.

Although FUSE does support extended attributes, the API does not have any methods to handle resource forks or security policy metadata. This means when you copy data into a FUSE filesystem on macOS, particuarly via the GUI, you may wind up with a litter of ._ auxiliary files. The macfuse driver has a noappledouble mount option that causes such files to be ignored, and their creation rejected.

That option solves one problem at the cost of another: When rsync updates a file, it creates a temporary in the same directory, using the target file's name prefixed by . and suffixed with a random nonce (as mktemp). So the temp for foo, for example, might be .foo.Z7cWwcUVha.

If the target file happens to begin with an underscore (e.g., __init__.py), this results in a temporary file name like .__init__.py.levxRH3Jsr. On a FUSE mount with noappledouble set, that temporary file begins with the forbidden ._ prefix, and will report an error.

To work around that problem, I thought I could use --inplace, bypassing the naming problem. But since I also want to preserve permissions, that ran into the problem described above with updating read-only files after mtime changes.

A Better Mitigation

So, we should not use --inplace for this. What can we do instead?

Recent versions of rsync have a better answer: When the --temp-dir flag is set, instead of creating temporary files in the same directory as the target file, it creates the temp file in that directory. Moreover, since rsync 3.3, doing this overrides the use of a . prefix on the file name.

This means, however, that you have to create a --temp-dir someplace, and ideally you want that to be on the same filesystem as the target, so that the result can be renamed into place rather than copied. You can do this via mktemp -p . -d inside the FUSE mount, and then clean up the resulting temp directory later.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment