Skip to content

pawelzelawski/libchevron

Repository files navigation

libchevron

Crash-safe atomic file replacement for Linux and OpenBSD.

libchevron implements the complete correct write-to-temp-then-rename sequence, handling all known failure modes so callers do not have to. It is the C equivalent of Go's renameio.


The Problem

Writing a file with open / write / close is not crash-safe. If the process crashes or the system loses power mid-write, the file is left partially written. The correct pattern is well-known: write to a temporary file, fsync it, rename it atomically over the target, then fsync the parent directory. In practice most project-internal implementations of this pattern get at least one step wrong. The failures are invisible during development and surface only under power loss in production.

libchevron encodes all seven failure modes correctly in one auditable place:

Failure mode How libchevron handles it
Missing parent directory fsync Performed unconditionally under CHEVRON_FULL
Wrong temporary file location Temp created in the same directory as the target
Incorrect permission handling fchown then fchmod on every path; setuid/setgid preserved
Not retrying fsync on EINTR All fsyncs use an EINTR-retry loop
Not checking write return values Write loop checks every return; retries short writes
Not checking close return values close on the temp fd is fatal before renameat
Predictable temporary file naming mkostemp with O_CLOEXEC; O_TMPFILE on Linux (unnamed inode)

Features

  • Single-shot and streaming interfaces over a single unified internal implementation - one place to audit, one place to fix
  • O_TMPFILE on Linux kernel 3.11+ - the temporary inode has no directory entry during the write phase, eliminating the symlink attack window; runtime detection with fallback to mkostemp on older kernels
  • Permission and ownership preservation - existing target's mode, uid, and gid are read and applied to the replacement file; fchown before fchmod so setuid/setgid bits survive
  • Three durability levels - full fsync of file and directory, file-only fsync, or rename-only
  • Precise error reporting - every failure returns a specific chevron_err_t and chevron_op_t identifying the class of failure and the exact operation that failed
  • Zero heap allocation - handle is stack-allocatable; no malloc or free anywhere in the library
  • Zero dependencies - libc and POSIX only
  • Dirfd-anchored operations - all commit-critical namespace operations anchored to the opened parent directory fd; namespace races are closed

Supported Platforms

Platform Path used
Linux kernel 3.11+ O_TMPFILE + linkat + renameat
Linux kernel < 3.11 mkostemp + renameat (runtime fallback)
OpenBSD mkostemp + renameat

Build Requirements

Requirement Notes
C11 compiler Clang (primary) or GCC
POSIX.1-2008 -D_POSIX_C_SOURCE=200809L
GNU make or BSD make GNU make ≥ 3.82 on Linux; BSD make on OpenBSD
ar For building the static library
clang Required for make test-tsan; GCC works for all other targets

Optional tools used by the build system:

Tool Target
valgrind make valgrind
clang-tidy make lint
cppcheck make lint
clang-format make format

Installation

# Build the optimised static library
make release

# Install to /usr/local (requires write permission)
make install

# Install to a custom prefix
make install PREFIX=/opt/local

This installs:

  • $(PREFIX)/lib/libchevron.a
  • $(PREFIX)/include/chevron.h

To build against the installed library:

cc -std=c11 -I/usr/local/include myprogram.c -L/usr/local/lib -lchevron -o myprogram

Usage

Single-shot

Write a buffer to a file atomically in one call:

#include <chevron.h>
#include <stdio.h>

int main(void)
{
    const char     *config = "[service]\nport=8080\n";
    chevron_error_t err;

    if (chevron_write("/etc/myapp/config.ini",
                      config, strlen(config),
                      CHEVRON_FULL,
                      CHEVRON_MODE_DEFAULT,
                      &err) != 0) {
        fprintf(stderr, "write failed: err=%d op=%d errno=%d\n",
                err.err, err.op, err.errno_value);
        return 1;
    }

    return 0;
}

CHEVRON_MODE_DEFAULT applies mode 0600 to new files and preserves the existing mode when replacing an existing file.

Streaming

Write data incrementally when the full content is not available up front:

#include <chevron.h>
#include <stdio.h>

int write_certificate(const char *path, const char *pem_chain, size_t len)
{
    chevron_handle_t h = CHEVRON_HANDLE_INIT;
    chevron_error_t  err;

    /* Open: validates arguments, opens parent dir, creates temp file */
    if (chevron_open(&h, path, CHEVRON_FULL, CHEVRON_MODE_DEFAULT, &err) != 0)
        goto fail;

    /* Write: may be called multiple times */
    if (chevron_write_chunk(&h, pem_chain, len, &err) != 0)
        goto abort;   /* handle already cleaned up on write failure */

    /* Commit: fchown, fchmod, fsync, close, renameat, dir fsync */
    if (chevron_commit(&h, &err) != 0)
        goto fail;    /* handle is inactive after commit regardless */

    return 0;

abort:
    /* Safe to call after write failure - no-op because handle is inactive */
    chevron_abort(&h);

fail:
    fprintf(stderr, "certificate write failed: err=%d op=%d errno=%d\n",
            err.err, err.op, err.errno_value);
    return -1;
}

Handle lifecycle rules:

  • Always initialise with CHEVRON_HANDLE_INIT - never zero-initialise (_fd == 0 is a valid live file descriptor)
  • After chevron_commit or chevron_abort, reinitialise with CHEVRON_HANDLE_INIT before reuse
  • After a chevron_write_chunk failure, the handle is already inactive; calling chevron_abort is safe and is a no-op
  • chevron_abort on an inactive handle is always a no-op

Durability Levels

Level File fsync Directory fsync Guarantee
CHEVRON_FULL Yes Yes Data and directory entry survive crash
CHEVRON_FILE Yes No Data survives crash; directory entry durability depends on a higher-level mechanism
CHEVRON_NONE No No Atomic replacement only; not durable across crash

Choose CHEVRON_FULL when you need the strongest guarantee. Choose CHEVRON_FILE when directory entry durability is handled elsewhere (for example a journal or a higher-level fsync). Choose CHEVRON_NONE when atomicity is required but durability is not (for example in-memory-backed tmpfs, or test environments).


Error Reporting

Every function returns 0 on success and -1 on failure. The optional chevron_error_t *err argument (may be NULL) provides detail:

typedef struct {
    chevron_err_t err;         /* class of failure */
    int           errno_value; /* errno at point of failure */
    chevron_op_t  op;          /* specific operation that failed */
} chevron_error_t;

chevron_err_t values: CHEVRON_ERR_NONE, CHEVRON_ERR_INVALID, CHEVRON_ERR_OPEN, CHEVRON_ERR_WRITE, CHEVRON_ERR_FSYNC, CHEVRON_ERR_CLOSE, CHEVRON_ERR_RENAME, CHEVRON_ERR_PERMISSION.

chevron_op_t values identify the exact step: CHEVRON_OP_OPEN_DIR, CHEVRON_OP_OPEN_TMP, CHEVRON_OP_STAT_TARGET, CHEVRON_OP_WRITE, CHEVRON_OP_FCHOWN, CHEVRON_OP_FCHMOD, CHEVRON_OP_FSYNC_FILE, CHEVRON_OP_CLOSE_TMP, CHEVRON_OP_RENAME, CHEVRON_OP_FSYNC_DIR.


Known Limitations

ACLs and extended attributes. Only mode, uid, and gid are preserved. POSIX ACLs, SELinux security contexts, and other extended attributes are silently discarded. Callers that depend on these must restore them after the call.

Orphaned temporary files. If the process crashes after temp creation but before renameat, the temp file remains as an orphan in the parent directory. The library cannot clean up what it did not create in the current invocation.

linkat EOPNOTSUPP. On Linux, if the O_TMPFILE open probe succeeds but linkat subsequently fails with EOPNOTSUPP (filesystem does not support AT_EMPTY_PATH), the library returns a hard failure. The mid-sequence fallback to mkostemp is not attempted because the unnamed inode cannot be unlinked by normal means.

Post-rename directory fsync failure. Under CHEVRON_FULL, if renameat succeeds but the subsequent directory fsync fails, the function returns -1. The new file may already be visible at the target path. What failed is directory entry durability, not the atomic replacement itself.

TOCTOU on permission preservation. fstatat reads the target's mode and ownership; fchown/fchmod then applies those values. A concurrent process can change the target's permissions in this window. This race cannot be eliminated without directory locking that is out of scope.

Silent ownership change on EPERM. When fchown fails with EPERM the replacement file is owned by the process's effective uid/gid, not the original owner. This is intentional: EPERM is non-fatal so the write still succeeds.

Directory fsync on FUSE and overlayfs. fsync on a directory fd may return EINVAL on these filesystems. Under CHEVRON_FULL this is a hard failure. Use CHEVRON_FILE or CHEVRON_NONE on these filesystems.

No multi-writer coordination. Concurrent writes to the same target path from multiple processes or threads are not serialised by the library. The caller is responsible for coordination.

No network filesystem guarantees. NFS, CIFS, and similar have different fsync and rename semantics. libchevron documents local POSIX filesystem behaviour only.

Relative paths resolved at open time. Relative paths are resolved against the process cwd during chevron_open. After open, all operations are dirfd-anchored and unaffected by chdir. A concurrent chdir during chevron_open can cause the open to resolve to the wrong directory. Use absolute paths in multithreaded programs.


Thread Safety

Concurrent calls using separate handles are safe. There is no global state. A single handle must not be used concurrently from multiple threads.

Relative paths are resolved against the process cwd during chevron_open. A concurrent chdir during chevron_open can cause the open to resolve to the wrong directory. Use absolute paths in multithreaded programs.


License

ISC License. See LICENSE.

About

Crash-safe atomic file replacement for C on Linux and OpenBSD. Handles all seven write-temp-then-rename failure modes. Zero heap, zero dependencies.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors