$RodHat_
Console Tips

renameat2(2): atomic file swap without a race window

Published by · Updated

This article may contain Amazon affiliate links. I may earn a commission if you buy through them. Disclosure.

Photo by Cong Long Vu
Photo: Cong Long Vu / Unsplash
On this page · 6 sections
  1. Synopsis
  2. RENAME_NOREPLACE
  3. RENAME_EXCHANGE
  4. The glibc situation
  5. What filesystems support it
  6. When not to bother

rename(2) is atomic on any filesystem worth using. The POSIX spec says so, and every production filesystem you’d bother running enforces it. If src and dst are on the same filesystem, rename(src, dst) replaces dst in a single VFS operation: no window where a reader opens the path and finds nothing.

What rename(2) can’t do: refuse to overwrite if the destination already exists, or swap two paths without either disappearing. Those two gaps are why renameat2(2) exists.

Synopsis

#include <fcntl.h>
#include <stdio.h>
#include <sys/syscall.h>
#include <unistd.h>

/* glibc wrapper added in 2.28; use syscall(2) on older systems */
int renameat2(int olddirfd, const char *oldpath,
              int newdirfd, const char *newpath,
              unsigned int flags);

olddirfd and newdirfd follow the same AT_FDCWD convention as openat(2): pass AT_FDCWD to use the process’s current directory. flags is where this gets interesting.

RENAME_NOREPLACE

renameat2(AT_FDCWD, "incoming/new-config",
          AT_FDCWD, "etc/app.conf",
          RENAME_NOREPLACE);

Fails with EEXIST if etc/app.conf already exists. rename(2) would silently clobber it. This is the right flag any time you want an atomic “move this file into place only if nothing is there yet,” without the check-then-rename race that an access(2) + rename(2) pair has.

RENAME_EXCHANGE

renameat2(AT_FDCWD, "etc/app.conf.new",
          AT_FDCWD, "etc/app.conf",
          RENAME_EXCHANGE);

Both paths swap names in a single VFS operation. etc/app.conf now contains what etc/app.conf.new had; etc/app.conf.new now contains the old live config. Both must exist before the call; if either is missing you get ENOENT.

No window exists where a reader opening etc/app.conf finds nothing. No window where the process gets an incomplete write, because you wrote the new file fully before the swap. After the call you have the old config sitting in .new, ready for inspection or deletion. This is the correct way to hot-swap a config file and it has been available since kernel 3.15.

Minimal C example for a deploy tool:

#include <fcntl.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>

/* Write new config to etc/app.conf.new first, then: */
int swap_config(void) {
    int r = syscall(SYS_renameat2,
                    AT_FDCWD, "etc/app.conf.new",
                    AT_FDCWD, "etc/app.conf",
                    2 /* RENAME_EXCHANGE */);
    if (r < 0) {
        perror("renameat2 EXCHANGE");
        return -1;
    }
    /* etc/app.conf.new now holds the previous config */
    unlink("etc/app.conf.new");
    return 0;
}

Pass 2 literally if you don’t have <linux/fs.h> handy. RENAME_NOREPLACE is 1. RENAME_EXCHANGE is 2. They are not combinable.

The glibc situation

glibc didn’t grow a renameat2 wrapper until 2.28 (August 2018). Anything running glibc 2.27 or older (RHEL 7, Ubuntu 16.04, any container image that hasn’t been updated since the Obama administration) needs syscall(2) directly:

#include <sys/syscall.h>
#include <unistd.h>

#ifndef __NR_renameat2
#  if defined(__x86_64__)
#    define __NR_renameat2 316
#  elif defined(__aarch64__)
#    define __NR_renameat2 276
#  else
#    error "unknown __NR_renameat2 for this arch"
#  endif
#endif

static inline int sys_renameat2(int olddfd, const char *old,
                                int newdfd, const char *new,
                                unsigned flags) {
    return (int)syscall(__NR_renameat2, olddfd, old, newdfd, new, flags);
}

Check arch/x86/entry/syscalls/syscall_64.tbl in the kernel source for your architecture’s number if you’re targeting something other than x86-64 or arm64.

What filesystems support it

ext4, XFS, btrfs, tmpfs, and overlayfs all support RENAME_EXCHANGE. FAT, exFAT, and most network filesystems do not; you get EINVAL or ENOSYS. NFSv3 has no atomic exchange primitive at the protocol level. If you’re deploying over NFS you’re still stuck with the write-to-tmp-then-rename pattern and accepting the overwrite.

When not to bother

If you’re just doing a write-to-tmp-then-rename and don’t care about the old file, plain rename(2) is fine. The atomicity guarantee is the same. Use RENAME_EXCHANGE when you need the old copy intact after the swap, or when losing the destination for any window would be wrong. Use RENAME_NOREPLACE when idempotency requires detecting an existing file rather than clobbering it.

For more on related syscalls in this space, the copy_file_range(2) tip covers kernel-side file copying and mount_setattr(2) covers live mount flag changes, both of which pair naturally with deployment tooling.

Sources

  1. renameat2(2) Linux man page (man7.org)
  2. Atomic file renaming (LWN, Linux 3.15) (LWN.net)