Skip to content

Instantly share code, notes, and snippets.

@creachadair
Created April 25, 2026 17:48
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 --partial-dir flag is set, instead of creating temporary files in the same directory as the target file, it creates a temporary directory for those temp files. Moreover, since rsync 3.3, doing this overrides the use of a . prefix on the file name.

This does mean you have to choose a --partial-dir name that will not conflict with other names in your directory, but that is easy enough to do for files you control. (If you do not, you could generate a UUID or other nonce name before running rsync, to reduce the probability of collision below a threshold of concern).

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