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