On November 4, 2026, Debian’s security team and FTP masters pushed an urgent update to the Advanced Package Tool: apt 3.0.4 for Debian 13 “Trixie” (DSA-6607-1) and apt 2.6.2 for Debian 12 “Bookworm” (DLA-4880-1). It closes a high-severity path to root. The changelog entry that fixes it, signed by APT maintainer Julian Andres Klode ([email protected]), is a single line: methods/*.cc: Open target files exclusively.
That line resolves CVE-2026-109518, a time-of-check-to-time-of-use (TOCTOU) and hard-link confusion flaw reported by security researcher Max Justicz.
Sysadmins have long relied on one assumption. Even if a Debian or Ubuntu mirror is served over plain HTTP, or reached across a hostile network, APT’s signature chain stops package tampering. The distribution’s offline OpenPGP keys sign the repository’s InRelease metadata (or Release plus Release.gpg). That metadata binds the SHA-256 digests of the Packages indices, and those bind the SHA-256 checksum and byte length of every .deb archive. A man-in-the-middle (MitM) attacker or a malicious mirror should not be able to change a single byte of a package without APT aborting the transaction.
The cryptography is untouched here. The flaw sits in the stateful filesystem layer where APT’s worker processes manage files between the moment a checksum is computed and the moment dpkg unpacks the archive as root. By combining HTTP redirects, download retries (Acquire::Retries), HTTP Range resumption, and hard-link reuse inside /var/cache/apt/archives/, an on-path attacker can make two download items share one inode and overwrite a verified .deb with malicious bytes after verification has already succeeded.

CVE-2026-109518 at a glance
| Item | Detail |
|---|---|
| CVE | CVE-2026-109518 |
| Component | APT (libapt-pkg and the helper binaries in /usr/lib/apt/methods/) |
| Flaw type | TOCTOU race and hard-link confusion |
| Attacker position | On-path network attacker or malicious mirror |
| Impact | Root code execution through dpkg maintainer scripts |
| Severity | 7.7 High, CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:P/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N |
| Reported by | Max Justicz |
| Fix | methods/*.cc: Open target files exclusively (Julian Andres Klode) |
| Patched | November 4, 2026 |
How libapt-pkg and the Acquire subsystem handle downloads
To see why a one-line change in methods/*.cc mattered, you need to know how libapt-pkg separates privilege and orchestrates network I/O.
Privilege separation between apt and its method binaries
When you run apt update or apt install, the privileged apt process does not perform network socket operations itself. It instantiates a pkgAcquire scheduler object (apt-pkg/acquire.cc), which spawns helper binaries from /usr/lib/apt/methods/, such as http, https, mirror, gpgv, and copy. These typically drop privileges to the unprivileged _apt system user, which can write only under /var/cache/apt/archives/partial/ and /var/lib/apt/lists/partial/.
The APT method interface and its IPC protocol
The privileged pkgAcquire coordinator and the unprivileged method binaries talk over standard input and standard output, using a plaintext, HTTP-like protocol defined in the APT Method Interface specification. Each message has a three-digit status code, a short informational header, a series of Key: Value header lines, and a terminating blank line.
When pkgAcquire needs a .deb, represented internally as a pkgAcqArchive instance (apt-pkg/acquire-item.cc), it assigns a staging path inside /var/cache/apt/archives/partial/ and sends a 600 URI Acquire message over the pipe to the http worker:
600 URI AcquireURI: http://deb.debian.org/debian/pool/main/b/bash/bash_5.2.37-2_amd64.debFilename: /var/cache/apt/archives/partial/bash_5.2.37-2_amd64.debExpected-SHA256: <64-hex-char-digest>Size: <expected-byte-size>
Where hashing and verification happen
The http method lives across methods/http.cc, methods/basehttp.cc, and methods/aptmethod.cc. As it connects to the remote server, it sends 102 Status messages back to the coordinator, then a 200 URI Start once the HTTP response headers (200 OK or 206 Partial Content) arrive. The worker computes the rolling hashes (MD5Sum, SHA1, SHA256, SHA512) itself as bytes stream from the socket into the target file descriptor, or hashes the existing prefix bytes on disk when it resumes a partial download. When the transfer finishes, it emits a 201 URI Done message carrying the digests of the file it just wrote.
On receiving that message, pkgAcqArchive::Done() compares the reported SHA256-Hash against the Expected-SHA256 recorded in the verified Packages index. Only when the digest and the file size both match does the coordinator promote the package out of the partial/ staging directory into /var/cache/apt/archives/, ready for dpkg --unpack and dpkg --configure at the end of the acquisition phase.
📬 Stay Ahead of Cyber Threats
Get the latest cybersecurity news, critical vulnerabilities, threat intelligence, tutorials, and exclusive giveaways delivered straight to your inbox. No spam. Unsubscribe anytime.
Subscribe to the Newsletter →
How hard links, redirects, and Acquire::Retries collide
CVE-2026-109518 emerges where three mechanisms in this pipeline intersect: hard-link creation during download state transitions, URI queue deduplication across HTTP redirects, and automatic download retries with Range resumption.
Download retries and HTTP Range resumption
Starting with Debian 12 (Bookworm) and Ubuntu 22.04 LTS (Jammy Jellyfish), APT changed the default value of Acquire::Retries from 0 to 3. When a download hits a transient HTTP error, a premature TCP connection reset, or a corrupt payload, pkgAcquire::Item::Failed() no longer aborts the installation. It decrements the item’s retry counter and re-queues the download. If partial data already exists on disk, or an earlier attempt left a file in place, the http method calls stat() on the destination, sends an HTTP Range: bytes=<offset>- or If-Range header, and opens the target file for writing so it can seek to the resume offset or truncate and rewrite the file.
HTTP redirects and the 103 Redirect message
Now suppose an attacker on the network path, or the operator of a community mirror listed in /etc/apt/sources.list, answers package requests with 301 Moved Permanently or 302 Found. The http method does not follow the redirect silently inside the same socket context. It sends a 103 Redirect IPC message with a New-URI field back to pkgAcquire.
Inside pkgAcquire::Worker::RunMessages(), the coordinator handles that message by updating the queued item’s active URI and re-enqueuing it on the right transport queue. During an apt install or apt full-upgrade that fetches many packages, an attacker can redirect two separate download contexts onto the same URI, or manipulate the order of redirects and retries across items. In APT’s acquisition engine, items that share target URIs or move between staging states (partial/ versus final archive paths, or shared QueueItem entries) use hard links (link()) and renames (rename()) to avoid writing the same data to disk twice.
Hard links and shared inodes
Ubuntu’s security advisory attributes the vulnerability to the reuse of hard links combined with redirect following and download retries. When a hard link ties a verified or staging file to a second context, whether between two download items or between partial/ and the verified cache directory during state transitions and retry setup, both directory entries point to the same filesystem inode.
POSIX permissions and contents belong to the inode, not to the directory entry. Any process that holds, or later opens, a writable file descriptor (O_WRONLY or O_RDWR) on either path modifies the underlying data blocks for both paths at once.
The attack, step by step
- Redirect convergence. A user, or an automated cron or systemd job, installs several packages over HTTP. The on-path attacker lets the first package (Package A) download, or uses
103 Redirectresponses so that two download contexts merge onto the same URI or staging target. - Verification with the link still in place. Package A completes. The
httpworker hashes the stream, reports201 URI Donewith a SHA-256 digest that matches the signed Packages index, andpkgAcquiremarks Package A as verified (StatDone). Because of hard-link reuse across the merged items, or a failed and retried transfer (a deliberate connection reset or HTTP error will do), a second download context still references a path hard-linked to the verified inode. - Overwrite after verification.
Acquire::Retriesis non-zero by default (3), so the interrupted or redirected second context calls thehttpworker again on the shared target file. Before the patch, the method opened that file with ordinary non-exclusive flags (O_CREAT | O_WRONLY, or appending to or truncating an existing file for200 OKor206 Partial Contentresumption) and made no attempt to guarantee exclusive creation or break existing hard links. - Root execution through dpkg. The worker writes attacker-controlled bytes into the open file descriptor. That descriptor points to the same inode as the already verified
.debsitting in the installation queue, so the package’s contents change silently after the SHA-256 check returned success. When acquisition ends and APT invokes dpkg as root, dpkg extracts the tampered archive and runs itspreinstorpostinstmaintainer scripts with full root privileges.
The fix: why opening target files exclusively works
When Julian Andres Klode committed the fix, the goal was to enforce strict inode isolation at the syscall boundary instead of patching every permutation of redirect deduplication in the high-level scheduler.
APT handles file descriptors through the FileFd class (apt-pkg/contrib/fileutl.cc and fileutl.h), which wraps POSIX open() with flags such as FileFd::WriteOnly, FileFd::Create, FileFd::Exclusive, FileFd::Atomic, and FileFd::Empty. Across the transport methods in methods/*.cc (including basehttp.cc, http.cc, file.cc, copy.cc, and ftp.cc), opening the destination exclusively (O_CREAT | O_EXCL, or unlinking and re-creating the file with FileFd::Exclusive) gives a hard kernel-level guarantee: a download worker never opens and writes an existing file that is hard-linked elsewhere or held by another transaction.
On Linux, open(path, O_CREAT | O_EXCL | O_WRONLY, 0644) fails with EEXIST if the path already exists, even as a dangling or hard-linked file left over from a redirect merge or an earlier verification, unless the file was unlinked first to sever the shared inode (st_nlink > 1). When the worker resumes or truncates a partial file in /var/cache/apt/archives/partial/, severing hard links and requiring exclusive creation means the descriptor held by the _apt process points to a freshly allocated or strictly isolated inode (st_nlink == 1). That inode cannot alias a verified package in /var/cache/apt/archives/.
So even if an on-path attacker builds a labyrinth of 302 redirects and TCP resets to confuse the URI queues, the kernel refuses to let a second download stream modify the inode of a previously verified file. The TOCTOU window closes because that inode can never be reopened for writing by another download worker.
Hash verification vouches for a sequence of bytes at the instant the hash function finalizes. Keeping those bytes unchanged until dpkg runs is the filesystem’s job, and this bug lived in the gap between the two.
Other security fixes in apt 3.0.4 and 3.3.6
The changelog for apt 3.0.4 (and upstream apt 3.3.6) carries six memory-safety, parser, and algorithmic fixes grouped under SECURITY UPDATE: Out-of-bounds accesses, plus a fix for Debian bug #1111554. Each defect sits in code that parses untrusted or semi-trusted input during package acquisition and resolution.
deb parser: unsigned underflow when trimming control newlines
The changelog entry reads deb: guard against unsigned underflow when trimming control newlines. When APT inspects a .deb archive (apt-pkg/deb/debfile.cc), it extracts the control.tar member to read the package metadata stanza. While stripping trailing newline characters from the buffer, the parser decremented an unsigned length variable (size_t) or buffer index in a loop. If a malformed .deb held an empty control buffer, or one made up only of newlines, decrementing a size_t of 0 wrapped it around to SIZE_MAX (0xFFFFFFFFFFFFFFFF on 64-bit systems) and triggered an out-of-bounds read before the start of the buffer. An explicit bounds guard (length > 0) prevents the underflow.
gpgv: reading past the null terminator in PushEntryWithKeyID
The gpgv method (methods/gpgv.cc) invokes gpgv to verify InRelease and Release.gpg signatures and parses its machine-readable --status-fd output ([GNUPG:] GOODSIG, VALIDSIG, ERRSIG, BADSIG). The helper PushEntryWithKeyID tokenizes status lines by scanning for whitespace and advancing a const char * pointer to extract the signer’s key ID or fingerprint. If a malformed or truncated status line lacked the expected trailing tokens, the pointer stepped past the C-string’s \0 terminator (p + 1 after hitting \0) and read out-of-bounds heap memory into the verification status vector. The fix stops advancing at the terminator.
debDebFileParser: rejecting a .deb with no Version field
When you install a local .deb directly (for example apt install ./custom.deb), debDebFileParser (apt-pkg/deb/deblistparser.cc) parses the control stanza and populates APT’s internal pkgCache. Debian Policy (section 5.6.12) makes the Version field mandatory in binary package control files. A corrupted or fuzz-generated .deb without one reached debDebFileParser::UsePackage() with an uninitialized, end-of-range VerIterator. Dereferencing it caused an out-of-bounds read and a segmentation fault (SIGSEGV). The parser now checks for a Version field and rejects such archives cleanly.
srvrec: res_query answers larger than the buffer
When APT discovers mirrors through DNS SRV records (_http._tcp.deb.debian.org), apt-pkg/contrib/srvrec.cc calls the libresolv function res_query() (or res_nquery()) with a fixed-size buffer. The classic BSD and glibc resolver API returns the actual length of the DNS response, not the number of bytes clamped to the caller’s buffer. If an attacker or misconfigured DNS server sent a large response (through EDNS0 or TCP fallback) that exceeded the buffer, res_query() returned a length larger than the buffer, and APT’s packet parser then iterated up to that length and read past the end. The fix, contributed by Ramesh Adhikari, rejects any response whose returned length exceeds sizeof(buffer).
tagfile: unbounded backward scan in Fill()
The pkgTagFile parser (apt-pkg/tagfile.cc) handles RFC-822-style Debian control files: Packages, Sources, InRelease, and Translation-*. Those files can run to tens of megabytes, so pkgTagFile::Fill() reads chunks into a sliding buffer and scans backward from the end of the chunk to find the last complete stanza boundary, marked by trailing newlines, so it does not split a stanza mid-line. If a chunk contained no newline at all (an abnormally long single line in a crafted index file, for instance), the backward loop had no lower-bound check against the start of the buffer and walked back into unmapped or adjacent heap memory before the allocation.
solver: workLess must be a strict weak ordering
C++ sorting and priority queue algorithms (std::sort, std::make_heap, std::priority_queue) require the comparison functor to be a strict weak ordering:
- Irreflexivity: not (a < a)
- Asymmetry: if a < b, then not (b < a)
- Transitivity: if a < b and b < c, then a < c
- Transitivity of equivalence: if a ~ b and b ~ c, then a ~ c
In APT’s newer dependency solver (apt-pkg/solver3.cc, merged through apt-team/apt!643 in commit 54ec815244b9edc4f594390e984c681fc0cb5ded), the workLess comparator that orders work items broke strict weak ordering under specific tie-breaking conditions. In libstdc++, passing such a comparator to std::sort is undefined behavior. It is known to push the partitioning pivot logic past array bounds, corrupting heap memory or crashing during complex dependency calculations.
Acquire: “Tried to start delayed item” warnings and OOM (Debian bug #1111554)
The last entry moves Tried to start delayed item [...], but failed to debugging and closes Debian bug #1111554 (and its merged duplicate #1152704). During large downloads, or daily unattended background updates (apt-daily.service), a temporarily saturated connection pool or pipeline made the Acquire loop emit that warning over and over. It flooded cron mail and journal logs. It also queued millions of identical strings in APT’s internal _error stack (GlobalError) during stalled transfers, and the resulting unbounded memory growth could exhaust memory (OOM) mid-download. Commit 02ee82958b5caef9efb49992870d149907869893 demoted the message so it appears only when Debug::Acquire::Transaction is explicitly enabled.
Does HTTPS protect you from CVE-2026-109518?
Every time an APT transport bug surfaces, as with CVE-2019-3462 (the 2019 HTTP redirect injection bug) or CVE-2016-1252, someone suggests that switching /etc/apt/sources.list from http:// to https:// makes the problem go away. For this one it does not.
TLS does protect you from a passive Wi-Fi eavesdropper, or from a rogue ISP router injecting 302 redirects into a connection headed for deb.debian.org. It does not protect you from a malicious or compromised repository mirror, a corporate caching proxy (apt-cacher-ng, Squid), or a compromised CDN edge node. Debian and Ubuntu treat mirrors as untrusted byte-delivery mechanisms. APT’s whole security architecture assumes a mirror operator can control every HTTP or HTTPS response and still cannot trick a client into installing an unsigned binary.
CVE-2026-109518 is a hard-link and file-descriptor exclusivity flaw triggered by redirects and interrupted connections. So any HTTPS endpoint in sources.list or /etc/apt/sources.list.d/, third-party vendor repositories included, that is compromised or malicious can run the same redirect-and-retry state machine through an encrypted tunnel. Encrypting the pipe does nothing when the party at the other end is the one sending the redirects.
Fixed APT versions by distribution
CVE-2026-109518 allows remote code execution as root under default configurations on Debian 12 and later, Debian 13 and later, and Ubuntu 22.04 LTS and later. Prioritize workstations, bare-metal servers, and container base images (including Dockerfile pipelines that run apt-get update && apt-get install). Check your installed version with apt-cache policy apt or dpkg -l apt and compare it with the releases issued on November 4, 2026:
Distribution Fixed in Advisory Debian Unstable (Sid) and Testing (Forky) apt 3.3.6 Debian 13 “Trixie” (stable) apt 3.0.4 DSA-6607-1 Debian 12 “Bookworm” (oldstable, LTS) apt 2.6.2 DLA-4880-1 Debian 11 “Bullseye” (ELTS) apt 2.2.5 Ubuntu 26.10 apt 3.3.3ubuntu0.1 Ubuntu 26.04 LTS apt 3.2.1ubuntu0.1 Ubuntu 24.04 LTS “Noble Numbat” apt 2.8.4ubuntu0.1 Ubuntu 22.04 LTS “Jammy Jellyfish” apt 2.4.15ubuntu0.1
How to update APT safely when your current apt is vulnerable
Patching a package manager has a catch-22: apt update && apt upgrade uses the unpatched apt binary to fetch the patched one. The attack needs download retries (Acquire::Retries above zero) and redirect following, so you can disable both for the upgrade transaction itself with command-line overrides:
sudo apt-get -o Acquire::Retries=0 -o Acquire::http::AllowRedirect=false updatesudo apt-get -o Acquire::Retries=0 -o Acquire::http::AllowRedirect=false --only-upgrade install apt libapt-pkg*
If your mirror, such as deb.debian.org, relies on HTTP redirects to route you to a CDN backend, leave out Acquire::http::AllowRedirect=false and rely on -o Acquire::Retries=0. That disables the download resumption path the exploit needs.
Air-gapped environments and legacy appliances that cannot install the updated package right away can enforce the retry mitigation globally with a drop-in configuration file:
echo 'Acquire::Retries "0";' | sudo tee /etc/apt/apt.conf.d/99-mitigate-cve-2026-109518
Once the system runs apt 3.0.4 (or the fixed version for your distribution), remove the workaround so APT regains its normal resilience against transient packet loss.
sudo rm /etc/apt/apt.conf.d/99-mitigate-cve-2026-109518









