Skip to content

fixed chunker: a short read is taken for EOF, silently truncating --read-special FIFO input #10565

Open

Description

@ThomasWaldmann

BUG (silent data loss)

System information

borg versions affected: master, 1.4.x and 1.2.x. Seen on macOS.

fixed versions: borg1 >= 1.4.6, borg2 >= 2.0.0b26

no new release for 1.2.x, but i backported the fix to 1.2-maint branch for package maintainers to cherry-pick it from there.

Describe the problem you’re observing

ChunkerFixed.chunkify() in src/borg/chunker.pyx treats any read that returns less than the requested block size (got < wanted) as end of file and stops chunking the whole input.

A short read is not EOF, though. Reads from a pipe, FIFO or some character devices often return less data than requested. With borg create --read-special --chunker-params fixed,..., borg then archives only the first part of such an input and silently drops the rest. There is no warning or error, and the return code is 0.

The buzhash chunker and stdin / --content-from-command input are not affected, because their buffered readers always fill the request completely.

Reproducing the problem

import os, threading
from borg.chunker import ChunkerFixed

r, w = os.pipe()
data = os.urandom(3 * 1024 * 1024)

def writer():
    with os.fdopen(w, 'wb') as f:
        f.write(data)

threading.Thread(target=writer).start()
ch = ChunkerFixed(4 * 1024 * 1024, 0, sparse=False)
total = sum(c.meta['size'] for c in ch.chunkify(None, r))
print('wrote', len(data), 'chunked', total)

On macOS this prints wrote 3145728 chunked 65536. Only the first 64 KiB, one pipe buffer, ends up in chunks. The writer then blocks forever, because nobody reads the rest.

The same thing happens end to end when you back up a FIFO with borg create --read-special --chunker-params fixed,4194304 while another process writes into it: the archived file is truncated.

Fix

After a short read, keep reading until the block is full or a read returns no data (real EOF). Filling the block also keeps the chunk boundaries at fixed offsets. That matters for dedup: if short pieces became chunks, boundaries would depend on how the kernel split the reads.

Activity

  1. ThomasWaldmann commented on Oct 29, 2026

    @ThomasWaldmann
    MemberAuthor

    Who is affected?

    Only a small group of users: the silent data loss needs all of the following.

    1. Fixed chunker: the archive is created with --chunker-params fixed,.... The default buzhash chunker uses a different code path and is not affected.
    2. A source that returns short reads: in practice, borg create --read-special reading a FIFO, including FIFOs given via --paths-from-stdin / --paths-from-command. A pipe returns at most its buffer size per read (64 KiB on Linux and macOS), so any block size above that triggers it. Character devices that return less than requested per read can trigger it too (e.g. tape devices with a smaller record size than the block size, serial devices), but that is rare.
    3. Platform:
      • macOS: affected (Python has no os.posix_fadvise there). The reproducer above shows 3 MiB written into a pipe ending up as 64 KiB of chunks.
      • OpenBSD: also no posix_fadvise, so it should behave like macOS (not tested).
      • Linux: FIFOs are not affected silently. posix_fadvise() on a pipe fails with ESPIPE, so borg reports the FIFO as a read error and exits with a warning instead of archiving partial content. Character devices pass posix_fadvise on Linux, so a short-reading character device could still be affected there.
      • FreeBSD / NetBSD: also have posix_fadvise, and FreeBSD documents ESPIPE for pipes, so they should fail with an error like Linux (not tested).

    Not affected

    • Regular files and block devices, the usual targets of the fixed chunker (disk images, LVM snapshots, database files): read() returns the full block there, except at the real end of the file.
    • stdin (borg create ... -) and --content-from-command: Python's buffered reader always fills the request completely.
    • borg import-tar and borg recreate --chunker-params fixed,...: their readers also fill the request completely.
    • borg 1.1.x: it has no fixed chunker.

    Symptoms

    • The FIFO item is stored with only the first read's worth of data: typically 64 KiB or less, or only the header if a header size was given.
    • borg create finishes with rc 0 and no warning.
    • The only side effect: borg closes the FIFO early, so the program writing into it gets EPIPE / SIGPIPE. Scripts that check the writer's exit status would have noticed a failure.

    How to check existing archives

    List the sizes of the FIFO items, e.g.:

    borg list --format '{size} {path}{NL}' REPO::ARCHIVE
    

    Items whose size is 64 KiB or less, but should be much bigger, are truncated.

  2. ThomasWaldmann commented on Oct 29, 2026

    @ThomasWaldmann
    MemberAuthor

    Note: all branches fixed, but keeping this open for visibility for a while...

  3. ThomasWaldmann commented on Oct 29, 2026

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions