Revision 7b70e9efb18c2cc3f219af399bd384c5801ba1d7 authored by Jeff King on 16 April 2024, 08:35:33 UTC, committed by Johannes Schindelin on 17 April 2024, 20:29:56 UTC
The upload-pack command tries to avoid trusting the repository in which it's run (e.g., by not running any hooks and not using any config that contains arbitrary commands). But if the server side of a fetch or a clone is a partial clone, then either upload-pack or its child pack-objects may run a lazy "git fetch" under the hood. And it is very easy to convince fetch to run arbitrary commands. The "server" side can be a local repository owned by someone else, who would be able to configure commands that are run during a clone with the current user's permissions. This issue has been designated CVE-2024-32004. The fix in this commit's parent helps in this scenario, as well as in related scenarios using SSH to clone, where the untrusted .git directory is owned by a different user id. But if you received one as a zip file, on a USB stick, etc, it may be owned by your user but still untrusted. This has been designated CVE-2024-32465. To mitigate the issue more completely, let's disable lazy fetching entirely during `upload-pack`. While fetching from a partial repository should be relatively rare, it is certainly not an unreasonable workflow. And thus we need to provide an escape hatch. This commit works by respecting a GIT_NO_LAZY_FETCH environment variable (to skip the lazy-fetch), and setting it in upload-pack, but only when the user has not already done so (which gives us the escape hatch). The name of the variable is specifically chosen to match what has already been added in 'master' via e6d5479e7a (git: extend --no-lazy-fetch to work across subprocesses, 2024-02-27). Since we're building this fix as a backport for older versions, we could cherry-pick that patch and its earlier steps. However, we don't really need the niceties (like a "--no-lazy-fetch" option) that it offers. By using the same name, everything should just work when the two are eventually merged, but here are a few notes: - the blocking of the fetch in e6d5479e7a is incomplete! It sets fetch_if_missing to 0 when we setup the repository variable, but that isn't enough. pack-objects in particular will call prefetch_to_pack() even if that variable is 0. This patch by contrast checks the environment variable at the lowest level before we call the lazy fetch, where we can be sure to catch all code paths. Possibly the setting of fetch_if_missing from e6d5479e7a can be reverted, but it may be useful to have. For example, some code may want to use that flag to change behavior before it gets to the point of trying to start the fetch. At any rate, that's all outside the scope of this patch. - there's documentation for GIT_NO_LAZY_FETCH in e6d5479e7a. We can live without that here, because for the most part the user shouldn't need to set it themselves. The exception is if they do want to override upload-pack's default, and that requires a separate documentation section (which is added here) - it would be nice to use the NO_LAZY_FETCH_ENVIRONMENT macro added by e6d5479e7a, but those definitions have moved from cache.h to environment.h between 2.39.3 and master. I just used the raw string literals, and we can replace them with the macro once this topic is merged to master. At least with respect to CVE-2024-32004, this does render this commit's parent commit somewhat redundant. However, it is worth retaining that commit as defense in depth, and because it may help other issues (e.g., symlink/hardlink TOCTOU races, where zip files are not really an interesting attack vector). The tests in t0411 still pass, but now we have _two_ mechanisms ensuring that the evil command is not run. Let's beef up the existing ones to check that they failed for the expected reason, that we refused to run upload-pack at all with an alternate user id. And add two new ones for the same-user case that both the restriction and its escape hatch. Signed-off-by: Jeff King <peff@peff.net> Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
1 parent f4aa8c8
wrapper.c
/*
* Various trivial helper wrappers around standard functions
*/
#include "cache.h"
#include "config.h"
static intmax_t count_fsync_writeout_only;
static intmax_t count_fsync_hardware_flush;
#ifdef HAVE_RTLGENRANDOM
/* This is required to get access to RtlGenRandom. */
#define SystemFunction036 NTAPI SystemFunction036
#include <NTSecAPI.h>
#undef SystemFunction036
#endif
static int memory_limit_check(size_t size, int gentle)
{
static size_t limit = 0;
if (!limit) {
limit = git_env_ulong("GIT_ALLOC_LIMIT", 0);
if (!limit)
limit = SIZE_MAX;
}
if (size > limit) {
if (gentle) {
error("attempting to allocate %"PRIuMAX" over limit %"PRIuMAX,
(uintmax_t)size, (uintmax_t)limit);
return -1;
} else
die("attempting to allocate %"PRIuMAX" over limit %"PRIuMAX,
(uintmax_t)size, (uintmax_t)limit);
}
return 0;
}
char *xstrdup(const char *str)
{
char *ret = strdup(str);
if (!ret)
die("Out of memory, strdup failed");
return ret;
}
static void *do_xmalloc(size_t size, int gentle)
{
void *ret;
if (memory_limit_check(size, gentle))
return NULL;
ret = malloc(size);
if (!ret && !size)
ret = malloc(1);
if (!ret) {
if (!gentle)
die("Out of memory, malloc failed (tried to allocate %lu bytes)",
(unsigned long)size);
else {
error("Out of memory, malloc failed (tried to allocate %lu bytes)",
(unsigned long)size);
return NULL;
}
}
#ifdef XMALLOC_POISON
memset(ret, 0xA5, size);
#endif
return ret;
}
void *xmalloc(size_t size)
{
return do_xmalloc(size, 0);
}
static void *do_xmallocz(size_t size, int gentle)
{
void *ret;
if (unsigned_add_overflows(size, 1)) {
if (gentle) {
error("Data too large to fit into virtual memory space.");
return NULL;
} else
die("Data too large to fit into virtual memory space.");
}
ret = do_xmalloc(size + 1, gentle);
if (ret)
((char*)ret)[size] = 0;
return ret;
}
void *xmallocz(size_t size)
{
return do_xmallocz(size, 0);
}
void *xmallocz_gently(size_t size)
{
return do_xmallocz(size, 1);
}
/*
* xmemdupz() allocates (len + 1) bytes of memory, duplicates "len" bytes of
* "data" to the allocated memory, zero terminates the allocated memory,
* and returns a pointer to the allocated memory. If the allocation fails,
* the program dies.
*/
void *xmemdupz(const void *data, size_t len)
{
return memcpy(xmallocz(len), data, len);
}
char *xstrndup(const char *str, size_t len)
{
char *p = memchr(str, '\0', len);
return xmemdupz(str, p ? p - str : len);
}
int xstrncmpz(const char *s, const char *t, size_t len)
{
int res = strncmp(s, t, len);
if (res)
return res;
return s[len] == '\0' ? 0 : 1;
}
void *xrealloc(void *ptr, size_t size)
{
void *ret;
if (!size) {
free(ptr);
return xmalloc(0);
}
memory_limit_check(size, 0);
ret = realloc(ptr, size);
if (!ret)
die("Out of memory, realloc failed");
return ret;
}
void *xcalloc(size_t nmemb, size_t size)
{
void *ret;
if (unsigned_mult_overflows(nmemb, size))
die("data too large to fit into virtual memory space");
memory_limit_check(size * nmemb, 0);
ret = calloc(nmemb, size);
if (!ret && (!nmemb || !size))
ret = calloc(1, 1);
if (!ret)
die("Out of memory, calloc failed");
return ret;
}
void xsetenv(const char *name, const char *value, int overwrite)
{
if (setenv(name, value, overwrite))
die_errno(_("could not setenv '%s'"), name ? name : "(null)");
}
/**
* xopen() is the same as open(), but it die()s if the open() fails.
*/
int xopen(const char *path, int oflag, ...)
{
mode_t mode = 0;
va_list ap;
/*
* va_arg() will have undefined behavior if the specified type is not
* compatible with the argument type. Since integers are promoted to
* ints, we fetch the next argument as an int, and then cast it to a
* mode_t to avoid undefined behavior.
*/
va_start(ap, oflag);
if (oflag & O_CREAT)
mode = va_arg(ap, int);
va_end(ap);
for (;;) {
int fd = open(path, oflag, mode);
if (fd >= 0)
return fd;
if (errno == EINTR)
continue;
if ((oflag & (O_CREAT | O_EXCL)) == (O_CREAT | O_EXCL))
die_errno(_("unable to create '%s'"), path);
else if ((oflag & O_RDWR) == O_RDWR)
die_errno(_("could not open '%s' for reading and writing"), path);
else if ((oflag & O_WRONLY) == O_WRONLY)
die_errno(_("could not open '%s' for writing"), path);
else
die_errno(_("could not open '%s' for reading"), path);
}
}
static int handle_nonblock(int fd, short poll_events, int err)
{
struct pollfd pfd;
if (err != EAGAIN && err != EWOULDBLOCK)
return 0;
pfd.fd = fd;
pfd.events = poll_events;
/*
* no need to check for errors, here;
* a subsequent read/write will detect unrecoverable errors
*/
poll(&pfd, 1, -1);
return 1;
}
/*
* xread() is the same a read(), but it automatically restarts read()
* operations with a recoverable error (EAGAIN and EINTR). xread()
* DOES NOT GUARANTEE that "len" bytes is read even if the data is available.
*/
ssize_t xread(int fd, void *buf, size_t len)
{
ssize_t nr;
if (len > MAX_IO_SIZE)
len = MAX_IO_SIZE;
while (1) {
nr = read(fd, buf, len);
if (nr < 0) {
if (errno == EINTR)
continue;
if (handle_nonblock(fd, POLLIN, errno))
continue;
}
return nr;
}
}
/*
* xwrite() is the same a write(), but it automatically restarts write()
* operations with a recoverable error (EAGAIN and EINTR). xwrite() DOES NOT
* GUARANTEE that "len" bytes is written even if the operation is successful.
*/
ssize_t xwrite(int fd, const void *buf, size_t len)
{
ssize_t nr;
if (len > MAX_IO_SIZE)
len = MAX_IO_SIZE;
while (1) {
nr = write(fd, buf, len);
if (nr < 0) {
if (errno == EINTR)
continue;
if (handle_nonblock(fd, POLLOUT, errno))
continue;
}
return nr;
}
}
/*
* xpread() is the same as pread(), but it automatically restarts pread()
* operations with a recoverable error (EAGAIN and EINTR). xpread() DOES
* NOT GUARANTEE that "len" bytes is read even if the data is available.
*/
ssize_t xpread(int fd, void *buf, size_t len, off_t offset)
{
ssize_t nr;
if (len > MAX_IO_SIZE)
len = MAX_IO_SIZE;
while (1) {
nr = pread(fd, buf, len, offset);
if ((nr < 0) && (errno == EAGAIN || errno == EINTR))
continue;
return nr;
}
}
ssize_t read_in_full(int fd, void *buf, size_t count)
{
char *p = buf;
ssize_t total = 0;
while (count > 0) {
ssize_t loaded = xread(fd, p, count);
if (loaded < 0)
return -1;
if (loaded == 0)
return total;
count -= loaded;
p += loaded;
total += loaded;
}
return total;
}
ssize_t write_in_full(int fd, const void *buf, size_t count)
{
const char *p = buf;
ssize_t total = 0;
while (count > 0) {
ssize_t written = xwrite(fd, p, count);
if (written < 0)
return -1;
if (!written) {
errno = ENOSPC;
return -1;
}
count -= written;
p += written;
total += written;
}
return total;
}
ssize_t pread_in_full(int fd, void *buf, size_t count, off_t offset)
{
char *p = buf;
ssize_t total = 0;
while (count > 0) {
ssize_t loaded = xpread(fd, p, count, offset);
if (loaded < 0)
return -1;
if (loaded == 0)
return total;
count -= loaded;
p += loaded;
total += loaded;
offset += loaded;
}
return total;
}
int xdup(int fd)
{
int ret = dup(fd);
if (ret < 0)
die_errno("dup failed");
return ret;
}
/**
* xfopen() is the same as fopen(), but it die()s if the fopen() fails.
*/
FILE *xfopen(const char *path, const char *mode)
{
for (;;) {
FILE *fp = fopen(path, mode);
if (fp)
return fp;
if (errno == EINTR)
continue;
if (*mode && mode[1] == '+')
die_errno(_("could not open '%s' for reading and writing"), path);
else if (*mode == 'w' || *mode == 'a')
die_errno(_("could not open '%s' for writing"), path);
else
die_errno(_("could not open '%s' for reading"), path);
}
}
FILE *xfdopen(int fd, const char *mode)
{
FILE *stream = fdopen(fd, mode);
if (!stream)
die_errno("Out of memory? fdopen failed");
return stream;
}
FILE *fopen_for_writing(const char *path)
{
FILE *ret = fopen(path, "w");
if (!ret && errno == EPERM) {
if (!unlink(path))
ret = fopen(path, "w");
else
errno = EPERM;
}
return ret;
}
static void warn_on_inaccessible(const char *path)
{
warning_errno(_("unable to access '%s'"), path);
}
int warn_on_fopen_errors(const char *path)
{
if (errno != ENOENT && errno != ENOTDIR) {
warn_on_inaccessible(path);
return -1;
}
return 0;
}
FILE *fopen_or_warn(const char *path, const char *mode)
{
FILE *fp = fopen(path, mode);
if (fp)
return fp;
warn_on_fopen_errors(path);
return NULL;
}
int xmkstemp(char *filename_template)
{
int fd;
char origtemplate[PATH_MAX];
strlcpy(origtemplate, filename_template, sizeof(origtemplate));
fd = mkstemp(filename_template);
if (fd < 0) {
int saved_errno = errno;
const char *nonrelative_template;
if (strlen(filename_template) != strlen(origtemplate))
filename_template = origtemplate;
nonrelative_template = absolute_path(filename_template);
errno = saved_errno;
die_errno("Unable to create temporary file '%s'",
nonrelative_template);
}
return fd;
}
/* Adapted from libiberty's mkstemp.c. */
#undef TMP_MAX
#define TMP_MAX 16384
int git_mkstemps_mode(char *pattern, int suffix_len, int mode)
{
static const char letters[] =
"abcdefghijklmnopqrstuvwxyz"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"0123456789";
static const int num_letters = ARRAY_SIZE(letters) - 1;
static const char x_pattern[] = "XXXXXX";
static const int num_x = ARRAY_SIZE(x_pattern) - 1;
char *filename_template;
size_t len;
int fd, count;
len = strlen(pattern);
if (len < num_x + suffix_len) {
errno = EINVAL;
return -1;
}
if (strncmp(&pattern[len - num_x - suffix_len], x_pattern, num_x)) {
errno = EINVAL;
return -1;
}
/*
* Replace pattern's XXXXXX characters with randomness.
* Try TMP_MAX different filenames.
*/
filename_template = &pattern[len - num_x - suffix_len];
for (count = 0; count < TMP_MAX; ++count) {
int i;
uint64_t v;
if (csprng_bytes(&v, sizeof(v)) < 0)
return error_errno("unable to get random bytes for temporary file");
/* Fill in the random bits. */
for (i = 0; i < num_x; i++) {
filename_template[i] = letters[v % num_letters];
v /= num_letters;
}
fd = open(pattern, O_CREAT | O_EXCL | O_RDWR, mode);
if (fd >= 0)
return fd;
/*
* Fatal error (EPERM, ENOSPC etc).
* It doesn't make sense to loop.
*/
if (errno != EEXIST)
break;
}
/* We return the null string if we can't find a unique file name. */
pattern[0] = '\0';
return -1;
}
int git_mkstemp_mode(char *pattern, int mode)
{
/* mkstemp is just mkstemps with no suffix */
return git_mkstemps_mode(pattern, 0, mode);
}
int xmkstemp_mode(char *filename_template, int mode)
{
int fd;
char origtemplate[PATH_MAX];
strlcpy(origtemplate, filename_template, sizeof(origtemplate));
fd = git_mkstemp_mode(filename_template, mode);
if (fd < 0) {
int saved_errno = errno;
const char *nonrelative_template;
if (!filename_template[0])
filename_template = origtemplate;
nonrelative_template = absolute_path(filename_template);
errno = saved_errno;
die_errno("Unable to create temporary file '%s'",
nonrelative_template);
}
return fd;
}
/*
* Some platforms return EINTR from fsync. Since fsync is invoked in some
* cases by a wrapper that dies on failure, do not expose EINTR to callers.
*/
static int fsync_loop(int fd)
{
int err;
do {
err = fsync(fd);
} while (err < 0 && errno == EINTR);
return err;
}
int git_fsync(int fd, enum fsync_action action)
{
switch (action) {
case FSYNC_WRITEOUT_ONLY:
count_fsync_writeout_only += 1;
#ifdef __APPLE__
/*
* On macOS, fsync just causes filesystem cache writeback but
* does not flush hardware caches.
*/
return fsync_loop(fd);
#endif
#ifdef HAVE_SYNC_FILE_RANGE
/*
* On linux 2.6.17 and above, sync_file_range is the way to
* issue a writeback without a hardware flush. An offset of
* 0 and size of 0 indicates writeout of the entire file and the
* wait flags ensure that all dirty data is written to the disk
* (potentially in a disk-side cache) before we continue.
*/
return sync_file_range(fd, 0, 0, SYNC_FILE_RANGE_WAIT_BEFORE |
SYNC_FILE_RANGE_WRITE |
SYNC_FILE_RANGE_WAIT_AFTER);
#endif
#ifdef fsync_no_flush
return fsync_no_flush(fd);
#endif
errno = ENOSYS;
return -1;
case FSYNC_HARDWARE_FLUSH:
count_fsync_hardware_flush += 1;
/*
* On macOS, a special fcntl is required to really flush the
* caches within the storage controller. As of this writing,
* this is a very expensive operation on Apple SSDs.
*/
#ifdef __APPLE__
return fcntl(fd, F_FULLFSYNC);
#else
return fsync_loop(fd);
#endif
default:
BUG("unexpected git_fsync(%d) call", action);
}
}
static void log_trace_fsync_if(const char *key, intmax_t value)
{
if (value)
trace2_data_intmax("fsync", the_repository, key, value);
}
void trace_git_fsync_stats(void)
{
log_trace_fsync_if("fsync/writeout-only", count_fsync_writeout_only);
log_trace_fsync_if("fsync/hardware-flush", count_fsync_hardware_flush);
}
static int warn_if_unremovable(const char *op, const char *file, int rc)
{
int err;
if (!rc || errno == ENOENT)
return 0;
err = errno;
warning_errno("unable to %s '%s'", op, file);
errno = err;
return rc;
}
int unlink_or_msg(const char *file, struct strbuf *err)
{
int rc = unlink(file);
assert(err);
if (!rc || errno == ENOENT)
return 0;
strbuf_addf(err, "unable to unlink '%s': %s",
file, strerror(errno));
return -1;
}
int unlink_or_warn(const char *file)
{
return warn_if_unremovable("unlink", file, unlink(file));
}
int rmdir_or_warn(const char *file)
{
return warn_if_unremovable("rmdir", file, rmdir(file));
}
int remove_or_warn(unsigned int mode, const char *file)
{
return S_ISGITLINK(mode) ? rmdir_or_warn(file) : unlink_or_warn(file);
}
static int access_error_is_ok(int err, unsigned flag)
{
return (is_missing_file_error(err) ||
((flag & ACCESS_EACCES_OK) && err == EACCES));
}
int access_or_warn(const char *path, int mode, unsigned flag)
{
int ret = access(path, mode);
if (ret && !access_error_is_ok(errno, flag))
warn_on_inaccessible(path);
return ret;
}
int access_or_die(const char *path, int mode, unsigned flag)
{
int ret = access(path, mode);
if (ret && !access_error_is_ok(errno, flag))
die_errno(_("unable to access '%s'"), path);
return ret;
}
char *xgetcwd(void)
{
struct strbuf sb = STRBUF_INIT;
if (strbuf_getcwd(&sb))
die_errno(_("unable to get current working directory"));
return strbuf_detach(&sb, NULL);
}
int xsnprintf(char *dst, size_t max, const char *fmt, ...)
{
va_list ap;
int len;
va_start(ap, fmt);
len = vsnprintf(dst, max, fmt, ap);
va_end(ap);
if (len < 0)
BUG("your snprintf is broken");
if (len >= max)
BUG("attempt to snprintf into too-small buffer");
return len;
}
void write_file_buf(const char *path, const char *buf, size_t len)
{
int fd = xopen(path, O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (write_in_full(fd, buf, len) < 0)
die_errno(_("could not write to '%s'"), path);
if (close(fd))
die_errno(_("could not close '%s'"), path);
}
void write_file(const char *path, const char *fmt, ...)
{
va_list params;
struct strbuf sb = STRBUF_INIT;
va_start(params, fmt);
strbuf_vaddf(&sb, fmt, params);
va_end(params);
strbuf_complete_line(&sb);
write_file_buf(path, sb.buf, sb.len);
strbuf_release(&sb);
}
void sleep_millisec(int millisec)
{
poll(NULL, 0, millisec);
}
int xgethostname(char *buf, size_t len)
{
/*
* If the full hostname doesn't fit in buf, POSIX does not
* specify whether the buffer will be null-terminated, so to
* be safe, do it ourselves.
*/
int ret = gethostname(buf, len);
if (!ret)
buf[len - 1] = 0;
return ret;
}
int is_empty_or_missing_file(const char *filename)
{
struct stat st;
if (stat(filename, &st) < 0) {
if (errno == ENOENT)
return 1;
die_errno(_("could not stat %s"), filename);
}
return !st.st_size;
}
int open_nofollow(const char *path, int flags)
{
#ifdef O_NOFOLLOW
return open(path, flags | O_NOFOLLOW);
#else
struct stat st;
if (lstat(path, &st) < 0)
return -1;
if (S_ISLNK(st.st_mode)) {
errno = ELOOP;
return -1;
}
return open(path, flags);
#endif
}
int csprng_bytes(void *buf, size_t len)
{
#if defined(HAVE_ARC4RANDOM) || defined(HAVE_ARC4RANDOM_LIBBSD)
/* This function never returns an error. */
arc4random_buf(buf, len);
return 0;
#elif defined(HAVE_GETRANDOM)
ssize_t res;
char *p = buf;
while (len) {
res = getrandom(p, len, 0);
if (res < 0)
return -1;
len -= res;
p += res;
}
return 0;
#elif defined(HAVE_GETENTROPY)
int res;
char *p = buf;
while (len) {
/* getentropy has a maximum size of 256 bytes. */
size_t chunk = len < 256 ? len : 256;
res = getentropy(p, chunk);
if (res < 0)
return -1;
len -= chunk;
p += chunk;
}
return 0;
#elif defined(HAVE_RTLGENRANDOM)
if (!RtlGenRandom(buf, len))
return -1;
return 0;
#elif defined(HAVE_OPENSSL_CSPRNG)
int res = RAND_bytes(buf, len);
if (res == 1)
return 0;
if (res == -1)
errno = ENOTSUP;
else
errno = EIO;
return -1;
#else
ssize_t res;
char *p = buf;
int fd, err;
fd = open("/dev/urandom", O_RDONLY);
if (fd < 0)
return -1;
while (len) {
res = xread(fd, p, len);
if (res < 0) {
err = errno;
close(fd);
errno = err;
return -1;
}
len -= res;
p += res;
}
close(fd);
return 0;
#endif
}
![swh spinner](/static/img/swh-spinner.gif)
Computing file changes ...