[packages/libtar] Rel 3. Fixes: CVE-2013-4420, CVE-2021-33643, CVE-2021-33644, CVE-2021-33645, CVE-2021-33646

arekm arekm at pld-linux.org
Sun Oct 18 02:08:05 CEST 2026


commit 427e8a44503c3eb1356bbcdbf96693f8a807a367
Author: Arkadiusz Miśkiewicz <arekm at maven.pl>
Date:   Sun Oct 18 02:05:46 2026 +0200

    Rel 3. Fixes: CVE-2013-4420, CVE-2021-33643, CVE-2021-33644,
    CVE-2021-33645, CVE-2021-33646

 libtar-CVE-2013-4420.patch        |  47 ++++++++++
 libtar-CVE-2021-33643-33646.patch |  43 +++++++++
 libtar-format.patch               |  12 +++
 libtar-gzip.patch                 | 177 ++++++++++++++++++++++++++++++++++++++
 libtar-nostrip.patch              |  12 +++
 libtar-test.c                     |  16 ++++
 libtar-tests.py                   | 135 +++++++++++++++++++++++++++++
 libtar.spec                       |  41 +++++++--
 8 files changed, 474 insertions(+), 9 deletions(-)
---
diff --git a/libtar.spec b/libtar.spec
index 1e0dc50..c3f52df 100644
--- a/libtar.spec
+++ b/libtar.spec
@@ -1,19 +1,30 @@
+#
+# Conditional build:
+%bcond_without	tests	# regression tests
+
 Summary:	C library for manipulating POSIX tar files
 Summary(pl.UTF-8):	Biblioteka C do manipulacji plikami tar zgodnymi z POSIX
 Name:		libtar
 Version:	1.2.20
-Release:	2
+Release:	3
 License:	BSD
 Group:		Libraries
-#Source0:	ftp://ftp.feep.net/pub/software/libtar/%{name}-%{version}.tar.gz
-Source0:	http://http.debian.net/debian/pool/main/libt/libtar/%{name}_%{version}.orig.tar.gz
+Source0:	https://deb.debian.org/debian/pool/main/libt/libtar/%{name}_%{version}.orig.tar.gz
 # Source0-md5:	6ced95ab3a4b33fbfe2dfb231d156cdb
-# dead currently
-#URL:		http://www.feep.net/libtar/
-URL:		http://repo.or.cz/w/libtar.git/
+Source1:	%{name}-tests.py
+Source2:	%{name}-test.c
+Patch0:		%{name}-format.patch
+Patch1:		%{name}-nostrip.patch
+Patch2:		%{name}-CVE-2013-4420.patch
+Patch3:		%{name}-CVE-2021-33643-33646.patch
+Patch4:		%{name}-gzip.patch
+URL:		https://repo.or.cz/w/libtar.git
 BuildRequires:	autoconf >= 2.61
 BuildRequires:	automake
 BuildRequires:	libtool
+%if %{with tests}
+BuildRequires:	python3
+%endif
 BuildRequires:	zlib-devel
 BuildRoot:	%{tmpdir}/%{name}-%{version}-root-%(id -u -n)
 
@@ -51,6 +62,11 @@ Statyczna biblioteka libtar.
 
 %prep
 %setup -q -n %{name}
+%patch -P0 -p1
+%patch -P1 -p1
+%patch -P2 -p1
+%patch -P3 -p1
+%patch -P4 -p1
 
 %build
 %{__libtoolize}
@@ -58,9 +74,16 @@ Statyczna biblioteka libtar.
 %{__autoconf}
 %{__autoheader}
 %{__automake}
-%configure
+%configure \
+	--with-zlib
 %{__make}
 
+%if %{with tests}
+%{__cc} %{rpmcppflags} %{rpmcflags} -Ilib -Ilisthash \
+	%{SOURCE2} -Llib/.libs -ltar %{rpmldflags} -o libtar-test
+%{__python3} %{SOURCE1} libtar/.libs/libtar lib/.libs ./libtar-test
+%endif
+
 %install
 rm -rf $RPM_BUILD_ROOT
 
@@ -78,11 +101,11 @@ rm -rf $RPM_BUILD_ROOT
 %doc COPYRIGHT ChangeLog ChangeLog-1.0.x README TODO
 %attr(755,root,root) %{_bindir}/libtar
 %attr(755,root,root) %{_libdir}/libtar.so.*.*.*
-%attr(755,root,root) %ghost %{_libdir}/libtar.so.0
+%ghost %{_libdir}/libtar.so.0
 
 %files devel
 %defattr(644,root,root,755)
-%attr(755,root,root) %{_libdir}/libtar.so
+%{_libdir}/libtar.so
 %{_libdir}/libtar.la
 %{_includedir}/libtar.h
 %{_includedir}/libtar_listhash.h
diff --git a/libtar-CVE-2013-4420.patch b/libtar-CVE-2013-4420.patch
new file mode 100644
index 0000000..5840b9e
--- /dev/null
+++ b/libtar-CVE-2013-4420.patch
@@ -0,0 +1,47 @@
+CVE-2013-4420: reject absolute paths and parent components before extraction.
+Reference: https://sources.debian.org/patches/libtar/1.2.20-8/CVE-2013-4420.patch/
+Reject unsafe paths instead of rewriting them; also check link targets and terminal '..'.
+Keep the public TAR layout unchanged; no dependency on Debian's no_static_buffers patch.
+--- a/lib/extract.c
++++ b/lib/extract.c
+@@ -86,6 +86,25 @@
+ }
+ 
+ 
++/* Reject archive paths that can leave the extraction directory. */
++static int
++safe_archive_path(const char *name)
++{
++	const char *end;
++	if (*name == '/')
++		return 0;
++	while (*name)
++	{
++		end = strchr(name, '/');
++		if (end == NULL)
++			end = name + strlen(name);
++		if (end - name == 2 && name[0] == '.' && name[1] == '.')
++			return 0;
++		name = *end ? end + 1 : end;
++	}
++	return 1;
++}
++
+ /* switchboard */
+ int
+ tar_extract_file(TAR *t, char *realname)
+@@ -94,6 +113,14 @@
+ 	char *lnp;
+ 	int pathname_len;
+ 	int realname_len;
++
++	if (!safe_archive_path(th_get_pathname(t)) ||
++	    ((TH_ISLNK(t) || TH_ISSYM(t)) &&
++	     !safe_archive_path(th_get_linkname(t))))
++	{
++		errno = EINVAL;
++		return -1;
++	}
+ 
+ 	if (t->options & TAR_NOOVERWRITE)
+ 	{
diff --git a/libtar-CVE-2021-33643-33646.patch b/libtar-CVE-2021-33643-33646.patch
new file mode 100644
index 0000000..0ca93bf
--- /dev/null
+++ b/libtar-CVE-2021-33643-33646.patch
@@ -0,0 +1,43 @@
+CVE-2021-33643/33644: reject empty GNU extensions; CVE-2021-33645/33646: free buffers on close.
+Sources: https://github.com/openembedded/meta-openembedded/tree/7eed950126b69e6ea04eea2cc6f900a4d17496ef/meta-oe/recipes-support/libtar/files
+Adapted from CVE-2021-33643-CVE-2021-33644.patch and
+CVE-2021-33640-CVE-2021-33645-CVE-2021-33646.patch (Fedora).
+Keep ownership in tar_close, avoiding the openEuler use-after-free (CVE-2021-33640).
+No th_pathname member is added to the public TAR structure.
+--- a/lib/block.c
++++ b/lib/block.c
+@@ -118,6 +118,11 @@
+ 	if (TH_ISLONGLINK(t))
+ 	{
+ 		sz = th_get_size(t);
++		if ((int)sz <= 0)
++		{
++			errno = EINVAL;
++			return -1;
++		}
+ 		blocks = (sz / T_BLOCKSIZE) + (sz % T_BLOCKSIZE ? 1 : 0);
+ 		if (blocks > ((size_t)-1 / T_BLOCKSIZE))
+ 		{
+@@ -168,6 +173,11 @@
+ 	if (TH_ISLONGNAME(t))
+ 	{
+ 		sz = th_get_size(t);
++		if ((int)sz <= 0)
++		{
++			errno = EINVAL;
++			return -1;
++		}
+ 		blocks = (sz / T_BLOCKSIZE) + (sz % T_BLOCKSIZE ? 1 : 0);
+ 		if (blocks > ((size_t)-1 / T_BLOCKSIZE))
+ 		{
+--- a/lib/handle.c
++++ b/lib/handle.c
+@@ -121,6 +121,8 @@
+ 		libtar_hash_free(t->h, ((t->oflags & O_ACCMODE) == O_RDONLY
+ 					? free
+ 					: (libtar_freefunc_t)tar_dev_free));
++	free(t->th_buf.gnu_longname);
++	free(t->th_buf.gnu_longlink);
+ 	free(t);
+ 
+ 	return i;
diff --git a/libtar-format.patch b/libtar-format.patch
new file mode 100644
index 0000000..6a09d55
--- /dev/null
+++ b/libtar-format.patch
@@ -0,0 +1,12 @@
+snprintf's dynamic field width takes int, not size_t.
+--- libtar/lib/util.c.orig
++++ libtar/lib/util.c
+@@ -158,7 +158,7 @@
+ void
+ int_to_oct_nonull(int num, char *oct, size_t octlen)
+ {
+-	snprintf(oct, octlen, "%*lo", octlen - 1, (unsigned long)num);
++	snprintf(oct, octlen, "%*lo", (int)octlen - 1, (unsigned long)num);
+ 	oct[octlen - 1] = ' ';
+ }
+ 
diff --git a/libtar-gzip.patch b/libtar-gzip.patch
new file mode 100644
index 0000000..61ef3f7
--- /dev/null
+++ b/libtar-gzip.patch
@@ -0,0 +1,177 @@
+Keep gzip pointers out of integer fd callbacks; propagate archive read errors.
+Reproducer: MALLOC_MMAP_THRESHOLD_=0 libtar -z -t archive.tar.gz crashes on LP64.
+The CLI owns one gzip stream; the library callback ABI remains unchanged.
+--- a/libtar/libtar.c
++++ b/libtar/libtar.c
+@@ -18,6 +18,8 @@
+ #include <errno.h>
+ #include <sys/param.h>
+ #include <stdlib.h>
++#include <stdarg.h>
++#include <limits.h>
+ 
+ #ifdef STDC_HEADERS
+ # include <string.h>
+@@ -57,12 +59,28 @@
+ 
+ int use_zlib = 0;
+ 
+-int
+-gzopen_frontend(char *pathname, int oflags, int mode)
++/* The command processes one archive at a time; callbacks still take real fds. */
++static gzFile gzip_stream;
++static int gzip_fd = -1;
++
++static int
++gzopen_frontend(const char *pathname, int oflags, ...)
+ {
+ 	char *gzoflags;
+-	gzFile gzf;
+-	int fd;
++	int fd, mode = 0;
++	va_list ap;
++
++	if (gzip_stream != NULL)
++	{
++		errno = EBUSY;
++		return -1;
++	}
++	if (oflags & O_CREAT)
++	{
++		va_start(ap, oflags);
++		mode = va_arg(ap, int);
++		va_end(ap);
++	}
+ 
+ 	switch (oflags & O_ACCMODE)
+ 	{
+@@ -83,24 +101,81 @@
+ 		return -1;
+ 
+ 	if ((oflags & O_CREAT) && fchmod(fd, mode))
+-		return -1;
+-
+-	gzf = gzdopen(fd, gzoflags);
+-	if (!gzf)
+-	{
++	{
++		int saved_errno = errno;
++		close(fd);
++		errno = saved_errno;
++		return -1;
++	}
++
++	gzip_stream = gzdopen(fd, gzoflags);
++	if (!gzip_stream)
++	{
++		close(fd);
+ 		errno = ENOMEM;
+ 		return -1;
+ 	}
+ 
+-	/* This is a bad thing to do on big-endian lp64 systems, where the
+-	   size and placement of integers is different than pointers.
+-	   However, to fix the problem 4 wrapper functions would be needed and
+-	   an extra bit of data associating GZF with the wrapper functions.  */
+-	return (int)gzf;
+-}
+-
+-tartype_t gztype = { (openfunc_t) gzopen_frontend, (closefunc_t) gzclose,
+-	(readfunc_t) gzread, (writefunc_t) gzwrite
++	gzip_fd = fd;
++	return fd;
++}
++
++static int
++gzclose_frontend(int fd)
++{
++	int result;
++	if (fd != gzip_fd || gzip_stream == NULL)
++	{
++		errno = EBADF;
++		return -1;
++	}
++	result = gzclose(gzip_stream);
++	gzip_stream = NULL;
++	gzip_fd = -1;
++	if (result == Z_OK)
++		return 0;
++	if (result != Z_ERRNO)
++		errno = EIO;
++	return -1;
++}
++
++static ssize_t
++gzread_frontend(int fd, void *buf, size_t count)
++{
++	int result;
++	if (fd != gzip_fd || gzip_stream == NULL)
++	{
++		errno = EBADF;
++		return -1;
++	}
++	result = gzread(gzip_stream, buf, count > INT_MAX ? INT_MAX : count);
++	if (result < 0)
++		errno = EIO;
++	return result;
++}
++
++static ssize_t
++gzwrite_frontend(int fd, const void *buf, size_t count)
++{
++	int result;
++	if (fd != gzip_fd || gzip_stream == NULL)
++	{
++		errno = EBADF;
++		return -1;
++	}
++	if (count == 0)
++		return 0;
++	result = gzwrite(gzip_stream, buf, count > INT_MAX ? INT_MAX : count);
++	if (result == 0)
++	{
++		errno = EIO;
++		return -1;
++	}
++	return result;
++}
++
++tartype_t gztype = { gzopen_frontend, gzclose_frontend,
++	gzread_frontend, gzwrite_frontend
+ };
+ 
+ #endif /* HAVE_LIBZ */
+@@ -193,6 +268,7 @@
+ 		{
+ 			fprintf(stderr, "tar_skip_regfile(): %s\n",
+ 				strerror(errno));
++			tar_close(t);
+ 			return -1;
+ 		}
+ 	}
+@@ -202,7 +278,7 @@
+ 	printf("EOF mark encountered after %ld bytes\n",
+ # ifdef HAVE_LIBZ
+ 	       (use_zlib
+-		? gzseek((gzFile) t->fd, 0, SEEK_CUR)
++		? gzseek(gzip_stream, 0, SEEK_CUR)
+ 		:
+ # endif
+ 	       lseek(t->fd, 0, SEEK_CUR)
+@@ -218,7 +294,7 @@
+ 		return -1;
+ 	}
+ 
+-	return 0;
++	return (i == 1 ? 0 : -1);
+ }
+ 
+ 
+@@ -250,6 +326,7 @@
+ 	if (tar_extract_all(t, rootdir) != 0)
+ 	{
+ 		fprintf(stderr, "tar_extract_all(): %s\n", strerror(errno));
++		tar_close(t);
+ 		return -1;
+ 	}
+ 
diff --git a/libtar-nostrip.patch b/libtar-nostrip.patch
new file mode 100644
index 0000000..cb41e94
--- /dev/null
+++ b/libtar-nostrip.patch
@@ -0,0 +1,12 @@
+Leave executable debug information for RPM's debuginfo extraction.
+--- libtar/libtar/Makefile.in.orig
++++ libtar/libtar/Makefile.in
+@@ -22,7 +22,7 @@
+ 
+ ### Installation programs and flags
+ INSTALL		= @INSTALL@
+-INSTALL_PROGRAM	= @INSTALL_PROGRAM@ -s
++INSTALL_PROGRAM	= @INSTALL_PROGRAM@
+ INSTALL_DATA	= @INSTALL_DATA@
+ LN_S		= @LN_S@
+ MKDIR		= @MKDIR@
diff --git a/libtar-test.c b/libtar-test.c
new file mode 100644
index 0000000..6c7cc9c
--- /dev/null
+++ b/libtar-test.c
@@ -0,0 +1,16 @@
+/* Read one header and close, including incomplete GNU extension error paths. */
+#include <libtar.h>
+#include <fcntl.h>
+#include <stdlib.h>
+
+int main(int argc, char **argv)
+{
+	TAR *t;
+	int status;
+	if (argc != 3 || tar_open(&t, argv[1], NULL, O_RDONLY, 0, 0) != 0)
+		return 2;
+	status = th_read(t);
+	if (tar_close(t) != 0)
+		return 3;
+	return status != atoi(argv[2]);
+}
diff --git a/libtar-tests.py b/libtar-tests.py
new file mode 100644
index 0000000..51ef666
--- /dev/null
+++ b/libtar-tests.py
@@ -0,0 +1,135 @@
+#!/usr/bin/python3
+"""Regression inputs for the PLD libtar patches; no network or system install."""
+import ctypes
+import os
+from pathlib import Path
+import resource
+import subprocess
+import sys
+import tarfile
+import tempfile
+
+exe, libdir, probe = map(lambda x: str(Path(x).resolve()), sys.argv[1:])
+env = dict(os.environ, LD_LIBRARY_PATH=libdir, MALLOC_MMAP_THRESHOLD_='0')
+resource.setrlimit(resource.RLIMIT_CORE, (0, 0))
+failures = []
+
+
+def run(args, ok=True):
+    p = subprocess.run(args, env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+    if (p.returncode == 0) != ok or p.returncode < 0:
+        raise AssertionError((args, p.returncode, p.stderr.decode(errors='replace')))
+    return p
+
+
+def header(name, kind=tarfile.REGTYPE, size=0, link=''):
+    h = tarfile.TarInfo(name)
+    h.type, h.size, h.linkname, h.mode = kind, size, link, 0o644
+    return h.tobuf(format=tarfile.USTAR_FORMAT)
+
+
+def check(label, func):
+    try:
+        func()
+        print('PASS:', label, flush=True)
+    except Exception as e:
+        failures.append(label)
+        print('FAIL:', label, repr(e), flush=True)
+
+
+def read_one(path, expected):
+    args = [probe, str(path), str(expected)]
+    if os.environ.get('LIBTAR_VALGRIND'):
+        args = ['valgrind', '--quiet', '--leak-check=full',
+                '--errors-for-leak-kinds=definite,indirect',
+                '--error-exitcode=99'] + args
+    run(args)
+
+
+with tempfile.TemporaryDirectory(prefix='libtar-tests-') as tmp:
+    os.chdir(tmp)
+    p = Path(tmp)
+
+    def roundtrip(compressed):
+        tag = 'gzip' if compressed else 'plain'
+        src = p / ('in-' + tag)
+        src.mkdir()
+        (src / 'file').write_bytes(b'libtar roundtrip\n' * 100)
+        (src / 'symlink').symlink_to('file')
+        os.link(src / 'file', src / 'hardlink')
+        archive = p / (tag + '.tar')
+        flags = ['-z'] if compressed else []
+        run([exe, *flags, '-C', str(src), '-c', str(archive), '.'])
+        run([exe, *flags, '-t', str(archive)])
+        dest = p / ('out-' + tag)
+        dest.mkdir()
+        run([exe, *flags, '-C', str(dest), '-x', str(archive)])
+        assert (dest / 'file').read_bytes() == (src / 'file').read_bytes()
+        assert (dest / 'symlink').is_symlink()
+        assert (dest / 'file').stat().st_ino == (dest / 'hardlink').stat().st_ino
+
+    check('plain archive create/list/extract with links', lambda: roundtrip(False))
+    check('gzip LP64 create/list/extract with mmap allocations', lambda: roundtrip(True))
+
+    for kind in (tarfile.GNUTYPE_LONGNAME, tarfile.GNUTYPE_LONGLINK):
+        label = kind.decode()
+        for size in (0, 0xffffffff):
+            path = p / ('bad-' + label + '-' + str(size) + '.tar')
+            path.write_bytes(header('././@LongLink', kind, size) + header('normal') + bytes(1024))
+            check('reject GNU %s size %d' % (label, size), lambda path=path: read_one(path, -1))
+        for truncated in (False, True):
+            path = p / ('long-' + label + str(truncated) + '.tar')
+            data = b'x' * 160 + b'\0'
+            path.write_bytes(header('././@LongLink', kind, len(data)) +
+                             (b'' if truncated else data.ljust(512, b'\0') + header('normal')))
+            check('close GNU %s truncated=%s' % (label, truncated),
+                  lambda path=path, truncated=truncated: read_one(path, -1 if truncated else 0))
+
+    def traversal(name, kind, link, number):
+        archive = p / ('traversal-%d.tar' % number)
+        archive.write_bytes(header(name, kind, 1 if kind == tarfile.REGTYPE else 0, link) +
+                            (b'X' + bytes(511) if kind == tarfile.REGTYPE else b'') + bytes(1024))
+        case = p / ('case-%d' % number)
+        case.mkdir()
+        dest = case / 'out'
+        dest.mkdir()
+        outside = case / 'escape'
+        outside.write_bytes(b'untouched')
+        # Relative hardlink targets are interpreted against the working directory.
+        old_cwd = Path.cwd()
+        try:
+            os.chdir(dest)
+            run([exe, '-x', str(archive)], ok=False)
+        finally:
+            os.chdir(old_cwd)
+        assert not list(dest.iterdir())
+        assert outside.read_bytes() == b'untouched'
+
+    attacks = [('../escape', tarfile.REGTYPE, ''),
+               ('a/../../escape', tarfile.REGTYPE, ''),
+               (str(p / 'escape'), tarfile.REGTYPE, ''),
+               ('link', tarfile.SYMTYPE, '..'),
+               ('link', tarfile.SYMTYPE, '../escape'),
+               ('link', tarfile.LNKTYPE, '../escape')]
+    for i, args in enumerate(attacks):
+        check('CVE-2013-4420 case %d' % i, lambda args=args, i=i: traversal(*args, i))
+
+    def malformed_listing():
+        path = p / 'broken.tar'
+        path.write_bytes(b'x' * 512)
+        run([exe, '-t', str(path)], ok=False)
+    check('CLI propagates malformed archive error', malformed_listing)
+
+    def octal():
+        lib = ctypes.CDLL(str(Path(libdir) / 'libtar.so.0.0.0'))
+        f = lib.int_to_oct_nonull
+        f.argtypes = [ctypes.c_int, ctypes.c_void_p, ctypes.c_size_t]
+        f.restype = None
+        for n in (0, 83, 4095):
+            out = ctypes.create_string_buffer(8)
+            f(n, out, 8)
+            assert out.raw == (format(n, 'o').rjust(7) + ' ').encode()
+    check('octal header formatting', octal)
+
+if failures:
+    sys.exit('Failed: ' + ', '.join(failures))
================================================================

---- gitweb:

http://git.pld-linux.org/gitweb.cgi/packages/libtar.git/commitdiff/427e8a44503c3eb1356bbcdbf96693f8a807a367



More information about the pld-cvs-commit mailing list