public inbox for git-commits@fedoraproject.org
help / color / mirror / Atom feed
From: Michal Domonkos <mdomonko@redhat.com>
To: git-commits@fedoraproject.org
Subject: [rpms/python3-rpm] epel10: Add support for %autosetup -C
Date: Thu, 25 Jun 2026 07:43:27 GMT [thread overview]
Message-ID: <178237340785.1.5304607219589051778.rpms-python3-rpm-6755696e77a3@fedoraproject.org> (raw)
A new commit has been pushed.
Repo : rpms/python3-rpm
Branch : epel10
Commit : 6755696e77a37edde42725c0decab56f93e796d7
Author : Michal Domonkos <mdomonko@redhat.com>
Date : 2026-06-18T09:49:28+02:00
Stats : +1170/-1 in 2 file(s)
URL : https://src.fedoraproject.org/rpms/python3-rpm/c/6755696e77a37edde42725c0decab56f93e796d7?branch=epel10
Log:
Add support for %autosetup -C
Resolves: RHEL-141269
---
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 <teknoraver@meta.com>
+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<num> Use -p<num> 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 <stdio.h>
+ #include <string.h>
+
++#include <archive.h>
++#include <archive_entry.h>
++
+ #include <rpm/rpmcli.h>
+ #include <rpm/rpmstring.h>
+
+@@ -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 <ffesti@redhat.com>
+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 <ffesti@redhat.com>
+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 <pmatilai@redhat.com>
+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 <ffesti@redhat.com>
+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 <ffesti@redhat.com>
+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 <ffesti@redhat.com>
+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 <pmatilai@redhat.com>
+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 <pmatilai@redhat.com>
+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 <mdomonko@redhat.com>
+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 <mdomonko@redhat.com>
+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 <mdomonko@redhat.com>
+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 <mdomonko@redhat.com>
+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 <mdomonko@redhat.com>
+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 <mdomonko@redhat.com> - 4.19.1.1-24
+- Add support for %%autosetup -C (RHEL-141269)
+
* Thu Feb 05 2026 Michal Domonkos <mdomonko@redhat.com> - 4.19.1.1-23
- Fix key import API to return NOTTRUSTED for disabled algorithms (RHEL-112394)
reply other threads:[~2026-06-25 7:43 UTC|newest]
Thread overview: [no followups] expand[flat|nested] mbox.gz Atom feed
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=178237340785.1.5304607219589051778.rpms-python3-rpm-6755696e77a3@fedoraproject.org \
--to=mdomonko@redhat.com \
--cc=git-commits@fedoraproject.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox