mirror of
https://git.busybox.net/busybox
synced 2026-02-15 14:06:55 +00:00
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>
232 lines
5.6 KiB
C
232 lines
5.6 KiB
C
/*
|
|
* 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;
|
|
}
|