diff --git a/0001-Prevent-buffer-overruns-in-findPreambleTag-for-langu.patch b/0001-Prevent-buffer-overruns-in-findPreambleTag-for-langu.patch new file mode 100644 index 0000000..0186fa1 --- /dev/null +++ b/0001-Prevent-buffer-overruns-in-findPreambleTag-for-langu.patch @@ -0,0 +1,97 @@ +From c0e4c08533e3e899d7e1cc0d9885e83ea1e1bdfb Mon Sep 17 00:00:00 2001 +From: Dave Cantrell +Date: Mon, 20 Apr 2026 15:05:04 -0400 +Subject: [PATCH] Prevent buffer overruns in findPreambleTag() for language + string +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +This is technically possible and there is a reproducer for it, but I +would not consider this a critical problem. If you have a really long +language identifier string in a preamble tag and it's larger than +BUFSIZ on the platform, you get a SIGSEGV. Not a surprise. + +In spec files you can have stuff like this in the preamble: + + Summary: Here is a short summary + Summary(de): Hier ist eine kurze Zusammenfassung + Summary(eo): Jen mallonga resumo + Summary(Elvish): Sí na- a estent summarui + Summary(ga_ie): Is coimriú ghearr é seo + Summary(Klingon): naDev 'oH ngaj summary + +And findPreambleTag() is eventually called to pick up those language +identifiers in parens. Usually the identifiers are two characters, +but sometimes they are three or four. The parser in the library scans +a character at a time until it hits the closing paren and just stuffs +it all in the 'lang' buffer. The problem is that buffer is BUFSIZ and +there is no bounds checking to see if the string in the spec file in +parens is larger than what BUFSIZ can hold. So it is technically +possible to provide a spec file with a completely useless huge string +as a language identifier that then crashes the spec file parser. + +This patch adds some bounds checking to that reading loop to prevent +this incredibly rare yet technically possible issue. I don't bother +growing the buffer if we're still reading characters because honestly +if we have more than BUFSIZ in parens, we've got a garbage spec file. + +The patch does ensure the unused space in BUFSIZ is NULL and that the +lang buffer is NULL terminated so it's moderately useful in later +parts of the code. + +Signed-off-by: Dave Cantrell +(backported from commit b6f2cc52fcc7eedf0095c2bf0495e4b6aafd87cb) +--- + build/parsePreamble.c | 12 ++++++++---- + 1 file changed, 8 insertions(+), 4 deletions(-) + +diff --git a/build/parsePreamble.c b/build/parsePreamble.c +index 3693746f8..e14664939 100644 +--- a/build/parsePreamble.c ++++ b/build/parsePreamble.c +@@ -1064,10 +1064,11 @@ static struct PreambleRec_s const preambleList[] = { + /** + */ + static int findPreambleTag(rpmSpec spec,rpmTagVal * tag, +- const char ** macro, char * lang) ++ const char ** macro, char * lang, size_t langsize) + { + PreambleRec p; + char *s; ++ size_t l = 0; + + for (p = preambleList; p->token != NULL; p++) { + if (!(p->token && !rstrncasecmp(spec->line, p->token, p->len))) +@@ -1097,14 +1098,17 @@ static int findPreambleTag(rpmSpec spec,rpmTagVal * tag, + case 2: + if (*s == ':') { + /* Type 1 is multilang, 2 is qualifiers with no defaults */ +- strcpy(lang, (p->type == 1) ? RPMBUILD_DEFAULT_LANG : ""); ++ rstrlcpy(lang, (p->type == 1) ? RPMBUILD_DEFAULT_LANG : "", langsize); ++ l = strlen(lang); + break; + } + if (*s != '(') return 1; + s++; + SKIPSPACE(s); +- while (!risspace(*s) && *s != ')') ++ while (!risspace(*s) && *s != ')' && l < (langsize - 1)) { + *lang++ = *s++; ++ l++; ++ } + *lang = '\0'; + SKIPSPACE(s); + if (*s != ')') return 1; +@@ -1173,7 +1177,7 @@ int parsePreamble(rpmSpec spec, int initialPackage) + linep = spec->line; + SKIPSPACE(linep); + if (*linep != '\0') { +- if (findPreambleTag(spec, &tag, ¯o, lang)) { ++ if (findPreambleTag(spec, &tag, ¯o, lang, sizeof(lang))) { + if (spec->lineNum == 1 && + (unsigned char)(spec->line[0]) == 0xed && + (unsigned char)(spec->line[1]) == 0xab && +-- +2.54.0 + diff --git a/plans/python-mockbuild.fmf b/plans/python-mockbuild.fmf index 2b25677..52c8183 100644 --- a/plans/python-mockbuild.fmf +++ b/plans/python-mockbuild.fmf @@ -25,4 +25,8 @@ prepare: how: shell script: touch /etc/yum.repos.d/test-dummy.repo +adjust: + - enabled: false + when: arch != x86_64 + continue: false diff --git a/rpm-4.19.x-add-autosetup-C.patch b/rpm-4.19.x-add-autosetup-C.patch new file mode 100644 index 0000000..e07211d --- /dev/null +++ b/rpm-4.19.x-add-autosetup-C.patch @@ -0,0 +1,1164 @@ +From 93dceb0dc1e05a6e71e966c27814364eb77346d2 Mon Sep 17 00:00:00 2001 +From: Matteo Croce +Date: Tue, 16 Jan 2024 17:26:16 -0500 +Subject: [PATCH 01/14] add build directory auto path to %autosetup + +Add a `-C` flag to %autosetup and %setup which ensures that the sources +will be extracted in the root of the build directory. +It works by inspecting the archive and stripping the first path entry +if the archive has a top level directory alone in the root. + +The archive inspection and path stripping is implemented in rpmuncompress +which now has a new `-C` flag: + + $ ~/rpm/usr/lib/rpm/rpmuncompress -x -v -C source-1.0.0 source-singleroot.tar.gz + mkdir 'source-1.0.0' ; /usr/bin/gzip -dc 'source-singleroot.tar.gz' | /usr/bin/tar -xvvof - -C 'source-1.0.0' --strip-components=1 + -rw-r--r-- root/root 32 2024-01-16 19:13 source-xxxxxxxxxx/file1 + -rw-r--r-- root/root 33886 2024-01-16 19:13 source-xxxxxxxxxx/file2 + drwxr-xr-x root/root 0 2024-01-16 19:13 source-xxxxxxxxxx/dir1/ + -r--r--r-- root/root 210 2024-01-16 19:13 source-xxxxxxxxxx/dir1/file3 + + $ find source-1.0.0 -ls + 92341 0 drwxr-xr-x 1 teknoraver teknoraver 28 Jan 16 19:16 source-1.0.0 + 92342 4 -rw-r--r-- 1 teknoraver teknoraver 32 Jan 16 19:13 source-1.0.0/file1 + 92343 36 -rw-r--r-- 1 teknoraver teknoraver 33886 Jan 16 19:13 source-1.0.0/file2 + 92344 0 drwxr-xr-x 1 teknoraver teknoraver 10 Jan 16 19:13 source-1.0.0/dir1 + 92345 4 -r--r--r-- 1 teknoraver teknoraver 210 Jan 16 19:13 source-1.0.0/dir1/file3 + + $ ~/rpm/usr/lib/rpm/rpmuncompress -x -v -C source-2.0.0 source-noroot.tar.gz + mkdir 'source-2.0.0' ; /usr/bin/gzip -dc 'source-noroot.tar.gz' | /usr/bin/tar -xvvof - -C 'source-2.0.0' + drwxr-xr-x root/root 0 2024-01-16 19:13 dir1/ + -r--r--r-- root/root 210 2024-01-16 19:13 dir1/file3 + -rw-r--r-- root/root 32 2024-01-16 19:13 file1 + -rw-r--r-- root/root 33886 2024-01-16 19:13 file2 + + $ find source-2.0.0 -ls + 92346 0 drwxr-xr-x 1 teknoraver teknoraver 28 Jan 16 19:17 source-2.0.0 + 92347 0 drwxr-xr-x 1 teknoraver teknoraver 10 Jan 16 19:13 source-2.0.0/dir1 + 92348 4 -r--r--r-- 1 teknoraver teknoraver 210 Jan 16 19:13 source-2.0.0/dir1/file3 + 92349 4 -rw-r--r-- 1 teknoraver teknoraver 32 Jan 16 19:13 source-2.0.0/file1 + 92350 36 -rw-r--r-- 1 teknoraver teknoraver 33886 Jan 16 19:13 source-2.0.0/file2 + +And it's exposed to %autosetup + + $ rpmbuild -bp test.spec + Executing(%prep): /bin/sh -e /var/tmp/rpm-tmp.NL55sI + + umask 022 + + cd /home/teknoraver/rpmbuild/BUILD + + cd /home/teknoraver/rpmbuild/BUILD + + rm -rf netperf-2.7.1 + + /usr/lib/rpm/rpmuncompress -x -C netperf-2.7.1 /home/teknoraver/rpmbuild/SOURCES/netperf-3bc455b.tar.gz + + STATUS=0 + + '[' 0 -ne 0 ']' + + cd netperf-2.7.1 + + rm -rf /home/teknoraver/rpmbuild/BUILD/netperf-2.7.1-SPECPARTS + + /usr/bin/mkdir -p /home/teknoraver/rpmbuild/BUILD/netperf-2.7.1-SPECPARTS + + /usr/bin/chmod -Rf a+rX,u+w,g-w,o-w . + + RPM_EC=0 + ++ jobs -p + + exit 0 + +(backported from commit 33853c73cf6af5ff3108fe5c59a0a7a1614bed99) +--- + CMakeLists.txt | 5 +-- + build/parsePrep.c | 13 +++++--- + docs/manual/spec.md | 3 ++ + macros.in | 4 +-- + tools/CMakeLists.txt | 1 + + tools/rpmuncompress.c | 74 +++++++++++++++++++++++++++++++++++++++++-- + 6 files changed, 87 insertions(+), 13 deletions(-) + +diff --git a/CMakeLists.txt b/CMakeLists.txt +index 6dbf179f3..7b9fbf56f 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -205,6 +205,7 @@ pkg_check_modules(ZSTD IMPORTED_TARGET libzstd>=1.3.8) + pkg_check_modules(LIBELF IMPORTED_TARGET libelf) + pkg_check_modules(LIBDW IMPORTED_TARGET libdw) + pkg_check_modules(LIBLZMA IMPORTED_TARGET liblzma>=5.2.0) ++pkg_check_modules(LIBARCHIVE REQUIRED IMPORTED_TARGET libarchive) + + # Lua module does not ship an IMPORTED target, define our own + add_library(LUA::LUA INTERFACE IMPORTED) +@@ -265,10 +266,6 @@ if (WITH_SELINUX) + pkg_check_modules(SELINUX REQUIRED IMPORTED_TARGET libselinux) + endif() + +-if (WITH_ARCHIVE) +- pkg_check_modules(LIBARCHIVE REQUIRED IMPORTED_TARGET libarchive) +-endif() +- + if (WITH_FSVERITY) + pkg_check_modules(FSVERITY REQUIRED IMPORTED_TARGET libfsverity) + endif() +diff --git a/build/parsePrep.c b/build/parsePrep.c +index 07d9a6923..022a5be7d 100644 +--- a/build/parsePrep.c ++++ b/build/parsePrep.c +@@ -124,7 +124,7 @@ exit: + * @param quietly should -vv be omitted from tar? + * @return expanded %setup macro (NULL on error) + */ +-static char *doUntar(rpmSpec spec, uint32_t c, int quietly) ++static char *doUntar(rpmSpec spec, uint32_t c, int quietly, int autoPath) + { + char *buf = NULL; + struct Source *sp; +@@ -135,7 +135,9 @@ static char *doUntar(rpmSpec spec, uint32_t c, int quietly) + } + + buf = rpmExpand("%{__rpmuncompress} -x ", +- quietly ? "" : "-v ","%{shescape:", sp->path, "}", NULL); ++ quietly ? "" : "-v ", ++ autoPath ? "-C %{buildsubdir} " : "", ++ "%{shescape:", sp->path, "}", NULL); + rstrcat(&buf, + "\nSTATUS=$?\n" + "if [ $STATUS -ne 0 ]; then\n" +@@ -165,12 +167,13 @@ static int doSetupMacro(rpmSpec spec, const char *line) + rpmRC rc = RPMRC_FAIL; + uint32_t num; + int leaveDirs = 0, skipDefaultAction = 0; +- int createDir = 0, quietly = 0; ++ int createDir = 0, quietly = 0, autoPath = 0; + char * dirName = NULL; + struct poptOption optionsTable[] = { + { NULL, 'a', POPT_ARG_STRING, NULL, 'a', NULL, NULL}, + { NULL, 'b', POPT_ARG_STRING, NULL, 'b', NULL, NULL}, + { NULL, 'c', 0, &createDir, 0, NULL, NULL}, ++ { NULL, 'C', 0, &autoPath, 0, NULL, NULL}, + { NULL, 'D', 0, &leaveDirs, 0, NULL, NULL}, + { NULL, 'n', POPT_ARG_STRING, &dirName, 0, NULL, NULL}, + { NULL, 'T', 0, &skipDefaultAction, 0, NULL, NULL}, +@@ -202,7 +205,7 @@ static int doSetupMacro(rpmSpec spec, const char *line) + goto exit; + } + +- { char *chptr = doUntar(spec, num, quietly); ++ { char *chptr = doUntar(spec, num, quietly, 0); + if (chptr == NULL) + goto exit; + +@@ -259,7 +262,7 @@ static int doSetupMacro(rpmSpec spec, const char *line) + + /* do the default action */ + if (!skipDefaultAction) { +- char *chptr = doUntar(spec, 0, quietly); ++ char *chptr = doUntar(spec, 0, quietly, autoPath); + if (!chptr) + goto exit; + appendBuf(spec, chptr, 1); +diff --git a/docs/manual/spec.md b/docs/manual/spec.md +index 098099875..3ace79e56 100644 +--- a/docs/manual/spec.md ++++ b/docs/manual/spec.md +@@ -489,6 +489,9 @@ can just create the directory. It accepts a number of options: + -a N unpack source N after changing to the build directory + -b N unpack source N before changing to the build directory + -c create the build directory (and change to it) before unpacking ++-C Create the build directory and ensure the archive contents ++ are unpacked there, stripping the top level directory in the archive ++ if it exists + -D do not delete the build directory prior to unpacking (used + when more than one source is to be unpacked with `-a` or `-b`) + -n DIR set the name of build directory (default is `%{name}-%{version}`) +diff --git a/macros.in b/macros.in +index ef413a358..19be79909 100644 +--- a/macros.in ++++ b/macros.in +@@ -1338,8 +1338,8 @@ end + # usage of git repository and per-patch commits. + # -N Disable automatic patch application + # -p Use -p for patch application +-%autosetup(a:b:cDn:TvNS:p:)\ +-%setup %{-a} %{-b} %{-c} %{-D} %{-n} %{-T} %{!-v:-q}\ ++%autosetup(a:b:cCDn:TvNS:p:)\ ++%setup %{-a} %{-b} %{-c} %{-C} %{-D} %{-n} %{-T} %{!-v:-q}\ + %{-S:%global __scm %{-S*}}\ + %{expand:%__scm_setup_%{__scm} %{!-v:-q}}\ + %{!-N:%autopatch %{-v} %{-p:-p%{-p*}}} +diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt +index 5be4cf2d0..ea452e738 100644 +--- a/tools/CMakeLists.txt ++++ b/tools/CMakeLists.txt +@@ -19,6 +19,7 @@ target_link_libraries(rpmlua PRIVATE LUA::LUA) + target_link_libraries(rpmbuild PRIVATE librpmbuild) + target_link_libraries(rpmspec PRIVATE librpmbuild) + target_link_libraries(rpmdeps PRIVATE librpmbuild) ++target_link_libraries(rpmuncompress PRIVATE PkgConfig::LIBARCHIVE) + + if (HAVE_STRCHRNUL) + add_executable(rpmsort rpmsort.c) +diff --git a/tools/rpmuncompress.c b/tools/rpmuncompress.c +index e13cc6a66..c4832332c 100644 +--- a/tools/rpmuncompress.c ++++ b/tools/rpmuncompress.c +@@ -6,6 +6,9 @@ + #include + #include + ++#include ++#include ++ + #include + #include + +@@ -14,6 +17,7 @@ + static int verbose = 0; + static int extract = 0; + static int dryrun = 0; ++static char *dstpath = NULL; + + static struct poptOption optionsTable[] = { + { "extract", 'x', POPT_ARG_VAL, &extract, 1, +@@ -22,6 +26,8 @@ static struct poptOption optionsTable[] = { + N_("provide more detailed output"), NULL }, + { "dry-run", 'n', POPT_ARG_VAL, &dryrun, 1, + N_("only print what would be done"), NULL }, ++ { "path", 'C', POPT_ARG_STRING, &dstpath, 0, ++ N_("extract into a specific path"), NULL }, + + POPT_AUTOALIAS + POPT_AUTOHELP +@@ -78,16 +84,78 @@ static char *doUncompress(const char *fn) + return cmd; + } + ++/** ++ * Detect if an archive has a single top level entry, and it's a directory. ++ * ++ * @param path path of the archive ++ * @return 1 if archive as only a directory as top level entry, ++ * 0 if it contains multiple top level entries or a single file ++ * -1 on archive error ++ */ ++static int singleRoot(const char *path) ++{ ++ struct archive *a; ++ struct archive_entry *entry; ++ int r, ret = -1, rootLen; ++ char *rootName = NULL; ++ ++ a = archive_read_new(); ++ archive_read_support_filter_all(a); ++ archive_read_support_format_all(a); ++ r = archive_read_open_filename(a, path, 10240); ++ if (r != ARCHIVE_OK) { ++ goto afree; ++ } ++ if (archive_read_next_header(a, &entry) != ARCHIVE_OK) { ++ goto afree; ++ } ++ rootName = xstrdup(archive_entry_pathname(entry)); ++ rootLen = strlen(rootName); ++ if (archive_entry_filetype(entry) != AE_IFDIR) { ++ /* Root entry is not a directory */ ++ ret = 0; ++ goto afree; ++ } ++ while (archive_read_next_header(a, &entry) == ARCHIVE_OK) { ++ if (strncmp(rootName, archive_entry_pathname(entry), rootLen)) { ++ /* multiple top level entries */ ++ ret = 0; ++ goto afree; ++ } ++ } ++ ret = 1; ++ ++afree: ++ free(rootName); ++ r = archive_read_free(a); ++ if (r != ARCHIVE_OK) ++ ret = -1; ++ ++ return ret; ++} ++ + static char *doUntar(const char *fn) + { + const struct archiveType_s *at = NULL; + char *buf = NULL; + char *tar = NULL; + const char *taropts = verbose ? "-xvvof" : "-xof"; ++ char *mkdir = NULL; ++ char *stripcd = NULL; + + if ((at = getArchiver(fn)) == NULL) + goto exit; + ++ if (dstpath) { ++ int sr = singleRoot(fn); ++ ++ /* the trick is simple, if the archive has multiple entries, ++ * just extract it into the specified destination path, otherwise ++ * strip the first path entry and extract in the destination path ++ */ ++ rasprintf(&mkdir, "mkdir '%s' ; ", dstpath); ++ rasprintf(&stripcd, " -C '%s' %s", dstpath, sr ? "--strip-components=1" : ""); ++ } + tar = rpmGetPath("%{__tar}", NULL); + if (at->compressed != COMPRESSED_NOT) { + char *zipper = NULL; +@@ -96,7 +164,7 @@ static char *doUntar(const char *fn) + zipper = rpmExpand(at->cmd, " ", at->unpack, " ", + verbose ? "" : at->quiet, NULL); + if (needtar) { +- rasprintf(&buf, "%s '%s' | %s %s -", zipper, fn, tar, taropts); ++ rasprintf(&buf, "%s %s '%s' | %s %s - %s", mkdir ?: "", zipper, fn, tar, taropts, stripcd ?: ""); + } else if (at->compressed == COMPRESSED_GEM) { + char *tmp = xstrdup(fn); + const char *bn = basename(tmp); +@@ -119,11 +187,13 @@ static char *doUntar(const char *fn) + } + free(zipper); + } else { +- rasprintf(&buf, "%s %s '%s'", tar, taropts, fn); ++ rasprintf(&buf, "%s %s %s '%s' %s", mkdir ?: "", tar, taropts, fn, stripcd ?: ""); + } + + exit: + free(tar); ++ free(mkdir); ++ free(stripcd); + return buf; + } + +-- +2.54.0 + + +From 8ee83a3174f4956baad3feb164d68d1b48e629b6 Mon Sep 17 00:00:00 2001 +From: Florian Festi +Date: Mon, 17 Jun 2024 15:03:28 +0200 +Subject: [PATCH 02/14] Pass TZ=UTC to zip in rpmuncompress + +The ZIP format has no notion of time zone, so timestamps are only +meaningful if it is known what time zone they were created in. Pass UTC +to prevent time stamps to depend on local time zone setting and make +builds from sources in zip files (more) reproducible. + +The other archive formats (7zip using UTC, Ruby gems using tar and tar +itself) don't have this issue. Everything else are stream compressors +that don't deal with file meta data. + +Tested manually with Fedora's xz-java-1.9.zip as mentioned in the issue. + +Resolves: #2955 +(cherry picked from commit b847608cc22236ae19af51dc3faef16398df4110) +--- + tools/rpmuncompress.c | 31 +++++++++++++++++-------------- + 1 file changed, 17 insertions(+), 14 deletions(-) + +diff --git a/tools/rpmuncompress.c b/tools/rpmuncompress.c +index c4832332c..4c9e46aad 100644 +--- a/tools/rpmuncompress.c ++++ b/tools/rpmuncompress.c +@@ -40,19 +40,20 @@ struct archiveType_s { + const char *cmd; + const char *unpack; + const char *quiet; ++ int setTZ; + } archiveTypes[] = { +- { COMPRESSED_NOT, 0, "%{__cat}" , "", "" }, +- { COMPRESSED_OTHER, 0, "%{__gzip}", "-dc", "" }, +- { COMPRESSED_BZIP2, 0, "%{__bzip2}", "-dc", "" }, +- { COMPRESSED_ZIP, 1, "%{__unzip}", "", "-qq" }, +- { COMPRESSED_LZMA, 0, "%{__xz}", "-dc", "" }, +- { COMPRESSED_XZ, 0, "%{__xz}", "-dc", "" }, +- { COMPRESSED_LZIP, 0, "%{__lzip}", "-dc", "" }, +- { COMPRESSED_LRZIP, 0, "%{__lrzip}", "-dqo-", "" }, +- { COMPRESSED_7ZIP, 1, "%{__7zip}", "x", "" }, +- { COMPRESSED_ZSTD, 0, "%{__zstd}", "-dc", "" }, +- { COMPRESSED_GEM, 1, "%{__gem}", "unpack", "" }, +- { -1, 0, NULL, NULL, NULL }, ++ { COMPRESSED_NOT, 0, "%{__cat}" , "", "", 0 }, ++ { COMPRESSED_OTHER, 0, "%{__gzip}", "-dc", "", 0 }, ++ { COMPRESSED_BZIP2, 0, "%{__bzip2}", "-dc", "", 0 }, ++ { COMPRESSED_ZIP, 1, "%{__unzip}", "", "-qq", 1 }, ++ { COMPRESSED_LZMA, 0, "%{__xz}", "-dc", "", 0 }, ++ { COMPRESSED_XZ, 0, "%{__xz}", "-dc", "", 0 }, ++ { COMPRESSED_LZIP, 0, "%{__lzip}", "-dc", "", 0 }, ++ { COMPRESSED_LRZIP, 0, "%{__lrzip}", "-dqo-", "", 0 }, ++ { COMPRESSED_7ZIP, 1, "%{__7zip}", "x", "", 0 }, ++ { COMPRESSED_ZSTD, 0, "%{__zstd}", "-dc", "", 0 }, ++ { COMPRESSED_GEM, 1, "%{__gem}", "unpack", "", 0 }, ++ { -1, 0, NULL, NULL, NULL, 0 }, + }; + + static const struct archiveType_s *getArchiver(const char *fn) +@@ -77,7 +78,8 @@ static char *doUncompress(const char *fn) + char *cmd = NULL; + const struct archiveType_s *at = getArchiver(fn); + if (at) { +- cmd = rpmExpand(at->cmd, " ", at->unpack, NULL); ++ cmd = rpmExpand(at->setTZ ? "TZ=UTC " : "", ++ at->cmd, " ", at->unpack, NULL); + /* path must not be expanded */ + cmd = rstrscat(&cmd, " ", fn, NULL); + } +@@ -161,7 +163,8 @@ static char *doUntar(const char *fn) + char *zipper = NULL; + int needtar = (at->extractable == 0); + +- zipper = rpmExpand(at->cmd, " ", at->unpack, " ", ++ zipper = rpmExpand(at->setTZ ? "TZ=UTC " : "", ++ at->cmd, " ", at->unpack, " ", + verbose ? "" : at->quiet, NULL); + if (needtar) { + rasprintf(&buf, "%s %s '%s' | %s %s - %s", mkdir ?: "", zipper, fn, tar, taropts, stripcd ?: ""); +-- +2.54.0 + + +From c4f56c0898b470811034f62099f70be733d58b36 Mon Sep 17 00:00:00 2001 +From: Florian Festi +Date: Mon, 17 Jun 2024 18:19:45 +0200 +Subject: [PATCH 03/14] Free cmd for --dry-run too + +Prevent memory leak and the memory sanatizer failing. + +(cherry picked from commit e8a252dca9f0731dc5f24d91447a6906363ff9ec) +--- + tools/rpmuncompress.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/tools/rpmuncompress.c b/tools/rpmuncompress.c +index 4c9e46aad..e8232acbc 100644 +--- a/tools/rpmuncompress.c ++++ b/tools/rpmuncompress.c +@@ -234,10 +234,10 @@ int main(int argc, char *argv[]) + if (WIFEXITED(status) && WEXITSTATUS(status) == 0) + ec = EXIT_SUCCESS; + } +- free(cmd); + } + + exit: ++ free(cmd); + rpmcliFini(optCon); + return ec; + } +-- +2.54.0 + + +From b94d7b57aaa80d7bb171da846215f306b3d78c3d Mon Sep 17 00:00:00 2001 +From: Panu Matilainen +Date: Tue, 27 Aug 2024 13:27:20 +0300 +Subject: [PATCH 04/14] Work around unowned directories in rpmuncompress -C + +Some archives have a directory prefix but are missing the leading +directory entry, work around it by just comparing the leading directory +components (if any) across the archive. + +Add tests for unowned directories with rpmuncompress -C + +GNU tar seems to always place the root directory node in there, but +clearly not all implementations do. One of them being Python tarfile +as of Python 3.12.5, so the test-tarballs created from the pre-existing +source-singleroot.tar.gz content with: + + import tarfile + + tar = tarfile.open("source-singleroot-unowned1.tar.gz", "w:gz") + for name in ["source-strip/file1", "source-strip/file2"]: + tar.add(name) + tar.close() + + tar = tarfile.open("source-singleroot-unowned2.tar.gz", "w:gz") + for name in ["source-strip/dir1", "source-strip/file1", + "source-strip/file2"]: + tar.add(name) + tar.close() + +Fixes: #3250 +(cherry picked from commit 671fc8e5d6633c14c9621903a23c0040acb43f65) +--- + tools/rpmuncompress.c | 13 ++++++++----- + 1 file changed, 8 insertions(+), 5 deletions(-) + +diff --git a/tools/rpmuncompress.c b/tools/rpmuncompress.c +index e8232acbc..2cf988dac 100644 +--- a/tools/rpmuncompress.c ++++ b/tools/rpmuncompress.c +@@ -112,15 +112,18 @@ static int singleRoot(const char *path) + goto afree; + } + rootName = xstrdup(archive_entry_pathname(entry)); +- rootLen = strlen(rootName); +- if (archive_entry_filetype(entry) != AE_IFDIR) { +- /* Root entry is not a directory */ ++ char *sep = strchr(rootName, '/'); ++ if (sep == NULL) { ++ /* No directories in the pathname */ + ret = 0; + goto afree; + } ++ ++ /* Do all entries in the archive start with the same lead directory? */ ++ rootLen = sep - rootName + 1; + while (archive_read_next_header(a, &entry) == ARCHIVE_OK) { +- if (strncmp(rootName, archive_entry_pathname(entry), rootLen)) { +- /* multiple top level entries */ ++ const char *p = archive_entry_pathname(entry); ++ if (strncmp(rootName, p, rootLen)) { + ret = 0; + goto afree; + } +-- +2.54.0 + + +From b754b7baa2ab07cb4b987b6c4d160a12197e320e Mon Sep 17 00:00:00 2001 +From: Florian Festi +Date: Thu, 29 Aug 2024 12:29:05 +0200 +Subject: [PATCH 05/14] Replace gcc only ? : operator usage + +(cherry picked from commit c90733d96e93f3544853821787190fbb01472b15) +--- + tools/rpmuncompress.c | 23 +++++++++++++---------- + 1 file changed, 13 insertions(+), 10 deletions(-) + +diff --git a/tools/rpmuncompress.c b/tools/rpmuncompress.c +index 2cf988dac..a3b7464d2 100644 +--- a/tools/rpmuncompress.c ++++ b/tools/rpmuncompress.c +@@ -152,14 +152,17 @@ static char *doUntar(const char *fn) + goto exit; + + if (dstpath) { +- int sr = singleRoot(fn); +- +- /* the trick is simple, if the archive has multiple entries, +- * just extract it into the specified destination path, otherwise +- * strip the first path entry and extract in the destination path +- */ +- rasprintf(&mkdir, "mkdir '%s' ; ", dstpath); +- rasprintf(&stripcd, " -C '%s' %s", dstpath, sr ? "--strip-components=1" : ""); ++ int sr = singleRoot(fn); ++ ++ /* the trick is simple, if the archive has multiple entries, ++ * just extract it into the specified destination path, otherwise ++ * strip the first path entry and extract in the destination path ++ */ ++ rasprintf(&mkdir, "mkdir '%s' ; ", dstpath); ++ rasprintf(&stripcd, " -C '%s' %s", dstpath, sr ? "--strip-components=1" : ""); ++ } else { ++ mkdir = xstrdup(""); ++ stripcd = xstrdup(""); + } + tar = rpmGetPath("%{__tar}", NULL); + if (at->compressed != COMPRESSED_NOT) { +@@ -170,7 +173,7 @@ static char *doUntar(const char *fn) + at->cmd, " ", at->unpack, " ", + verbose ? "" : at->quiet, NULL); + if (needtar) { +- rasprintf(&buf, "%s %s '%s' | %s %s - %s", mkdir ?: "", zipper, fn, tar, taropts, stripcd ?: ""); ++ rasprintf(&buf, "%s %s '%s' | %s %s - %s", mkdir, zipper, fn, tar, taropts, stripcd); + } else if (at->compressed == COMPRESSED_GEM) { + char *tmp = xstrdup(fn); + const char *bn = basename(tmp); +@@ -193,7 +196,7 @@ static char *doUntar(const char *fn) + } + free(zipper); + } else { +- rasprintf(&buf, "%s %s %s '%s' %s", mkdir ?: "", tar, taropts, fn, stripcd ?: ""); ++ rasprintf(&buf, "%s %s %s '%s' %s", mkdir, tar, taropts, fn, stripcd); + } + + exit: +-- +2.54.0 + + +From dc2a2d06006b3f55dfd4e80bcd8f8e801ff0c0c9 Mon Sep 17 00:00:00 2001 +From: Florian Festi +Date: Thu, 29 Aug 2024 15:40:18 +0200 +Subject: [PATCH 06/14] Return unique top directory iff it exists + +Used in the next commit. + +(cherry picked from commit 7b46a0d0c336ee865821de53cdcfff09752669ed) +--- + tools/rpmuncompress.c | 18 +++++++++++------- + 1 file changed, 11 insertions(+), 7 deletions(-) + +diff --git a/tools/rpmuncompress.c b/tools/rpmuncompress.c +index a3b7464d2..88956d173 100644 +--- a/tools/rpmuncompress.c ++++ b/tools/rpmuncompress.c +@@ -90,11 +90,11 @@ static char *doUncompress(const char *fn) + * Detect if an archive has a single top level entry, and it's a directory. + * + * @param path path of the archive +- * @return 1 if archive as only a directory as top level entry, +- * 0 if it contains multiple top level entries or a single file +- * -1 on archive error ++ * @return only top level directory (if any), ++ * NULL if it contains multiple top level entries or a single file ++ * or on archive error + */ +-static int singleRoot(const char *path) ++static char * singleRoot(const char *path) + { + struct archive *a; + struct archive_entry *entry; +@@ -128,15 +128,18 @@ static int singleRoot(const char *path) + goto afree; + } + } ++ *sep = '\0'; + ret = 1; + + afree: +- free(rootName); + r = archive_read_free(a); + if (r != ARCHIVE_OK) + ret = -1; + +- return ret; ++ if (ret != 1) ++ rootName = _free(rootName); ++ ++ return rootName; + } + + static char *doUntar(const char *fn) +@@ -152,7 +155,7 @@ static char *doUntar(const char *fn) + goto exit; + + if (dstpath) { +- int sr = singleRoot(fn); ++ char * sr = singleRoot(fn); + + /* the trick is simple, if the archive has multiple entries, + * just extract it into the specified destination path, otherwise +@@ -160,6 +163,7 @@ static char *doUntar(const char *fn) + */ + rasprintf(&mkdir, "mkdir '%s' ; ", dstpath); + rasprintf(&stripcd, " -C '%s' %s", dstpath, sr ? "--strip-components=1" : ""); ++ free(sr); + } else { + mkdir = xstrdup(""); + stripcd = xstrdup(""); +-- +2.54.0 + + +From 5847a366c9172eaabd2743119c8f8b94b0b3e9ef Mon Sep 17 00:00:00 2001 +From: Florian Festi +Date: Thu, 29 Aug 2024 15:11:51 +0200 +Subject: [PATCH 07/14] rpmuncompress: Support -C for zip and 7zip archives + +As both don't support an equivalent to tars --strip-components=1 we move +the files out of top directory ourselves with shell commands. + +Support for Ruby gems is still missing - iff at all possible or +desirable. + +Resolves: #3249 +(backported from commit abf057616d8f73cc0540520121ea3c4cad5d8cdd) +--- + tools/rpmuncompress.c | 58 +++++++++++++++++++++++++++++-------------- + 1 file changed, 40 insertions(+), 18 deletions(-) + +diff --git a/tools/rpmuncompress.c b/tools/rpmuncompress.c +index 88956d173..692ca322f 100644 +--- a/tools/rpmuncompress.c ++++ b/tools/rpmuncompress.c +@@ -40,19 +40,20 @@ struct archiveType_s { + const char *cmd; + const char *unpack; + const char *quiet; ++ const char *dest; + int setTZ; + } archiveTypes[] = { +- { COMPRESSED_NOT, 0, "%{__cat}" , "", "", 0 }, +- { COMPRESSED_OTHER, 0, "%{__gzip}", "-dc", "", 0 }, +- { COMPRESSED_BZIP2, 0, "%{__bzip2}", "-dc", "", 0 }, +- { COMPRESSED_ZIP, 1, "%{__unzip}", "", "-qq", 1 }, +- { COMPRESSED_LZMA, 0, "%{__xz}", "-dc", "", 0 }, +- { COMPRESSED_XZ, 0, "%{__xz}", "-dc", "", 0 }, +- { COMPRESSED_LZIP, 0, "%{__lzip}", "-dc", "", 0 }, +- { COMPRESSED_LRZIP, 0, "%{__lrzip}", "-dqo-", "", 0 }, +- { COMPRESSED_7ZIP, 1, "%{__7zip}", "x", "", 0 }, +- { COMPRESSED_ZSTD, 0, "%{__zstd}", "-dc", "", 0 }, +- { COMPRESSED_GEM, 1, "%{__gem}", "unpack", "", 0 }, ++ { COMPRESSED_NOT, 0, "%{__cat}" , "", "", "", 0 }, ++ { COMPRESSED_OTHER, 0, "%{__gzip}", "-dc", "", "", 0 }, ++ { COMPRESSED_BZIP2, 0, "%{__bzip2}", "-dc", "", "", 0 }, ++ { COMPRESSED_ZIP, 1, "%{__unzip}", "", "-qq", "-d", 1 }, ++ { COMPRESSED_LZMA, 0, "%{__xz}", "-dc", "", "", 0 }, ++ { COMPRESSED_XZ, 0, "%{__xz}", "-dc", "", "", 0 }, ++ { COMPRESSED_LZIP, 0, "%{__lzip}", "-dc", "", "", 0 }, ++ { COMPRESSED_LRZIP, 0, "%{__lrzip}", "-dqo-", "", "", 0 }, ++ { COMPRESSED_7ZIP, 1, "%{__7zip}", "x", "-bso0 -bsp0", "-o", 0 }, ++ { COMPRESSED_ZSTD, 0, "%{__zstd}", "-dc", "", "", 0 }, ++ { COMPRESSED_GEM, 1, "%{__gem}", "unpack", "", "--target=", 0 }, + { -1, 0, NULL, NULL, NULL, 0 }, + }; + +@@ -154,15 +155,37 @@ static char *doUntar(const char *fn) + if ((at = getArchiver(fn)) == NULL) + goto exit; + ++ int needtar = (at->extractable == 0); ++ + if (dstpath) { + char * sr = singleRoot(fn); + +- /* the trick is simple, if the archive has multiple entries, +- * just extract it into the specified destination path, otherwise +- * strip the first path entry and extract in the destination path ++ /* if the archive has multiple entries, just extract it into the ++ * specified destination path, otherwise also strip the first path ++ * entry + */ +- rasprintf(&mkdir, "mkdir '%s' ; ", dstpath); +- rasprintf(&stripcd, " -C '%s' %s", dstpath, sr ? "--strip-components=1" : ""); ++ if (needtar) { ++ rasprintf(&mkdir, "mkdir -p '%s' ; ", dstpath); ++ rasprintf(&stripcd, " -C '%s' %s", dstpath, sr ? "--strip-components=1" : ""); ++ } else { ++ if (sr) { ++ rasprintf(&mkdir, "mkdir -p '%s' ; tmp=`mktemp -d -p'%s'` ; ", ++ dstpath, dstpath); ++ char * moveup; ++ /* Extract into temp directory to avoid collisions */ ++ /* then move files in top dir two levels up */ ++ rasprintf( ++ &moveup, ++ " && " ++ "(shopt -s dotglob; mv \"$tmp\"/'%s'/* '%s') && " ++ "rmdir \"$tmp\"/'%s' \"$tmp\" ", sr, dstpath, sr); ++ rasprintf(&stripcd, "%s\"$tmp\" %s", at->dest, moveup); ++ free(moveup); ++ } else { ++ rasprintf(&mkdir, "mkdir -p '%s' ; ", dstpath); ++ rasprintf(&stripcd, "%s'%s'", at->dest, dstpath); ++ } ++ } + free(sr); + } else { + mkdir = xstrdup(""); +@@ -171,7 +194,6 @@ static char *doUntar(const char *fn) + tar = rpmGetPath("%{__tar}", NULL); + if (at->compressed != COMPRESSED_NOT) { + char *zipper = NULL; +- int needtar = (at->extractable == 0); + + zipper = rpmExpand(at->setTZ ? "TZ=UTC " : "", + at->cmd, " ", at->unpack, " ", +@@ -196,7 +218,7 @@ static char *doUntar(const char *fn) + free(gem); + free(tmp); + } else { +- rasprintf(&buf, "%s '%s'", zipper, fn); ++ rasprintf(&buf, "%s%s '%s' %s", mkdir, zipper, fn, stripcd); + } + free(zipper); + } else { +-- +2.54.0 + + +From 127d4a2947005688e645b9e9545b178efd5c0e6c Mon Sep 17 00:00:00 2001 +From: Panu Matilainen +Date: Mon, 2 Sep 2024 10:51:57 +0300 +Subject: [PATCH 08/14] Fix rpmuncompress going interactive on re-extraction of + zip / 7zip archives + +Testing this is a PITA compared to the issue itself: we can't use the +existing -C tests because that fails due to mv, and to avoid -C we +need to control where it runs, and to do that we need --chdir which +causes extra warnings from brap so we need to ignore stderr which +we would not want to do here, really. + +Fixes: #2779 +(cherry picked from commit fa3ec7e3d73a93911bb8d3d1ac87c2d64eeda680) +--- + tools/rpmuncompress.c | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/tools/rpmuncompress.c b/tools/rpmuncompress.c +index 692ca322f..30bd74a2a 100644 +--- a/tools/rpmuncompress.c ++++ b/tools/rpmuncompress.c +@@ -46,12 +46,12 @@ struct archiveType_s { + { COMPRESSED_NOT, 0, "%{__cat}" , "", "", "", 0 }, + { COMPRESSED_OTHER, 0, "%{__gzip}", "-dc", "", "", 0 }, + { COMPRESSED_BZIP2, 0, "%{__bzip2}", "-dc", "", "", 0 }, +- { COMPRESSED_ZIP, 1, "%{__unzip}", "", "-qq", "-d", 1 }, ++ { COMPRESSED_ZIP, 1, "%{__unzip}", "-u", "-qq", "-d", 1 }, + { COMPRESSED_LZMA, 0, "%{__xz}", "-dc", "", "", 0 }, + { COMPRESSED_XZ, 0, "%{__xz}", "-dc", "", "", 0 }, + { COMPRESSED_LZIP, 0, "%{__lzip}", "-dc", "", "", 0 }, + { COMPRESSED_LRZIP, 0, "%{__lrzip}", "-dqo-", "", "", 0 }, +- { COMPRESSED_7ZIP, 1, "%{__7zip}", "x", "-bso0 -bsp0", "-o", 0 }, ++ { COMPRESSED_7ZIP, 1, "%{__7zip}", "x -y", "-bso0 -bsp0", "-o", 0 }, + { COMPRESSED_ZSTD, 0, "%{__zstd}", "-dc", "", "", 0 }, + { COMPRESSED_GEM, 1, "%{__gem}", "unpack", "", "--target=", 0 }, + { -1, 0, NULL, NULL, NULL, 0 }, +-- +2.54.0 + + +From b28d1c4b8cafc45498b93a324cb8dd52ab2a753b Mon Sep 17 00:00:00 2001 +From: Panu Matilainen +Date: Thu, 3 Oct 2024 12:51:05 +0300 +Subject: [PATCH 09/14] Fix up rpmuncompress not being compiled as C++ + +This one lone source got overlooked in the initial C++ enablement +in commit 37cdcc29269eb5cd603c69be07f83f113ce479cd. C++ doesn't like +jumping over variable declarations, so just move the declarations +early to minimally fix the build. + +Nicely goes to show the danger of such hacks - you can always miss +something. On the library side things are likely to blow up in +linkage but you never know really. + +(backported from commit 826339518a5ee43498ab0fe047a87540a980232e) +--- + tools/rpmuncompress.c | 6 ++++-- + 1 file changed, 4 insertions(+), 2 deletions(-) + +diff --git a/tools/rpmuncompress.c b/tools/rpmuncompress.c +index 30bd74a2a..932ae3a3d 100644 +--- a/tools/rpmuncompress.c ++++ b/tools/rpmuncompress.c +@@ -101,6 +101,7 @@ static char * singleRoot(const char *path) + struct archive_entry *entry; + int r, ret = -1, rootLen; + char *rootName = NULL; ++ char *sep = NULL; + + a = archive_read_new(); + archive_read_support_filter_all(a); +@@ -113,7 +114,7 @@ static char * singleRoot(const char *path) + goto afree; + } + rootName = xstrdup(archive_entry_pathname(entry)); +- char *sep = strchr(rootName, '/'); ++ sep = strchr(rootName, '/'); + if (sep == NULL) { + /* No directories in the pathname */ + ret = 0; +@@ -151,11 +152,12 @@ static char *doUntar(const char *fn) + const char *taropts = verbose ? "-xvvof" : "-xof"; + char *mkdir = NULL; + char *stripcd = NULL; ++ int needtar = 0; + + if ((at = getArchiver(fn)) == NULL) + goto exit; + +- int needtar = (at->extractable == 0); ++ needtar = (at->extractable == 0); + + if (dstpath) { + char * sr = singleRoot(fn); +-- +2.54.0 + + +From 0988bb56aa86c3fb0719e45910621068830f916b Mon Sep 17 00:00:00 2001 +From: Michal Domonkos +Date: Tue, 9 Jun 2026 15:57:08 +0200 +Subject: [PATCH 10/14] Refactor singleRoot() + +Locate the slash first in the returned const string and only duplicate +it when actually needed. This will save us an extra string copy in the +next commits. + +No functional change. + +(cherry picked from commit 24744c59d359cc51fc644ddf2707c4ef1a639cf6) +--- + tools/rpmuncompress.c | 18 ++++++++++++------ + 1 file changed, 12 insertions(+), 6 deletions(-) + +diff --git a/tools/rpmuncompress.c b/tools/rpmuncompress.c +index 932ae3a3d..68d1481f6 100644 +--- a/tools/rpmuncompress.c ++++ b/tools/rpmuncompress.c +@@ -100,8 +100,9 @@ static char * singleRoot(const char *path) + struct archive *a; + struct archive_entry *entry; + int r, ret = -1, rootLen; ++ const char *p = NULL; ++ const char *sep = NULL; + char *rootName = NULL; +- char *sep = NULL; + + a = archive_read_new(); + archive_read_support_filter_all(a); +@@ -113,24 +114,29 @@ static char * singleRoot(const char *path) + if (archive_read_next_header(a, &entry) != ARCHIVE_OK) { + goto afree; + } +- rootName = xstrdup(archive_entry_pathname(entry)); +- sep = strchr(rootName, '/'); ++ ++ /* Extract the lead directory from the first entry */ ++ p = archive_entry_pathname(entry); ++ sep = strchr(p, '/'); + if (sep == NULL) { + /* No directories in the pathname */ + ret = 0; + goto afree; ++ } else { ++ rootName = xstrdup(p); ++ rootLen = sep - p + 1; + } + + /* Do all entries in the archive start with the same lead directory? */ +- rootLen = sep - rootName + 1; + while (archive_read_next_header(a, &entry) == ARCHIVE_OK) { +- const char *p = archive_entry_pathname(entry); ++ p = archive_entry_pathname(entry); + if (strncmp(rootName, p, rootLen)) { + ret = 0; + goto afree; + } + } +- *sep = '\0'; ++ ++ rootName[rootLen - 1] = '\0'; + ret = 1; + + afree: +-- +2.54.0 + + +From 3a441228720f71bcc132e535c36913be9cc69322 Mon Sep 17 00:00:00 2001 +From: Michal Domonkos +Date: Tue, 9 Jun 2026 16:34:25 +0200 +Subject: [PATCH 11/14] Invert conditional in singleRoot() + +No functional change, just makes the next commit simpler. + +(cherry picked from commit 94cfcceff77d730f1c3432fdb572fc175a3ffdf6) +--- + tools/rpmuncompress.c | 8 ++++---- + 1 file changed, 4 insertions(+), 4 deletions(-) + +diff --git a/tools/rpmuncompress.c b/tools/rpmuncompress.c +index 68d1481f6..eef2f4f0e 100644 +--- a/tools/rpmuncompress.c ++++ b/tools/rpmuncompress.c +@@ -118,13 +118,13 @@ static char * singleRoot(const char *path) + /* Extract the lead directory from the first entry */ + p = archive_entry_pathname(entry); + sep = strchr(p, '/'); +- if (sep == NULL) { ++ if (sep) { ++ rootName = xstrdup(p); ++ rootLen = sep - p + 1; ++ } else { + /* No directories in the pathname */ + ret = 0; + goto afree; +- } else { +- rootName = xstrdup(p); +- rootLen = sep - p + 1; + } + + /* Do all entries in the archive start with the same lead directory? */ +-- +2.54.0 + + +From 18cc9e175ac6807148144099acea22147da3dddf Mon Sep 17 00:00:00 2001 +From: Michal Domonkos +Date: Tue, 9 Jun 2026 16:20:59 +0200 +Subject: [PATCH 12/14] Handle singleroot archives without trailing slash + +While some tar implementations (like GNU tar) do include a trailing +slash in directory entries, others (like ptar(1) written in Perl) do +not. Handle the latter in the singleroot detection logic as well. + +Add a test archive generated with ptar(1) that has the following layout: + + source-strip + source-strip/file2 + source-strip/file1 + source-strip/dir1 + source-strip/dir1/file3 + +Fixes: #4182 +(cherry picked from commit 27ff4d457ff66673f66b0933234e5ed56932641f) +--- + tools/rpmuncompress.c | 3 +++ + 1 file changed, 3 insertions(+) + +diff --git a/tools/rpmuncompress.c b/tools/rpmuncompress.c +index eef2f4f0e..6aff47757 100644 +--- a/tools/rpmuncompress.c ++++ b/tools/rpmuncompress.c +@@ -121,6 +121,9 @@ static char * singleRoot(const char *path) + if (sep) { + rootName = xstrdup(p); + rootLen = sep - p + 1; ++ } else if (archive_entry_filetype(entry) == AE_IFDIR) { ++ rootName = rstrscat(NULL, p, "/", NULL); ++ rootLen = strlen(rootName); + } else { + /* No directories in the pathname */ + ret = 0; +-- +2.54.0 + + +From ccdd46de999955b28ff5ae54b738137a8e14a8c7 Mon Sep 17 00:00:00 2001 +From: Michal Domonkos +Date: Fri, 12 Jun 2026 13:13:22 +0200 +Subject: [PATCH 13/14] Prevent command injection in rpmuncompress(1) + +When extracting a singleroot archive in -C mode with an archiver that +can uncompress and extract in one go (such as zip), we will include the +top-level directory name verbatim in the shell command that does our own +variant of tar's --strip-components=1. This allows an attacker to craft +such an archive where the top-level directory name contains arbitrary +shell code enclosed in single quotes, which rpmuncompress will happily +execute when merely extracting it. + +That is nasty since one does not expect a utility like rpmuncompress to +*execute* anything from the archive when extracting it. This is now even +more relevant with commit a698d1b6b18e430806c54755af56d47ac401e430 which +exposed rpmuncompress for general use by installing it into $PATH. + +Fix by simply *not* passing the top-level directory name, as returned by +singleRoot(), to the shell, ever. Luckily, we don't really need to since +we know that the temp directory will only have a single top-level entry, +and therefore can use a glob to just let the shell expand to it. + +This relies on our singleroot detection logic *and* the archiver having +a common understanding of what a singleroot archive is, of course, which +may not be ideal. What could theoretically happen is that we consider an +archive singleroot but the archiver, for some reason, extracts multiple +top-level entries, or the single entry will have a different name. This +could result in the destination directory to have an unexpected layout +once we do the move. That said, this is all just speculation, and even +if true, does not seem to be a security flaw unlike the one being fixed. + +An alternative fix would be sanitizing the directory name before passing +it to the shell (like the suggested patch in CVE-2026-44604) but that is +too brittle, error prone and just unnecessarily complicated. + +Ideally, we would just not shell out at all, and use the libarchive API +to do the whole dance, which we may in the future (see #4236). But for +now, this minimal fix should do the job. + +Add also a test archive created with the following Python 3 code adapted +from the CVE: + + import zipfile + name = "evil'$(touch beenhere)'" + with zipfile.ZipFile("source-singleroot-evil.zip", "w") as z: + z.writestr(f"{name}/README.txt", "x") + +Fixes: CVE-2026-44604 +(cherry picked from commit 0693ce7674c423f382b148763a29f7faa0aacb41) +--- + tools/rpmuncompress.c | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/tools/rpmuncompress.c b/tools/rpmuncompress.c +index 6aff47757..3652cc6bb 100644 +--- a/tools/rpmuncompress.c ++++ b/tools/rpmuncompress.c +@@ -188,8 +188,8 @@ static char *doUntar(const char *fn) + rasprintf( + &moveup, + " && " +- "(shopt -s dotglob; mv \"$tmp\"/'%s'/* '%s') && " +- "rmdir \"$tmp\"/'%s' \"$tmp\" ", sr, dstpath, sr); ++ "(shopt -s dotglob; mv \"$tmp\"/*/* '%s') && " ++ "rmdir \"$tmp\"/* \"$tmp\" ", dstpath); + rasprintf(&stripcd, "%s\"$tmp\" %s", at->dest, moveup); + free(moveup); + } else { +-- +2.54.0 + + +From 6ee2186a8b246d7cd8efff6c449c509669206760 Mon Sep 17 00:00:00 2001 +From: Michal Domonkos +Date: Fri, 12 Jun 2026 13:16:39 +0200 +Subject: [PATCH 14/14] Revert "Return unique top directory iff it exists" + +Now that we no longer need the singleroot name in the caller, go back to +just returning an integer. This also prevents an accidental inclusion of +the name in a shell command in the future (see previous commit). + +Adjust and simplify the singleRoot() description comment while at it. + +This reverts commit 7b46a0d0c336ee865821de53cdcfff09752669ed. + +(cherry picked from commit 3a9145cddff47e6324a9fff3c9a6ae036d7a7db5) +--- + tools/rpmuncompress.c | 18 +++++++----------- + 1 file changed, 7 insertions(+), 11 deletions(-) + +diff --git a/tools/rpmuncompress.c b/tools/rpmuncompress.c +index 3652cc6bb..7233e4bb9 100644 +--- a/tools/rpmuncompress.c ++++ b/tools/rpmuncompress.c +@@ -91,11 +91,11 @@ static char *doUncompress(const char *fn) + * Detect if an archive has a single top level entry, and it's a directory. + * + * @param path path of the archive +- * @return only top level directory (if any), +- * NULL if it contains multiple top level entries or a single file +- * or on archive error ++ * @return 1 if the archive has a single top level, directory entry, ++ * 0 otherwise, ++ * -1 on archive error + */ +-static char * singleRoot(const char *path) ++static int singleRoot(const char *path) + { + struct archive *a; + struct archive_entry *entry; +@@ -139,18 +139,15 @@ static char * singleRoot(const char *path) + } + } + +- rootName[rootLen - 1] = '\0'; + ret = 1; + + afree: ++ free(rootName); + r = archive_read_free(a); + if (r != ARCHIVE_OK) + ret = -1; + +- if (ret != 1) +- rootName = _free(rootName); +- +- return rootName; ++ return ret; + } + + static char *doUntar(const char *fn) +@@ -169,7 +166,7 @@ static char *doUntar(const char *fn) + needtar = (at->extractable == 0); + + if (dstpath) { +- char * sr = singleRoot(fn); ++ int sr = singleRoot(fn); + + /* if the archive has multiple entries, just extract it into the + * specified destination path, otherwise also strip the first path +@@ -197,7 +194,6 @@ static char *doUntar(const char *fn) + rasprintf(&stripcd, "%s'%s'", at->dest, dstpath); + } + } +- free(sr); + } else { + mkdir = xstrdup(""); + stripcd = xstrdup(""); +-- +2.54.0 + diff --git a/rpm-4.19.x-add-parkdb.patch b/rpm-4.19.x-add-parkdb.patch new file mode 100644 index 0000000..ae88c3f --- /dev/null +++ b/rpm-4.19.x-add-parkdb.patch @@ -0,0 +1,428 @@ +From 17a703eb3dfa7e2da4f441bd200d2548aef64025 Mon Sep 17 00:00:00 2001 +From: Michal Domonkos +Date: Tue, 2 Jun 2026 10:02:53 +0200 +Subject: [PATCH 1/2] Extract common database rebuild logic + +No functional change, just prepares the ground for the next commit. + +(backported from commit 1dcddf95b7d956f5d024f63a531ea58bc485efe5) +--- + lib/rpmts.c | 9 ++++++--- + 1 file changed, 6 insertions(+), 3 deletions(-) + +diff --git a/lib/rpmts.c b/lib/rpmts.c +index 2f293e751..bc52a2f28 100644 +--- a/lib/rpmts.c ++++ b/lib/rpmts.c +@@ -137,12 +137,10 @@ int rpmtsSetDBMode(rpmts ts, int dbmode) + return rc; + } + +- +-int rpmtsRebuildDB(rpmts ts) ++static int rebuildDB(rpmts ts, int rebuildflags) + { + int rc = -1; + rpmtxn txn = NULL; +- int rebuildflags = 0; + + /* Cannot do this on a populated transaction set */ + if (rpmtsNElements(ts) > 0) +@@ -162,6 +160,11 @@ int rpmtsRebuildDB(rpmts ts) + return rc; + } + ++int rpmtsRebuildDB(rpmts ts) ++{ ++ return rebuildDB(ts, 0); ++} ++ + int rpmtsVerifyDB(rpmts ts) + { + int rc = -1; +-- +2.54.0 + + +From 845be24a9128d73d6307cec8692c5783a7671a61 Mon Sep 17 00:00:00 2001 +From: Michal Domonkos +Date: Wed, 3 Jun 2026 14:19:41 +0200 +Subject: [PATCH 2/2] Add support for database parking + +Prior to including the RPM database on read-only media (such as a base +image in rpm-ostree), it should undergo some housekeeping and cleanup. +Add a new "parking" operation to rpmdb(8) and the corresponding API to +do just that. + +This operation consists of a database rebuild coupled with an optional, +backend specific action triggered by passing a new flag RPMDB_FLAG_PARK +on database open. Since the action may take place on database close, let +the backend report a failure to park that way by handling the result in +rpmdbRebuild(). + +Implement such an action in the sqlite backend where it actually makes a +difference: Commit a8588f8e3970223f0b2fcc5d679dcb176f2e317b enabled WAL +mode but sqlite recommends that the database be converted to the default +DELETE journal mode prior to "burning" it onto read-only media [*]. Make +sure to do the conversion on database close since we may write to the +database during the rebuild, and that should always happen in WAL mode. + +As a nice side effect with the sqlite backend, parking the database will +automatically remove the *-wal and *-shm files from %_dbpath, making it +usable in reproducible images. Hint at that in the man page but make it +clear that this is not a guarantee (but rather an implementation detail +of sqlite). Make use of this property in the test, though. + +By parking, the database does not become read-only, and the parked state +does not stick when opening the database for writing (as we re-establish +WAL mode in such a case). This allows for the database to be immediately +writable inside a container running on top of a base image that includes +the database, without the user having to unpark it first. + +[*] https://sqlite.org/wal.html#read_only_databases + +Fixes: #2219 +(backported from commit 1825517392c1a04b1f3bc6a89d308fdda2927edd) +--- + docs/man/rpmdb.8.md | 13 +++++++++++-- + include/rpm/rpmts.h | 7 +++++++ + lib/backend/dbi.h | 1 + + lib/backend/sqlite.c | 17 +++++++++++++++-- + lib/rpmdb.c | 7 +++++-- + lib/rpmdb_internal.h | 1 + + lib/rpmts.c | 5 +++++ + python/rpmts-py.c | 15 +++++++++++++++ + tools/rpmdb.c | 6 ++++++ + 9 files changed, 66 insertions(+), 6 deletions(-) + +diff --git a/docs/man/rpmdb.8.md b/docs/man/rpmdb.8.md +index 8ad1ce3bb..7d308ca98 100644 +--- a/docs/man/rpmdb.8.md ++++ b/docs/man/rpmdb.8.md +@@ -12,7 +12,7 @@ rpmdb - RPM Database Tool + SYNOPSIS + ======== + +-**rpmdb** {**\--initdb\|\--rebuilddb**} ++**rpmdb** {**\--initdb\|\--rebuilddb\|\--parkdb**} + + **rpmdb** {**\--verifydb**} + +@@ -23,7 +23,7 @@ DESCRIPTION + + The general form of an rpmdb command is + +-**rpm** {**\--initdb\|\--rebuilddb**} \[**-v**\] \[**\--dbpath ++**rpm** {**\--initdb\|\--rebuilddb\|\--parkdb**} \[**-v**\] \[**\--dbpath + ***DIRECTORY*\] \[**\--root ***DIRECTORY*\] + + Use **\--initdb** to create a new database if one doesn\'t already exist +@@ -38,6 +38,15 @@ for transfporting to another host or database type. + **\--importdb** imports a database from a header-list format as created + by **\--exportdb**. + ++**\--parkdb** parks the database. This prepares and optimizes the database for ++inclusion on read-only media, such as immutable OS images. Write operations, ++such as RPM transactions, will unpark the database and continue normally, ++leaving the database unparked when finished. Thus, to unpark manually, use ++**\--rebuilddb**. Depending on the backend used, this operation may include the ++removal of auxiliary, backend-specific files from disk, such as the write-ahead ++log or shared memory index, and thus facilitate bit-for-bit reproducible OS ++images. ++ + SEE ALSO + ======== + +diff --git a/include/rpm/rpmts.h b/include/rpm/rpmts.h +index 5c168820f..d390f2ac4 100644 +--- a/include/rpm/rpmts.h ++++ b/include/rpm/rpmts.h +@@ -308,6 +308,13 @@ int rpmtsSetDBMode(rpmts ts, int dbmode); + */ + int rpmtsRebuildDB(rpmts ts); + ++/** \ingroup rpmts ++ * Park the database used by the transaction. ++ * @param ts transaction set ++ * @return 0 on success ++ */ ++int rpmtsParkDB(rpmts ts); ++ + /** \ingroup rpmts + * Verify the database used by the transaction. + * @param ts transaction set +diff --git a/lib/backend/dbi.h b/lib/backend/dbi.h +index 00ada038e..6e6226ff7 100644 +--- a/lib/backend/dbi.h ++++ b/lib/backend/dbi.h +@@ -13,6 +13,7 @@ enum rpmdbFlags { + RPMDB_FLAG_REBUILD = (1 << 1), + RPMDB_FLAG_VERIFYONLY = (1 << 2), + RPMDB_FLAG_SALVAGE = (1 << 3), ++ RPMDB_FLAG_PARK = (1 << 4), + }; + + typedef enum dbCtrlOp_e { +diff --git a/lib/backend/sqlite.c b/lib/backend/sqlite.c +index 76e9cae72..6cedcce16 100644 +--- a/lib/backend/sqlite.c ++++ b/lib/backend/sqlite.c +@@ -197,10 +197,23 @@ static int sqlite_fini(rpmdb rdb) + if (sqlite3_db_readonly(sdb, NULL) == 0) { + sqlexec(sdb, "PRAGMA optimize"); + sqlexec(sdb, "PRAGMA wal_checkpoint = TRUNCATE"); ++ ++ /* Park the database if requested */ ++ if ((rdb->db_flags & RPMDB_FLAG_PARK) != 0) { ++ int zero = 0; ++ /* Make sure any WAL and SHM files are cleaned up */ ++ sqlite3_file_control(sdb, NULL, SQLITE_FCNTL_PERSIST_WAL, ++ &zero); ++ rc = sqlexec(sdb, "PRAGMA journal_mode = DELETE"); ++ if (rc == 0) ++ rpmlog(RPMLOG_DEBUG, _("rpmdb sqlite parking done\n")); ++ else ++ rpmlog(RPMLOG_ERR, _("rpmdb sqlite parking failed\n")); ++ } + } + rdb->db_dbenv = NULL; + int xx = sqlite3_close(sdb); +- rc = (xx != SQLITE_OK); ++ rc += (xx != SQLITE_OK); + } + } + +@@ -329,7 +342,7 @@ static int sqlite_Close(dbiIndex dbi, unsigned int flags) + int rc = 0; + if (rdb->db_flags & RPMDB_FLAG_REBUILD) + rc = init_index(dbi, rpmTagGetValue(dbi->dbi_file)); +- sqlite_fini(dbi->dbi_rpmdb); ++ rc += sqlite_fini(dbi->dbi_rpmdb); + dbiFree(dbi); + return rc; + } +diff --git a/lib/rpmdb.c b/lib/rpmdb.c +index 117df46e1..39b2e723e 100644 +--- a/lib/rpmdb.c ++++ b/lib/rpmdb.c +@@ -2450,7 +2450,9 @@ int rpmdbRebuild(const char * prefix, rpmts ts, + goto exit; + } + if (openDatabase(prefix, newdbpath, &newdb, +- (O_RDWR | O_CREAT), 0644, RPMDB_FLAG_REBUILD)) { ++ (O_RDWR | O_CREAT), 0644, RPMDB_FLAG_REBUILD | ++ (rebuildflags & RPMDB_REBUILD_FLAG_PARK ? ++ RPMDB_FLAG_PARK : 0))) { + rc = 1; + goto exit; + } +@@ -2498,7 +2500,8 @@ int rpmdbRebuild(const char * prefix, rpmts ts, + + rpmdbClose(olddb); + dbCtrl(newdb, DB_CTRL_INDEXSYNC); +- rpmdbClose(newdb); ++ if (rpmdbClose(newdb)) ++ failed = 1; + + if (failed) { + rpmlog(RPMLOG_WARNING, +diff --git a/lib/rpmdb_internal.h b/lib/rpmdb_internal.h +index 0590013db..36e18b71a 100644 +--- a/lib/rpmdb_internal.h ++++ b/lib/rpmdb_internal.h +@@ -22,6 +22,7 @@ extern "C" { + + enum rpmdbRebuildFlags_e { + RPMDB_REBUILD_FLAG_SALVAGE = (1 << 0), ++ RPMDB_REBUILD_FLAG_PARK = (1 << 1), + }; + + /** \ingroup rpmdb +diff --git a/lib/rpmts.c b/lib/rpmts.c +index bc52a2f28..96dd31b0a 100644 +--- a/lib/rpmts.c ++++ b/lib/rpmts.c +@@ -165,6 +165,11 @@ int rpmtsRebuildDB(rpmts ts) + return rebuildDB(ts, 0); + } + ++int rpmtsParkDB(rpmts ts) ++{ ++ return rebuildDB(ts, RPMDB_REBUILD_FLAG_PARK); ++} ++ + int rpmtsVerifyDB(rpmts ts) + { + int rc = -1; +diff --git a/python/rpmts-py.c b/python/rpmts-py.c +index ae2eeea28..dc81d3489 100644 +--- a/python/rpmts-py.c ++++ b/python/rpmts-py.c +@@ -375,6 +375,18 @@ rpmts_RebuildDB(rpmtsObject * s) + return Py_BuildValue("i", rc); + } + ++static PyObject * ++rpmts_ParkDB(rpmtsObject * s) ++{ ++ int rc; ++ ++ Py_BEGIN_ALLOW_THREADS ++ rc = rpmtsParkDB(s->ts); ++ Py_END_ALLOW_THREADS ++ ++ return Py_BuildValue("i", rc); ++} ++ + static PyObject * + rpmts_VerifyDB(rpmtsObject * s) + { +@@ -799,6 +811,9 @@ Remove all elements from the transaction set\n" }, + {"rebuildDB", (PyCFunction) rpmts_RebuildDB, METH_NOARGS, + "ts.rebuildDB() -> None\n\ + - Rebuild the default transaction rpmdb.\n" }, ++ {"parkDB", (PyCFunction) rpmts_ParkDB, METH_NOARGS, ++"ts.parkDB() -> None\n\ ++- Park the default transaction rpmdb.\n" }, + {"verifyDB", (PyCFunction) rpmts_VerifyDB, METH_NOARGS, + "ts.verifyDB() -> None\n\ + - Verify the default transaction rpmdb.\n" }, +diff --git a/tools/rpmdb.c b/tools/rpmdb.c +index 36efff8af..3de81be26 100644 +--- a/tools/rpmdb.c ++++ b/tools/rpmdb.c +@@ -13,6 +13,7 @@ enum modes { + MODE_EXPORTDB = (1 << 3), + MODE_IMPORTDB = (1 << 4), + MODE_SALVAGEDB = (1 << 5), ++ MODE_PARKDB = (1 << 6), + }; + + static int mode = 0; +@@ -23,6 +24,8 @@ static struct poptOption dbOptsTable[] = { + { "rebuilddb", '\0', (POPT_ARG_VAL|POPT_ARGFLAG_OR), &mode, MODE_REBUILDDB, + N_("rebuild database inverted lists from installed package headers"), + NULL}, ++ { "parkdb", '\0', (POPT_ARG_VAL|POPT_ARGFLAG_OR), ++ &mode, MODE_PARKDB, N_("park database"), NULL}, + { "verifydb", '\0', (POPT_ARG_VAL|POPT_ARGFLAG_OR), + &mode, MODE_VERIFYDB, N_("verify database"), NULL}, + { "salvagedb", '\0', (POPT_ARG_VAL|POPT_ARGFLAG_OR|POPT_ARGFLAG_DOC_HIDDEN), +@@ -117,6 +120,9 @@ int main(int argc, char *argv[]) + ec = rpmtsRebuildDB(ts); + rpmtsSetVSFlags(ts, ovsflags); + } break; ++ case MODE_PARKDB: ++ ec = rpmtsParkDB(ts); ++ break; + case MODE_VERIFYDB: + ec = rpmtsVerifyDB(ts); + break; +-- +2.54.0 + +diff -up rpm-4.19.1.1/docs/man/rpmdb.8.orig rpm-4.19.1.1/docs/man/rpmdb.8 +--- rpm-4.19.1.1/docs/man/rpmdb.8.orig 2026-06-18 09:27:25.469107665 +0200 ++++ rpm-4.19.1.1/docs/man/rpmdb.8 2026-06-18 09:27:53.907945350 +0200 +@@ -1,70 +1,60 @@ +-.\" Automatically generated by Pandoc 3.1.3 ++.\" Automatically generated by Pandoc 3.1.11.1 + .\" +-.\" Define V font for inline verbatim, using C font in formats +-.\" that render this, and otherwise B font. +-.ie "\f[CB]x\f[]"x" \{\ +-. ftr V B +-. ftr VI BI +-. ftr VB B +-. ftr VBI BI +-.\} +-.el \{\ +-. ftr V CR +-. ftr VI CI +-. ftr VB CB +-. ftr VBI CBI +-.\} + .TH "RPMDB" "8" "29 June 2010" "" "" +-.hy + .SH NAME +-.PP +-rpmdb - RPM Database Tool ++rpmdb \- RPM Database Tool + .SH SYNOPSIS ++\f[B]rpmdb\f[R] {\f[B]\-\-initdb|\-\-rebuilddb|\-\-parkdb\f[R]} + .PP +-\f[B]rpmdb\f[R] {\f[B]--initdb|--rebuilddb\f[R]} +-.PP +-\f[B]rpmdb\f[R] {\f[B]--verifydb\f[R]} ++\f[B]rpmdb\f[R] {\f[B]\-\-verifydb\f[R]} + .PP +-\f[B]rpmdb\f[R] {\f[B]--exportdb|--importdb\f[R]} ++\f[B]rpmdb\f[R] {\f[B]\-\-exportdb|\-\-importdb\f[R]} + .SH DESCRIPTION +-.PP + The general form of an rpmdb command is + .PP +-\f[B]rpm\f[R] {\f[B]--initdb|--rebuilddb\f[R]} [\f[B]-v\f[R]] +-[\f[B]--dbpath \f[R]\f[I]DIRECTORY\f[R]] [\f[B]--root +-\f[R]\f[I]DIRECTORY\f[R]] ++\f[B]rpm\f[R] {\f[B]\-\-initdb|\-\-rebuilddb|\-\-parkdb\f[R]} ++[\f[B]\-v\f[R]] [\f[B]\-\-dbpath \f[R]\f[I]DIRECTORY\f[R]] ++[\f[B]\-\-root \f[R]\f[I]DIRECTORY\f[R]] + .PP +-Use \f[B]--initdb\f[R] to create a new database if one doesn\[aq]t ++Use \f[B]\-\-initdb\f[R] to create a new database if one doesn\[aq]t + already exist (existing database is not overwritten), use +-\f[B]--rebuilddb\f[R] to rebuild the database indices from the installed +-package headers. ++\f[B]\-\-rebuilddb\f[R] to rebuild the database indices from the ++installed package headers. + .PP +-\f[B]--verifydb\f[R] performs a low-level integrity check on the ++\f[B]\-\-verifydb\f[R] performs a low\-level integrity check on the + database. + .PP +-\f[B]--exportdb\f[R] exports the database in header-list format, ++\f[B]\-\-exportdb\f[R] exports the database in header\-list format, + suitable for transfporting to another host or database type. + .PP +-\f[B]--importdb\f[R] imports a database from a header-list format as +-created by \f[B]--exportdb\f[R]. +-.SH SEE ALSO ++\f[B]\-\-importdb\f[R] imports a database from a header\-list format as ++created by \f[B]\-\-exportdb\f[R]. + .PP ++\f[B]\-\-parkdb\f[R] parks the database. ++This prepares and optimizes the database for inclusion on read\-only ++media, such as immutable OS images. ++Write operations, such as RPM transactions, will unpark the database and ++continue normally, leaving the database unparked when finished. ++Thus, to unpark manually, use \f[B]\-\-rebuilddb\f[R]. ++Depending on the backend used, this operation may include the removal of ++auxiliary, backend\-specific files from disk, such as the write\-ahead ++log or shared memory index, and thus facilitate bit\-for\-bit ++reproducible OS images. ++.SH SEE ALSO + \f[B]popt\f[R](3), \f[B]rpm\f[R](8), \f[B]rpmkeys\f[R](8), + \f[B]rpmsign\f[R](8), \f[B]rpm2cpio\f[R](8), \f[B]rpmbuild\f[R](8), + \f[B]rpmspec\f[R](8) + .PP +-\f[B]rpm --help\f[R] - as rpm supports customizing the options via popt +-aliases it\[aq]s impossible to guarantee that what\[aq]s described in +-the manual matches what\[aq]s available. ++\f[B]rpm \-\-help\f[R] \- as rpm supports customizing the options via ++popt aliases it\[aq]s impossible to guarantee that what\[aq]s described ++in the manual matches what\[aq]s available. + .PP + \f[B]http://www.rpm.org/ \f[R] + .SH AUTHORS + .IP +-.nf +-\f[C] ++.EX + Marc Ewing + Jeff Johnson + Erik Troan + Panu Matilainen +-\f[R] +-.fi ++.EE diff --git a/rpm-4.19.x-improve-syslog.patch b/rpm-4.19.x-improve-syslog.patch new file mode 100644 index 0000000..9973a2c --- /dev/null +++ b/rpm-4.19.x-improve-syslog.patch @@ -0,0 +1,699 @@ +From 76be5b707d1d55388bcb6bd77c8cc351152612de Mon Sep 17 00:00:00 2001 +From: Panu Matilainen +Date: Wed, 8 Apr 2026 10:40:39 +0300 +Subject: [PATCH 1/6] Fix wrong operation names used in the syslog plugin + +There are more rpmte types than just TR_ADDED and TR_REMOVED these days, +but this code was never updated to match. As a result, it'll log +anything but installs (such as --restore) as erasure which can be a +little hair-raising when looking at the log. + +Fixes: #4172 +(cherry picked from commit bbba5b986ff468badb49a822bd70b68dcf606cef) +--- + plugins/syslog.c | 13 ++++++++++++- + 1 file changed, 12 insertions(+), 1 deletion(-) + +diff --git a/plugins/syslog.c b/plugins/syslog.c +index f7b1a1388..2dac9872f 100644 +--- a/plugins/syslog.c ++++ b/plugins/syslog.c +@@ -74,13 +74,24 @@ static rpmRC syslog_tsm_post(rpmPlugin plugin, rpmts ts, int res) + return RPMRC_OK; + } + ++static const char *getOp(rpmte te) ++{ ++ switch (rpmteType(te)) { ++ case TR_ADDED: return "install"; ++ case TR_REMOVED: return "erase"; ++ case TR_RPMDB: return "rpmdb"; ++ case TR_RESTORED: return "restore"; ++ } ++ return ""; ++} ++ + static rpmRC syslog_psm_post(rpmPlugin plugin, rpmte te, int res) + { + struct logstat * state = rpmPluginGetData(plugin); + + if (state->logging) { + int lvl = LOG_NOTICE; +- const char *op = (rpmteType(te) == TR_ADDED) ? "install" : "erase"; ++ const char *op = getOp(te); + const char *outcome = "success"; + /* XXX: Permit configurable header queryformat? */ + const char *pkg = rpmteNEVRA(te); +-- +2.54.0 + + +From fc5c083b06ae1b24c879c8db3e4af5d57074700a Mon Sep 17 00:00:00 2001 +From: Panu Matilainen +Date: Wed, 15 Apr 2026 16:29:31 +0300 +Subject: [PATCH 2/6] Use a sane identity string for syslog + +The old "[RPM]" string is a PITA to grep for and annoying with journalctl +too. Lets just use plain "rpm" to reduce friction. And with that, +"journalctl -t rpm" is actually a pretty handy way to look at the +transaction history. + +In theory, this could of course break somebody's scripts. But +considering how broken the output has been without anybody complaining, +it suggests nobody is actually using this plugin and so, changing cannot +hurt. + +(cherry picked from commit 099128396c2a78cc2ca576be7c98dc269d11a457) +--- + plugins/syslog.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/plugins/syslog.c b/plugins/syslog.c +index 2dac9872f..e05a9b4d1 100644 +--- a/plugins/syslog.c ++++ b/plugins/syslog.c +@@ -16,7 +16,7 @@ struct logstat { + static rpmRC syslog_init(rpmPlugin plugin, rpmts ts) + { + /* XXX make this configurable? */ +- const char * log_ident = "[RPM]"; ++ const char * log_ident = "rpm"; + struct logstat * state = rcalloc(1, sizeof(*state)); + + rpmPluginSetData(plugin, state); +-- +2.54.0 + + +From 91d39dbbbed2ad5537551aef5dd3bd6d65510bec Mon Sep 17 00:00:00 2001 +From: Panu Matilainen +Date: Wed, 15 Apr 2026 13:53:57 +0300 +Subject: [PATCH 3/6] Add + use an internal macro for determining script-only + install goals + +No functional changes, but we'll need this info in another spot in +the next commit. + +(cherry picked from commit bfefc137eb2d24c76858cc131b998a9fac99a679) +--- + lib/rpmte.c | 2 +- + lib/rpmte_internal.h | 2 ++ + 2 files changed, 3 insertions(+), 1 deletion(-) + +diff --git a/lib/rpmte.c b/lib/rpmte.c +index 657b4e11d..3802aeaec 100644 +--- a/lib/rpmte.c ++++ b/lib/rpmte.c +@@ -815,7 +815,7 @@ int rpmteAddOp(rpmte te) + int rpmteProcess(rpmte te, pkgGoal goal, int num) + { + /* Only install/erase resets pkg file info */ +- int scriptstage = (goal != PKG_INSTALL && goal != PKG_ERASE && goal != PKG_RESTORE); ++ int scriptstage = isScriptStage(goal); + int test = (rpmtsFlags(te->ts) & RPMTRANS_FLAG_TEST); + int reset_fi = (scriptstage == 0 && test == 0); + int failed = 1; +diff --git a/lib/rpmte_internal.h b/lib/rpmte_internal.h +index e616c0561..881772c95 100644 +--- a/lib/rpmte_internal.h ++++ b/lib/rpmte_internal.h +@@ -22,6 +22,8 @@ typedef enum pkgGoal_e { + PKG_TRANSFILETRIGGERUN = RPMTAG_TRANSFILETRIGGERUN, + } pkgGoal; + ++#define isScriptStage(_g) ((_g) != PKG_INSTALL && (_g) != PKG_ERASE && (_g) != PKG_RESTORE) ++ + /** \ingroup rpmte + * Transaction element ordering chain linkage. + */ +-- +2.54.0 + + +From 38e1fedd72cb14b9bf8e0a428a79071eb0e510dc Mon Sep 17 00:00:00 2001 +From: Panu Matilainen +Date: Wed, 15 Apr 2026 13:59:16 +0300 +Subject: [PATCH 4/6] Only execute psm_pre and psm_post for the main package + operation + +These hooks getting called multiple times with no means to identify +why it's getting called makes them impossible to use meaningfully. +Only run the psm hooks on non-script goals: install, erase and restore. + +It is of course a change to what is now a public API semantics, +but the former behavior is really a bug that we're fixing here. + +With this change, the syslog plugin no longer emits duplicates and +in a strange order at that. + +Fixes: #1254 +(backported from commit 0a45aefa01d1517184c41f233f4b53cebd78cf8d) +--- + docs/manual/plugins.md | 2 +- + lib/psm.c | 7 ++++--- + 2 files changed, 5 insertions(+), 4 deletions(-) + +diff --git a/docs/manual/plugins.md b/docs/manual/plugins.md +index e53eb9e3d..b34c182f4 100644 +--- a/docs/manual/plugins.md ++++ b/docs/manual/plugins.md +@@ -44,7 +44,7 @@ Post hook is guaranteed to execute whenever pre hook was executed. + + ## Per transaction element hooks + +-Hooks `psm_pre` and `psm_post` occur before and after the processing of single transaction element within the transaction. Both hooks can get executed multiple times for a single element as these hooks get called separately for %pretrans and %posttrans scriptlets in addition to the main package install or erase action. ++Hooks `psm_pre` and `psm_post` occur before and after the processing of single transaction element within the transaction during install, erase and restore operations. + + Post hook is guaranteed to execute whenever pre hook was executed. + +diff --git a/lib/psm.c b/lib/psm.c +index 39261b9f4..820704cab 100644 +--- a/lib/psm.c ++++ b/lib/psm.c +@@ -1118,7 +1118,8 @@ static rpmRC runGoal(rpmpsm psm, pkgGoal goal) + rpmRC rpmpsmRun(rpmts ts, rpmte te, pkgGoal goal) + { + rpmpsm psm = NULL; +- rpmRC rc = RPMRC_FAIL; ++ rpmRC rc = RPMRC_OK; ++ int runplugins = (isScriptStage(goal) == 0); + + /* Psm can't fail in test mode, just return early */ + if (rpmtsFlags(ts) & RPMTRANS_FLAG_TEST) +@@ -1126,7 +1127,7 @@ rpmRC rpmpsmRun(rpmts ts, rpmte te, pkgGoal goal) + + psm = rpmpsmNew(ts, te, goal); + /* Run pre transaction element hook for all plugins */ +- if (rpmChrootIn() == 0) { ++ if (runplugins && rpmChrootIn() == 0) { + rc = rpmpluginsCallPsmPre(rpmtsPlugins(ts), te); + rpmChrootOut(); + } +@@ -1135,7 +1136,7 @@ rpmRC rpmpsmRun(rpmts ts, rpmte te, pkgGoal goal) + rc = runGoal(psm, goal); + + /* Run post transaction element hook for all plugins (even on failure) */ +- if (rpmChrootIn() == 0) { ++ if (runplugins && rpmChrootIn() == 0) { + rpmpluginsCallPsmPost(rpmtsPlugins(ts), te, rc); + rpmChrootOut(); + } +-- +2.54.0 + + +From 4f85d8cf98e67200f82a6cef666d6bfecb084f21 Mon Sep 17 00:00:00 2001 +From: Panu Matilainen +Date: Wed, 20 May 2026 16:33:02 +0300 +Subject: [PATCH 5/6] Add a configurable output target to the syslog plugin + +Allow optionally redirecting the plugin output to stderr instead of +the actual syslog. We'll need this in the next commit to make testing +actually feasible. + +(backported from commit fbf4d588458700da25f193770d10a3632c2c4836) +--- + docs/man/rpm-plugin-syslog.8.md | 9 +++++-- + plugins/syslog.c | 43 +++++++++++++++++++++++++++------ + 2 files changed, 43 insertions(+), 9 deletions(-) + +diff --git a/docs/man/rpm-plugin-syslog.8.md b/docs/man/rpm-plugin-syslog.8.md +index e9fa48e1b..47a6bc549 100644 +--- a/docs/man/rpm-plugin-syslog.8.md ++++ b/docs/man/rpm-plugin-syslog.8.md +@@ -18,8 +18,13 @@ The plugin writes basic information about rpm transactions to the syslog + Configuration + ============= + +-There are currently no options for this plugin in particular. See +-**rpm-plugins**(8) on how to control plugins in general. ++**%\_\_transaction\_syslog\_target** ++: Specify target output for the plugin. Supported values are: ++ ++ - **syslog**: system log (default) ++ - **stderr**: standard error ++ ++See **rpm-plugins**(8) on how to control plugins in general. + + SEE ALSO + ======== +diff --git a/plugins/syslog.c b/plugins/syslog.c +index e05a9b4d1..5db295f4b 100644 +--- a/plugins/syslog.c ++++ b/plugins/syslog.c +@@ -1,34 +1,63 @@ + #include "system.h" + ++#include + #include + #include + ++#include ++#include + #include + #include + #include "rpmplugin.h" + ++typedef void (*loggerfunc)(int prio, const char *fmt, ...); ++ + struct logstat { + int logging; + unsigned int scriptfail; + unsigned int pkgfail; ++ loggerfunc log; + }; + ++static void errlog(int prio, const char *fmt, ...) ++{ ++ va_list ap; ++ ++ va_start(ap, fmt); ++ vfprintf(stderr, fmt, ap); ++ fprintf(stderr, "\n"); ++ va_end(ap); ++} ++ + static rpmRC syslog_init(rpmPlugin plugin, rpmts ts) + { + /* XXX make this configurable? */ + const char * log_ident = "rpm"; + struct logstat * state = rcalloc(1, sizeof(*state)); ++ char *target = rpmExpand("%{?__transaction_syslog_target}", NULL); ++ ++ if (rstreq(target, "stderr")) { ++ state->log = errlog; ++ } else { ++ if (*target && !rstreq(target, "syslog")) { ++ rpmlog(RPMLOG_WARNING, ++ _("unknown syslog target %s, using 'syslog'\n"), target); ++ } ++ state->log = syslog; ++ openlog(log_ident, (LOG_PID), LOG_USER); ++ } + + rpmPluginSetData(plugin, state); +- openlog(log_ident, (LOG_PID), LOG_USER); ++ free(target); + return RPMRC_OK; + } + + static void syslog_cleanup(rpmPlugin plugin) + { + struct logstat * state = rpmPluginGetData(plugin); ++ if (state->log == syslog) ++ closelog(); + free(state); +- closelog(); + } + + static rpmRC syslog_tsm_pre(rpmPlugin plugin, rpmts ts) +@@ -51,7 +80,7 @@ static rpmRC syslog_tsm_pre(rpmPlugin plugin, rpmts ts) + state->logging = 0; + + if (state->logging) { +- syslog(LOG_NOTICE, "Transaction ID %x started", rpmtsGetTid(ts)); ++ state->log(LOG_NOTICE, "Transaction ID %x started", rpmtsGetTid(ts)); + } + + return RPMRC_OK; +@@ -63,10 +92,10 @@ static rpmRC syslog_tsm_post(rpmPlugin plugin, rpmts ts, int res) + + if (state->logging) { + if (state->pkgfail || state->scriptfail) { +- syslog(LOG_WARNING, "%u elements failed, %u scripts failed", ++ state->log(LOG_WARNING, "%u elements failed, %u scripts failed", + state->pkgfail, state->scriptfail); + } +- syslog(LOG_NOTICE, "Transaction ID %x finished: %d", ++ state->log(LOG_NOTICE, "Transaction ID %x finished: %d", + rpmtsGetTid(ts), res); + } + +@@ -102,7 +131,7 @@ static rpmRC syslog_psm_post(rpmPlugin plugin, rpmte te, int res) + state->pkgfail++; + } + +- syslog(lvl, "%s %s: %s", op, pkg, outcome); ++ state->log(lvl, "%s %s: %s", op, pkg, outcome); + } + return RPMRC_OK; + } +@@ -113,7 +142,7 @@ static rpmRC syslog_scriptlet_post(rpmPlugin plugin, + struct logstat * state = rpmPluginGetData(plugin); + + if (state->logging && res) { +- syslog(LOG_WARNING, "scriptlet %s failure: %d\n", s_name, res); ++ state->log(LOG_WARNING, "scriptlet %s failure: %d\n", s_name, res); + state->scriptfail++; + } + return RPMRC_OK; +-- +2.54.0 + + +From eb44ea8ebb305bf13208dac5512bd6e00c3087ee Mon Sep 17 00:00:00 2001 +From: Panu Matilainen +Date: Wed, 20 May 2026 16:37:13 +0300 +Subject: [PATCH 6/6] Report meaningful operation names in syslog output + +Determine and output human understandable operations, including the +related transaction elements where relevant and explain it in the +manual. Drop the comment about making things more configurable, that's +not helpful to the community (except perhaps if the default is bad, +as it originally was). + +Add tests now that we can actually test it. + +Related: RHEL-155272 +(backported from commit 98bcdb37397c23c6689ab64824875f4021a986ed) +--- + docs/man/rpm-plugin-syslog.8.md | 48 ++++++++++++++++ + plugins/syslog.c | 97 +++++++++++++++++++++++++++++---- + 2 files changed, 134 insertions(+), 11 deletions(-) + +diff --git a/docs/man/rpm-plugin-syslog.8.md b/docs/man/rpm-plugin-syslog.8.md +index 47a6bc549..08944c6b7 100644 +--- a/docs/man/rpm-plugin-syslog.8.md ++++ b/docs/man/rpm-plugin-syslog.8.md +@@ -26,6 +26,54 @@ Configuration + + See **rpm-plugins**(8) on how to control plugins in general. + ++Output ++====== ++ ++The plugin uses **rpm** as its syslog identity. ++ ++The package level output during transactions is as follows: ++ ++_OPERATION_ _NEVRA_ [**(from:** _RELNEVRA_**)**]**:** _STATUS_ ++ ++_OPERATION_ is one of: ++ ++- **cleanup**: cleanup of the previous version in **upgrade** or **downgrade** ++- **downgrade**: downgrade an installed package to an older version ++- **erase**: erase package from the system ++- **install**: install a new package on the system ++- **replace**: replace (obsolete) a previously installed package with another one ++- **restore**: restore permissions and similar on an installed package ++- **upgrade**: upgrade an installed package to a newer version ++ ++_NEVRA_ identifies the transaction element being operated on. ++Note that operations **downgrade**, **replace** and **upgrade** consist of two ++elements: install of one package version and removal of another. ++In these cases, the related element identifier _RELNEVRA_ is indicated ++in parentheses after **from:**. _STATUS_ is one of **success** or **failure**. ++ ++Examples ++======== ++ ++**journalctl -t rpm** ++: Inspect RPM transaction history on a systemd-based OS. ++ ++**Package aaa upgrade from 1.2-1 to 2.0-1** ++``` ++upgrade: aaa-2.0-1.noarch (from: aaa-1.2-1.noarch): success ++cleanup: aaa-1.2-1.noarch (from: aaa-2.0-1.noarch): success ++``` ++ ++**Package bbb 1.3-5 obsoleting aaa 1.2-1** ++``` ++replace: bbb-1.3-5.noarch (from: aaa-1.2-1.noarch): success ++erase: aaa-1.2-1.noarch (from: bbb-1.3-5.noarch): success ++``` ++ ++**Package aaa 1.2-1 install failure** ++``` ++install: aaa-1.2-1.noarch: failure ++``` ++ + SEE ALSO + ======== + +diff --git a/plugins/syslog.c b/plugins/syslog.c +index 5db295f4b..dbcc018db 100644 +--- a/plugins/syslog.c ++++ b/plugins/syslog.c +@@ -6,6 +6,7 @@ + + #include + #include ++#include + #include + #include + #include "rpmplugin.h" +@@ -16,6 +17,7 @@ struct logstat { + int logging; + unsigned int scriptfail; + unsigned int pkgfail; ++ rpmts ts; + loggerfunc log; + }; + +@@ -31,7 +33,6 @@ static void errlog(int prio, const char *fmt, ...) + + static rpmRC syslog_init(rpmPlugin plugin, rpmts ts) + { +- /* XXX make this configurable? */ + const char * log_ident = "rpm"; + struct logstat * state = rcalloc(1, sizeof(*state)); + char *target = rpmExpand("%{?__transaction_syslog_target}", NULL); +@@ -65,6 +66,7 @@ static rpmRC syslog_tsm_pre(rpmPlugin plugin, rpmts ts) + struct logstat * state = rpmPluginGetData(plugin); + + /* Reset counters */ ++ state->ts = ts; + state->scriptfail = 0; + state->pkgfail = 0; + +@@ -103,15 +105,89 @@ static rpmRC syslog_tsm_post(rpmPlugin plugin, rpmts ts, int res) + return RPMRC_OK; + } + +-static const char *getOp(rpmte te) ++static rpmte findDependent(rpmts ts, rpmte te) + { ++ rpmte dep = NULL; ++ rpmte p = NULL; ++ rpmtsi pi = rpmtsiInit(ts); ++ while ((p = rpmtsiNext(pi, 0)) != NULL) { ++ if (rpmteDependsOn(p) == te) { ++ dep = p; ++ break; ++ } ++ } ++ rpmtsiFree(pi); ++ ++ return dep; ++} ++ ++static int isObsolete(rpmte a, rpmte b) ++{ ++ return strcmp(rpmteN(a), rpmteN(b)); ++} ++ ++static int isDowngrade(rpmte a, rpmte b) ++{ ++ int downgrade = 0; ++ rpmver av = rpmverParse(rpmteEVR(a)); ++ rpmver bv = rpmverParse(rpmteEVR(b)); ++ ++ if (av && bv && rpmverCmp(av, bv) < 0) ++ downgrade = 1; ++ ++ rpmverFree(av); ++ rpmverFree(bv); ++ return downgrade; ++} ++ ++static char *getOp(rpmts ts, rpmte te) ++{ ++ char *ret = NULL; ++ const char *op = NULL; ++ rpmte dep = NULL; ++ + switch (rpmteType(te)) { +- case TR_ADDED: return "install"; +- case TR_REMOVED: return "erase"; +- case TR_RPMDB: return "rpmdb"; +- case TR_RESTORED: return "restore"; ++ case TR_ADDED: ++ dep = findDependent(ts, te); ++ if (dep) { ++ if (isObsolete(te, dep)) { ++ op = "replace"; ++ } else { ++ op = isDowngrade(te, dep) ? "downgrade" : "upgrade"; ++ } ++ } else { ++ op = "install"; ++ } ++ break; ++ case TR_REMOVED: ++ dep = rpmteDependsOn(te); ++ if (dep && !isObsolete(te, dep)) { ++ op = "cleanup"; ++ } else { ++ op = "erase"; ++ } ++ break; ++ case TR_RPMDB: ++ /* not an operation */ ++ break; ++ case TR_RESTORED: ++ op = "restore"; ++ break; ++ default: ++ op = ""; ++ break; + } +- return ""; ++ ++ if (op) { ++ if (dep) { ++ rasprintf(&ret, "%s: %s (from: %s)", op, ++ rpmteNEVRA(te), rpmteNEVRA(dep)); ++ } else { ++ rasprintf(&ret, "%s: %s", op, rpmteNEVRA(te)); ++ } ++ } ++ ++ return ret; + } + + static rpmRC syslog_psm_post(rpmPlugin plugin, rpmte te, int res) +@@ -120,10 +196,8 @@ static rpmRC syslog_psm_post(rpmPlugin plugin, rpmte te, int res) + + if (state->logging) { + int lvl = LOG_NOTICE; +- const char *op = getOp(te); ++ char *op = getOp(state->ts, te); + const char *outcome = "success"; +- /* XXX: Permit configurable header queryformat? */ +- const char *pkg = rpmteNEVRA(te); + + if (res != RPMRC_OK) { + lvl = LOG_WARNING; +@@ -131,7 +205,8 @@ static rpmRC syslog_psm_post(rpmPlugin plugin, rpmte te, int res) + state->pkgfail++; + } + +- state->log(lvl, "%s %s: %s", op, pkg, outcome); ++ state->log(lvl, "%s: %s", op, outcome); ++ free(op); + } + return RPMRC_OK; + } +-- +2.54.0 + +diff -up rpm-4.19.1.1/docs/man/rpm-plugin-syslog.8.orig rpm-4.19.1.1/docs/man/rpm-plugin-syslog.8 +--- rpm-4.19.1.1/docs/man/rpm-plugin-syslog.8.orig 2026-06-18 09:26:45.106337957 +0200 ++++ rpm-4.19.1.1/docs/man/rpm-plugin-syslog.8 2026-06-18 09:26:48.072321034 +0200 +@@ -1,32 +1,81 @@ +-.\" Automatically generated by Pandoc 3.1.3 ++.\" Automatically generated by Pandoc 3.1.11.1 + .\" +-.\" Define V font for inline verbatim, using C font in formats +-.\" that render this, and otherwise B font. +-.ie "\f[CB]x\f[]"x" \{\ +-. ftr V B +-. ftr VI BI +-. ftr VB B +-. ftr VBI BI +-.\} +-.el \{\ +-. ftr V CR +-. ftr VI CI +-. ftr VB CB +-. ftr VBI CBI +-.\} +-.TH "RPM-SYSLOG" "8" "14 Apr 2016" "" "" +-.hy ++.TH "RPM\-SYSLOG" "8" "14 Apr 2016" "" "" + .SH NAME +-.PP +-rpm-plugin-syslog - Syslog plugin for the RPM Package Manager ++rpm\-plugin\-syslog \- Syslog plugin for the RPM Package Manager + .SH Description +-.PP + The plugin writes basic information about rpm transactions to the syslog +-- like transactions run and packages installed or removed. ++\- like transactions run and packages installed or removed. + .SH Configuration ++.TP ++\f[B]%__transaction_syslog_target\f[R] ++Specify target output for the plugin. ++Supported values are: ++.RS ++.IP \[bu] 2 ++\f[B]syslog\f[R]: system log (default) ++.IP \[bu] 2 ++\f[B]stderr\f[R]: standard error ++.RE + .PP +-There are currently no options for this plugin in particular. +-See \f[B]rpm-plugins\f[R](8) on how to control plugins in general. +-.SH SEE ALSO ++See \f[B]rpm\-plugins\f[R](8) on how to control plugins in general. ++.SH Output ++The plugin uses \f[B]rpm\f[R] as its syslog identity. ++.PP ++The package level output during transactions is as follows: ++.PP ++\f[I]OPERATION\f[R] \f[I]NEVRA\f[R] [\f[B](from:\f[R] ++\f[I]RELNEVRA\f[R]\f[B])\f[R]]\f[B]:\f[R] \f[I]STATUS\f[R] + .PP +-\f[B]rpm\f[R](8), \f[B]rpm-plugins\f[R](8) ++\f[I]OPERATION\f[R] is one of: ++.IP \[bu] 2 ++\f[B]cleanup\f[R]: cleanup of the previous version in \f[B]upgrade\f[R] ++or \f[B]downgrade\f[R] ++.IP \[bu] 2 ++\f[B]downgrade\f[R]: downgrade an installed package to an older version ++.IP \[bu] 2 ++\f[B]erase\f[R]: erase package from the system ++.IP \[bu] 2 ++\f[B]install\f[R]: install a new package on the system ++.IP \[bu] 2 ++\f[B]replace\f[R]: replace (obsolete) a previously installed package ++with another one ++.IP \[bu] 2 ++\f[B]restore\f[R]: restore permissions and similar on an installed ++package ++.IP \[bu] 2 ++\f[B]upgrade\f[R]: upgrade an installed package to a newer version ++.PP ++\f[I]NEVRA\f[R] identifies the transaction element being operated on. ++Note that operations \f[B]downgrade\f[R], \f[B]replace\f[R] and ++\f[B]upgrade\f[R] consist of two elements: install of one package ++version and removal of another. ++In these cases, the related element identifier \f[I]RELNEVRA\f[R] is ++indicated in parentheses after \f[B]from:\f[R]. ++\f[I]STATUS\f[R] is one of \f[B]success\f[R] or \f[B]failure\f[R]. ++.SH Examples ++.TP ++\f[B]journalctl \-t rpm\f[R] ++Inspect RPM transaction history on a systemd\-based OS. ++.PP ++\f[B]Package aaa upgrade from 1.2\-1 to 2.0\-1\f[R] ++.IP ++.EX ++upgrade: aaa\-2.0\-1.noarch (from: aaa\-1.2\-1.noarch): success ++cleanup: aaa\-1.2\-1.noarch (from: aaa\-2.0\-1.noarch): success ++.EE ++.PP ++\f[B]Package bbb 1.3\-5 obsoleting aaa 1.2\-1\f[R] ++.IP ++.EX ++replace: bbb\-1.3\-5.noarch (from: aaa\-1.2\-1.noarch): success ++erase: aaa\-1.2\-1.noarch (from: bbb\-1.3\-5.noarch): success ++.EE ++.PP ++\f[B]Package aaa 1.2\-1 install failure\f[R] ++.IP ++.EX ++install: aaa\-1.2\-1.noarch: failure ++.EE ++.SH SEE ALSO ++\f[B]rpm\f[R](8), \f[B]rpm\-plugins\f[R](8) diff --git a/rpm.spec b/rpm.spec index b65563f..9f9be71 100644 --- a/rpm.spec +++ b/rpm.spec @@ -27,7 +27,7 @@ %global rpmver 4.19.1.1 #global snapver rc1 -%global baserelease 23 +%global baserelease 25 %global sover 10 %global srcver %{rpmver}%{?snapver:-%{snapver}} @@ -178,6 +178,11 @@ rpm-4.19.x-multisig-verify-fixes.patch rpm-4.19.x-nsswitch-enable.patch 0001-Fix-empty-password-field-in-passwd-group-causing-ent.patch +rpm-4.19.x-add-autosetup-C.patch +rpm-4.19.x-add-parkdb.patch +rpm-4.19.x-improve-syslog.patch +0001-Prevent-buffer-overruns-in-findPreambleTag-for-langu.patch + # These are not yet upstream rpm-4.7.1-geode-i686.patch @@ -196,7 +201,8 @@ Requires(meta): %{name} = %{version}-%{release} # >= 1.4.0 required for pgpVerifySignature2() and pgpPrtParams2() Requires: rpm-sequoia%{_isa} >= 1.9.0 # Most systems should have a central package operations log -Recommends: rpm-plugin-audit +Requires(meta): (rpm-plugin-audit if audit) +Requires(meta): (rpm-plugin-syslog if syslog) %endif %description libs @@ -667,9 +673,19 @@ fi %doc %{_defaultdocdir}/rpm/API/ %changelog -* Fri Feb 06 2026 Eduard Abdullin - 4.19.1.1-23.alma.1 +* Sat Jun 20 2026 Eduard Abdullin - 4.19.1.1-25.alma.1 - Fix: Treat x86_64_v2 as x86_64 in architecture checks +* Fri Jun 19 2026 Michal Domonkos - 4.19.1.1-25 +- Apply forgotten patch for RHEL-169755 + +* Thu Jun 18 2026 Michal Domonkos - 4.19.1.1-24 +- Add support for %%autosetup -C (RHEL-141269) +- Add support for database parking (RHEL-126405) +- Make syslog plugin actually useful and also required (RHEL-155272) +- Require rpm-plugin-audit instead of just recommending (RHEL-139071) +- Fix buffer overruns with long language strings (RHEL-169755) + * Thu Feb 05 2026 Michal Domonkos - 4.19.1.1-23 - Fix key import API to return NOTTRUSTED for disabled algorithms (RHEL-112394)