I mentioned atomicity at the end of my previous article and had intended to write about how to make all the previous operations atomic, but doing so requires understanding how to atomically clobber files before we can atomically move them.

The naive approach would be to check whether the file existed before attempting the operation.

However this runs the risk of the file being added or removed between the check and the operation, in what's known as a TOCTOU attack.

We previously mentioned how to atomically create a file without clobbering, but we may instead explicitly want to clobber it, or not care.

For opening a file, this is just a matter of using different flags.

enum clobber {
    CLOBBER_PERMITTED     = 'p',
    CLOBBER_REQUIRED      = 'R',
    CLOBBER_FORBIDDEN     = 'N',
    CLOBBER_TRY_REQUIRED  = 'r',
    CLOBBER_TRY_FORBIDDEN = 'n',
};

int create_file(const char *path, mode_t mode, int flags,
                enum clobber clobber) {
    switch (clobber) {
        case CLOBBER_PERMITTED:
            flags |= O_CREAT;
            break;
        case CLOBBER_REQUIRED:
        case CLOBBER_TRY_REQUIRED:
            flags &= ~O_CREAT;
            break;
        case CLOBBER_FORBIDDEN:
        case CLOBBER_TRY_FORBIDDEN:
            flags |= O_CREAT|O_EXCL;
            break;
        default:
            assert(0);
    }
    return open(path, flags, mode);
}

For renaming a file things get a bit more awkward.

There are flags for changing how the rename behaves when the file exists, but there isn't one for requiring that it does so.

Instead there's RENAME_EXCHANGE which will fail if the target does not exist and the source file will replace the target on success, but it has the side effect of leaving the target file behind where the source file was.

This can be remedied by calling unlink(2).

int rename_file(const char *src, const char *tgt, enum clobber clobber) {
    int ret = -1;
    int renameflags = 0;

    switch (clobber) {
        case CLOBBER_REQUIRED:
        case CLOBBER_TRY_REQUIRED:
            renameflags = RENAME_EXCHANGE;
            break;
        case CLOBBER_FORBIDDEN:
        case CLOBBER_TRY_FORBIDDEN:
            renameflags = RENAME_NOREPLACE;
            break;
        default:
            assert(0);
    }

    ret = renameat2(AT_FDCWD, src, AT_FDCWD, tgt, renameflags);
    if (ret == 0) {
        if (clobber == CLOBBER_REQUIRED || clobber == CLOBBER_TRY_REQUIRED) {
            ret = unlink(src);
        }
        return ret;
    }

    if ((errno == ENOSYS || errno == EINVAL)
        && (clobber != CLOBBER_REQUIRED
            && clobber != CLOBBER_FORBIDDEN)) {
        ret = rename(src, tgt);
    }

cleanup:
    return ret;
}

A test program, clobbering.c and the accompanying Makefile may be downloaded.

This test program will rename if passed two file paths and copy standard input to a file if only passed one path.