Version in base suite: 3.0.3 Base version: apt_3.0.3 Target version: apt_3.0.4 Base file: /srv/ftp-master.debian.org/ftp/pool/main/a/apt/apt_3.0.3.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/a/apt/apt_3.0.4.dsc .gitlab-ci.yml | 4 - CMakeLists.txt | 2 apt-pkg/acquire.cc | 6 - apt-pkg/contrib/srvrec.cc | 5 + apt-pkg/deb/debindexfile.cc | 2 apt-pkg/deb/deblistparser.cc | 5 + apt-pkg/solver3.cc | 2 apt-pkg/tagfile.cc | 2 debian/changelog | 21 ++++++ doc/apt-verbatim.ent | 2 doc/po/apt-doc.pot | 4 - methods/gpgv.cc | 3 methods/http.cc | 18 +++++ methods/rred.cc | 6 - methods/store.cc | 4 - po/apt-all.pot | 4 - test/integration/framework | 4 - test/integration/test-apt-download-onto-hardlink | 71 +++++++++++++++++++++++ test/integration/test-apt-no-version | 35 +++++++++++ 19 files changed, 176 insertions(+), 24 deletions(-) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpiyt1nlth/apt_3.0.3.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpiyt1nlth/apt_3.0.4.dsc: no acceptable signature found diff -Nru apt-3.0.3/.gitlab-ci.yml apt-3.0.4/.gitlab-ci.yml --- apt-3.0.3/.gitlab-ci.yml 2025-06-24 17:02:46.000000000 +0000 +++ apt-3.0.4/.gitlab-ci.yml 2026-11-02 13:41:34.000000000 +0000 @@ -1,4 +1,4 @@ -image: debian:testing +image: debian:trixie variables: DEBIAN_FRONTEND: noninteractive CCACHE_DIR: $CI_PROJECT_DIR/.ccache @@ -43,7 +43,7 @@ test as user: image: - name: debian:testing + name: debian:trixie docker: platform: linux/i386 stage: test diff -Nru apt-3.0.3/CMakeLists.txt apt-3.0.4/CMakeLists.txt --- apt-3.0.3/CMakeLists.txt 2025-06-24 17:02:46.000000000 +0000 +++ apt-3.0.4/CMakeLists.txt 2026-11-02 13:41:34.000000000 +0000 @@ -204,7 +204,7 @@ # Configure some variables like package, version and architecture. set(PACKAGE ${PROJECT_NAME}) set(PACKAGE_MAIL "APT Development Team ") -set(PACKAGE_VERSION "3.0.3") +set(PACKAGE_VERSION "3.0.4") string(REGEX MATCH "^[0-9.]+" PROJECT_VERSION ${PACKAGE_VERSION}) if (NOT DEFINED DPKG_DATADIR) diff -Nru apt-3.0.3/apt-pkg/acquire.cc apt-3.0.4/apt-pkg/acquire.cc --- apt-3.0.3/apt-pkg/acquire.cc 2025-06-24 17:02:46.000000000 +0000 +++ apt-3.0.4/apt-pkg/acquire.cc 2026-11-02 13:41:34.000000000 +0000 @@ -740,10 +740,8 @@ if (not I->Cycle()) // Queue got stuck, unstuck it. goto stop; fetchAfter = now; // need to time out in select() below - if (I->Items->Owner->Status == pkgAcquire::Item::StatIdle) - { - _error->Warning("Tried to start delayed item %s, but failed", I->Items->Description.c_str()); - } + if (Debug && I->Items->Owner->Status == pkgAcquire::Item::StatIdle) + clog << "Tried to start delayed item but failed:" << I->Items->Description.c_str() << std::endl; } else if (f < fetchAfter || fetchAfter == time_point{}) { diff -Nru apt-3.0.3/apt-pkg/contrib/srvrec.cc apt-3.0.4/apt-pkg/contrib/srvrec.cc --- apt-3.0.3/apt-pkg/contrib/srvrec.cc 2025-06-24 17:02:46.000000000 +0000 +++ apt-3.0.4/apt-pkg/contrib/srvrec.cc 2026-11-02 13:41:34.000000000 +0000 @@ -81,6 +81,11 @@ #endif //__RES >= 19991006 if (answer_len == -1) return false; + // res_query can report a length bigger than answer if the reply got + // truncated, treat that the same as any other bad reply instead of + // trusting a length that reaches past our buffer + if (answer_len > (int)sizeof(answer)) + return _error->Warning("res_query returned too much data (%i)", answer_len); if (answer_len < (int)sizeof(HEADER)) return _error->Warning("Not enough data from res_query (%i)", answer_len); diff -Nru apt-3.0.3/apt-pkg/deb/debindexfile.cc apt-3.0.4/apt-pkg/deb/debindexfile.cc --- apt-3.0.3/apt-pkg/deb/debindexfile.cc 2025-06-24 17:02:46.000000000 +0000 +++ apt-3.0.4/apt-pkg/deb/debindexfile.cc 2026-11-02 13:41:34.000000000 +0000 @@ -185,7 +185,7 @@ return _error->Error(_("Could not read meta data from %s"), debfile.c_str()); // trim off newlines - while (extractor.Control[extractor.Length] == '\n') + while (extractor.Length > 0 && extractor.Control[extractor.Length] == '\n') extractor.Control[extractor.Length--] = '\0'; const char *Control = extractor.Control; while (isspace_ascii(Control[0])) diff -Nru apt-3.0.3/apt-pkg/deb/deblistparser.cc apt-3.0.4/apt-pkg/deb/deblistparser.cc --- apt-3.0.3/apt-pkg/deb/deblistparser.cc 2025-06-24 17:02:46.000000000 +0000 +++ apt-3.0.4/apt-pkg/deb/deblistparser.cc 2026-11-02 13:41:34.000000000 +0000 @@ -24,6 +24,8 @@ #include #include +#include + #include #include #include @@ -1033,6 +1035,9 @@ bool debDebFileParser::UsePackage(pkgCache::PkgIterator &Pkg, pkgCache::VerIterator &Ver) { + if (Ver.end()) + return _error->Error("Package %s has no Version field", Pkg.Name()); + if (not debListParser::UsePackage(Pkg, Ver)) return false; // we use the full file path as a provides so that the file is found by its name diff -Nru apt-3.0.3/apt-pkg/solver3.cc apt-3.0.4/apt-pkg/solver3.cc --- apt-3.0.3/apt-pkg/solver3.cc 2025-06-24 17:02:46.000000000 +0000 +++ apt-3.0.4/apt-pkg/solver3.cc 2026-11-02 13:41:34.000000000 +0000 @@ -227,7 +227,7 @@ return clause->group > b.clause->group; if ((size < 2) != (b.size < 2)) return b.size < 2; - if (size == 1 && b.size == 1) // Special case: 'shortcircuit' optional packages + if (size < 2 && b.size < 2) // Special case: 'shortcircuit' optional packages return clause->solutions.size() < b.clause->solutions.size(); return false; } diff -Nru apt-3.0.3/apt-pkg/tagfile.cc apt-3.0.4/apt-pkg/tagfile.cc --- apt-3.0.3/apt-pkg/tagfile.cc 2025-06-24 17:02:46.000000000 +0000 +++ apt-3.0.4/apt-pkg/tagfile.cc 2026-11-02 13:41:34.000000000 +0000 @@ -412,7 +412,7 @@ // Append a double new line if one does not exist unsigned int LineCount = 0; - for (const char *E = d->End - 1; E - d->End < 6 && (*E == '\n' || *E == '\r'); E--) + for (const char *E = d->End - 1; E >= d->Buffer && d->End - E < 6 && (*E == '\n' || *E == '\r'); E--) if (*E == '\n') ++LineCount; if (LineCount < 2) diff -Nru apt-3.0.3/debian/changelog apt-3.0.4/debian/changelog --- apt-3.0.3/debian/changelog 2025-06-24 17:02:46.000000000 +0000 +++ apt-3.0.4/debian/changelog 2026-11-02 13:41:34.000000000 +0000 @@ -1,3 +1,24 @@ +apt (3.0.4) trixie-security; urgency=medium + + * SECURITY UPDATE: Download verification bypass + - methods/*.cc: Open target files exclusively + - CVE-2026-109518 + * SECURITY UPDATE: Out-of-bounds accesses + - deb: guard against unsigned underflow when trimming control newlines + - gpgv: don't advance past the null terminator in PushEntryWithKeyID + - debDebFileParser: reject .deb without a Version field + - srvrec: reject res_query answers bigger than our buffer + - tagfile: fix unbounded backward scan in Fill()'s trailing-newline check + - solver: Ensure workLess is a strict weak ordering + * Memory exhaustion during download + - Move "Tried to start delayed item […], but failed" to debugging + (Closes: #1111554) + * Test suite reliability: + - test: Limit valgrind to 1024 open files + - .gitlab-ci.yml: Point at debian:trixie + + -- Julian Andres Klode Mon, 02 Nov 2026 14:41:34 +0100 + apt (3.0.3) unstable; urgency=medium [ Américo Monteiro ] diff -Nru apt-3.0.3/doc/apt-verbatim.ent apt-3.0.4/doc/apt-verbatim.ent --- apt-3.0.3/doc/apt-verbatim.ent 2025-06-24 17:02:46.000000000 +0000 +++ apt-3.0.4/doc/apt-verbatim.ent 2026-11-02 13:41:34.000000000 +0000 @@ -268,7 +268,7 @@ "> - + diff -Nru apt-3.0.3/doc/po/apt-doc.pot apt-3.0.4/doc/po/apt-doc.pot --- apt-3.0.3/doc/po/apt-doc.pot 2025-06-24 17:02:46.000000000 +0000 +++ apt-3.0.4/doc/po/apt-doc.pot 2026-11-02 13:41:34.000000000 +0000 @@ -5,9 +5,9 @@ #, fuzzy msgid "" msgstr "" -"Project-Id-Version: apt-doc 3.0.3\n" +"Project-Id-Version: apt-doc 3.0.4\n" "Report-Msgid-Bugs-To: APT Development Team \n" -"POT-Creation-Date: 2025-06-24 17:03+0000\n" +"POT-Creation-Date: 2026-11-02 15:14+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff -Nru apt-3.0.3/methods/gpgv.cc apt-3.0.4/methods/gpgv.cc --- apt-3.0.3/methods/gpgv.cc 2025-06-24 17:02:46.000000000 +0000 +++ apt-3.0.4/methods/gpgv.cc 2026-11-02 13:41:34.000000000 +0000 @@ -135,7 +135,8 @@ while (*p && !isspace(*p)) ++p; // skip the separator whitespace - ++p; + if (*p != '\0') + ++p; // skip the hexdigit fingerprint while (*p && isxdigit(*p)) ++p; diff -Nru apt-3.0.3/methods/http.cc apt-3.0.4/methods/http.cc --- apt-3.0.3/methods/http.cc 2025-06-24 17:02:46.000000000 +0000 +++ apt-3.0.4/methods/http.cc 2026-11-02 13:41:34.000000000 +0000 @@ -1003,8 +1003,24 @@ auto ret = BaseHttpMethod::DealWithHeaders(Res, Req); if (ret != BaseHttpMethod::FILE_IS_OPEN) return ret; - if (Req.File.Open(Queue->DestFile, FileFd::WriteAny) == false) + + // If we are resuming, we first need to rescue the data we already have + FileFd partial; + if (Req.StartPos > 0 && FileExists(Queue->DestFile)) + { + if (not partial.Open(Queue->DestFile, FileFd::ReadOnly)) + return ERROR_NOT_FROM_SERVER; + } + // Open the file exclusively to avoid injection attacks by links + if (Req.File.Open(Queue->DestFile, FileFd::WriteAny | FileFd::Exclusive) == false) return ERROR_NOT_FROM_SERVER; + if (partial.IsOpen()) + { + // The exclusive open unlinked the old file; copy its content + // into the new one and rewind for hashing. + if (not CopyFile(partial, Req.File) || not partial.Close() || not Req.File.Seek(0)) + return ERROR_NOT_FROM_SERVER; + } FailFile = Queue->DestFile; (void)(FailFile.c_str()); // Make sure we don't do a malloc in the signal handler diff -Nru apt-3.0.3/methods/rred.cc apt-3.0.4/methods/rred.cc --- apt-3.0.3/methods/rred.cc 2025-06-24 17:02:46.000000000 +0000 +++ apt-3.0.4/methods/rred.cc 2026-11-02 13:41:34.000000000 +0000 @@ -718,7 +718,7 @@ std::clog << "FAILED to open inp " << Path << std::endl; return _error->Error("Failed to open inp %s", Path.c_str()); } - if (out.Open(Itm->DestFile, FileFd::WriteOnly | FileFd::Create | FileFd::Empty | FileFd::BufferedWrite, FileFd::Extension) == false) + if (out.Open(Itm->DestFile, FileFd::WriteOnly | FileFd::Create | FileFd::Exclusive | FileFd::BufferedWrite, FileFd::Extension) == false) { if (Debug == true) std::clog << "FAILED to open out " << Itm->DestFile << std::endl; @@ -834,9 +834,9 @@ std::clog << "Patching " << CmdL.FileList[0] << " into " << CmdL.FileList[1] << "\n"; input.Open(CmdL.FileList[0], FileFd::ReadOnly,FileFd::Extension); if (compressor == nullptr) - output.Open(CmdL.FileList[1], FileFd::WriteOnly | FileFd::Create | FileFd::Empty | FileFd::BufferedWrite, FileFd::Extension); + output.Open(CmdL.FileList[1], FileFd::WriteOnly | FileFd::Create | FileFd::Exclusive | FileFd::BufferedWrite, FileFd::Extension); else - output.Open(CmdL.FileList[1], FileFd::WriteOnly | FileFd::Create | FileFd::Empty | FileFd::BufferedWrite, *compressor); + output.Open(CmdL.FileList[1], FileFd::WriteOnly | FileFd::Create | FileFd::Exclusive | FileFd::BufferedWrite, *compressor); argi = 2; } else diff -Nru apt-3.0.3/methods/store.cc apt-3.0.4/methods/store.cc --- apt-3.0.3/methods/store.cc 2025-06-24 17:02:46.000000000 +0000 +++ apt-3.0.4/methods/store.cc 2026-11-02 13:41:34.000000000 +0000 @@ -90,8 +90,8 @@ { if (_config->FindB("Method::Compress", false) == false) To.Open(Itm->DestFile, FileFd::WriteOnly | FileFd::Create | FileFd::Atomic, FileFd::Extension); - else if (OpenFileWithCompressorByName(To, Itm->DestFile, FileFd::WriteOnly | FileFd::Create | FileFd::Empty, Binary) == false) - return false; + else if (OpenFileWithCompressorByName(To, Itm->DestFile, FileFd::WriteOnly | FileFd::Create | FileFd::Exclusive, Binary) == false) + return false; if (To.IsOpen() == false || To.Failed() == true) return false; diff -Nru apt-3.0.3/po/apt-all.pot apt-3.0.4/po/apt-all.pot --- apt-3.0.3/po/apt-all.pot 2025-06-24 17:02:46.000000000 +0000 +++ apt-3.0.4/po/apt-all.pot 2026-11-02 13:41:34.000000000 +0000 @@ -5,9 +5,9 @@ #, fuzzy msgid "" msgstr "" -"Project-Id-Version: apt 3.0.3\n" +"Project-Id-Version: apt 3.0.4\n" "Report-Msgid-Bugs-To: APT Development Team \n" -"POT-Creation-Date: 2025-06-24 17:03+0000\n" +"POT-Creation-Date: 2026-11-02 15:14+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff -Nru apt-3.0.3/test/integration/framework apt-3.0.4/test/integration/framework --- apt-3.0.3/test/integration/framework 2025-06-24 17:02:46.000000000 +0000 +++ apt-3.0.4/test/integration/framework 2026-11-02 13:41:34.000000000 +0000 @@ -264,9 +264,9 @@ CMD="${APTCMDLINEBINDIR}/${CMD}" fi if command valgrind --version >/dev/null 2>&1; then - runapt command valgrind -q --error-exitcode=7 "$CMD" "$@" + runapt command prlimit --nofile=1024 valgrind -q --error-exitcode=7 "$CMD" "$@" else - runapt command "$CMD" "$@" + runapt command prlimit --nofile=1024 "$CMD" "$@" fi } diff -Nru apt-3.0.3/test/integration/test-apt-download-onto-hardlink apt-3.0.4/test/integration/test-apt-download-onto-hardlink --- apt-3.0.3/test/integration/test-apt-download-onto-hardlink 1970-01-01 00:00:00.000000000 +0000 +++ apt-3.0.4/test/integration/test-apt-download-onto-hardlink 2026-11-02 13:41:34.000000000 +0000 @@ -0,0 +1,71 @@ +#!/bin/sh +set -e + +TESTDIR="$(readlink -f "$(dirname "$0")")" +. "$TESTDIR/framework" +setupenvironment +configarchitecture 'amd64' + +changetowebserver +webserverconfig 'aptwebserver::support::range' 'true' +webserverconfig 'aptwebserver::response-header::Accept-Ranges' 'bytes' + +TESTFILE='aptarchive/testfile' +cp -a "${TESTDIR}/framework" "$TESTFILE" +DOWN='downloaded/testfile' +VICTIM='downloaded/victim' +DOWNLOADLOG='rootdir/tmp/testdownloadfile.log' + +testdownload() { + rm -f "$DOWNLOADLOG" + msgtest "Download testfile over $1" + if downloadfile "http://localhost:${APTHTTPPORT}/testfile" "$DOWN" '' > "$DOWNLOADLOG" 2>&1; then + msgpass + else + cat >&2 "$DOWNLOADLOG" + msgfail + fi + msgtest 'Downloaded file has the expected content' + if cmp --silent "$DOWN" "$TESTFILE"; then + msgpass + else + msgfail + fi + msgtest 'Victim file was not modified by the download' + if cmp --silent "$VICTIM" victim.expected; then + msgpass + else + msgfail + fi + testsuccess test ! "$DOWN" -ef "$VICTIM" +} + +msgmsg 'Fresh download onto a hardlink to the victim file' +echo 'do not modify me' > "$VICTIM" +cp -a "$VICTIM" victim.expected +ln -f "$VICTIM" "$DOWN" +testdownload 'hardlink' +testwebserverlaststatuscode '200' "$DOWNLOADLOG" + +msgmsg 'Resumed download onto a hardlink to the victim file' +rm -f "$DOWN" "$VICTIM" +dd if="$TESTFILE" of="$VICTIM" bs=1 count=$(( $(stat -c '%s' "$TESTFILE") / 2 )) 2>/dev/null +touch -d "$(stat --format '%y' "$TESTFILE")" "$VICTIM" +cp -a "$VICTIM" victim.expected +ln -f "$VICTIM" "$DOWN" +testdownload 'partial hardlink' +testwebserverlaststatuscode '206' "$DOWNLOADLOG" + +msgmsg 'Fresh download onto a symlink to the victim file' +rm -f "$DOWN" "$VICTIM" +echo 'do not modify me' > "$VICTIM" +cp -a "$VICTIM" victim.expected +ln -s "$(readlink -f "$VICTIM")" "$DOWN" +testdownload 'symlink' +testwebserverlaststatuscode '200' "$DOWNLOADLOG" +msgtest 'Symlink was replaced by a regular file' +if [ -L "$DOWN" ]; then + msgfail +else + msgpass +fi diff -Nru apt-3.0.3/test/integration/test-apt-no-version apt-3.0.4/test/integration/test-apt-no-version --- apt-3.0.3/test/integration/test-apt-no-version 1970-01-01 00:00:00.000000000 +0000 +++ apt-3.0.4/test/integration/test-apt-no-version 2026-11-02 13:41:34.000000000 +0000 @@ -0,0 +1,35 @@ +#!/bin/sh +set -e + +TESTDIR="$(readlink -f "$(dirname "$0")")" +. "$TESTDIR/framework" +setupenvironment +configarchitecture 'amd64' + +msgtest 'Regression test: .deb file without Version field' 'is rejected' +# dpkg-deb refuses to build a control file without Version, so craft a +# malformed .deb archive by hand for this regression test. +mkdir -p ./incoming/control ./incoming/data +printf '2.0\n' > ./incoming/debian-binary +cat > ./incoming/control/control < ./placeholder && tar --owner=0 --group=0 -czf ../data.tar.gz ./placeholder +) +# assemble the .deb ar archive +DEBFILE='./incoming/pkg-no-version_0_all.deb' +testsuccess --nomsg ar -rc "$DEBFILE" ./incoming/debian-binary ./incoming/control.tar.gz ./incoming/data.tar.gz + +# apt show on a local .deb parses it via debDebFileParser; without a +# Version field this must fail gracefully rather than crash. +testfailure apt install -s "$DEBFILE" +testsuccess grep 'has no Version field' rootdir/tmp/testfailure.output +msgpass