[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