2362 Commits
Author SHA1 Message Date
TW 15738b8fb5 Merge pull request #10517 from ThomasWaldmann/robustunpacker-pypy-bufferror
RobustUnpacker: release all memoryviews of the resync buffer before resizing it
2026-10-11 21:53:00 +02:00
Thomas WaldmannandClaude Fable 5.1 4da7021dc5 RobustUnpacker: release all memoryviews of the resync buffer before resizing it
Since the PyPy nightly of 2026-10-11 (PyPy 8.0.2-alpha0, rev 544bd2f768f9), 12 tests
in archive_test.py (test_missing_chunk, test_corrupt_chunk, test_resync_*) fail with:

    BufferError: Existing exports of data: object cannot be re-sized

at `del self._buffered_data[:drop_len]` in RobustUnpacker._search().

_search() scanned the buffer inside `with memoryview(self._buffered_data) as data:`
and called valid_msgpacked_dict(data[offset:]) for every offset, which also sliced the
key out of that view. CPython frees these unreferenced child views immediately, so the
bytearray is exportless again as soon as the with block releases the parent view.
PyPy has no refcounting: the child views stay alive until the GC runs. PyPy 8.0.0 did
not count child views as exports of the bytearray, the new nightly does (like CPython
always did), so the resize after the with block fails.

Fix: valid_msgpacked_dict(d, offset=0) only indexes d, it never slices it, and _search()
passes (data, offset) instead of a slice. Together with the explicitly released slice in
_try_item_start(), no unreleased view of _buffered_data exists anymore when it is
resized. The scan loop also gets a bit faster on CPython (no slice object per offset).

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-11 19:45:09 +02:00
Thomas WaldmannandClaude Opus 5.5 a879f5c723 security: same local repo dir via another path is not a relocation, fixes #8188
The "repository was previously located at" check compared path strings only,
so reaching the same local repository directory via a symlink, a bind mount or
a different letter case on a case-insensitive filesystem looked like a
relocation. Answering yes rewrote the stored location, so alternating between
the paths kept triggering the prompt (breaking e.g. unattended prune).

If the strings differ, but both are local paths referring to the same existing
directory (same st_dev/st_ino, st_ino != 0), do not warn. The stored location
and all displayed paths stay as they were. Remote locations are not affected,
and a real move/rename (old path gone) still triggers the warning.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-11 18:46:42 +02:00
TW 306a3c8e90 Merge pull request #10497 from ThomasWaldmann/encrypt-files-cache
seal the files cache with the repository key, fixes #5041
2026-10-11 01:19:46 +02:00
TW 7d7d74139c Merge pull request #10498 from ThomasWaldmann/stickers-1902
add borg sticker design source, fixes #1902
2026-10-11 00:09:40 +02:00
Thomas WaldmannandClaude Opus 5.5 185c5a3125 cache: seal the files cache with the repository key, fixes #5041
The files cache (file sizes, timestamps, inode numbers, chunk lists and
--digests content digests) was stored as plaintext msgpack in the local
cache directory, protected only by a checksum without a key, so anyone
who could read the cache directory could read it, and anyone who could
write it could make borg skip changed files.

It is now written as a sealed stream: encrypted and authenticated with
the repository key (authenticated only in the authenticated-* modes),
bound to the repository id and the file name of the files cache. A files
cache that fails the verification is ignored with a warning (the exit
code is not changed) and rebuilt from the previous archive.

The checksums in the [integrity] section of the cache config are not
needed any more, so the cache config is no longer rewritten on every
cache close().

The FAQ about cache security now describes what is protected and what is not.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-11 00:06:11 +02:00
Thomas WaldmannandClaude Opus 5.5 8cdc03b67e crypto: add sealed streams (key-sealed frames over a byte stream)
A sealed stream cuts a byte stream into 1 MiB frames and puts every
frame into the key's envelope (encrypted and authenticated in the
encrypting modes, authenticated only in the authenticated-* modes),
so neither the writer nor the reader needs memory for more than a frame.

The AAD of a frame is a caller-given context plus the frame number and
a "last frame" flag, so the reader detects changed, reordered, dropped
or duplicated frames, truncation and trailing data. Every frame is
sealed with encrypt_oneshot(), so sealed streams are thread-safe.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-11 00:06:04 +02:00
TW a17617fa43 Merge pull request #10505 from ThomasWaldmann/mount-opt-strip-numeric
mount: add strip_components, numeric_ids and archive_dir_format mount options, fixes #4880
2026-10-10 22:46:03 +02:00
Thomas Waldmann d1be425e84 update CHANGES 2026-10-08 19:43:07 +02:00
TW 257212e23d Merge pull request #10488 from mvanhorn/fix/10485-pack-max-env-validation
fix: validate BORG_PACK_MAX_SIZE and BORG_PACK_MAX_COUNT
2026-10-07 17:56:33 +02:00
Thomas WaldmannandClaude Opus 5.5 c7f2f2d8ce mount: add archive_dir_format mount option
Same as BORG_MOUNT_ARCHIVE_DIR_FORMAT, but usable from fstab and autofs
entries; if both are given, the mount option is used. As mount options
are separated by commas, the format can not contain a comma.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-07 11:10:08 +02:00
Mrityunjay Raj fd560dce0b docs: document repo corruption recovery, refs #9825 2026-10-07 13:44:32 +05:30
Thomas WaldmannandClaude Opus 5.5 8a84a81788 docs: faq: update sparse file support, refs #14
borg create --sparse seeks over holes, but the archive still does not
record the hole layout, so extract --sparse can not reproduce it exactly.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-06 19:06:00 +02:00
Matt Van HornandClaude Opus 5.5 52aee23df1 fix: validate BORG_PACK_MAX_SIZE and BORG_PACK_MAX_COUNT, fixes #10485
Repository.__init__ now parses and validates both variables, so a bad
value fails before the store is created, opened or locked. Each must be
a positive integer, and BORG_PACK_MAX_SIZE must be below the new
MAX_PACK_SIZE_LIMIT of 2126512128 (2 GiB - 20 MiB). PackWriter.add keeps
the object that crosses the cap, so packs stay below 2 GiB, clear of OS
bugs with files of 2 GiB or more. A rejected value raises Error with the
variable name and the value.

With only BORG_PACK_MAX_COUNT set, the writer still gets
MAX_PACK_SIZE_LIMIT - 1 as its byte ceiling. pack_max_size returns the
user's size, or DEFAULT_PACK_MAX_SIZE when that variable is unset, so
compaction keeps targeting that value.

The environment-variable help and the packs internals page state the
limit, and repository tests cover rejected values (also on repository
creation), the size one below the limit, count-only mode, the unset
defaults, and both variables set together.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_019wSaUMZrtm6XSzwEcxavkv
2026-10-05 23:37:11 -07:00
Thomas WaldmannandClaude Opus 5.5 12ec7e9eb2 add borg sticker design source, fixes #1902
GIMP source (editable text layers) and a preview of the chosen
rectangular design "C" from the ticket, plus a README with the
font, colours and printing notes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-06 00:09:27 +02:00
Thomas WaldmannandClaude Opus 5.5 07b8d0271c locking: BORG_LOCK_RECHECK_DELAY sets the race recheck delay, fixes #9899
acquire() creates its lock object, waits for the race recheck delay and
lists the lock objects again to detect other clients racing for the lock.

With list-after-write consistency (local filesystems, sftp, rest, AWS S3,
MinIO), at least the client that created its lock object last sees the
other's lock in its second listing, so the delay is not needed for
correctness and the 0.01s default stays.

Stores that only list a new object after a lag (e.g. NFS clients caching
directory listings, some cloud storages via rclone) need a delay of at
least that lag. That lag is a property of the storage, not of the request
latency, so it is not measured, but set via BORG_LOCK_RECHECK_DELAY.
Invalid values abort with an error.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-05 23:04:09 +02:00
TW 63d3ef5939 Merge pull request #10478 from mr-raj12/spinner-chunkindex-10082
chunk index: show a spinner or percentage while loading / rebuilding it, refs #10082
2026-10-05 12:08:59 +02:00
Mrityunjay Raj 6c797114b9 chunk index: show a spinner or percentage while loading / rebuilding it, refs #10082 2026-10-05 11:20:08 +05:30
Mrityunjay Raj 8211bb7d76 compact: reclaim superseded duplicates left after a full check --repair, fixes #10471
- pass 1 walks the gaps of the packs that have any and counts their superseded duplicates as
  reclaimable. A pack holding only unused indexed objects and superseded duplicates is deleted.
  The walk refreshes the lock per pack, shows progress and stops on Ctrl-C.
- compact_pack drops the superseded duplicates found in pass 1 (superseded_ranges).
- superseded_gap_ranges reads GAP_READ_SIZE bytes after a small object, so small objects share a
  store request, and META_READ_SIZE bytes after a large one.
- compact and repo-compress check the index entries of every pack. A pack in which they overlap
  or reach past the end of the file is treated as untrusted. compact logs it as an error and
  keeps it unchanged.
- a tiny pack that the all-packs gate keeps from being dropped or rewritten is a merge candidate.
2026-10-05 10:50:26 +05:30
Mrityunjay Raj e84e8f9095 check, compact, repo-compress: keep a superseded duplicate indexed in a pack with a defect chunk or in a truncated pack 2026-10-05 10:42:59 +05:30
Mrityunjay Raj 5e4f8f8749 compact, repo-compress: keep a superseded duplicate indexed in a missing or corrupt pack, fixes #10474 2026-10-05 10:42:44 +05:30
Thomas WaldmannandClaude Opus 5.5 f2c40bcfa2 read item metadata chunks with gather_many, refs #10054
An archive's item metadata chunks are written into the same packs as the
file content chunks (one ChunkBuffer flush per 8 MiB of items). Reading
them via get_many loaded each whole 50 MB pack just to take a few MB of
item metadata out of it: e.g. "borg list" of an archive with 3 MB of item
metadata loaded 49.6 MB.

DownloadPipeline.fetch_many now uses gather_many for ROBJ_ARCHIVE_STREAM
(unpack_many / iter_items: list, diff, extract, mount, compact, recreate,
transfer, analyze, ...), as do archive_get_items for the item_ptrs and the
archives check for the item metadata chunks. File content is still read
via get_many (whole packs).

The legacy repositories get a gather_many that just calls get_many, so
transfer from a borg 1.x repository still works.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-03 23:39:42 +02:00
TW 25c2211d66 Merge pull request #10451 from mr-raj12/docs-packs-update
docs: update packs internals to match the current code
2026-10-03 20:29:10 +02:00
Mrityunjay Raj 962964e191 docs: packs: the figure TODO names only pack-objheader.png 2026-10-03 22:55:30 +05:30
Mrityunjay Raj 588f5f05c3 check: show progress while --repair re-reads the written packs, fixes #9443 2026-10-03 22:45:47 +05:30
Mrityunjay Raj 33b78e9d78 check: show progress while --repair removes defect chunks, refs #9443
Adds the check.remove_defect_chunks progress indicator, one step per defect chunk.
2026-10-03 22:45:43 +05:30
Mrityunjay Raj 1cac8b3c45 docs: the pack salvage removes packs and invalidates the stored chunk index, refs #10026 2026-09-30 04:58:36 +05:30
Mrityunjay Raj dee80e1a41 check: raise a dedicated error if objects can not be authenticated, refs #10026 2026-09-30 04:58:36 +05:30
Mrityunjay Raj 42678c7885 check: --repair salvages the packs recorded corrupt, refs #10026 2026-09-30 04:43:21 +05:30
Mrityunjay Raj a0ebb7e8a1 docs: packs: address review feedback 2026-09-30 01:14:34 +05:30
Mrityunjay Raj 774df94fc2 docs: update packs internals to match the current code
Correct what borg compact reclaims after a crash, how compact/repo-compress
rewrite index/, when the index is rebuilt from the packs and when the repair
walk is used. Add pack writer and read path docs.
2026-09-28 20:50:49 +05:30
Thomas Waldmann 82744708c0 update CHANGES 2026-09-27 23:02:37 +02:00
Thomas Waldmann 97f03619c7 build_usage build_man 2026-09-27 23:02:35 +02:00
Thomas WaldmannandClaude Fable 5.1 26ff43fdcc repo-create: always write config/defaults, so its removal is detected, refs #346
Without the object, the commands silently fell back to the built-in defaults, so
anybody with write access to the store could remove a repository default (e.g. an
"obfuscate" compression) unnoticed: the key protects the content of the object,
not its presence.

Now "borg repo-create" always writes config/defaults (empty if no default was
given), and a missing object is an error (Repository.DefaultsMissing, rc 34) like
one that fails the authentication: the commands using the defaults refuse to run.
"borg check" reports such an object, "borg check --repair" replaces it by empty
defaults, so the repository can be used again (with the built-in defaults).

with_repository reads the defaults only after the security checks (assert_secure).

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-27 17:09:59 +02:00
Thomas Waldmann 3e0acd9c9c update CHANGES 2026-09-27 15:19:58 +02:00
Thomas WaldmannandClaude Opus 5.5 f694ba3e69 repo-create: store default compression and chunker params in the repository, fixes #346
"borg repo-create --compression SPEC --chunker-params PARAMS" stores repository
defaults in the new config/defaults store object.

Compression: the commands that compress data (create, recreate, import-tar,
transfer, repo-compress, debug put-obj) use the repository default if no
compression was given via the command line, the environment or default.yaml;
without a repository default, they use lz4 as before.

Chunker params: "--chunker-params default" now means the repository default,
else the built-in CHUNKER_PARAMS: ChunkerParams("default") returns the
DEFAULT_CHUNKER_PARAMS marker and with_repository replaces it by the real
chunker params. borg create and borg import-tar use the marker as their parser
default, so they use the repository default if no chunker params were given.
borg recreate and borg transfer still only rechunk if --chunker-params is
given; with "default", they rechunk to the repository default.

config/defaults is stored in the key's store object envelope, so it is
authenticated: nobody without the key can change the defaults (e.g. remove an
"obfuscate" compression) without being noticed. config/config can not hold
them, as it is read before the key is known and is not authenticated.
config/defaults is read at most once per Repository object.

borg repo-info shows the defaults (also in --json).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 03:41:25 +02:00
Thomas WaldmannandClaude Opus 5.5 dca232741b require borgstore 0.7.0
borgstore 0.7.0 adds Store.gather (many byte ranges with one call, one
roundtrip for REST) and fixes partial loads with a negative offset from
cached namespaces.

Store.defrag now requires all sources to be items of the given namespace,
which borg's defrag calls (hex pack names in "packs") already satisfy.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-26 12:04:56 +02:00
Thomas Waldmann ce507ec480 update CHANGES 2026-09-25 21:05:38 +02:00
Thomas WaldmannandClaude Opus 5.5 dd902f8db5 docs: lock objects are protected by the repository key, #8386
Update the data structures and security docs: the lock objects are in the
store object envelope, locking needs the key (it is loaded before waiting for
the lock), unreadable lock objects are treated as exclusive locks, and what
the store still sees about the locks. Add a change log entry.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-25 19:16:40 +02:00
Mrityunjay Raj 1f7fc576fd compact, repo-compress: clarify corrupt pack help and warning, add stats and internals docs, refs #10410 2026-09-25 09:47:02 +05:30
TW 3e640ecd4e Merge pull request #10422 from ThomasWaldmann/legacy-authenticated-key
legacy: decrypt the key of borg 1.x authenticated repositories
2026-09-24 22:28:18 +02:00
Thomas WaldmannandClaude Opus 5.5 b2de6a99b4 remove CHANGES.rst symlink
It just pointed to docs/changes.rst, refer to that file directly.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-24 22:00:31 +02:00
Thomas WaldmannandClaude Opus 5.5 3b6a377894 legacy: decrypt the key of borg 1.x authenticated repositories
borg 1.x protected the key of the authenticated and authenticated-blake2
modes with the passphrase (pbkdf2, "sha256" algorithm), like the key of
the encrypted modes. The legacy authenticated key classes lacked
Pbkdf2FileMixin, so borg 2 could not decrypt that key and --from-borg1
(e.g. borg transfer) failed with "passphrase is incorrect".

The test uses keys exported from borg 1.4.5 authenticated and
authenticated-blake2 repositories.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-24 21:35:34 +02:00
TW 8867ca828e Merge pull request #10418 from ThomasWaldmann/legacy-repo-error
repository: clear error for borg 1.x repositories, fixes crash w/o --from-borg1
2026-09-24 21:14:20 +02:00
Thomas WaldmannandClaude Opus 5.5 4164c094fd repository: clear error for borg 1.x repositories, fixes crash w/o --from-borg1
Opening a borg 1.x repository without --from-borg1 crashed with a traceback while
loading the config/config store object, because a borg 1.x repository has a config
*file* where borg 2 has the config/ namespace (NotADirectoryError for a local
repository, HTTP 500 via ssh://; InvalidRepository on Windows).

borgstore 0.6.4 reports a name below an object as ObjectNotFound on all platforms,
so require it. If config/config is not found, look at "config" via the store
backend and raise Repository.LegacyRepository (rc 29) if it is a file, telling the
user to use --from-borg1 (e.g. with borg transfer).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-24 20:31:01 +02:00
Thomas WaldmannandClaude Opus 5.5 d1c77716c2 legacy client: work with a borg 1.x "borg serve"
--from-borg1 via ssh:// (e.g. borg transfer from a borg 1.x repository)
failed with a borg 1.x "borg serve" on the remote host:

- the client called info() after open, which borg 1.x servers do not
  have ("Borg server is too old for info"),
- then close() sent the close RPC, which borg 1.x servers do not have
  either ("RPC method close is not valid"), masking the first error,
- get_manifest is also missing on borg 1.x servers,
- borg 1.x servers pack bytes into the msgpack "raw" type, which the
  client unpacks to str, so e.g. the repository id and object data
  arrived as str.

For borg 1.x servers, the client now uses repository version 1 (the only
version borg 1.x serves), does not send the close RPC (borg 1.x closes
the repository when stdin gets closed), gets the manifest object via get
and converts the bytes results back to bytes.

Tested with a real borg 1.4.5 "borg serve": repo-list, version,
repo-create --other-repo and transfer with --from-borg1, repokey and
keyfile mode. The testsuite emulates a borg 1.x server.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-24 17:12:53 +02:00
Thomas WaldmannandClaude Opus 5.5 db0c4ac2c1 docs: ssh:// repository URLs, #9765
Describe ssh:// repository URLs for current repositories (served by a
remote "borg serve --rest") and for legacy borg 1.x repositories (with
--from-borg1, served by a remote "borg serve") and add a change log
entry.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-24 15:55:01 +02:00
Thomas Waldmann dc3c3628a6 update CHANGES
also: fix some reST syntax issues with "*"
2026-09-23 22:52:51 +02:00
TW 0c02052864 Merge pull request #10406 from ThomasWaldmann/encrypt-cache-index
authenticate (and encrypt) index and cache store objects, fixes #9819, fixes #10235
2026-09-23 22:07:29 +02:00
TW 96ead8a309 Merge pull request #10379 from mr-raj12/check-finish-narrow-walk-8466
check --repair: re-read only the packs the repair wrote, refs #8466
2026-09-23 14:17:06 +02:00