From 271b202be3503330c8e03dac7afed7e5b660735d Mon Sep 17 00:00:00 2001 From: Tomas Bajer Date: Thu, 29 Jan 2026 14:54:57 +0100 Subject: [PATCH 1/8] Temporarily enable python-mockbuild test for x86_64 arch only --- plans/python-mockbuild.fmf | 4 ++++ 1 file changed, 4 insertions(+) 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 From 6755696e77a37edde42725c0decab56f93e796d7 Mon Sep 17 00:00:00 2001 From: Michal Domonkos Date: Thu, 18 Jun 2026 09:39:24 +0200 Subject: [PATCH 2/8] Add support for %autosetup -C Resolves: RHEL-141269 --- rpm-4.19.x-add-autosetup-C.patch | 1164 ++++++++++++++++++++++++++++++ rpm.spec | 7 +- 2 files changed, 1170 insertions(+), 1 deletion(-) create mode 100644 rpm-4.19.x-add-autosetup-C.patch 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.spec b/rpm.spec index b6583a2..b49dafd 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 24 %global sover 10 %global srcver %{rpmver}%{?snapver:-%{snapver}} @@ -175,6 +175,8 @@ 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 + # These are not yet upstream rpm-4.7.1-geode-i686.patch @@ -664,6 +666,9 @@ fi %doc %{_defaultdocdir}/rpm/API/ %changelog +* Thu Jun 18 2026 Michal Domonkos - 4.19.1.1-24 +- Add support for %%autosetup -C (RHEL-141269) + * Thu Feb 05 2026 Michal Domonkos - 4.19.1.1-23 - Fix key import API to return NOTTRUSTED for disabled algorithms (RHEL-112394) From 2310f53fa8e5bb5c7b4dc0ff28f64a661916514c Mon Sep 17 00:00:00 2001 From: Michal Domonkos Date: Thu, 18 Jun 2026 09:39:57 +0200 Subject: [PATCH 3/8] Add support for database parking Resolves: RHEL-126405 --- rpm-4.19.x-add-parkdb.patch | 428 ++++++++++++++++++++++++++++++++++++ rpm.spec | 2 + 2 files changed, 430 insertions(+) create mode 100644 rpm-4.19.x-add-parkdb.patch 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.spec b/rpm.spec index b49dafd..1c3a235 100644 --- a/rpm.spec +++ b/rpm.spec @@ -176,6 +176,7 @@ 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 # These are not yet upstream rpm-4.7.1-geode-i686.patch @@ -668,6 +669,7 @@ fi %changelog * 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) * Thu Feb 05 2026 Michal Domonkos - 4.19.1.1-23 - Fix key import API to return NOTTRUSTED for disabled algorithms (RHEL-112394) From 2b9f6dd2dff663a6ac316026f8f23dcb9ea6cd9a Mon Sep 17 00:00:00 2001 From: Michal Domonkos Date: Thu, 18 Jun 2026 09:40:20 +0200 Subject: [PATCH 4/8] Make syslog plugin output more useful and required Install the plugin when the syslog provide is installed. Systemd provides that name since journald handles the syslog facilities. Resolves: RHEL-155272 --- rpm-4.19.x-improve-syslog.patch | 699 ++++++++++++++++++++++++++++++++ rpm.spec | 3 + 2 files changed, 702 insertions(+) create mode 100644 rpm-4.19.x-improve-syslog.patch 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 1c3a235..d512737 100644 --- a/rpm.spec +++ b/rpm.spec @@ -177,6 +177,7 @@ rpm-4.19.x-nsswitch-enable.patch rpm-4.19.x-add-autosetup-C.patch rpm-4.19.x-add-parkdb.patch +rpm-4.19.x-improve-syslog.patch # These are not yet upstream rpm-4.7.1-geode-i686.patch @@ -197,6 +198,7 @@ Requires(meta): %{name} = %{version}-%{release} Requires: rpm-sequoia%{_isa} >= 1.9.0 # Most systems should have a central package operations log Recommends: rpm-plugin-audit +Requires(meta): (rpm-plugin-syslog if syslog) %endif %description libs @@ -670,6 +672,7 @@ fi * 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) * Thu Feb 05 2026 Michal Domonkos - 4.19.1.1-23 - Fix key import API to return NOTTRUSTED for disabled algorithms (RHEL-112394) From 5edc9f46664bdb523aa45e4c5c2ed45295ea31b5 Mon Sep 17 00:00:00 2001 From: Michal Domonkos Date: Thu, 18 Jun 2026 09:41:10 +0200 Subject: [PATCH 5/8] Fix buffer overruns with long language strings Resolves: RHEL-169755 --- ...verruns-in-findPreambleTag-for-langu.patch | 97 +++++++++++++++++++ rpm.spec | 1 + 2 files changed, 98 insertions(+) create mode 100644 0001-Prevent-buffer-overruns-in-findPreambleTag-for-langu.patch 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/rpm.spec b/rpm.spec index d512737..4c4e903 100644 --- a/rpm.spec +++ b/rpm.spec @@ -673,6 +673,7 @@ fi - Add support for %%autosetup -C (RHEL-141269) - Add support for database parking (RHEL-126405) - Make syslog plugin actually useful and also required (RHEL-155272) +- 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) From f80449bf3993c1cdf0ea6a4755d6921dda3e885f Mon Sep 17 00:00:00 2001 From: Michal Domonkos Date: Thu, 18 Jun 2026 09:41:25 +0200 Subject: [PATCH 6/8] Require rpm-plugin-audit Much like we now require the syslog plugin, do the same with audit. This aligns with the RHEL content guidelines as well, which is the what the referenced ticket is about. Resolves: RHEL-139071 --- rpm.spec | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rpm.spec b/rpm.spec index 4c4e903..1862264 100644 --- a/rpm.spec +++ b/rpm.spec @@ -197,7 +197,7 @@ 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 @@ -674,6 +674,7 @@ fi - Add support for database parking (RHEL-126405) - Make syslog plugin actually useful and also required (RHEL-155272) - Fix buffer overruns with long language strings (RHEL-169755) +- Require rpm-plugin-audit instead of just recommending (RHEL-139071) * Thu Feb 05 2026 Michal Domonkos - 4.19.1.1-23 - Fix key import API to return NOTTRUSTED for disabled algorithms (RHEL-112394) From 477f64dc397456cc306892cdc4bbb917bc274abe Mon Sep 17 00:00:00 2001 From: Michal Domonkos Date: Fri, 19 Jun 2026 13:04:31 +0200 Subject: [PATCH 7/8] Apply forgotten patch for RHEL-169755 Related: RHEL-169755 --- rpm.spec | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/rpm.spec b/rpm.spec index 1862264..2876174 100644 --- a/rpm.spec +++ b/rpm.spec @@ -27,7 +27,7 @@ %global rpmver 4.19.1.1 #global snapver rc1 -%global baserelease 24 +%global baserelease 25 %global sover 10 %global srcver %{rpmver}%{?snapver:-%{snapver}} @@ -178,6 +178,7 @@ rpm-4.19.x-nsswitch-enable.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 @@ -669,6 +670,9 @@ fi %doc %{_defaultdocdir}/rpm/API/ %changelog +* 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) From df29bc37775e324e1678fbfeda2e80b8ba3e1db7 Mon Sep 17 00:00:00 2001 From: Michal Domonkos Date: Fri, 19 Jun 2026 13:05:43 +0200 Subject: [PATCH 8/8] Cluster plugin changes together in changelog Related: RHEL-139071 --- rpm.spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rpm.spec b/rpm.spec index 2876174..e8d4084 100644 --- a/rpm.spec +++ b/rpm.spec @@ -677,8 +677,8 @@ fi - Add support for %%autosetup -C (RHEL-141269) - Add support for database parking (RHEL-126405) - Make syslog plugin actually useful and also required (RHEL-155272) -- Fix buffer overruns with long language strings (RHEL-169755) - 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)