1.2-maint: CVE-2026-62268 and other fixes#9916
Open
ThomasWaldmann wants to merge 7 commits into
Open
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## 1.2-maint #9916 +/- ##
=============================================
- Coverage 81.05% 80.99% -0.07%
=============================================
Files 38 38
Lines 10854 10859 +5
Branches 1669 1674 +5
=============================================
- Hits 8798 8795 -3
- Misses 1516 1523 +7
- Partials 540 541 +1 ☔ View full report in Codecov by Harness. |
ThomasWaldmann
force-pushed
the
1.2-fixes
branch
from
July 19, 2026 12:58
83d39d1 to
6384274
Compare
ThomasWaldmann
force-pushed
the
1.2-fixes
branch
from
July 19, 2026 15:35
170c30f to
d03a620
Compare
ThomasWaldmann
marked this pull request as draft
July 19, 2026 22:17
…mbedded "..")
CVE-2026-62268 Fix 1/2
A maliciously crafted archive could contain a symlink (e.g. "evil" -> "/etc")
followed by an item below it ("evil/passwd"), or an item whose path embeds ".."
(e.g. "a/../../etc/passwd"). When extracting such items, borg followed the symlink
/ resolved the ".." in both the "remove existing file" step and the open()/mkdir()/
symlink() step, so it could delete or overwrite files outside the extraction
directory.
Note: sounds bad, but no big issue in practice: to create such a malicious archive,
the attacker would need repository access and (for encrypted or authenticated repos)
also the borg key and passphrase.
borg create never produces such archives (it does not follow symlinks and stores
normalized relative paths), so extract_item() now verifies, before any destructive
operation, that the item's parent path contains no ".." and no symlinked (or other
non-directory) component. Unsafe items are skipped with a warning (EXIT_WARNING)
and extraction continues. Verified-safe parent directories are cached per extraction
(Archive.safe_dirs) so each directory is lstat'd at most once.
Added regression tests for both attack vectors and a benign deep-tree extraction.
Also: skip redundant make_parent stat using safe_dirs cache
Performance optimization building on the parent-path security fix: the parent
path guard in extract_item already lstat's every existing parent directory and
records it in Archive.safe_dirs. make_parent therefore no longer needs to call
os.path.exists() for a parent that is already known to be a real directory - it
returns early on a safe_dirs hit, and caches directories it has to create (and
dest) so later items skip the stat too.
This roughly halves the per-item directory-metadata syscalls in the common case
(deep tree, shared parents) without changing behavior.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CVE-2026-62268 Fix 2/2 The hardlink source (item.source) was used unvalidated as the os.link() target during extraction. A maliciously crafted archive could set a source that traverses a symlink (e.g. "evil/secret" with "evil" -> "/etc") or contains "..", causing borg to hardlink an arbitrary external file (e.g. /etc/shadow) into the extracted tree - an information disclosure, especially dangerous when restoring as root. Note: sounds bad, but no big issue in practice: to create such a malicious archive, the attacker would need repository access and (for encrypted or authenticated repos) also the borg key and passphrase. extract_helper now validates the hardlink source path with the same Archive._check_safe_parent() check used for item paths, refusing unsafe ones with a warning (and continuing with the rest of the archive). os.link() is now called with follow_symlinks=False (where supported) so a symlinked final source component links the symlink itself - a faithful restore - rather than the external file it targets. Adds BackupHardlinkSourceError and regression tests for both the symlinked-source and embedded-".." source vectors. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
process_file() ran process_file_chunks() inside `with backup_io('read')`.
That block is meant to guard reading the *source* file, but the source reads
are already guarded individually by backup_io_iter(). The outer wrapper also
caught add_chunk()'s (and maybe_checkpoint()'s) *repository* writes, so a
repository IO failure -- e.g. the repo running out of space -- was wrapped
into a per-file BackupOSError tagged "read". Borg then emitted misleading
"<path>: read: [Errno 28] No space left on device" warnings, pointlessly
retried the file, and only treated a critical repository error as a
non-critical per-file one, contrary to the BackupOSError docstring ("Any
unwrapped IO error is critical and aborts execution (for example repository
IO failure)").
In 1.4 the transactional repository still rolls the partial transaction back
on the eventual failure, so this is not data loss here (unlike borg2, where
it silently commits a corrupt archive). But the misclassification -- wrong
warning text and needless read-retries of a repository-full condition -- is
wrong regardless.
Drop the outer backup_io('read') wrapper. Source reads stay per-file
warnings (backup_io_iter is unchanged); repository OSErrors are now left
unwrapped and critical, aborting promptly with the correct error.
Verified on a space-limited macOS ramdisk: before, create emitted many
"read: [Errno 28]" retry warnings then rolled back; after, it aborts
immediately ("No space left on device, cleaning up partial transaction"),
commits no archive, and the repo stays consistent. Normal backups and
unreadable-source-file handling (per-file warning) are unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013bFWgq2KV6oYxmfH6Zqvp3
fix: resolve KeyError and memory leaks in LRUCache - __setitem__: assign value before popping from _lru to avoid KeyError when exceeding capacity. - clear(): clear the _lru list also to prevent stale keys causing KeyErrors during future evictions. (cherry picked from commit 32cfddc)
- better check return value of fd.read(n) and reject if it returns more bytes than requested. - avoid giving len<=0 to posix_fadvise(), which could drop the rest of the file from cache. - buzhash: check for len == 0 edge case - correctly Py_DECREF in cases of errors - check for malloc/calloc failures (cherry picked from commit 37f66f1)
- avoid buckets_length integer overflow on 32bit systems via huge num_buckets - always initialize index-> min_empty and num_empty - correctly free memory when header validation fails. this is a minor issue, because borg will terminate in that case anyway. - make it possible to lookup in compacted hashtables - deal safely with empty index: we must use num_buckets = 1 to avoid division by zero and sanity check in hashindex_read. - reinitialize upper/lower limit and min_empty after compact - fix size_idx / fit_size / grow_size / shrink_size (mind array bounds) - deal with growing when already at max capacity - hashindex_resize: replace num_entries assertion, rather return error - BaseIndex.clear: always stay in valid state Do not free the old index before we successfully have allocated a new one. This is a minor issue as the Exception raised would terminate borg anyway. (cherry picked from commit cd2f5a0)
The previous code performed allocations and buffer acquisitions before the `try` block. If a later allocation or buffer acquisition failed, execution did not enter the `finally` block, so resources acquired earlier in the setup path could leak. Move allocation and buffer acquisition into the guarded block, initialize raw output pointers to `NULL`, and only call `PyMem_Free` or `PyBuffer_Release` for resources that were actually acquired. (cherry picked from commit 2d7d5f2)
ThomasWaldmann
force-pushed
the
1.2-fixes
branch
from
July 20, 2026 13:26
fa52539 to
c2d0844
Compare
ThomasWaldmann
marked this pull request as ready for review
July 20, 2026 14:00
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.