File 0001-tar-strip-unsafe-hardlink-components-GNU-tar-does-th.patch of Package busybox.43070

From a610654a442738de577e0f874060a98f6b575598 Mon Sep 17 00:00:00 2001
From: Denys Vlasenko <vda.linux@googlemail.com>
Date: Thu, 29 Jan 2026 11:48:02 +0100
Subject: [PATCH] tar: strip unsafe hardlink components - GNU tar does the same

Defends against files like these (python reproducer):

import tarfile
ti = tarfile.TarInfo("leak_hosts")
ti.type = tarfile.LNKTYPE
ti.linkname = "/etc/hosts"  # or "../etc/hosts" or ".."
ti.size = 0
with tarfile.open("/tmp/hardlink.tar", "w") as t:
	t.addfile(ti)

function                                             old     new   delta
skip_unsafe_prefix                                     -     127    +127
get_header_tar                                      1752    1754      +2
.rodata                                           106861  106856      -5
unzip_main                                          2715    2706      -9
strip_unsafe_prefix                                  102      18     -84
------------------------------------------------------------------------------
(add/remove: 1/0 grow/shrink: 1/3 up/down: 129/-98)            Total: 31 bytes

Signed-off-by: Denys Vlasenko <vda.linux@googlemail.com>
(cherry picked from commit 3fb6b31c716669e12f75a2accd31bb7685b1a1cb)
---
 archival/libarchive/data_extract_all.c      |   3 +-
 archival/libarchive/get_header_tar.c        |  11 +-
 archival/libarchive/unsafe_prefix.c         |  30 ++-
 archival/libarchive/unsafe_symlink_target.c |   1 +
 archival/tar.c                              |   2 +-
 archival/unzip.c                            |   3 +-
 include/bb_archive.h                        |   3 +-
 networking/httpd_ratelimit_cgi.c            | 232 ++++++++++++++++++++
 8 files changed, 273 insertions(+), 12 deletions(-)
 create mode 100644 networking/httpd_ratelimit_cgi.c

diff --git a/archival/libarchive/data_extract_all.c b/archival/libarchive/data_extract_all.c
index 049c2c156..0e57a7b03 100644
--- a/archival/libarchive/data_extract_all.c
+++ b/archival/libarchive/data_extract_all.c
@@ -177,8 +177,7 @@ void FAST_FUNC data_extract_all(archive_handle_t *archive_handle)
 
 		/* To avoid a directory traversal attack via symlinks,
 		 * do not restore symlinks with ".." components
-		 * or symlinks starting with "/", unless a magic
-		 * envvar is set.
+		 * or symlinks starting with "/"
 		 *
 		 * For example, consider a .tar created via:
 		 *  $ tar cvf bug.tar anything.txt
diff --git a/archival/libarchive/get_header_tar.c b/archival/libarchive/get_header_tar.c
index d26868bf8..dc0f7e038 100644
--- a/archival/libarchive/get_header_tar.c
+++ b/archival/libarchive/get_header_tar.c
@@ -452,8 +452,15 @@ char FAST_FUNC get_header_tar(archive_handle_t *archive_handle)
 #endif
 
 	/* Everything up to and including last ".." component is stripped */
-	overlapping_strcpy(file_header->name, strip_unsafe_prefix(file_header->name));
-//TODO: do the same for file_header->link_target?
+	strip_unsafe_prefix(file_header->name);
+	if (file_header->link_target) {
+		/* GNU tar 1.34 examples:
+		 * tar: Removing leading '/' from hard link targets
+		 * tar: Removing leading '../' from hard link targets
+		 * tar: Removing leading 'etc/../' from hard link targets
+		 */
+		strip_unsafe_prefix(file_header->link_target);
+	}
 
 	/* Strip trailing '/' in directories */
 	/* Must be done after mode is set as '/' is used to check if it's a directory */
diff --git a/archival/libarchive/unsafe_prefix.c b/archival/libarchive/unsafe_prefix.c
index 33e487bf9..62f676ea6 100644
--- a/archival/libarchive/unsafe_prefix.c
+++ b/archival/libarchive/unsafe_prefix.c
@@ -5,11 +5,11 @@
 #include "libbb.h"
 #include "bb_archive.h"
 
-const char* FAST_FUNC strip_unsafe_prefix(const char *str)
+const char* FAST_FUNC skip_unsafe_prefix(const char *str)
 {
 	const char *cp = str;
 	while (1) {
-		char *cp2;
+		const char *cp2;
 		if (*cp == '/') {
 			cp++;
 			continue;
@@ -18,10 +18,25 @@ const char* FAST_FUNC strip_unsafe_prefix(const char *str)
 			cp += 3;
 			continue;
 		}
-		cp2 = strstr(cp, "/../");
+		cp2 = cp;
+ find_dotdot:
+		cp2 = strstr(cp2, "/..");
 		if (!cp2)
-			break;
-		cp = cp2 + 4;
+			break; /* No (more) malicious components */
+
+		/* We found "/..something" */
+		cp2 += 3;
+		if (*cp2 != '/') {
+			if (*cp2 == '\0') {
+				/* Trailing "/..": malicious, return "" */
+				/* (causes harmless errors trying to create or hardlink a file named "") */
+				return cp2;
+			}
+			/* "/..name" is not malicious, look for next "/.." */
+			goto find_dotdot;
+		}
+		/* Found "/../": malicious, advance past it */
+		cp = cp2 + 1;
 	}
 	if (cp != str) {
 		static smallint warned = 0;
@@ -33,3 +48,8 @@ const char* FAST_FUNC strip_unsafe_prefix(const char *str)
 	}
 	return cp;
 }
+
+void FAST_FUNC strip_unsafe_prefix(char *str)
+{
+	overlapping_strcpy(str, skip_unsafe_prefix(str));
+}
diff --git a/archival/libarchive/unsafe_symlink_target.c b/archival/libarchive/unsafe_symlink_target.c
index f8dc8033d..d764c89ab 100644
--- a/archival/libarchive/unsafe_symlink_target.c
+++ b/archival/libarchive/unsafe_symlink_target.c
@@ -36,6 +36,7 @@ void FAST_FUNC create_links_from_list(llist_t *list)
 				*list->data ? "hard" : "sym",
 				list->data + 1, target
 			);
+			/* Note: GNU tar 1.34 errors out only _after_ all links are (attempted to be) created */
 		}
 		list = list->link;
 	}
diff --git a/archival/tar.c b/archival/tar.c
index 9de37592e..cf8c2d1f6 100644
--- a/archival/tar.c
+++ b/archival/tar.c
@@ -475,7 +475,7 @@ static int FAST_FUNC writeFileToTarball(struct recursive_state *state,
 	DBG("writeFileToTarball('%s')", fileName);
 
 	/* Strip leading '/' and such (must be before memorizing hardlink's name) */
-	header_name = strip_unsafe_prefix(fileName);
+	header_name = skip_unsafe_prefix(fileName);
 
 	if (header_name[0] == '\0')
 		return TRUE;
diff --git a/archival/unzip.c b/archival/unzip.c
index fc92ac661..d36b531ee 100644
--- a/archival/unzip.c
+++ b/archival/unzip.c
@@ -842,7 +842,8 @@ int unzip_main(int argc, char **argv)
 		unzip_skip(zip.fmt.extra_len);
 
 		/* Guard against "/abspath", "/../" and similar attacks */
-		overlapping_strcpy(dst_fn, strip_unsafe_prefix(dst_fn));
+// NB: UnZip 6.00 has option -: to disable this
+		strip_unsafe_prefix(dst_fn);
 
 		/* Filter zip entries */
 		if (find_list_entry(zreject, dst_fn)
diff --git a/include/bb_archive.h b/include/bb_archive.h
index e0ef8fc4e..1dc77f31d 100644
--- a/include/bb_archive.h
+++ b/include/bb_archive.h
@@ -202,7 +202,8 @@ char get_header_tar_xz(archive_handle_t *archive_handle) FAST_FUNC;
 void seek_by_jump(int fd, off_t amount) FAST_FUNC;
 void seek_by_read(int fd, off_t amount) FAST_FUNC;
 
-const char *strip_unsafe_prefix(const char *str) FAST_FUNC;
+const char *skip_unsafe_prefix(const char *str) FAST_FUNC;
+void strip_unsafe_prefix(char *str) FAST_FUNC;
 void create_or_remember_link(llist_t **link_placeholders,
 		const char *target,
 		const char *linkname,
diff --git a/networking/httpd_ratelimit_cgi.c b/networking/httpd_ratelimit_cgi.c
new file mode 100644
index 000000000..a254e6c6b
--- /dev/null
+++ b/networking/httpd_ratelimit_cgi.c
@@ -0,0 +1,232 @@
+/*
+ * Copyright (c) 2026 Denys Vlasenko <vda.linux@googlemail.com>
+ *
+ * Licensed under GPLv2, see file LICENSE in this source tree.
+ */
+
+/*
+ * This program is a CGI application. It is intended to rate-limit
+ * invocations of another, presumably resource-intensive CGI
+ * which you want to only allow less than N instances at any one time.
+ *
+ * Any extra clients who try to run the CGI will get the
+ * "429 Too Many Requests" HTTP response.
+ *
+ * The most efficient way to do so is to use a shebang-style executable file:
+ * #!/path/to/httpd_ratelimit_cgi /tmp/lockdir 99 /path/to/expensive_cgi
+ */
+
+/* Build a-la
+i486-linux-uclibc-gcc \
+-static -static-libgcc \
+-D_LARGEFILE_SOURCE -D_LARGEFILE64_SOURCE -D_FILE_OFFSET_BITS=64 \
+-Wall -Wshadow -Wwrite-strings -Wundef -Wstrict-prototypes -Werror \
+-Wold-style-definition -Wdeclaration-after-statement -Wno-pointer-sign \
+-Wmissing-prototypes -Wmissing-declarations \
+-Os -fno-builtin-strlen -finline-limit=0 -fomit-frame-pointer \
+-ffunction-sections -fdata-sections -fno-guess-branch-probability \
+-funsigned-char \
+-falign-functions=1 -falign-jumps=1 -falign-labels=1 -falign-loops=1 \
+-march=i386 -mpreferred-stack-boundary=2 \
+-Wl,-Map -Wl,link.map -Wl,--warn-common -Wl,--sort-common -Wl,--gc-sections \
+httpd_ratelimit_cgi.c -o httpd_ratelimit_cgi
+*/
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <errno.h>
+#include <signal.h>
+#include <sys/stat.h> /* mkdir */
+#include <limits.h>
+
+static void full_write(int fd, const void *buf, size_t len)
+{
+	ssize_t cc;
+
+	while (len) {
+		cc = write(fd, buf, len);
+
+		if (cc < 0)
+			return;
+		buf = ((const char *)buf) + cc;
+		len -= cc;
+	}
+}
+
+static void full_write2(int fd, const char *msg, const char *msg2)
+{
+	full_write(fd, msg, strlen(msg));
+	full_write(fd, " '", 2);
+	full_write(fd, msg2, strlen(msg2));
+	full_write(fd, "'\n", 2);
+}
+
+static void write_and_die(int fd, const char *msg)
+{
+	full_write(fd, msg, strlen(msg));
+	exit(0);
+}
+
+static void write_and_die2(int fd, const char *msg, const char *msg2)
+{
+	full_write2(fd, msg, msg2);
+	exit(0);
+}
+
+static void fmt_ul(char *dst, unsigned long n)
+{
+	char buf[sizeof(n)*3 + 2];
+	char *p;
+
+	p = buf + sizeof(buf) - 1;
+	*p = '\0';
+	do {
+		*--p = (n % 10) + '0';
+		n /= 10;
+	} while (n);
+	strcpy(dst, p);
+}
+
+static long get_no(const char *s)
+{
+	const char *start = s;
+	long v = 0;
+	while (*s >= '0' && *s <= '9')
+		v = v * 10 + (*s++ - '0');
+	if (start == s || *s != '\0' /*|| v < 0*/)
+		return -1;
+	return v;
+}
+
+int main(int argc, char **argv)
+{
+	const char *lock_dir = ".";
+	unsigned long max_slots;
+	char *sp;
+	char *symno;
+	unsigned slot_num;
+	pid_t my_pid;
+	char my_pid_str[sizeof(long)*3];
+
+	argv++;
+	if (!argv[0] || !argv[1])
+		write_and_die(2, "Usage: ratelimit [LOCKDIR] MAX_PROCS PROG [ARGS]\n");
+
+	/* ratelimit "[LOCKDIR] MAX_PROCS PROG" SHEBANG [ARGS] syntax?
+	 * This happens if we are running as shebang file
+	 * of the form "!#/path/to/ratelimit [/tmp/cgit] 10 CGI_BINARY"
+	 * (in this case argv[1] is the shebang's filename) */
+	sp = strchr(argv[0], ' ');
+	if (sp) {
+		*sp++ = '\0';
+		/* convert to ratelimit "SOME\0THING" SHEBANG [ARGS] form */
+		/*                       argv1 ^ */
+		argv[1] = sp;
+		sp = strchr(sp, ' ');
+		if (sp) { /* "THING" also has a space? There is a LOCKDIR! */
+			*sp++ = '\0';
+			/* convert to ratelimit "SOME\0THI\0G" SHEBANG [ARGS] form */
+			/*                        argv0^    ^argv1 */
+			lock_dir = argv[0];
+			argv[0] = argv[1];
+			argv[1] = sp;
+			goto get_max;
+		}
+	}
+
+	max_slots = get_no(argv[0]);
+	if (max_slots > 9999) {
+		/* ratelimit LOCKDIR MAX_PROCS PROG [ARGS] */
+		lock_dir = argv[0];
+		if (!lock_dir[0])
+			write_and_die2(2, "Bad LOCKDIR", argv[0]);
+		argv++;
+ get_max:
+		max_slots = get_no(argv[0]);
+		if (max_slots > 9999)
+			write_and_die2(2, "Bad MAX_PROCS", argv[0]);
+	}
+	argv++; /* points to PROG [ARGS] */
+
+    {
+	char slot_path[strlen(lock_dir) + 16];
+	symno = stpcpy(stpcpy(slot_path, lock_dir), "/lock.");
+
+	my_pid = getpid();
+	fmt_ul(my_pid_str, my_pid);
+
+	/* Ensure lock directory exists (idempotent, ignores errors) */
+	if (lock_dir[0] != '.' || lock_dir[1]) /* Don't bother with "." */
+		mkdir(lock_dir, 0755);
+
+	/* Starting slot varies per process */
+	slot_num = my_pid;
+
+	/* max_slots = 0 is allowed for testing */
+	if (max_slots != 0) for (int i = 0; i < max_slots; i++) {
+		slot_num = (slot_num + 1) % max_slots;
+		fmt_ul(symno, slot_num);
+
+		while (1) {
+			char buf[32];
+			ssize_t len;
+			long old_pid;
+
+			/* Try to claim atomically */
+			if (symlink(my_pid_str, slot_path) == 0)
+				goto exec;
+
+			/* Only handle EEXIST - other errors skip to next slot */
+			if (errno != EEXIST)
+				break;
+
+			/* Read existing target PID */
+			len = readlink(slot_path, buf, sizeof(buf) - 1);
+			if (len < 1) {
+				/* Broken/empty - clean up and retry */
+				unlink(slot_path);
+				continue;
+			}
+			buf[len] = '\0';
+
+			/* Parse PID */
+			old_pid = get_no(buf);
+			if (old_pid <= 0 || old_pid > INT_MAX) {
+				/* Invalid PID string - clean up and retry */
+				unlink(slot_path);
+				continue;
+			}
+
+			/* Check if old process is alive */
+			if (kill(old_pid, 0) == 0 || errno != ESRCH) {
+				/* Alive (or unexpected error): slot in use, try next */
+				break;
+			}
+
+			/* Dead: clean up and retry this slot */
+			unlink(slot_path);
+			/* Loop continues to retry symlink() */
+		}
+	}
+
+	/* No slot available, return 429 */
+	write_and_die(1, "Status: 429 Too Many Requests\r\n"
+	"Content-Type: text/plain\r\n"
+	"Retry-After: 60\r\n"
+	"Connection: close\r\n"
+	"\r\n"
+	"Too many concurrent requests\n"
+	);
+	return 0;
+    }
+
+exec:
+	execv(argv[0], argv);
+	full_write2(2, "can't execute", argv[0]);
+	write_and_die(1, "Status: 500 Internal Server Error\r\n"
+	"Content-Type: text/plain\r\n"
+	"\r\n"
+	"Can't execute binary\n"
+	);
+	return 1;
+}
-- 
2.52.0

openSUSE Build Service is sponsored by