public inbox for linux-ext4@vger.kernel.org
 help / color / mirror / Atom feed
* [PATCH 0/2] ext2: fix WARN_ON in drop_nlink() triggered by corrupt images
@ 2026-04-01 22:08 Vasiliy Kovalev
  2026-04-01 22:08 ` [PATCH 1/2] ext2: validate i_nlink before decrement in ext2_unlink() Vasiliy Kovalev
                   ` (2 more replies)
  0 siblings, 3 replies; 4+ messages in thread
From: Vasiliy Kovalev @ 2026-04-01 22:08 UTC (permalink / raw)
  To: Jan Kara, Andrew Morton, Alexey Dobriyan, linux-ext4
  Cc: linux-kernel, lvc-project, kovalev

A crafted ext2 image can contain a directory entry pointing to an inode
whose on-disk i_links_count is zero. ext2 mounts such an image without
error. Any subsequent syscall that decrements i_nlink on that inode
triggers WARN_ON inside drop_nlink() in fs/inode.c.

These patches prevent the warning by validating i_nlink before decrementing
it in ext2_unlink() and ext2_rename(), reporting the corruption via
ext2_error() instead.

The issues were found by Linux Verification Center (linuxtesting.org)
with Syzkaller.

Vasiliy Kovalev (2):
  ext2: validate i_nlink before decrement in ext2_unlink()
  ext2: guard against zero i_nlink on new_inode in ext2_rename()

 fs/ext2/namei.c | 14 +++++++++++++-
 1 file changed, 13 insertions(+), 1 deletion(-)

--- [Reproducer for PATCH 1/2: ext2_unlink] ---

#define _GNU_SOURCE

#include <dirent.h>
#include <endian.h>
#include <errno.h>
#include <fcntl.h>
#include <sched.h>
#include <setjmp.h>
#include <signal.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/mount.h>
#include <sys/prctl.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>

#include <linux/loop.h>

#ifndef __NR_memfd_create
#define __NR_memfd_create 319
#endif

static unsigned long long procid;

static void sleep_ms(uint64_t ms)
{
	usleep(ms * 1000);
}

static uint64_t current_time_ms(void)
{
	struct timespec ts;
	if (clock_gettime(CLOCK_MONOTONIC, &ts))
	exit(1);
	return (uint64_t)ts.tv_sec * 1000 + (uint64_t)ts.tv_nsec / 1000000;
}

static void use_temporary_dir(void)
{
	char tmpdir_template[] = "./syzkaller.XXXXXX";
	char* tmpdir = mkdtemp(tmpdir_template);
	if (!tmpdir)
	exit(1);
	if (chmod(tmpdir, 0777))
	exit(1);
	if (chdir(tmpdir))
	exit(1);
}

static bool write_file(const char* file, const char* what, ...)
{
	char buf[1024];
	va_list args;
	va_start(args, what);
	vsnprintf(buf, sizeof(buf), what, args);
	va_end(args);
	buf[sizeof(buf) - 1] = 0;
	int len = strlen(buf);
	int fd = open(file, O_WRONLY | O_CLOEXEC);
	if (fd == -1)
		return false;
	if (write(fd, buf, len) != len) {
		int err = errno;
		close(fd);
		errno = err;
		return false;
	}
	close(fd);
	return true;
}

#define MAXBITS 15
#define MAXLCODES 286
#define MAXDCODES 30
#define MAXCODES (MAXLCODES + MAXDCODES)
#define FIXLCODES 288

struct puff_state {
	unsigned char* out;
	unsigned long outlen;
	unsigned long outcnt;
	const unsigned char* in;
	unsigned long inlen;
	unsigned long incnt;
	int bitbuf;
	int bitcnt;
	jmp_buf env;
};
static int puff_bits(struct puff_state* s, int need)
{
	long val = s->bitbuf;
	while (s->bitcnt < need) {
		if (s->incnt == s->inlen)
			longjmp(s->env, 1);
		val |= (long)(s->in[s->incnt++]) << s->bitcnt;
		s->bitcnt += 8;
	}
	s->bitbuf = (int)(val >> need);
	s->bitcnt -= need;
	return (int)(val & ((1L << need) - 1));
}
static int puff_stored(struct puff_state* s)
{
	s->bitbuf = 0;
	s->bitcnt = 0;
	if (s->incnt + 4 > s->inlen)
		return 2;
	unsigned len = s->in[s->incnt++];
	len |= s->in[s->incnt++] << 8;
	if (s->in[s->incnt++] != (~len & 0xff) ||
	    s->in[s->incnt++] != ((~len >> 8) & 0xff))
		return -2;
	if (s->incnt + len > s->inlen)
		return 2;
	if (s->outcnt + len > s->outlen)
		return 1;
	for (; len--; s->outcnt++, s->incnt++) {
		if (s->in[s->incnt])
			s->out[s->outcnt] = s->in[s->incnt];
	}
	return 0;
}
struct puff_huffman {
	short* count;
	short* symbol;
};
static int puff_decode(struct puff_state* s, const struct puff_huffman* h)
{
	int first = 0;
	int index = 0;
	int bitbuf = s->bitbuf;
	int left = s->bitcnt;
	int code = first = index = 0;
	int len = 1;
	short* next = h->count + 1;
	while (1) {
		while (left--) {
			code |= bitbuf & 1;
			bitbuf >>= 1;
			int count = *next++;
			if (code - count < first) {
				s->bitbuf = bitbuf;
				s->bitcnt = (s->bitcnt - len) & 7;
				return h->symbol[index + (code - first)];
			}
			index += count;
			first += count;
			first <<= 1;
			code <<= 1;
			len++;
		}
		left = (MAXBITS + 1) - len;
		if (left == 0)
			break;
		if (s->incnt == s->inlen)
			longjmp(s->env, 1);
		bitbuf = s->in[s->incnt++];
		if (left > 8)
			left = 8;
	}
	return -10;
}
static int puff_construct(struct puff_huffman* h, const short* length, int n)
{
	int len;
	for (len = 0; len <= MAXBITS; len++)
		h->count[len] = 0;
	int symbol;
	for (symbol = 0; symbol < n; symbol++)
		(h->count[length[symbol]])++;
	if (h->count[0] == n)
		return 0;
	int left = 1;
	for (len = 1; len <= MAXBITS; len++) {
		left <<= 1;
		left -= h->count[len];
		if (left < 0)
			return left;
	}
	short offs[MAXBITS + 1];
	offs[1] = 0;
	for (len = 1; len < MAXBITS; len++)
		offs[len + 1] = offs[len] + h->count[len];
	for (symbol = 0; symbol < n; symbol++)
		if (length[symbol] != 0)
			h->symbol[offs[length[symbol]]++] = symbol;
	return left;
}
static int puff_codes(struct puff_state* s,
		      const struct puff_huffman* lencode,
		      const struct puff_huffman* distcode)
{
	static const short lens[29] = {
				       3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31,
				       35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258};
	static const short lext[29] = {
				       0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2,
				       3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0};
	static const short dists[30] = {
					1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193,
					257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145,
					8193, 12289, 16385, 24577};
	static const short dext[30] = {
				       0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6,
				       7, 7, 8, 8, 9, 9, 10, 10, 11, 11,
				       12, 12, 13, 13};
	int symbol;
	do {
		symbol = puff_decode(s, lencode);
		if (symbol < 0)
			return symbol;
		if (symbol < 256) {
			if (s->outcnt == s->outlen)
				return 1;
			if (symbol)
				s->out[s->outcnt] = symbol;
			s->outcnt++;
		} else if (symbol > 256) {
			symbol -= 257;
			if (symbol >= 29)
				return -10;
			int len = lens[symbol] + puff_bits(s, lext[symbol]);
			symbol = puff_decode(s, distcode);
			if (symbol < 0)
				return symbol;
			unsigned dist = dists[symbol] + puff_bits(s, dext[symbol]);
			if (dist > s->outcnt)
				return -11;
			if (s->outcnt + len > s->outlen)
				return 1;
			while (len--) {
				if (dist <= s->outcnt && s->out[s->outcnt - dist])
					s->out[s->outcnt] = s->out[s->outcnt - dist];
				s->outcnt++;
			}
		}
	} while (symbol != 256);
	return 0;
}
static int puff_fixed(struct puff_state* s)
{
	static int virgin = 1;
	static short lencnt[MAXBITS + 1], lensym[FIXLCODES];
	static short distcnt[MAXBITS + 1], distsym[MAXDCODES];
	static struct puff_huffman lencode, distcode;
	if (virgin) {
		lencode.count = lencnt;
		lencode.symbol = lensym;
		distcode.count = distcnt;
		distcode.symbol = distsym;
		short lengths[FIXLCODES];
		int symbol;
		for (symbol = 0; symbol < 144; symbol++)
			lengths[symbol] = 8;
		for (; symbol < 256; symbol++)
			lengths[symbol] = 9;
		for (; symbol < 280; symbol++)
			lengths[symbol] = 7;
		for (; symbol < FIXLCODES; symbol++)
			lengths[symbol] = 8;
		puff_construct(&lencode, lengths, FIXLCODES);
		for (symbol = 0; symbol < MAXDCODES; symbol++)
			lengths[symbol] = 5;
		puff_construct(&distcode, lengths, MAXDCODES);
		virgin = 0;
	}
	return puff_codes(s, &lencode, &distcode);
}
static int puff_dynamic(struct puff_state* s)
{
	static const short order[19] =
	    {16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15};
	int nlen = puff_bits(s, 5) + 257;
	int ndist = puff_bits(s, 5) + 1;
	int ncode = puff_bits(s, 4) + 4;
	if (nlen > MAXLCODES || ndist > MAXDCODES)
		return -3;
	short lengths[MAXCODES];
	int index;
	for (index = 0; index < ncode; index++)
		lengths[order[index]] = puff_bits(s, 3);
	for (; index < 19; index++)
		lengths[order[index]] = 0;
	short lencnt[MAXBITS + 1], lensym[MAXLCODES];
	struct puff_huffman lencode = {lencnt, lensym};
	int err = puff_construct(&lencode, lengths, 19);
	if (err != 0)
		return -4;
	index = 0;
	while (index < nlen + ndist) {
		int symbol;
		int len;
		symbol = puff_decode(s, &lencode);
		if (symbol < 0)
			return symbol;
		if (symbol < 16)
			lengths[index++] = symbol;
		else {
			len = 0;
			if (symbol == 16) {
				if (index == 0)
					return -5;
				len = lengths[index - 1];
				symbol = 3 + puff_bits(s, 2);
			} else if (symbol == 17)
				symbol = 3 + puff_bits(s, 3);
			else
				symbol = 11 + puff_bits(s, 7);
			if (index + symbol > nlen + ndist)
				return -6;
			while (symbol--)
				lengths[index++] = len;
		}
	}
	if (lengths[256] == 0)
		return -9;
	err = puff_construct(&lencode, lengths, nlen);
	if (err && (err < 0 || nlen != lencode.count[0] + lencode.count[1]))
		return -7;
	short distcnt[MAXBITS + 1], distsym[MAXDCODES];
	struct puff_huffman distcode = {distcnt, distsym};
	err = puff_construct(&distcode, lengths + nlen, ndist);
	if (err && (err < 0 || ndist != distcode.count[0] + distcode.count[1]))
		return -8;
	return puff_codes(s, &lencode, &distcode);
}
static int puff(
    unsigned char* dest,
    unsigned long* destlen,
    const unsigned char* source,
    unsigned long sourcelen)
{
	struct puff_state s = {
	    .out = dest,
	    .outlen = *destlen,
	    .outcnt = 0,
	    .in = source,
	    .inlen = sourcelen,
	    .incnt = 0,
	    .bitbuf = 0,
	    .bitcnt = 0,
	};
	int err;
	if (setjmp(s.env) != 0)
		err = 2;
	else {
		int last;
		do {
			last = puff_bits(&s, 1);
			int type = puff_bits(&s, 2);
			err = type == 0 ? puff_stored(&s) : (type == 1 ? puff_fixed(&s) : (type == 2 ? puff_dynamic(&s) : -1));
			if (err != 0)
				break;
		} while (!last);
	}
	*destlen = s.outcnt;
	return err;
}

#define ZLIB_HEADER_WIDTH 2

static int puff_zlib_to_file(const unsigned char* source, unsigned long sourcelen, int dest_fd)
{
	if (sourcelen < ZLIB_HEADER_WIDTH)
		return 0;
	source += ZLIB_HEADER_WIDTH;
	sourcelen -= ZLIB_HEADER_WIDTH;
	const unsigned long max_destlen = 132 << 20;
	void* ret = mmap(0, max_destlen, PROT_WRITE | PROT_READ, MAP_PRIVATE | MAP_ANON, -1, 0);
	if (ret == MAP_FAILED)
		return -1;
	unsigned char* dest = (unsigned char*)ret;
	unsigned long destlen = max_destlen;
	int err = puff(dest, &destlen, source, sourcelen);
	if (err) {
		munmap(dest, max_destlen);
		errno = -err;
		return -1;
	}
	if (write(dest_fd, dest, destlen) != (ssize_t)destlen) {
		munmap(dest, max_destlen);
		return -1;
	}
	return munmap(dest, max_destlen);
}

static int setup_loop_device(unsigned char* data, unsigned long size, const char* loopname, int* loopfd_p)
{
	int err = 0, loopfd = -1;
	int memfd = syscall(__NR_memfd_create, "syzkaller", 0);
	if (memfd == -1) {
		err = errno;
		goto error;
	}
	if (puff_zlib_to_file(data, size, memfd)) {
		err = errno;
		goto error_close_memfd;
	}
	loopfd = open(loopname, O_RDWR);
	if (loopfd == -1) {
		err = errno;
		goto error_close_memfd;
	}
	if (ioctl(loopfd, LOOP_SET_FD, memfd)) {
		if (errno != EBUSY) {
			err = errno;
			goto error_close_loop;
		}
		ioctl(loopfd, LOOP_CLR_FD, 0);
		usleep(1000);
		if (ioctl(loopfd, LOOP_SET_FD, memfd)) {
			err = errno;
			goto error_close_loop;
		}
	}
	close(memfd);
	*loopfd_p = loopfd;
	return 0;

error_close_loop:
	close(loopfd);
error_close_memfd:
	close(memfd);
error:
	errno = err;
	return -1;
}

static void reset_loop_device(const char* loopname)
{
	int loopfd = open(loopname, O_RDWR);
	if (loopfd == -1) {
		return;
	}
	if (ioctl(loopfd, LOOP_CLR_FD, 0)) {
	}
	close(loopfd);
}

static long syz_mount_image(
    volatile long fsarg,
    volatile long dir,
    volatile long flags,
    volatile long optsarg,
    volatile long change_dir,
    volatile unsigned long size,
    volatile long image)
{
	unsigned char* data = (unsigned char*)image;
	int res = -1, err = 0, need_loop_device = !!size;
	char* mount_opts = (char*)optsarg;
	char* target = (char*)dir;
	char* fs = (char*)fsarg;
	char* source = NULL;
	char loopname[64];
	if (need_loop_device) {
		int loopfd;
		memset(loopname, 0, sizeof(loopname));
		snprintf(loopname, sizeof(loopname), "/dev/loop%llu", procid);
		if (setup_loop_device(data, size, loopname, &loopfd) == -1)
			return -1;
		close(loopfd);
		source = loopname;
	}
	mkdir(target, 0777);
	char opts[256];
	memset(opts, 0, sizeof(opts));
	if (strlen(mount_opts) > (sizeof(opts) - 32)) {
	}
	strncpy(opts, mount_opts, sizeof(opts) - 32);
	if (strcmp(fs, "iso9660") == 0) {
		flags |= MS_RDONLY;
	} else if (strncmp(fs, "ext", 3) == 0) {
		bool has_remount_ro = false;
		char* remount_ro_start = strstr(opts, "errors=remount-ro");
		if (remount_ro_start != NULL) {
			char after = *(remount_ro_start + strlen("errors=remount-ro"));
			char before = remount_ro_start == opts ? '\0' : *(remount_ro_start - 1);
			has_remount_ro = ((before == '\0' || before == ',') && (after == '\0' || after == ','));
		}
		if (strstr(opts, "errors=panic") || !has_remount_ro)
			strcat(opts, ",errors=continue");
	} else if (strcmp(fs, "xfs") == 0) {
		strcat(opts, ",nouuid");
	} else if (strncmp(fs, "gfs2", 4) == 0 && (strstr(opts, "errors=panic") || strstr(opts, "debug"))) {
		strcat(opts, ",errors=withdraw");
	}
	res = mount(source, target, fs, flags, opts);
	if (res == -1) {
		err = errno;
		goto error_clear_loop;
	}
	res = open(target, O_RDONLY | O_DIRECTORY);
	if (res == -1) {
		err = errno;
		goto error_clear_loop;
	}
	if (change_dir) {
		res = chdir(target);
		if (res == -1) {
			err = errno;
		}
	}

error_clear_loop:
	if (need_loop_device)
		reset_loop_device(loopname);
	errno = err;
	return res;
}

#define FS_IOC_SETFLAGS _IOW('f', 2, long)
static void remove_dir(const char* dir)
{
	int iter = 0;
	DIR* dp = 0;
	const int umount_flags = MNT_FORCE | UMOUNT_NOFOLLOW;

retry:
		while (umount2(dir, umount_flags) == 0) {
		}
	dp = opendir(dir);
	if (dp == NULL) {
		if (errno == EMFILE) {
	exit(1);
		}
	exit(1);
	}
	struct dirent* ep = 0;
	while ((ep = readdir(dp))) {
		if (strcmp(ep->d_name, ".") == 0 || strcmp(ep->d_name, "..") == 0)
			continue;
		char filename[FILENAME_MAX];
		snprintf(filename, sizeof(filename), "%s/%s", dir, ep->d_name);
			while (umount2(filename, umount_flags) == 0) {
			}
		struct stat st;
		if (lstat(filename, &st))
	exit(1);
		if (S_ISDIR(st.st_mode)) {
			remove_dir(filename);
			continue;
		}
		int i;
		for (i = 0;; i++) {
			if (unlink(filename) == 0)
				break;
			if (errno == EPERM) {
				int fd = open(filename, O_RDONLY);
				if (fd != -1) {
					long flags = 0;
					if (ioctl(fd, FS_IOC_SETFLAGS, &flags) == 0) {
					}
					close(fd);
					continue;
				}
			}
			if (errno == EROFS) {
				break;
			}
			if (errno != EBUSY || i > 100)
	exit(1);
				if (umount2(filename, umount_flags))
	exit(1);
		}
	}
	closedir(dp);
	for (int i = 0;; i++) {
		if (rmdir(dir) == 0)
			break;
		if (i < 100) {
			if (errno == EPERM) {
				int fd = open(dir, O_RDONLY);
				if (fd != -1) {
					long flags = 0;
					if (ioctl(fd, FS_IOC_SETFLAGS, &flags) == 0) {
					}
					close(fd);
					continue;
				}
			}
			if (errno == EROFS) {
				break;
			}
			if (errno == EBUSY) {
					if (umount2(dir, umount_flags))
	exit(1);
				continue;
			}
			if (errno == ENOTEMPTY) {
				if (iter < 100) {
					iter++;
					goto retry;
				}
			}
		}
	exit(1);
	}
}

static void kill_and_wait(int pid, int* status)
{
	kill(-pid, SIGKILL);
	kill(pid, SIGKILL);
	for (int i = 0; i < 100; i++) {
		if (waitpid(-1, status, WNOHANG | __WALL) == pid)
			return;
		usleep(1000);
	}
	DIR* dir = opendir("/sys/fs/fuse/connections");
	if (dir) {
		for (;;) {
			struct dirent* ent = readdir(dir);
			if (!ent)
				break;
			if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0)
				continue;
			char abort[300];
			snprintf(abort, sizeof(abort), "/sys/fs/fuse/connections/%s/abort", ent->d_name);
			int fd = open(abort, O_WRONLY);
			if (fd == -1) {
				continue;
			}
			if (write(fd, abort, 1) < 0) {
			}
			close(fd);
		}
		closedir(dir);
	} else {
	}
	while (waitpid(-1, status, __WALL) != pid) {
	}
}

static void reset_loop()
{
	char buf[64];
	snprintf(buf, sizeof(buf), "/dev/loop%llu", procid);
	int loopfd = open(buf, O_RDWR);
	if (loopfd != -1) {
		ioctl(loopfd, LOOP_CLR_FD, 0);
		close(loopfd);
	}
}

static void setup_test()
{
	prctl(PR_SET_PDEATHSIG, SIGKILL, 0, 0, 0);
	setpgrp();
	write_file("/proc/self/oom_score_adj", "1000");
	if (symlink("/dev/binderfs", "./binderfs")) {
	}
}

#define USLEEP_FORKED_CHILD (3 * 50 *1000)

static long handle_clone_ret(long ret)
{
	if (ret != 0) {
		return ret;
	}
	usleep(USLEEP_FORKED_CHILD);
	syscall(__NR_exit, 0);
	while (1) {
	}
}

static long syz_clone(volatile long flags, volatile long stack, volatile long stack_len,
		      volatile long ptid, volatile long ctid, volatile long tls)
{
	long sp = (stack + stack_len) & ~15;
	long ret = (long)syscall(__NR_clone, flags & ~CLONE_VM, sp, ptid, ctid, tls);
	return handle_clone_ret(ret);
}

static void execute_one(void);

#define WAIT_FLAGS __WALL

static void loop(void)
{
	int iter = 0;
	for (;; iter++) {
		char cwdbuf[32];
		sprintf(cwdbuf, "./%d", iter);
		if (mkdir(cwdbuf, 0777))
	exit(1);
		reset_loop();
		int pid = fork();
		if (pid < 0)
	exit(1);
		if (pid == 0) {
			if (chdir(cwdbuf))
	exit(1);
			setup_test();
			execute_one();
			exit(0);
		}
		int status = 0;
		uint64_t start = current_time_ms();
		for (;;) {
			sleep_ms(10);
			if (waitpid(-1, &status, WNOHANG | WAIT_FLAGS) == pid)
				break;
			if (current_time_ms() - start < 5000)
				continue;
			kill_and_wait(pid, &status);
			break;
		}
		remove_dir(cwdbuf);
	}
}

void execute_one(void)
{
		if (write(1, "executing program\n", sizeof("executing program\n") - 1)) {}
memcpy((void*)0x200000000240, "ext2\000", 5);
memcpy((void*)0x200000000440, "./file1\000", 8);
*(uint8_t*)0x200000000480 = 0;
memcpy((void*)0x2000000008c0, "\x78\x9c\xec\xdc\x4f\x68\x1c\x55\x18\x00\xf0\x6f\x66\xbb\xd5\xb6\x89\xa9\x6d\xfd\x57\x95\x16\x14\x2a\x88\xf9\xab\x98\x9b\x54\xef\x0a\x56\xc1\x6b\x30\x9b\xb6\x74\xdb\x48\x12\xa9\x09\xb4\xd8\xb3\xe2\x21\x08\x0a\x9e\xf4\xec\xa1\x37\xf1\xa6\x37\x2f\x1e\x04\x0f\x82\x17\x0f\x05\x41\x28\xf6\xe0\xc1\xff\x2b\xb3\x33\xb3\xa6\xe9\x6e\x68\x93\xdd\xae\x76\x7e\x3f\x98\xdd\xf7\x66\x66\xf9\xde\xcb\xf2\x4d\xde\x7c\x64\x12\x40\x65\x1d\xcd\x5e\x92\x88\x91\x88\xf8\x3e\x22\xc6\xf2\xee\xf5\x27\x1c\xcd\xdf\xd6\x67\x2e\x9f\xca\xb6\x24\x5a\xad\x13\x3f\x27\xed\xf3\x2e\xce\x5c\x3e\x55\x9e\x5a\x7e\x6e\x5f\x44\xbc\x1d\x11\x77\x47\xc4\x81\x88\xa8\x75\x89\xbb\xbc\xba\x76\x66\xae\xd9\x6c\x2c\x15\xfd\x89\x95\xb3\x6f\x4c\x2c\xaf\xae\x3d\x75\xfa\xec\xdc\xc9\xc6\xc9\xc6\xb9\xa9\x99\x67\x9f\x9e\x9c\x7d\x66\x72\x76\xb6\x6f\x73\xbd\xfa\xe5\xbb\xaf\xc5\x2b\x2f\x4d\xbf\x77\xe4\x87\x4f\x5e\xf8\xfc\xe0\x85\x6c\xbc\x23\xc5\xb1\x8d\xf3\xe8\xa7\xa4\xc7\xfe\x07\x07\x11\x6c\x88\xf6\x0c\x7b\x00\x6c\x4b\x96\x9b\xbb\x22\xa2\xde\xce\xff\xb1\xa8\xb5\x7b\x40\x15\xb4\x5a\xad\x23\x5b\x1f\x06\xee\x5c\x89\x24\x87\x8a\x2a\x7f\xd1\x67\xf7\xbf\xe5\x76\x7b\x56\x1e\xb9\xab\xc7\xf3\x1b\x90\x8b\x45\x6d\x61\xbd\x13\x7f\x57\xa4\xc5\x39\x75\xf7\x97\xc0\x00\x7c\x91\x5d\x7f\x26\xbb\x5d\xff\xd2\xeb\x6a\x74\x7b\x8b\xba\xe6\x48\x44\x8c\x46\xc4\x3d\x45\xad\x74\x7f\x44\xdc\x5b\xd4\x39\x0f\x46\xc4\xa1\x88\xb8\xef\x16\xe2\x97\xd7\xbf\xf5\x1b\xae\x7f\x69\xe7\xfa\x57\x8b\x88\xfb\x77\x30\xc7\x77\x9a\xd7\xae\xf4\x8c\x7f\x69\x34\x0e\x77\x8d\x9f\x74\x2a\x41\x49\x76\x23\x18\x11\x0f\x6c\x33\xfe\x67\xcf\x5d\xf8\xaa\xd7\xb1\xd6\xc7\x11\xc7\xa2\x7b\xfc\xd8\x10\x7f\x8b\xfa\xf0\xc4\xc2\xe9\x66\x63\x32\x7f\xed\x1a\xe3\xf9\x57\x17\xbe\xee\x3d\xff\xfc\xbb\xbd\x21\xfe\x86\xa2\x75\x6d\x87\xf5\xda\x2b\x7b\x5e\x8e\xbf\xb6\x88\xff\xc4\x63\xdd\xbf\xff\x03\xc5\x39\xd9\xfc\xff\x8e\x88\xdf\x22\xe2\xf7\x88\xf8\x23\x22\xfe\x8c\x88\x87\x22\xe2\x70\x44\x3c\x1c\x11\x8f\x6c\x11\xff\xf8\x37\xdf\x2e\xf6\x3a\x96\xc5\x9f\xef\xf1\xf3\x4f\x37\xc4\x7f\x74\x3b\x13\x2f\x9c\xff\xf1\xd3\x4b\x3b\xf8\x38\x00\xd0\x67\x69\x7b\x4d\x9b\xa4\xe3\x9d\x76\x9a\x8e\x8f\xe7\x6b\xdd\x43\xb1\x37\x6d\x2e\x2e\xaf\x3c\xb9\xb0\xf8\xe6\xb9\xf9\x7c\xed\xbb\x3f\xea\x69\xb9\xd2\xca\xd7\xbf\xf5\x24\xeb\x4f\x15\x6b\xe1\xb2\x3f\xbd\xa9\x3f\x53\xac\x93\xdf\xaf\xed\x69\xf7\xc7\x5f\x5f\x6c\xce\x0f\x7b\xf2\x50\x71\xfb\x36\xe5\xff\x2f\xb5\x3c\xff\x81\x8a\xf0\x27\x3f\x50\x5d\xf2\x1f\xaa\x4b\xfe\x43\x75\xc9\x7f\xa8\x2e\xf9\x0f\xd5\x25\xff\xa1\xba\xe4\x3f\x54\x97\xfc\x87\xea\x92\xff\x50\x5d\xf2\x1f\xaa\xeb\xe6\xf2\x7f\xf7\xc0\xc7\x01\xdc\x56\x23\x3d\x9e\xff\x19\xdd\xf0\xec\xce\x64\xf1\xbc\xfb\x77\xb5\xfa\x5d\xe5\xb3\x3e\xc0\xff\x5f\x63\xa9\xf3\x9f\x78\x37\x35\x86\x3d\x32\x60\xd0\xfe\x4d\xfa\x61\x8f\x04\x00\x00\x00\x00\xe8\xb7\x5e\xd5\xff\x7e\x36\x86\x3d\x47\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x5b\x93\xfe\x94\x44\x44\xb6\x1d\x1b\x7b\x7c\x64\xf3\xd1\xdd\xc9\xaf\xb5\xf6\x7b\x44\x9c\xff\xe8\xc4\x07\x6f\xcd\xad\xac\x2c\x4d\x65\xfb\xaf\x75\xf6\xaf\x7c\x58\xec\x9f\x1e\xc6\xf8\x81\x9b\x55\xe6\x69\x99\xc7\x40\x75\x2d\xaf\xae\x9d\x99\x6b\x36\x1b\x4b\x1a\xfd\x68\xbc\xf8\xdf\x18\x86\xc6\x9d\xd2\xa8\xc7\x60\x43\xfc\x13\x00\x00\xff\xff\x52\x90\x78\x22", 999);
syz_mount_image(/*fs=*/0x200000000240, /*dir=*/0x200000000440, /*flags=*/0, /*opts=*/0x200000000480, /*chdir=*/1, /*size=*/0x3e7, /*img=*/0x2000000008c0);
syz_clone(/*flags=*/0, /*stack=*/0, /*stack_len=*/0, /*parentid=*/0, /*childtid=*/0, /*tls=*/0);

}
int main(void)
{
		syscall(__NR_mmap, /*addr=*/0x1ffffffff000ul, /*len=*/0x1000ul, /*prot=*/0ul, /*flags=MAP_FIXED|MAP_ANONYMOUS|MAP_PRIVATE*/0x32ul, /*fd=*/(intptr_t)-1, /*offset=*/0ul);
	syscall(__NR_mmap, /*addr=*/0x200000000000ul, /*len=*/0x1000000ul, /*prot=PROT_WRITE|PROT_READ|PROT_EXEC*/7ul, /*flags=MAP_FIXED|MAP_ANONYMOUS|MAP_PRIVATE*/0x32ul, /*fd=*/(intptr_t)-1, /*offset=*/0ul);
	syscall(__NR_mmap, /*addr=*/0x200001000000ul, /*len=*/0x1000ul, /*prot=*/0ul, /*flags=MAP_FIXED|MAP_ANONYMOUS|MAP_PRIVATE*/0x32ul, /*fd=*/(intptr_t)-1, /*offset=*/0ul);
	const char* reason;
	(void)reason;
			use_temporary_dir();
			loop();
	return 0;
}

--- [Reproducer for PATCH 2/2: ext2_rename] ---

#define _GNU_SOURCE

#include <dirent.h>
#include <endian.h>
#include <errno.h>
#include <fcntl.h>
#include <setjmp.h>
#include <signal.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/mount.h>
#include <sys/prctl.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>

#include <linux/loop.h>

#ifndef __NR_memfd_create
#define __NR_memfd_create 319
#endif

static unsigned long long procid;

static void sleep_ms(uint64_t ms)
{
	usleep(ms * 1000);
}

static uint64_t current_time_ms(void)
{
	struct timespec ts;
	if (clock_gettime(CLOCK_MONOTONIC, &ts))
	exit(1);
	return (uint64_t)ts.tv_sec * 1000 + (uint64_t)ts.tv_nsec / 1000000;
}

static bool write_file(const char* file, const char* what, ...)
{
	char buf[1024];
	va_list args;
	va_start(args, what);
	vsnprintf(buf, sizeof(buf), what, args);
	va_end(args);
	buf[sizeof(buf) - 1] = 0;
	int len = strlen(buf);
	int fd = open(file, O_WRONLY | O_CLOEXEC);
	if (fd == -1)
		return false;
	if (write(fd, buf, len) != len) {
		int err = errno;
		close(fd);
		errno = err;
		return false;
	}
	close(fd);
	return true;
}

#define MAXBITS 15
#define MAXLCODES 286
#define MAXDCODES 30
#define MAXCODES (MAXLCODES + MAXDCODES)
#define FIXLCODES 288

struct puff_state {
	unsigned char* out;
	unsigned long outlen;
	unsigned long outcnt;
	const unsigned char* in;
	unsigned long inlen;
	unsigned long incnt;
	int bitbuf;
	int bitcnt;
	jmp_buf env;
};
static int puff_bits(struct puff_state* s, int need)
{
	long val = s->bitbuf;
	while (s->bitcnt < need) {
		if (s->incnt == s->inlen)
			longjmp(s->env, 1);
		val |= (long)(s->in[s->incnt++]) << s->bitcnt;
		s->bitcnt += 8;
	}
	s->bitbuf = (int)(val >> need);
	s->bitcnt -= need;
	return (int)(val & ((1L << need) - 1));
}
static int puff_stored(struct puff_state* s)
{
	s->bitbuf = 0;
	s->bitcnt = 0;
	if (s->incnt + 4 > s->inlen)
		return 2;
	unsigned len = s->in[s->incnt++];
	len |= s->in[s->incnt++] << 8;
	if (s->in[s->incnt++] != (~len & 0xff) ||
	    s->in[s->incnt++] != ((~len >> 8) & 0xff))
		return -2;
	if (s->incnt + len > s->inlen)
		return 2;
	if (s->outcnt + len > s->outlen)
		return 1;
	for (; len--; s->outcnt++, s->incnt++) {
		if (s->in[s->incnt])
			s->out[s->outcnt] = s->in[s->incnt];
	}
	return 0;
}
struct puff_huffman {
	short* count;
	short* symbol;
};
static int puff_decode(struct puff_state* s, const struct puff_huffman* h)
{
	int first = 0;
	int index = 0;
	int bitbuf = s->bitbuf;
	int left = s->bitcnt;
	int code = first = index = 0;
	int len = 1;
	short* next = h->count + 1;
	while (1) {
		while (left--) {
			code |= bitbuf & 1;
			bitbuf >>= 1;
			int count = *next++;
			if (code - count < first) {
				s->bitbuf = bitbuf;
				s->bitcnt = (s->bitcnt - len) & 7;
				return h->symbol[index + (code - first)];
			}
			index += count;
			first += count;
			first <<= 1;
			code <<= 1;
			len++;
		}
		left = (MAXBITS + 1) - len;
		if (left == 0)
			break;
		if (s->incnt == s->inlen)
			longjmp(s->env, 1);
		bitbuf = s->in[s->incnt++];
		if (left > 8)
			left = 8;
	}
	return -10;
}
static int puff_construct(struct puff_huffman* h, const short* length, int n)
{
	int len;
	for (len = 0; len <= MAXBITS; len++)
		h->count[len] = 0;
	int symbol;
	for (symbol = 0; symbol < n; symbol++)
		(h->count[length[symbol]])++;
	if (h->count[0] == n)
		return 0;
	int left = 1;
	for (len = 1; len <= MAXBITS; len++) {
		left <<= 1;
		left -= h->count[len];
		if (left < 0)
			return left;
	}
	short offs[MAXBITS + 1];
	offs[1] = 0;
	for (len = 1; len < MAXBITS; len++)
		offs[len + 1] = offs[len] + h->count[len];
	for (symbol = 0; symbol < n; symbol++)
		if (length[symbol] != 0)
			h->symbol[offs[length[symbol]]++] = symbol;
	return left;
}
static int puff_codes(struct puff_state* s,
		      const struct puff_huffman* lencode,
		      const struct puff_huffman* distcode)
{
	static const short lens[29] = {
				       3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31,
				       35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258};
	static const short lext[29] = {
				       0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2,
				       3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0};
	static const short dists[30] = {
					1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193,
					257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145,
					8193, 12289, 16385, 24577};
	static const short dext[30] = {
				       0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6,
				       7, 7, 8, 8, 9, 9, 10, 10, 11, 11,
				       12, 12, 13, 13};
	int symbol;
	do {
		symbol = puff_decode(s, lencode);
		if (symbol < 0)
			return symbol;
		if (symbol < 256) {
			if (s->outcnt == s->outlen)
				return 1;
			if (symbol)
				s->out[s->outcnt] = symbol;
			s->outcnt++;
		} else if (symbol > 256) {
			symbol -= 257;
			if (symbol >= 29)
				return -10;
			int len = lens[symbol] + puff_bits(s, lext[symbol]);
			symbol = puff_decode(s, distcode);
			if (symbol < 0)
				return symbol;
			unsigned dist = dists[symbol] + puff_bits(s, dext[symbol]);
			if (dist > s->outcnt)
				return -11;
			if (s->outcnt + len > s->outlen)
				return 1;
			while (len--) {
				if (dist <= s->outcnt && s->out[s->outcnt - dist])
					s->out[s->outcnt] = s->out[s->outcnt - dist];
				s->outcnt++;
			}
		}
	} while (symbol != 256);
	return 0;
}
static int puff_fixed(struct puff_state* s)
{
	static int virgin = 1;
	static short lencnt[MAXBITS + 1], lensym[FIXLCODES];
	static short distcnt[MAXBITS + 1], distsym[MAXDCODES];
	static struct puff_huffman lencode, distcode;
	if (virgin) {
		lencode.count = lencnt;
		lencode.symbol = lensym;
		distcode.count = distcnt;
		distcode.symbol = distsym;
		short lengths[FIXLCODES];
		int symbol;
		for (symbol = 0; symbol < 144; symbol++)
			lengths[symbol] = 8;
		for (; symbol < 256; symbol++)
			lengths[symbol] = 9;
		for (; symbol < 280; symbol++)
			lengths[symbol] = 7;
		for (; symbol < FIXLCODES; symbol++)
			lengths[symbol] = 8;
		puff_construct(&lencode, lengths, FIXLCODES);
		for (symbol = 0; symbol < MAXDCODES; symbol++)
			lengths[symbol] = 5;
		puff_construct(&distcode, lengths, MAXDCODES);
		virgin = 0;
	}
	return puff_codes(s, &lencode, &distcode);
}
static int puff_dynamic(struct puff_state* s)
{
	static const short order[19] =
	    {16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15};
	int nlen = puff_bits(s, 5) + 257;
	int ndist = puff_bits(s, 5) + 1;
	int ncode = puff_bits(s, 4) + 4;
	if (nlen > MAXLCODES || ndist > MAXDCODES)
		return -3;
	short lengths[MAXCODES];
	int index;
	for (index = 0; index < ncode; index++)
		lengths[order[index]] = puff_bits(s, 3);
	for (; index < 19; index++)
		lengths[order[index]] = 0;
	short lencnt[MAXBITS + 1], lensym[MAXLCODES];
	struct puff_huffman lencode = {lencnt, lensym};
	int err = puff_construct(&lencode, lengths, 19);
	if (err != 0)
		return -4;
	index = 0;
	while (index < nlen + ndist) {
		int symbol;
		int len;
		symbol = puff_decode(s, &lencode);
		if (symbol < 0)
			return symbol;
		if (symbol < 16)
			lengths[index++] = symbol;
		else {
			len = 0;
			if (symbol == 16) {
				if (index == 0)
					return -5;
				len = lengths[index - 1];
				symbol = 3 + puff_bits(s, 2);
			} else if (symbol == 17)
				symbol = 3 + puff_bits(s, 3);
			else
				symbol = 11 + puff_bits(s, 7);
			if (index + symbol > nlen + ndist)
				return -6;
			while (symbol--)
				lengths[index++] = len;
		}
	}
	if (lengths[256] == 0)
		return -9;
	err = puff_construct(&lencode, lengths, nlen);
	if (err && (err < 0 || nlen != lencode.count[0] + lencode.count[1]))
		return -7;
	short distcnt[MAXBITS + 1], distsym[MAXDCODES];
	struct puff_huffman distcode = {distcnt, distsym};
	err = puff_construct(&distcode, lengths + nlen, ndist);
	if (err && (err < 0 || ndist != distcode.count[0] + distcode.count[1]))
		return -8;
	return puff_codes(s, &lencode, &distcode);
}
static int puff(
    unsigned char* dest,
    unsigned long* destlen,
    const unsigned char* source,
    unsigned long sourcelen)
{
	struct puff_state s = {
	    .out = dest,
	    .outlen = *destlen,
	    .outcnt = 0,
	    .in = source,
	    .inlen = sourcelen,
	    .incnt = 0,
	    .bitbuf = 0,
	    .bitcnt = 0,
	};
	int err;
	if (setjmp(s.env) != 0)
		err = 2;
	else {
		int last;
		do {
			last = puff_bits(&s, 1);
			int type = puff_bits(&s, 2);
			err = type == 0 ? puff_stored(&s) : (type == 1 ? puff_fixed(&s) : (type == 2 ? puff_dynamic(&s) : -1));
			if (err != 0)
				break;
		} while (!last);
	}
	*destlen = s.outcnt;
	return err;
}

#define ZLIB_HEADER_WIDTH 2

static int puff_zlib_to_file(const unsigned char* source, unsigned long sourcelen, int dest_fd)
{
	if (sourcelen < ZLIB_HEADER_WIDTH)
		return 0;
	source += ZLIB_HEADER_WIDTH;
	sourcelen -= ZLIB_HEADER_WIDTH;
	const unsigned long max_destlen = 132 << 20;
	void* ret = mmap(0, max_destlen, PROT_WRITE | PROT_READ, MAP_PRIVATE | MAP_ANON, -1, 0);
	if (ret == MAP_FAILED)
		return -1;
	unsigned char* dest = (unsigned char*)ret;
	unsigned long destlen = max_destlen;
	int err = puff(dest, &destlen, source, sourcelen);
	if (err) {
		munmap(dest, max_destlen);
		errno = -err;
		return -1;
	}
	if (write(dest_fd, dest, destlen) != (ssize_t)destlen) {
		munmap(dest, max_destlen);
		return -1;
	}
	return munmap(dest, max_destlen);
}

static int setup_loop_device(unsigned char* data, unsigned long size, const char* loopname, int* loopfd_p)
{
	int err = 0, loopfd = -1;
	int memfd = syscall(__NR_memfd_create, "syzkaller", 0);
	if (memfd == -1) {
		err = errno;
		goto error;
	}
	if (puff_zlib_to_file(data, size, memfd)) {
		err = errno;
		goto error_close_memfd;
	}
	loopfd = open(loopname, O_RDWR);
	if (loopfd == -1) {
		err = errno;
		goto error_close_memfd;
	}
	if (ioctl(loopfd, LOOP_SET_FD, memfd)) {
		if (errno != EBUSY) {
			err = errno;
			goto error_close_loop;
		}
		ioctl(loopfd, LOOP_CLR_FD, 0);
		usleep(1000);
		if (ioctl(loopfd, LOOP_SET_FD, memfd)) {
			err = errno;
			goto error_close_loop;
		}
	}
	close(memfd);
	*loopfd_p = loopfd;
	return 0;

error_close_loop:
	close(loopfd);
error_close_memfd:
	close(memfd);
error:
	errno = err;
	return -1;
}

static void reset_loop_device(const char* loopname)
{
	int loopfd = open(loopname, O_RDWR);
	if (loopfd == -1) {
		return;
	}
	if (ioctl(loopfd, LOOP_CLR_FD, 0)) {
	}
	close(loopfd);
}

static long syz_mount_image(
    volatile long fsarg,
    volatile long dir,
    volatile long flags,
    volatile long optsarg,
    volatile long change_dir,
    volatile unsigned long size,
    volatile long image)
{
	unsigned char* data = (unsigned char*)image;
	int res = -1, err = 0, need_loop_device = !!size;
	char* mount_opts = (char*)optsarg;
	char* target = (char*)dir;
	char* fs = (char*)fsarg;
	char* source = NULL;
	char loopname[64];
	if (need_loop_device) {
		int loopfd;
		memset(loopname, 0, sizeof(loopname));
		snprintf(loopname, sizeof(loopname), "/dev/loop%llu", procid);
		if (setup_loop_device(data, size, loopname, &loopfd) == -1)
			return -1;
		close(loopfd);
		source = loopname;
	}
	mkdir(target, 0777);
	char opts[256];
	memset(opts, 0, sizeof(opts));
	if (strlen(mount_opts) > (sizeof(opts) - 32)) {
	}
	strncpy(opts, mount_opts, sizeof(opts) - 32);
	if (strcmp(fs, "iso9660") == 0) {
		flags |= MS_RDONLY;
	} else if (strncmp(fs, "ext", 3) == 0) {
		bool has_remount_ro = false;
		char* remount_ro_start = strstr(opts, "errors=remount-ro");
		if (remount_ro_start != NULL) {
			char after = *(remount_ro_start + strlen("errors=remount-ro"));
			char before = remount_ro_start == opts ? '\0' : *(remount_ro_start - 1);
			has_remount_ro = ((before == '\0' || before == ',') && (after == '\0' || after == ','));
		}
		if (strstr(opts, "errors=panic") || !has_remount_ro)
			strcat(opts, ",errors=continue");
	} else if (strcmp(fs, "xfs") == 0) {
		strcat(opts, ",nouuid");
	} else if (strncmp(fs, "gfs2", 4) == 0 && (strstr(opts, "errors=panic") || strstr(opts, "debug"))) {
		strcat(opts, ",errors=withdraw");
	}
	res = mount(source, target, fs, flags, opts);
	if (res == -1) {
		err = errno;
		goto error_clear_loop;
	}
	res = open(target, O_RDONLY | O_DIRECTORY);
	if (res == -1) {
		err = errno;
		goto error_clear_loop;
	}
	if (change_dir) {
		res = chdir(target);
		if (res == -1) {
			err = errno;
		}
	}

error_clear_loop:
	if (need_loop_device)
		reset_loop_device(loopname);
	errno = err;
	return res;
}

static void kill_and_wait(int pid, int* status)
{
	kill(-pid, SIGKILL);
	kill(pid, SIGKILL);
	for (int i = 0; i < 100; i++) {
		if (waitpid(-1, status, WNOHANG | __WALL) == pid)
			return;
		usleep(1000);
	}
	DIR* dir = opendir("/sys/fs/fuse/connections");
	if (dir) {
		for (;;) {
			struct dirent* ent = readdir(dir);
			if (!ent)
				break;
			if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0)
				continue;
			char abort[300];
			snprintf(abort, sizeof(abort), "/sys/fs/fuse/connections/%s/abort", ent->d_name);
			int fd = open(abort, O_WRONLY);
			if (fd == -1) {
				continue;
			}
			if (write(fd, abort, 1) < 0) {
			}
			close(fd);
		}
		closedir(dir);
	} else {
	}
	while (waitpid(-1, status, __WALL) != pid) {
	}
}

static void reset_loop()
{
	char buf[64];
	snprintf(buf, sizeof(buf), "/dev/loop%llu", procid);
	int loopfd = open(buf, O_RDWR);
	if (loopfd != -1) {
		ioctl(loopfd, LOOP_CLR_FD, 0);
		close(loopfd);
	}
}

static void setup_test()
{
	prctl(PR_SET_PDEATHSIG, SIGKILL, 0, 0, 0);
	setpgrp();
	write_file("/proc/self/oom_score_adj", "1000");
}

static void execute_one(void);

#define WAIT_FLAGS __WALL

static void loop(void)
{
	int iter = 0;
	for (;; iter++) {
		reset_loop();
		int pid = fork();
		if (pid < 0)
	exit(1);
		if (pid == 0) {
			setup_test();
			execute_one();
			exit(0);
		}
		int status = 0;
		uint64_t start = current_time_ms();
		for (;;) {
			sleep_ms(10);
			if (waitpid(-1, &status, WNOHANG | WAIT_FLAGS) == pid)
				break;
			if (current_time_ms() - start < 5000)
				continue;
			kill_and_wait(pid, &status);
			break;
		}
	}
}

void execute_one(void)
{
		if (write(1, "executing program\n", sizeof("executing program\n") - 1)) {}
memcpy((void*)0x200000000400, "ext2\000", 5);
memcpy((void*)0x200000000440, "./file0\000", 8);
*(uint8_t*)0x200000000480 = 0;
memcpy((void*)0x2000000004c0, "\x78\x9c\xec\xdc\xcd\x6b\x1c\x65\x1c\x07\xf0\xef\xce\x36\xda\x97\xd4\xd6\xb6\xbe\x55\xa5\x05\x85\x0a\x62\x9a\x36\x8a\xbd\x49\xf5\x0f\x10\xac\x82\xd7\x60\xd3\x17\xba\x6d\x24\x89\xd4\x16\x5a\xec\x5d\x3c\x14\x41\xc1\x93\x9e\x3d\xf4\x26\xde\xf4\xe6\xc5\x83\xe0\x41\xf0\xe2\xa1\x20\x08\xc5\x1e\x3c\xf8\xbe\x32\xbb\xb3\xeb\x36\xee\x06\xdb\x26\x6e\xec\x7c\x3e\x30\xbb\xcf\x33\x33\xcb\xef\x79\x92\xfc\x26\xcf\xfc\xc8\x24\x40\x6d\xed\x2d\x5f\x1a\xc9\x64\x92\x6f\x93\x6c\xeb\x76\x6f\x3c\x61\x6f\xf7\xed\xf2\xcc\x95\x13\xe5\xd6\x48\xbb\x7d\xe4\xc7\x46\xe7\xbc\x8b\x33\x57\x4e\xf4\x4e\xed\x7d\x6e\x4b\x92\xb7\x92\x6c\x4c\xb2\x23\x49\x73\x48\xdc\xc5\x73\xe7\x4f\xcd\xb6\x5a\x73\x0b\x55\x7f\xff\xd2\xe9\xd7\xf7\x2f\x9e\x3b\xff\xd4\xc9\xd3\xb3\xc7\xe7\x8e\xcf\x9d\x39\x30\xf3\xec\xd3\xd3\x87\x9e\x99\x3e\x74\x68\xd5\xe6\x7a\xed\xf3\xb7\x5f\xcd\xcb\x2f\x1e\x7c\x67\xcf\x77\x1f\xbd\xf0\xe9\xce\x0b\xe5\x78\x27\xab\x63\x83\xf3\x58\x4d\x8d\x11\xfb\x1f\x5c\x8b\x60\x63\xb4\x69\xdc\x03\xe0\x96\x94\xb9\xb9\x21\xc9\x44\x27\xff\xb7\xa5\xd9\xe9\x01\x75\xd0\x6e\xb7\xf7\xac\x7c\x18\xb8\x73\x35\x24\x39\xd4\x54\xef\x17\x7d\x79\xff\xdb\xdb\xfe\x9b\x95\x47\xd7\xb5\xc3\xdd\x1b\x90\x8b\x55\x6d\xe1\x72\x3f\xfe\x86\x14\xd5\x39\x13\xee\x2f\x81\x35\xf0\x59\x79\xfd\x99\x1e\x76\xfd\x2b\x6e\xa8\xd1\x6d\xae\xea\x9a\x93\x49\xb6\x26\xb9\xa7\xaa\x95\x6e\x4f\x72\x6f\x55\xe7\xdc\x99\x64\x57\x92\xfb\x6e\x22\x7e\xef\xfa\x77\xf9\x1f\xd7\xbf\xa2\x7f\xfd\x6b\x26\xb9\xff\x36\xe6\x38\xd1\xba\x7e\x75\xb0\x3f\x58\x8f\xbd\x76\x29\xd9\x3d\x34\x7e\xa3\x5f\x09\x6a\x94\x37\x82\x49\x1e\xb8\xc5\xf8\x9f\x3c\x77\xe1\x8b\x51\xc7\xda\x1f\x26\xfb\x32\x3c\x7e\x06\xe2\xaf\x50\x1f\xde\x7f\xec\x64\x6b\x6e\xba\xfb\x3a\x34\xc6\xf3\xaf\x1c\xfb\x72\x54\xfc\x72\xfe\x9b\x87\xc5\x2f\xd2\x9f\x7f\xf3\x36\xeb\xb5\x57\x37\xbd\x94\x3f\x56\x88\xff\xc4\x63\xc3\xbf\xff\x3b\x06\xe6\xff\x67\x92\x5f\x92\xfc\x9a\xe4\xb7\x24\xbf\x27\x79\x28\xc9\xee\x24\x0f\x27\x79\x64\x85\xf8\x87\xbf\xfa\x7a\x7e\xd4\xb1\x32\xfe\xd1\x11\x5f\xff\x62\x20\xfe\xa3\xb7\x32\xf1\xca\xd9\xef\x3f\xbe\x74\x1b\x1f\x07\x00\x56\x59\xd1\x59\xd3\x36\x8a\xa9\x7e\xbb\x28\xa6\xa6\xba\x6b\xdd\x5d\xd9\x5c\xb4\xe6\x17\x97\x9e\x3c\x36\xff\xc6\x99\xa3\xdd\xb5\xef\xf6\x4c\x14\xbd\x95\x56\x77\xfd\x3b\xd1\x28\xfb\x07\xaa\xb5\x70\xaf\x7f\x70\x59\x7f\xa6\x5a\x27\xbf\xdb\xdc\xd4\xe9\x4f\xbd\x36\xdf\x3a\x3a\xee\xc9\x43\xcd\x6d\x59\x96\xff\x3f\x35\xbb\xf9\x0f\xd4\x84\x3f\xf9\x81\xfa\x92\xff\x50\x5f\xf2\x1f\xea\x4b\xfe\x43\x7d\xc9\x7f\xa8\x2f\xf9\x0f\xf5\x25\xff\xa1\xbe\xe4\x3f\xd4\x97\xfc\x87\xfa\x92\xff\x50\x5f\xf2\x1f\x6a\x69\x72\xc4\xf3\x3f\x5b\x07\x9e\xdd\x99\xae\x9e\x77\xff\xa6\x39\x71\x77\xef\x59\x1f\xe0\xff\x6f\x6e\xa1\xff\x9f\x78\x97\x35\xc6\x3d\x32\x60\xad\xfd\x9d\xf4\xe3\x1e\x09\x00\x00\x00\x00\xb0\xda\x46\x55\xff\x57\xb3\x31\xee\x39\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xdc\x9c\xe2\x87\x46\x92\x72\xdb\xb7\xed\xf1\xc9\xe5\x47\xef\x6a\xfc\xdc\xec\xbc\x27\x39\xfb\xc1\x91\xf7\xde\x9c\x5d\x5a\x5a\x38\x50\xee\xbf\xde\xdf\xbf\xf4\x7e\xb5\xff\xe0\x38\xc6\x0f\xfc\x5b\xbd\x3c\xed\xe5\x31\x50\x5f\x8b\xe7\xce\x9f\x9a\x6d\xb5\xe6\x16\xee\x9c\xc6\xc6\x24\xeb\x60\x18\x1a\xeb\xa9\xb1\xbd\xfa\x79\x5f\x2f\xe3\x59\x2f\x8d\xbf\x02\x00\x00\xff\xff\x87\xb5\x73\xae", 1002);
syz_mount_image(/*fs=*/0x200000000400, /*dir=*/0x200000000440, /*flags=*/0, /*opts=*/0x200000000480, /*chdir=*/1, /*size=*/0x3ea, /*img=*/0x2000000004c0);
memcpy((void*)0x200000000000, "./file0/file0\000", 14);
memcpy((void*)0x2000000000c0, "./file1\000", 8);
	syscall(__NR_rename, /*old=*/0x200000000000ul, /*new=*/0x2000000000c0ul);

}
int main(void)
{
		syscall(__NR_mmap, /*addr=*/0x1ffffffff000ul, /*len=*/0x1000ul, /*prot=*/0ul, /*flags=MAP_FIXED|MAP_ANONYMOUS|MAP_PRIVATE*/0x32ul, /*fd=*/(intptr_t)-1, /*offset=*/0ul);
	syscall(__NR_mmap, /*addr=*/0x200000000000ul, /*len=*/0x1000000ul, /*prot=PROT_WRITE|PROT_READ|PROT_EXEC*/7ul, /*flags=MAP_FIXED|MAP_ANONYMOUS|MAP_PRIVATE*/0x32ul, /*fd=*/(intptr_t)-1, /*offset=*/0ul);
	syscall(__NR_mmap, /*addr=*/0x200001000000ul, /*len=*/0x1000ul, /*prot=*/0ul, /*flags=MAP_FIXED|MAP_ANONYMOUS|MAP_PRIVATE*/0x32ul, /*fd=*/(intptr_t)-1, /*offset=*/0ul);
	const char* reason;
	(void)reason;
			loop();
	return 0;
}

--
2.50.1


^ permalink raw reply	[flat|nested] 4+ messages in thread

* [PATCH 1/2] ext2: validate i_nlink before decrement in ext2_unlink()
  2026-04-01 22:08 [PATCH 0/2] ext2: fix WARN_ON in drop_nlink() triggered by corrupt images Vasiliy Kovalev
@ 2026-04-01 22:08 ` Vasiliy Kovalev
  2026-04-01 22:08 ` [PATCH 2/2] ext2: guard against zero i_nlink on new_inode in ext2_rename() Vasiliy Kovalev
  2026-04-04 15:27 ` [PATCH 0/2] ext2: fix WARN_ON in drop_nlink() triggered by corrupt images Vasiliy Kovalev
  2 siblings, 0 replies; 4+ messages in thread
From: Vasiliy Kovalev @ 2026-04-01 22:08 UTC (permalink / raw)
  To: Jan Kara, Andrew Morton, Alexey Dobriyan, linux-ext4
  Cc: linux-kernel, lvc-project, kovalev

A crafted ext2 image can provide a directory entry pointing to an inode
with i_links_count == 0 on disk. Calling unlink() on such an entry
triggers WARN_ON inside drop_nlink():

WARNING: CPU: 3 PID: 609 at fs/inode.c:336 drop_nlink+0xad/0xd0 fs/inode.c:336
CPU: 3 UID: 0 PID: 609 Comm: syz-executor Not tainted 6.12.77+ #1
Call Trace:
 <TASK>
 inode_dec_link_count include/linux/fs.h:2518 [inline]
 ext2_unlink+0x26c/0x300 fs/ext2/namei.c:295
 vfs_unlink+0x2fc/0x9b0 fs/namei.c:4477
 do_unlinkat+0x53e/0x730 fs/namei.c:4541
 __do_sys_unlink fs/namei.c:4589 [inline]
 __se_sys_unlink fs/namei.c:4587 [inline]
 __x64_sys_unlink+0xc6/0x110 fs/namei.c:4587
 do_syscall_x64 arch/x86/entry/common.c:47 [inline]
 do_syscall_64+0xf5/0x220 arch/x86/entry/common.c:78
 entry_SYSCALL_64_after_hwframe+0x77/0x7f
 </TASK>

At the point of the crash, ext2_delete_entry() has already committed
the removal of the directory entry to disk, so returning an error is
not an option. Instead, skip the decrement and report the corruption
via ext2_error(), which marks the superblock as having errors. The
inode will be reclaimed when its last reference is dropped.

Found by Linux Verification Center (linuxtesting.org) with Syzkaller.

Cc: stable@vger.kernel.org
Fixes: a513b035eadf ("[PATCH] ext2: switch to inode_inc_count, inode_dec_count")
Signed-off-by: Vasiliy Kovalev <kovalev@altlinux.org>
---
 fs/ext2/namei.c | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/fs/ext2/namei.c b/fs/ext2/namei.c
index bde617a66cec..ea49e8f2b292 100644
--- a/fs/ext2/namei.c
+++ b/fs/ext2/namei.c
@@ -293,7 +293,12 @@ static int ext2_unlink(struct inode *dir, struct dentry *dentry)
 		goto out;
 
 	inode_set_ctime_to_ts(inode, inode_get_ctime(dir));
-	inode_dec_link_count(inode);
+	if (!inode->i_nlink)
+		ext2_error(inode->i_sb, __func__,
+			   "inode %lu has zero i_nlink on unlink, fs may be corrupt",
+			   inode->i_ino);
+	else
+		inode_dec_link_count(inode);
 	err = 0;
 out:
 	return err;
-- 
2.50.1


^ permalink raw reply related	[flat|nested] 4+ messages in thread

* [PATCH 2/2] ext2: guard against zero i_nlink on new_inode in ext2_rename()
  2026-04-01 22:08 [PATCH 0/2] ext2: fix WARN_ON in drop_nlink() triggered by corrupt images Vasiliy Kovalev
  2026-04-01 22:08 ` [PATCH 1/2] ext2: validate i_nlink before decrement in ext2_unlink() Vasiliy Kovalev
@ 2026-04-01 22:08 ` Vasiliy Kovalev
  2026-04-04 15:27 ` [PATCH 0/2] ext2: fix WARN_ON in drop_nlink() triggered by corrupt images Vasiliy Kovalev
  2 siblings, 0 replies; 4+ messages in thread
From: Vasiliy Kovalev @ 2026-04-01 22:08 UTC (permalink / raw)
  To: Jan Kara, Andrew Morton, Alexey Dobriyan, linux-ext4
  Cc: linux-kernel, lvc-project, kovalev

A crafted ext2 image can provide a target inode with i_links_count == 0
on disk. When rename() resolves to an existing target, ext2_rename()
calls drop_nlink(new_inode) for the directory case and
inode_dec_link_count(new_inode) unconditionally. Both reach
drop_nlink(), which triggers WARN_ON:

WARNING: CPU: 0 PID: 646 at fs/inode.c:336 drop_nlink+0xad/0xd0 fs/inode.c:336
CPU: 0 UID: 0 PID: 646 Comm: syz.0.17 Not tainted 6.12.77+ #1
Call Trace:
 <TASK>
 inode_dec_link_count include/linux/fs.h:2518 [inline]
 ext2_rename+0x35e/0x850 fs/ext2/namei.c:374
 vfs_rename+0xf2f/0x2060 fs/namei.c:5021
 do_renameat2+0xbe2/0xd50 fs/namei.c:5178
 __do_sys_rename fs/namei.c:5225 [inline]
 __se_sys_rename fs/namei.c:5223 [inline]
 __x64_sys_rename+0x7e/0xa0 fs/namei.c:5223
 do_syscall_x64 arch/x86/entry/common.c:47 [inline]
 do_syscall_64+0xf5/0x220 arch/x86/entry/common.c:78
 entry_SYSCALL_64_after_hwframe+0x77/0x7f
 </TASK>

No disk state has been modified at this point in the function, so
return -EFSCORRUPTED after reporting the corruption via ext2_error().

Found by Linux Verification Center (linuxtesting.org) with Syzkaller.

Cc: stable@vger.kernel.org
Fixes: 9a53c3a783c2 ("[PATCH] r/o bind mounts: unlink: monitor i_nlink")
Signed-off-by: Vasiliy Kovalev <kovalev@altlinux.org>
---
 fs/ext2/namei.c | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/fs/ext2/namei.c b/fs/ext2/namei.c
index ea49e8f2b292..419e844f2e54 100644
--- a/fs/ext2/namei.c
+++ b/fs/ext2/namei.c
@@ -334,6 +334,13 @@ static int ext2_rename (struct mnt_idmap * idmap,
 	bool old_is_dir = S_ISDIR(old_inode->i_mode);
 	int err;
 
+	if (new_inode && new_inode->i_nlink == 0) {
+		ext2_error(old_dir->i_sb, __func__,
+			   "target inode %lu has zero i_nlink, filesystem may be corrupt",
+			   new_inode->i_ino);
+		return -EFSCORRUPTED;
+	}
+
 	if (flags & ~RENAME_NOREPLACE)
 		return -EINVAL;
 
-- 
2.50.1


^ permalink raw reply related	[flat|nested] 4+ messages in thread

* Re: [PATCH 0/2] ext2: fix WARN_ON in drop_nlink() triggered by corrupt images
  2026-04-01 22:08 [PATCH 0/2] ext2: fix WARN_ON in drop_nlink() triggered by corrupt images Vasiliy Kovalev
  2026-04-01 22:08 ` [PATCH 1/2] ext2: validate i_nlink before decrement in ext2_unlink() Vasiliy Kovalev
  2026-04-01 22:08 ` [PATCH 2/2] ext2: guard against zero i_nlink on new_inode in ext2_rename() Vasiliy Kovalev
@ 2026-04-04 15:27 ` Vasiliy Kovalev
  2 siblings, 0 replies; 4+ messages in thread
From: Vasiliy Kovalev @ 2026-04-04 15:27 UTC (permalink / raw)
  To: Jan Kara, Andrew Morton, Alexey Dobriyan, linux-ext4
  Cc: linux-kernel, lvc-project

On 4/2/26 01:08, Vasiliy Kovalev wrote:
> A crafted ext2 image can contain a directory entry pointing to an inode
> whose on-disk i_links_count is zero. ext2 mounts such an image without
> error. Any subsequent syscall that decrements i_nlink on that inode
> triggers WARN_ON inside drop_nlink() in fs/inode.c.
> 
> These patches prevent the warning by validating i_nlink before decrementing
> it in ext2_unlink() and ext2_rename(), reporting the corruption via
> ext2_error() instead.
> 
> The issues were found by Linux Verification Center (linuxtesting.org)
> with Syzkaller.
> 
> Vasiliy Kovalev (2):
>    ext2: validate i_nlink before decrement in ext2_unlink()
>    ext2: guard against zero i_nlink on new_inode in ext2_rename()

Syzkaller found a third trigger via ext2_rmdir(). Rather than adding
another guard in namei.c, I fixed the root cause in ext2_iget() instead 
- a single check there covers all three cases at once.

New patch: 
https://lore.kernel.org/all/20260404152011.2590197-1-kovalev@altlinux.org/

If the previous two patches have not been picked up yet, please
consider this one as a replacement for the entire series.

>   fs/ext2/namei.c | 14 +++++++++++++-
>   1 file changed, 13 insertions(+), 1 deletion(-)
> 
> --- [Reproducer for PATCH 1/2: ext2_unlink] ---
> [...]
> 
> --- [Reproducer for PATCH 2/2: ext2_rename] ---
> [...]

-- 
Thanks,
Vasiliy

^ permalink raw reply	[flat|nested] 4+ messages in thread

end of thread, other threads:[~2026-04-04 15:27 UTC | newest]

Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-04-01 22:08 [PATCH 0/2] ext2: fix WARN_ON in drop_nlink() triggered by corrupt images Vasiliy Kovalev
2026-04-01 22:08 ` [PATCH 1/2] ext2: validate i_nlink before decrement in ext2_unlink() Vasiliy Kovalev
2026-04-01 22:08 ` [PATCH 2/2] ext2: guard against zero i_nlink on new_inode in ext2_rename() Vasiliy Kovalev
2026-04-04 15:27 ` [PATCH 0/2] ext2: fix WARN_ON in drop_nlink() triggered by corrupt images Vasiliy Kovalev

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox