| 0/0 |
2026/06/01 16:33 |
flow |
assessment-security |
45m
Results: map[DenialOfService:false Explanation:The bug report indicates a `WARN_ON(!new_inode)` being triggered in
`btrfs_rename` (specifically in `fs/btrfs/inode.c`).
### Root Cause Analysis
The warning is triggered by the following code block in `btrfs_rename`:
```c
ret = btrfs_check_dir_item_collision(dest, new_dir->i_ino,
&new_fname.disk_name);
if (ret) {
if (ret == -EEXIST) {
/* we shouldn't get
* eexist without a new_inode */
if (WARN_ON(!new_inode)) {
goto out_fscrypt_names;
}
} else {
/* maybe -EOVERFLOW */
goto out_fscrypt_names;
}
}
```
When a file is renamed, `btrfs_rename` checks if the destination name already
exists in the directory b-tree by calling `btrfs_check_dir_item_collision`. If
the exact name is found, it returns `-EEXIST`.
Normally, if the destination file exists, the VFS (Virtual File System) layer
would have already found it during the path lookup phase
(`lookup_one_qstr_excl`), and `new_inode` would point to the existing file's
inode. In this case, `-EEXIST` is expected, the warning is bypassed, and the
rename operation proceeds to overwrite the existing file.
However, if `new_inode` is `NULL`, it means the VFS lookup determined that the
destination file does *not* exist (resulting in a negative dentry). If
`btrfs_check_dir_item_collision` then finds the name in the b-tree, it reveals a
structural inconsistency in the filesystem: a directory item exists for the
name, but the corresponding inode item is missing.
When the VFS performs the lookup, `btrfs_lookup_dentry` finds the directory item
but fails to find the inode item (`btrfs_iget` returns `-ENOENT`), causing it to
return a negative dentry. Later, `btrfs_rename` finds the orphaned directory
item and triggers the `WARN_ON`.
### Security Impact
This inconsistency can only be triggered by a corrupted or maliciously crafted
BTRFS image.
- **Exploitable:** No. The bug is merely a `WARN_ON` that detects filesystem
corruption. The unexpected condition is properly handled by the `goto
out_fscrypt_names;` error path, which safely aborts the operation and returns
`-EEXIST` to the user-space caller. There is no memory corruption, out-of-bounds
access, or privilege escalation.
- **Denial of Service:** No. The warning does not halt or crash the system in
production builds (unless `panic_on_warn` is explicitly enabled by the
administrator, which is not the default). It simply returns an error.
- **Access Vectors:** Mounting a BTRFS filesystem requires `CAP_SYS_ADMIN` in
the initial namespace (BTRFS does not support `FS_USERNS_MOUNT`). Therefore,
unprivileged local users or users in a user namespace cannot trigger this bug
directly. However, it can be triggered if a system is configured to auto-mount
external media (e.g., a malicious USB drive plugged into a desktop machine).
In conclusion, this is a benign warning designed to catch filesystem corruption
and gracefully error out, posing no significant security risk. Exploitable:false FilesystemTrigger:true NetworkTrigger:false PeripheralTrigger:true RemoteTrigger:false Unprivileged:false UserNamespace:false VMGuestTrigger:false VMHostTrigger:false]
|
| 1/1 |
2026/06/01 16:33 |
action |
syz-repro-to-c-repro |
0m
Results: map[SimplifiedCRepro:// autogenerated by syzkaller (https://github.com/google/syzkaller)
#define _GNU_SOURCE
#include <endian.h>
#include <errno.h>
#include <fcntl.h>
#include <setjmp.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/stat.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <unistd.h>
#include <linux/loop.h>
#ifndef __NR_memfd_create
#define __NR_memfd_create 319
#endif
static unsigned long long procid;
//% This code is derived from puff.{c,h}, found in the zlib development. The
//% original files come with the following copyright notice:
//% Copyright (C) 2002-2013 Mark Adler, all rights reserved
//% version 2.3, 21 Jan 2013
//% This software is provided 'as-is', without any express or implied
//% warranty. In no event will the author be held liable for any damages
//% arising from the use of this software.
//% Permission is granted to anyone to use this software for any purpose,
//% including commercial applications, and to alter it and redistribute it
//% freely, subject to the following restrictions:
//% 1. The origin of this software must not be misrepresented; you must not
//% claim that you wrote the original software. If you use this software
//% in a product, an acknowledgment in the product documentation would be
//% appreciated but is not required.
//% 2. Altered source versions must be plainly marked as such, and must not be
//% misrepresented as being the original software.
//% 3. This notice may not be removed or altered from any source distribution.
//% Mark Adler madler@alumni.caltech.edu
//% BEGIN CODE DERIVED FROM puff.{c,h}
#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;
}
//% END CODE DERIVED FROM puff.{c,h}
#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;
}
uint64_t r[3] = {0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff};
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;
intptr_t res = 0;
if (write(1, "executing program\n", sizeof("executing program\n") - 1)) {}
// syz_mount_image$btrfs arguments: [
// fs: ptr[in, buffer] {
// buffer: {62 74 72 66 73 00} (length 0x6)
// }
// dir: ptr[in, buffer] {
// buffer: {2e 2f 66 69 6c 65 31 00} (length 0x8)
// }
// flags: mount_flags = 0x800810 (8 bytes)
// opts: ptr[in, fs_options[btrfs_options]] {
// fs_options[btrfs_options] {
// elems: array[fs_opt_elem[btrfs_options]] {
// fs_opt_elem[btrfs_options] {
// elem: union btrfs_options {
// nossd_spread: buffer: {6e 6f 73 73 64 5f 73 70 72 65 61 64} (length 0xc)
// }
// comma: const = 0x2c (1 bytes)
// }
// fs_opt_elem[btrfs_options] {
// elem: union btrfs_options {
// nodatasum: buffer: {6e 6f 64 61 74 61 73 75 6d} (length 0x9)
// }
// comma: const = 0x2c (1 bytes)
// }
// fs_opt_elem[btrfs_options] {
// elem: union btrfs_options {
// compress_force: buffer: {63 6f 6d 70 72 65 73 73 2d 66 6f 72 63 65} (length 0xe)
// }
// comma: const = 0x2c (1 bytes)
// }
// fs_opt_elem[btrfs_options] {
// elem: union btrfs_options {
// compress_algo: fs_opt["compress", stringnoz[btrfs_compress_algos]] {
// name: buffer: {63 6f 6d 70 72 65 73 73} (length 0x8)
// eq: const = 0x3d (1 bytes)
// val: buffer: {6c 7a 6f} (length 0x3)
// }
// }
// comma: const = 0x2c (1 bytes)
// }
// fs_opt_elem[btrfs_options] {
// elem: union btrfs_options {
// flushoncommit: buffer: {66 6c 75 73 68 6f 6e 63 6f 6d 6d 69 74} (length 0xd)
// }
// comma: const = 0x2c (1 bytes)
// }
// fs_opt_elem[btrfs_options] {
// elem: union btrfs_options {
// autodefrag: buffer: {61 75 74 6f 64 65 66 72 61 67} (length 0xa)
// }
// comma: const = 0x2c (1 bytes)
// }
// fs_opt_elem[btrfs_options] {
// elem: union btrfs_options {
// max_inline: fs_opt["max_inline", array[flags[fs_opt_digits_suffix, int8]]] {
// name: buffer: {6d 61 78 5f 69 6e 6c 69 6e 65} (length 0xa)
// eq: const = 0x3d (1 bytes)
// val: array[fs_opt_digits_suffix] {
// fs_opt_digits_suffix = 0x30 (1 bytes)
// fs_opt_digits_suffix = 0x37 (1 bytes)
// fs_opt_digits_suffix = 0x65 (1 bytes)
// }
// }
// }
// comma: const = 0x2c (1 bytes)
// }
// fs_opt_elem[btrfs_options] {
// elem: union btrfs_options {
// nodiscard: buffer: {6e 6f 64 69 73 63 61 72 64} (length 0x9)
// }
// comma: const = 0x2c (1 bytes)
// }
// }
// common: array[fs_opt_elem[fs_options_common]] {
// }
// null: const = 0x0 (1 bytes)
// }
// }
// chdir: int8 = 0xfb (1 bytes)
// size: len = 0x510a (8 bytes)
// img: ptr[in, buffer] {
// buffer: (compressed buffer with length 0x510a)
// }
// ]
// returns fd_dir
memcpy((void*)0x200000005100, "btrfs\000", 6);
memcpy((void*)0x200000000040, "./file1\000", 8);
memcpy((void*)0x200000000140, "nossd_spread", 12);
*(uint8_t*)0x20000000014c = 0x2c;
memcpy((void*)0x20000000014d, "nodatasum", 9);
*(uint8_t*)0x200000000156 = 0x2c;
memcpy((void*)0x200000000157, "compress-force", 14);
*(uint8_t*)0x200000000165 = 0x2c;
memcpy((void*)0x200000000166, "compress", 8);
*(uint8_t*)0x20000000016e = 0x3d;
memcpy((void*)0x20000000016f, "lzo", 3);
*(uint8_t*)0x200000000172 = 0x2c;
memcpy((void*)0x200000000173, "flushoncommit", 13);
*(uint8_t*)0x200000000180 = 0x2c;
memcpy((void*)0x200000000181, "autodefrag", 10);
*(uint8_t*)0x20000000018b = 0x2c;
memcpy((void*)0x20000000018c, "max_inline", 10);
*(uint8_t*)0x200000000196 = 0x3d;
*(uint8_t*)0x200000000197 = 0x30;
*(uint8_t*)0x200000000198 = 0x37;
*(uint8_t*)0x200000000199 = 0x65;
*(uint8_t*)0x20000000019a = 0x2c;
memcpy((void*)0x20000000019b, "nodiscard", 9);
*(uint8_t*)0x2000000001a4 = 0x2c;
*(uint8_t*)0x2000000001a5 = 0;
memcpy((void*)0x20000000d000, "... [truncated large byte array] ...", 20746);
syz_mount_image(/*fs=*/0x200000005100, /*dir=*/0x200000000040, /*flags=MS_I_VERSION|MS_SYNCHRONOUS|MS_NODIRATIME*/0x800810, /*opts=*/0x200000000140, /*chdir=*/0xfb, /*size=*/0x510a, /*img=*/0x20000000d000);
// openat arguments: [
// fd: fd_dir (resource)
// file: ptr[in, buffer] {
// buffer: {2e 00} (length 0x2)
// }
// flags: open_flags = 0x100 (4 bytes)
// mode: open_mode = 0x52 (2 bytes)
// ]
// returns fd
memcpy((void*)0x200000000000, ".\000", 2);
res = syscall(__NR_openat, /*fd=*/0xffffff9c, /*file=*/0x200000000000ul, /*flags=O_NOCTTY*/0x100, /*mode=S_IWOTH|S_IWGRP|S_IXUSR*/0x52);
if (res != -1)
r[0] = res;
// openat$sysfs arguments: [
// fd: const = 0xffffffffffffff9c (8 bytes)
// dir: ptr[in, buffer] {
// buffer: {2f 73 79 73 2f 70 6f 77 65 72 2f 6d 65 6d 5f 73 6c 65 65 70} (length 0x14)
// }
// flags: open_flags = 0x100102 (4 bytes)
// mode: open_mode = 0x8 (2 bytes)
// ]
// returns fd
memcpy((void*)0x200000000400, "/sys/power/mem_sleep", 20);
syscall(__NR_openat, /*fd=*/0xffffffffffffff9cul, /*dir=*/0x200000000400ul, /*flags=O_NOCTTY|O_RDWR|0x100000*/0x100102, /*mode=S_IXGRP*/8);
// rename arguments: [
// old: ptr[in, buffer] {
// buffer: {2e 2f 66 69 6c 65 31 00} (length 0x8)
// }
// new: ptr[in, buffer] {
// buffer: {2e 02 00} (length 0x3)
// }
// ]
memcpy((void*)0x200000001040, "./file1\000", 8);
memcpy((void*)0x200000000240, ".\002\000", 3);
syscall(__NR_rename, /*old=*/0x200000001040ul, /*new=*/0x200000000240ul);
// rename arguments: [
// old: ptr[in, buffer] {
// buffer: {2e 2f 66 69 6c 65 31 00} (length 0x8)
// }
// new: ptr[in, buffer] {
// buffer: {2e 02 00} (length 0x3)
// }
// ]
memcpy((void*)0x200000000280, "./file1\000", 8);
memcpy((void*)0x2000000002c0, ".\002\000", 3);
syscall(__NR_rename, /*old=*/0x200000000280ul, /*new=*/0x2000000002c0ul);
// openat arguments: [
// fd: fd_dir (resource)
// file: nil
// flags: open_flags = 0xc4042 (4 bytes)
// mode: open_mode = 0x1ff (2 bytes)
// ]
// returns fd
res = syscall(__NR_openat, /*fd=*/0xffffff9c, /*file=*/0ul, /*flags=O_NOATIME|O_DIRECT|O_CREAT|O_CLOEXEC|0x2*/0xc4042, /*mode=S_IXOTH|S_IWOTH|S_IROTH|S_IXGRP|S_IWGRP|S_IRGRP|S_IXUSR|S_IWUSR|0x100*/0x1ff);
if (res != -1)
r[1] = res;
// openat arguments: [
// fd: fd_dir (resource)
// file: ptr[in, buffer] {
// buffer: {2e 2f 66 69 6c 65 31 00} (length 0x8)
// }
// flags: open_flags = 0x8042 (4 bytes)
// mode: open_mode = 0xc6 (2 bytes)
// ]
// returns fd
memcpy((void*)0x200000000980, "./file1\000", 8);
syscall(__NR_openat, /*fd=*/0xffffff9c, /*file=*/0x200000000980ul, /*flags=O_LARGEFILE|O_CREAT|O_RDWR*/0x8042, /*mode=S_IWOTH|S_IROTH|S_IXUSR|S_IWUSR*/0xc6);
// quotactl$Q_QUOTAON arguments: [
// cmd: quota_cmd_quota_on = 0xffffffff80000200 (8 bytes)
// special: nil
// id: uid (resource)
// addr: ptr[in, buffer] {
// buffer: {13 13 77 c5 fc 35 d4 14 54 d5 d4 1d 29 ad 1a 60 29 59 81 46 e6 be 16 6e 41 ad 0d bd 40 54 03 3c 9f 33 bb da 82 24 a2 f3 d7 72 e7 63 6e 48 b3 3c bf 70 83 72 e8 f1 b9 93 3e c5 12 77 43 be 22 06 20 9e f0 2d f9 cb f2 f6 e8 80 d3 38 2f 00} (length 0x4e)
// }
// ]
memcpy((void*)0x200000000280, "... [truncated large byte array] ...", 78);
syscall(__NR_quotactl, /*cmd=Q_QUOTAON_USR*/0xffffffff80000200ul, /*special=*/0ul, /*id=*/0, /*addr=*/0x200000000280ul);
// mkdir arguments: [
// path: nil
// mode: open_mode = 0x102 (8 bytes)
// ]
syscall(__NR_mkdir, /*path=*/0ul, /*mode=S_IWOTH|S_IRUSR*/0x102ul);
// openat arguments: [
// fd: fd_dir (resource)
// file: nil
// flags: open_flags = 0x101042 (4 bytes)
// mode: open_mode = 0x15 (2 bytes)
// ]
// returns fd
syscall(__NR_openat, /*fd=*/0xffffff9c, /*file=*/0ul, /*flags=O_SYNC|O_CREAT|O_RDWR*/0x101042, /*mode=S_IXOTH|S_IROTH|S_IWGRP*/0x15);
// write$FUSE_INIT arguments: [
// fd: fd_fuse (resource)
// arg: nil
// len: bytesize = 0x0 (8 bytes)
// ]
syscall(__NR_write, /*fd=*/(intptr_t)-1, /*arg=*/0ul, /*len=*/0ul);
// openat$nullb arguments: [
// fd: const = 0xffffffffffffff9c (8 bytes)
// file: ptr[in, buffer] {
// buffer: {2f 64 65 76 2f 6e 75 6c 6c 62 30 00} (length 0xc)
// }
// flags: open_flags = 0x42 (4 bytes)
// mode: const = 0x0 (2 bytes)
// ]
// returns fd_block
memcpy((void*)0x2000000001c0, "/dev/nullb0\000", 12);
res = syscall(__NR_openat, /*fd=*/0xffffffffffffff9cul, /*file=*/0x2000000001c0ul, /*flags=O_CREAT|O_RDWR*/0x42, /*mode=*/0);
if (res != -1)
r[2] = res;
// sendfile arguments: [
// fdout: fd (resource)
// fdin: fd (resource)
// off: nil
// count: intptr = 0x20fffe82 (8 bytes)
// ]
syscall(__NR_sendfile, /*fdout=*/r[1], /*fdin=*/r[2], /*off=*/0ul, /*count=*/0x20fffe82ul);
// openat$pfkey arguments: [
// fd: const = 0xffffffffffffff9c (8 bytes)
// file: ptr[in, buffer] {
// buffer: {2f 70 72 6f 63 2f 73 65 6c 66 2f 6e 65 74 2f 70 66 6b 65 79 00} (length 0x15)
// }
// flags: open_flags = 0x20800 (4 bytes)
// mode: const = 0x0 (2 bytes)
// ]
// returns fd
memcpy((void*)0x200000000080, "/proc/self/net/pfkey\000", 21);
syscall(__NR_openat, /*fd=*/0xffffffffffffff9cul, /*file=*/0x200000000080ul, /*flags=O_NONBLOCK|O_NOFOLLOW*/0x20800, /*mode=*/0);
// ioctl$FS_IOC_REMOVE_ENCRYPTION_KEY arguments: [
// fd: fd_dir (resource)
// cmd: const = 0x8004587d (4 bytes)
// arg: ptr[inout, fscrypt_remove_key_arg] {
// fscrypt_remove_key_arg {
// key_spec: union fscrypt_key_specifier {
// id: fscrypt_key_specifier__by_identifier {
// type: const = 0x2 (4 bytes)
// reserved: const = 0x0 (4 bytes)
// identifier: union fscrypt_key_identifier {
// d: buffer: {7e b8 0a f3 f2 4e f0 86 72 6a 4c ea 3a 15 4c e0} (length 0x10)
// }
// reserved2: buffer: {00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00} (length 0x10)
// }
// }
// removal_status_flags: int32 = 0x0 (4 bytes)
// reserved: buffer: {00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00} (length 0x14)
// }
// }
// ]
*(uint32_t*)0x200000000080 = 2;
*(uint32_t*)0x200000000084 = 0;
memcpy((void*)0x200000000088, "~\270\n\363\362N\360\206rjL\352:\025L\340", 16);
memset((void*)0x200000000098, 0, 16);
memset((void*)0x2000000000ac, 0, 20);
syscall(__NR_ioctl, /*fd=*/r[0], /*cmd=*/0x8004587d, /*arg=*/0x200000000080ul);
// openat arguments: [
// fd: fd_dir (resource)
// file: nil
// flags: open_flags = 0x40441 (4 bytes)
// mode: open_mode = 0x106 (2 bytes)
// ]
// returns fd
syscall(__NR_openat, /*fd=*/0xffffff9c, /*file=*/0ul, /*flags=O_NOATIME|O_CREAT|O_APPEND|O_WRONLY*/0x40441, /*mode=S_IWOTH|S_IROTH|S_IRUSR*/0x106);
// syz_mount_image$msdos arguments: [
// fs: ptr[in, buffer] {
// buffer: {6d 73 64 6f 73 00} (length 0x6)
// }
// dir: ptr[in, buffer] {
// buffer: {2e 00} (length 0x2)
// }
// flags: mount_flags = 0x1a6a038 (8 bytes)
// opts: ptr[inout, array[ANYUNION]] {
// array[ANYUNION] {
// }
// }
// chdir: int8 = 0xb (1 bytes)
// size: len = 0x0 (8 bytes)
// img: ptr[in, buffer] {
// buffer: (compressed buffer with length 0x0)
// }
// ]
// returns fd_dir
memcpy((void*)0x200000000f40, "msdos\000", 6);
memcpy((void*)0x200000000f00, ".\000", 2);
syz_mount_image(/*fs=*/0x200000000f40, /*dir=*/0x200000000f00, /*flags=MS_I_VERSION|MS_PRIVATE|MS_UNBINDABLE|MS_SYNCHRONOUS|MS_STRICTATIME|MS_SILENT|0x202028*/0x1a6a038, /*opts=*/0x200000002cc0, /*chdir=*/0xb, /*size=*/0, /*img=*/0x200000000000);
return 0;
}
]
|
| 2/1 |
2026/06/01 16:33 |
action |
kernel-checkouter |
2m
Results: map[KernelSrc:/app/workdir/cache/src/80153c45923bfc3aacb7708cb5d19b4c6e47c097]
|
| 3/1 |
2026/06/01 16:35 |
action |
kernel-builder |
25m
Results: map[KernelObj:/app/workdir/cache/build/386c8ca0f19b0b29abf98eb449da6a08d4ef345d]
|
| 4/1 |
2026/06/01 17:01 |
action |
codesearch-prepare |
14m
Results: map[Index:codesearch-index]
|
| 5/1 |
2026/06/01 17:16 |
agent |
expert |
3m
Model:
Results: map[DenialOfService:false Exploitable:false FilesystemTrigger:true NetworkTrigger:false PeripheralTrigger:true RemoteTrigger:false Unprivileged:false UserNamespace:false VMGuestTrigger:false VMHostTrigger:false]
Instruction: You are an experienced Linux kernel security engineer. Your task is to analyze given kernel bug report
and determine its security impact based on the following dimensions.
Use the provided tools to examine the source code, check for capability checks (e.g., capable(), ns_capable()),
and understand the nature of the bug. Analyze the given kernel build and configuration.
You can check the kernel config by grepping ".config" file; you can check kernel cmdline by grepping
".config" file for "CONFIG_CMDLINE=". Assume sysctl parameters have default values.
But analyze for the corresponding production build w/o debugging tools enabled (like KASAN, KMSAN, UBSAN).
Try different strategies when analyzing the bug:
- think of ways in which the vulnerable code is unreachable
- or the other way around: try to come up with different ideas of how an unprivileged user can reach the bug
If still unsure err on the side of the bug being non-exploitable/not-accessible.
In the final reply, provide a reasoning for your assessment.
Analysis dimensions:
* Exploitable:
Determine if the bug can result in memory corruption or elevated privileges.
Memory safety issues are almost always exploitable (KASAN or UBSAN reports for use-after-free, out-of-bounds;
refcounting issues, corrupted lists, etc). When kernel is crashing on a completely wild pointer access
(e.g. user-space address, or non-canonical address, but not on NULL or address corresponding to KASAN shadow
for NULL address), including both data accesses and control transfers, that also usually implies possibility
of exploitation. Such reports usually say "unable to handle kernel paging request".
Uses of uninitialized values detected by KMSAN may be exploitable b/c attacker frequently can affect uninit
values with spraying techniques. However, for these exploitability depends on how exactly the uninit value
is used in the code, and what it affects.
Think of what happens after the bug is triggered. Some bugs cause kernel panic and halt execution,
they are harder to exploit. For example, BUG reports halts the kernel. However, WARNING reports don't halt
execution in production builds. Debug bug detection tools (like KASAN, KMSAN, KCSAN, UBSAN) are also not enabled
in production builds, so attacker can freely exploit these bugs w/o being detected by these tools.
If you see an integer overflow, think how the overflowed value used later (if it's used as allocation size,
or an array index). If you see an out-of-bounds read, think if it's followed by an out-of-bounds write as well.
Some KCSAN data-races may be exploitable by skilled attackers as well. Think what data structures got corrupted
as the result of data races and how. However, note that kernel has lots of "benign" data races that don't lead
to any runtime misbehavior at all.
* Denial Of Service:
Determine if the bug can result in denial-of-service. Most bugs can, since they cause system crash,
hangs, deadlocks, or resource leaks. This is mostly applicable to WARNING bugs that won't cause system crash
in production. For these think what will be consequences of the violation of the kernel assumptions flagged
by the WARNING. In some cases the unexpected condition is also properly handled by the normal control flow
(e.g. with "if (WARN_ON(...))"), these won't cause denial-of-service. If the condition is not handled,
then it may or may not cause denial-of-service.
* Accessible From Unprivileged Processes:
Determine if the bug can be reached from a typical (non-root) user process that does NOT have any special capabilities
(like CAP_SYS_ADMIN, CAP_NET_ADMIN, CAP_NET_RAW, CAP_PERFMON) or access to device nodes restricted to root.
Assume that unprivileged_bpf_disabled=1, that is eBPF loading is not accessible. However, cBPF (classical BPF)
is still accessible to non-root processes.
Assume that user namespaces are not accessible, that is, the process cannot get the mentioned capabilities even
within a new user namespace (checked by ns_capable() function in the kernel sources).
* Accessible From User Namespaces:
Determine if the bug can be reached within a user-namespace where the process has all capabilities
(including CAP_SYS_ADMIN, CAP_NET_ADMIN, CAP_NET_RAW, CAP_PERFMON). Such capabilities are checked with ns_capable()
function in the kernel sources.
* VM Guest Trigger:
Determine if the bug can be triggered from the context of a typical KVM guest (e.g., set up by a QEMU VMM).
Consider accesses to standard Linux host paravirtualized features (virtio-blk, virtio-net, etc.),
and handling of VM exits in the KVM code.
* VM Host Trigger in The Confidential Computing Context:
Determine if the bug can be triggered in a confidential computing guest kernel from the context of a KVM host.
Consider access to standard Linux guest paravirtualized features (virtio-blk, virtio-net, etc.).
* Ethernet Network Trigger:
Determine if the bug can be triggered by processing ingress network Ethernet traffic, either directly (network stack)
or via drivers exposed to network data.
* Other Remote Trigger:
Determine if the bug can be triggered by processing remote traffic other than Ethernet (Wifi, Bluetooth, NFC, etc).
* Peripheral Trigger:
Determine if the bug can be triggered via an untrusted peripheral device that can be physically plugged
into a system, such as a USB device or a niche hardware driver handling external hardware inputs.
This is particularly important for mobile and desktop environments where users can plug in unknown devices.
* Malicious Filesystem Trigger:
Determine if the bug can be triggered by the kernel mounting and parsing a malicious filesystem image.
This is highly critical for Desktop and Mobile environments where external media or downloaded images
might be auto-mounted.
Don't make assumptions about the kernel source code (it may be different from what you assume it is).
Extensively use the provided code access tools (codesearch-*, git-*, grepper, etc)
to examine the actual source code, and confirm any assumptions.
Prefer calling several tools at the same time to save round-trips.
Use set-results tool to provide results of the analysis.
It must be called exactly once before the final reply.
Ignore results of this tool.
Prompt:
The kernel bug report is:
------------[ cut here ]------------
!new_inode
WARNING: fs/btrfs/inode.c:8616 at btrfs_rename fs/btrfs/inode.c:8616 [inline], CPU#1: syz.1.22/6085
WARNING: fs/btrfs/inode.c:8616 at btrfs_rename2+0x1c07/0x40d0 fs/btrfs/inode.c:8854, CPU#1: syz.1.22/6085
Modules linked in:
CPU: 1 UID: 0 PID: 6085 Comm: syz.1.22 Not tainted syzkaller #0 PREEMPT_{RT,(full)}
Hardware name: Google Google Compute Engine/Google Compute Engine, BIOS Google 04/18/2026
RIP: 0010:btrfs_rename fs/btrfs/inode.c:8616 [inline]
RIP: 0010:btrfs_rename2+0x1c07/0x40d0 fs/btrfs/inode.c:8854
Code: 00 e8 4d 43 dc fd 41 80 f6 01 45 0f b6 c6 48 8b 7c 24 18 48 c7 c6 46 09 67 8d ba 3b 21 00 00 e9 53 03 00 00 e8 2a 43 dc fd 90 <0f> 0b 90 41 bf ef ff ff ff e9 f2 f9 ff ff 4c 89 7c 24 08 4c 8b 7c
RSP: 0018:ffffc900046cf800 EFLAGS: 00010293
RAX: ffffffff83e83636 RBX: fffff520008d9f24 RCX: ffff888029319ec0
RDX: 0000000000000000 RSI: 0000000000000000 RDI: 0000000000000000
RBP: ffffc900046cfbb0 R08: ffff888029319ec0 R09: 0000000000000002
R10: 0000000000000100 R11: 00000000ffffffef R12: ffffc900046cfa38
R13: 1ffff1100bdaf993 R14: 00000000ffffffef R15: ffff8880457d4c58
FS: 00007f08aa1556c0(0000) GS:ffff888126274000(0000) knlGS:0000000000000000
CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: 00007f6dee4c5000 CR3: 000000003d816000 CR4: 00000000003526f0
Call Trace:
<TASK>
vfs_rename+0xa96/0xeb0 fs/namei.c:6054
filename_renameat2+0x539/0x9c0 fs/namei.c:6172
__do_sys_rename fs/namei.c:6216 [inline]
__se_sys_rename+0x55/0x2c0 fs/namei.c:6212
do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline]
do_syscall_64+0x15f/0xf80 arch/x86/entry/syscall_64.c:94
entry_SYSCALL_64_after_hwframe+0x77/0x7f
RIP: 0033:0x7f08aab1ce59
Code: ff c3 66 2e 0f 1f 84 00 00 00 00 00 0f 1f 44 00 00 48 89 f8 48 89 f7 48 89 d6 48 89 ca 4d 89 c2 4d 89 c8 4c 8b 4c 24 08 0f 05 <48> 3d 01 f0 ff ff 73 01 c3 48 c7 c1 e8 ff ff ff f7 d8 64 89 01 48
RSP: 002b:00007f08aa155028 EFLAGS: 00000246 ORIG_RAX: 0000000000000052
RAX: ffffffffffffffda RBX: 00007f08aad96090 RCX: 00007f08aab1ce59
RDX: 0000000000000000 RSI: 00002000000002c0 RDI: 0000200000000280
RBP: 00007f08aabb2d6f R08: 0000000000000000 R09: 0000000000000000
R10: 0000000000000000 R11: 0000000000000246 R12: 0000000000000000
R13: 00007f08aad96128 R14: 00007f08aad96090 R15: 00007ffc34538de8
</TASK>
It is reproducible with the followint program.
Keep in mind that it may lack the precise threading, sandboxing, and some arguments of a working reproducer.
But it should give an idea of the involved syscalls.
// autogenerated by syzkaller (https://github.com/google/syzkaller)
#define _GNU_SOURCE
#include <endian.h>
#include <errno.h>
#include <fcntl.h>
#include <setjmp.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/stat.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <unistd.h>
#include <linux/loop.h>
#ifndef __NR_memfd_create
#define __NR_memfd_create 319
#endif
static unsigned long long procid;
//% This code is derived from puff.{c,h}, found in the zlib development. The
//% original files come with the following copyright notice:
//% Copyright (C) 2002-2013 Mark Adler, all rights reserved
//% version 2.3, 21 Jan 2013
//% This software is provided 'as-is', without any express or implied
//% warranty. In no event will the author be held liable for any damages
//% arising from the use of this software.
//% Permission is granted to anyone to use this software for any purpose,
//% including commercial applications, and to alter it and redistribute it
//% freely, subject to the following restrictions:
//% 1. The origin of this software must not be misrepresented; you must not
//% claim that you wrote the original software. If you use this software
//% in a product, an acknowledgment in the product documentation would be
//% appreciated but is not required.
//% 2. Altered source versions must be plainly marked as such, and must not be
//% misrepresented as being the original software.
//% 3. This notice may not be removed or altered from any source distribution.
//% Mark Adler madler@alumni.caltech.edu
//% BEGIN CODE DERIVED FROM puff.{c,h}
#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;
}
//% END CODE DERIVED FROM puff.{c,h}
#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;
}
uint64_t r[3] = {0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff};
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;
intptr_t res = 0;
if (write(1, "executing program\n", sizeof("executing program\n") - 1)) {}
// syz_mount_image$btrfs arguments: [
// fs: ptr[in, buffer] {
// buffer: {62 74 72 66 73 00} (length 0x6)
// }
// dir: ptr[in, buffer] {
// buffer: {2e 2f 66 69 6c 65 31 00} (length 0x8)
// }
// flags: mount_flags = 0x800810 (8 bytes)
// opts: ptr[in, fs_options[btrfs_options]] {
// fs_options[btrfs_options] {
// elems: array[fs_opt_elem[btrfs_options]] {
// fs_opt_elem[btrfs_options] {
// elem: union btrfs_options {
// nossd_spread: buffer: {6e 6f 73 73 64 5f 73 70 72 65 61 64} (length 0xc)
// }
// comma: const = 0x2c (1 bytes)
// }
// fs_opt_elem[btrfs_options] {
// elem: union btrfs_options {
// nodatasum: buffer: {6e 6f 64 61 74 61 73 75 6d} (length 0x9)
// }
// comma: const = 0x2c (1 bytes)
// }
// fs_opt_elem[btrfs_options] {
// elem: union btrfs_options {
// compress_force: buffer: {63 6f 6d 70 72 65 73 73 2d 66 6f 72 63 65} (length 0xe)
// }
// comma: const = 0x2c (1 bytes)
// }
// fs_opt_elem[btrfs_options] {
// elem: union btrfs_options {
// compress_algo: fs_opt["compress", stringnoz[btrfs_compress_algos]] {
// name: buffer: {63 6f 6d 70 72 65 73 73} (length 0x8)
// eq: const = 0x3d (1 bytes)
// val: buffer: {6c 7a 6f} (length 0x3)
// }
// }
// comma: const = 0x2c (1 bytes)
// }
// fs_opt_elem[btrfs_options] {
// elem: union btrfs_options {
// flushoncommit: buffer: {66 6c 75 73 68 6f 6e 63 6f 6d 6d 69 74} (length 0xd)
// }
// comma: const = 0x2c (1 bytes)
// }
// fs_opt_elem[btrfs_options] {
// elem: union btrfs_options {
// autodefrag: buffer: {61 75 74 6f 64 65 66 72 61 67} (length 0xa)
// }
// comma: const = 0x2c (1 bytes)
// }
// fs_opt_elem[btrfs_options] {
// elem: union btrfs_options {
// max_inline: fs_opt["max_inline", array[flags[fs_opt_digits_suffix, int8]]] {
// name: buffer: {6d 61 78 5f 69 6e 6c 69 6e 65} (length 0xa)
// eq: const = 0x3d (1 bytes)
// val: array[fs_opt_digits_suffix] {
// fs_opt_digits_suffix = 0x30 (1 bytes)
// fs_opt_digits_suffix = 0x37 (1 bytes)
// fs_opt_digits_suffix = 0x65 (1 bytes)
// }
// }
// }
// comma: const = 0x2c (1 bytes)
// }
// fs_opt_elem[btrfs_options] {
// elem: union btrfs_options {
// nodiscard: buffer: {6e 6f 64 69 73 63 61 72 64} (length 0x9)
// }
// comma: const = 0x2c (1 bytes)
// }
// }
// common: array[fs_opt_elem[fs_options_common]] {
// }
// null: const = 0x0 (1 bytes)
// }
// }
// chdir: int8 = 0xfb (1 bytes)
// size: len = 0x510a (8 bytes)
// img: ptr[in, buffer] {
// buffer: (compressed buffer with length 0x510a)
// }
// ]
// returns fd_dir
memcpy((void*)0x200000005100, "btrfs\000", 6);
memcpy((void*)0x200000000040, "./file1\000", 8);
memcpy((void*)0x200000000140, "nossd_spread", 12);
*(uint8_t*)0x20000000014c = 0x2c;
memcpy((void*)0x20000000014d, "nodatasum", 9);
*(uint8_t*)0x200000000156 = 0x2c;
memcpy((void*)0x200000000157, "compress-force", 14);
*(uint8_t*)0x200000000165 = 0x2c;
memcpy((void*)0x200000000166, "compress", 8);
*(uint8_t*)0x20000000016e = 0x3d;
memcpy((void*)0x20000000016f, "lzo", 3);
*(uint8_t*)0x200000000172 = 0x2c;
memcpy((void*)0x200000000173, "flushoncommit", 13);
*(uint8_t*)0x200000000180 = 0x2c;
memcpy((void*)0x200000000181, "autodefrag", 10);
*(uint8_t*)0x20000000018b = 0x2c;
memcpy((void*)0x20000000018c, "max_inline", 10);
*(uint8_t*)0x200000000196 = 0x3d;
*(uint8_t*)0x200000000197 = 0x30;
*(uint8_t*)0x200000000198 = 0x37;
*(uint8_t*)0x200000000199 = 0x65;
*(uint8_t*)0x20000000019a = 0x2c;
memcpy((void*)0x20000000019b, "nodiscard", 9);
*(uint8_t*)0x2000000001a4 = 0x2c;
*(uint8_t*)0x2000000001a5 = 0;
memcpy((void*)0x20000000d000, "... [truncated large byte array] ...", 20746);
syz_mount_image(/*fs=*/0x200000005100, /*dir=*/0x200000000040, /*flags=MS_I_VERSION|MS_SYNCHRONOUS|MS_NODIRATIME*/0x800810, /*opts=*/0x200000000140, /*chdir=*/0xfb, /*size=*/0x510a, /*img=*/0x20000000d000);
// openat arguments: [
// fd: fd_dir (resource)
// file: ptr[in, buffer] {
// buffer: {2e 00} (length 0x2)
// }
// flags: open_flags = 0x100 (4 bytes)
// mode: open_mode = 0x52 (2 bytes)
// ]
// returns fd
memcpy((void*)0x200000000000, ".\000", 2);
res = syscall(__NR_openat, /*fd=*/0xffffff9c, /*file=*/0x200000000000ul, /*flags=O_NOCTTY*/0x100, /*mode=S_IWOTH|S_IWGRP|S_IXUSR*/0x52);
if (res != -1)
r[0] = res;
// openat$sysfs arguments: [
// fd: const = 0xffffffffffffff9c (8 bytes)
// dir: ptr[in, buffer] {
// buffer: {2f 73 79 73 2f 70 6f 77 65 72 2f 6d 65 6d 5f 73 6c 65 65 70} (length 0x14)
// }
// flags: open_flags = 0x100102 (4 bytes)
// mode: open_mode = 0x8 (2 bytes)
// ]
// returns fd
memcpy((void*)0x200000000400, "/sys/power/mem_sleep", 20);
syscall(__NR_openat, /*fd=*/0xffffffffffffff9cul, /*dir=*/0x200000000400ul, /*flags=O_NOCTTY|O_RDWR|0x100000*/0x100102, /*mode=S_IXGRP*/8);
// rename arguments: [
// old: ptr[in, buffer] {
// buffer: {2e 2f 66 69 6c 65 31 00} (length 0x8)
// }
// new: ptr[in, buffer] {
// buffer: {2e 02 00} (length 0x3)
// }
// ]
memcpy((void*)0x200000001040, "./file1\000", 8);
memcpy((void*)0x200000000240, ".\002\000", 3);
syscall(__NR_rename, /*old=*/0x200000001040ul, /*new=*/0x200000000240ul);
// rename arguments: [
// old: ptr[in, buffer] {
// buffer: {2e 2f 66 69 6c 65 31 00} (length 0x8)
// }
// new: ptr[in, buffer] {
// buffer: {2e 02 00} (length 0x3)
// }
// ]
memcpy((void*)0x200000000280, "./file1\000", 8);
memcpy((void*)0x2000000002c0, ".\002\000", 3);
syscall(__NR_rename, /*old=*/0x200000000280ul, /*new=*/0x2000000002c0ul);
// openat arguments: [
// fd: fd_dir (resource)
// file: nil
// flags: open_flags = 0xc4042 (4 bytes)
// mode: open_mode = 0x1ff (2 bytes)
// ]
// returns fd
res = syscall(__NR_openat, /*fd=*/0xffffff9c, /*file=*/0ul, /*flags=O_NOATIME|O_DIRECT|O_CREAT|O_CLOEXEC|0x2*/0xc4042, /*mode=S_IXOTH|S_IWOTH|S_IROTH|S_IXGRP|S_IWGRP|S_IRGRP|S_IXUSR|S_IWUSR|0x100*/0x1ff);
if (res != -1)
r[1] = res;
// openat arguments: [
// fd: fd_dir (resource)
// file: ptr[in, buffer] {
// buffer: {2e 2f 66 69 6c 65 31 00} (length 0x8)
// }
// flags: open_flags = 0x8042 (4 bytes)
// mode: open_mode = 0xc6 (2 bytes)
// ]
// returns fd
memcpy((void*)0x200000000980, "./file1\000", 8);
syscall(__NR_openat, /*fd=*/0xffffff9c, /*file=*/0x200000000980ul, /*flags=O_LARGEFILE|O_CREAT|O_RDWR*/0x8042, /*mode=S_IWOTH|S_IROTH|S_IXUSR|S_IWUSR*/0xc6);
// quotactl$Q_QUOTAON arguments: [
// cmd: quota_cmd_quota_on = 0xffffffff80000200 (8 bytes)
// special: nil
// id: uid (resource)
// addr: ptr[in, buffer] {
// buffer: {13 13 77 c5 fc 35 d4 14 54 d5 d4 1d 29 ad 1a 60 29 59 81 46 e6 be 16 6e 41 ad 0d bd 40 54 03 3c 9f 33 bb da 82 24 a2 f3 d7 72 e7 63 6e 48 b3 3c bf 70 83 72 e8 f1 b9 93 3e c5 12 77 43 be 22 06 20 9e f0 2d f9 cb f2 f6 e8 80 d3 38 2f 00} (length 0x4e)
// }
// ]
memcpy((void*)0x200000000280, "... [truncated large byte array] ...", 78);
syscall(__NR_quotactl, /*cmd=Q_QUOTAON_USR*/0xffffffff80000200ul, /*special=*/0ul, /*id=*/0, /*addr=*/0x200000000280ul);
// mkdir arguments: [
// path: nil
// mode: open_mode = 0x102 (8 bytes)
// ]
syscall(__NR_mkdir, /*path=*/0ul, /*mode=S_IWOTH|S_IRUSR*/0x102ul);
// openat arguments: [
// fd: fd_dir (resource)
// file: nil
// flags: open_flags = 0x101042 (4 bytes)
// mode: open_mode = 0x15 (2 bytes)
// ]
// returns fd
syscall(__NR_openat, /*fd=*/0xffffff9c, /*file=*/0ul, /*flags=O_SYNC|O_CREAT|O_RDWR*/0x101042, /*mode=S_IXOTH|S_IROTH|S_IWGRP*/0x15);
// write$FUSE_INIT arguments: [
// fd: fd_fuse (resource)
// arg: nil
// len: bytesize = 0x0 (8 bytes)
// ]
syscall(__NR_write, /*fd=*/(intptr_t)-1, /*arg=*/0ul, /*len=*/0ul);
// openat$nullb arguments: [
// fd: const = 0xffffffffffffff9c (8 bytes)
// file: ptr[in, buffer] {
// buffer: {2f 64 65 76 2f 6e 75 6c 6c 62 30 00} (length 0xc)
// }
// flags: open_flags = 0x42 (4 bytes)
// mode: const = 0x0 (2 bytes)
// ]
// returns fd_block
memcpy((void*)0x2000000001c0, "/dev/nullb0\000", 12);
res = syscall(__NR_openat, /*fd=*/0xffffffffffffff9cul, /*file=*/0x2000000001c0ul, /*flags=O_CREAT|O_RDWR*/0x42, /*mode=*/0);
if (res != -1)
r[2] = res;
// sendfile arguments: [
// fdout: fd (resource)
// fdin: fd (resource)
// off: nil
// count: intptr = 0x20fffe82 (8 bytes)
// ]
syscall(__NR_sendfile, /*fdout=*/r[1], /*fdin=*/r[2], /*off=*/0ul, /*count=*/0x20fffe82ul);
// openat$pfkey arguments: [
// fd: const = 0xffffffffffffff9c (8 bytes)
// file: ptr[in, buffer] {
// buffer: {2f 70 72 6f 63 2f 73 65 6c 66 2f 6e 65 74 2f 70 66 6b 65 79 00} (length 0x15)
// }
// flags: open_flags = 0x20800 (4 bytes)
// mode: const = 0x0 (2 bytes)
// ]
// returns fd
memcpy((void*)0x200000000080, "/proc/self/net/pfkey\000", 21);
syscall(__NR_openat, /*fd=*/0xffffffffffffff9cul, /*file=*/0x200000000080ul, /*flags=O_NONBLOCK|O_NOFOLLOW*/0x20800, /*mode=*/0);
// ioctl$FS_IOC_REMOVE_ENCRYPTION_KEY arguments: [
// fd: fd_dir (resource)
// cmd: const = 0x8004587d (4 bytes)
// arg: ptr[inout, fscrypt_remove_key_arg] {
// fscrypt_remove_key_arg {
// key_spec: union fscrypt_key_specifier {
// id: fscrypt_key_specifier__by_identifier {
// type: const = 0x2 (4 bytes)
// reserved: const = 0x0 (4 bytes)
// identifier: union fscrypt_key_identifier {
// d: buffer: {7e b8 0a f3 f2 4e f0 86 72 6a 4c ea 3a 15 4c e0} (length 0x10)
// }
// reserved2: buffer: {00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00} (length 0x10)
// }
// }
// removal_status_flags: int32 = 0x0 (4 bytes)
// reserved: buffer: {00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00} (length 0x14)
// }
// }
// ]
*(uint32_t*)0x200000000080 = 2;
*(uint32_t*)0x200000000084 = 0;
memcpy((void*)0x200000000088, "~\270\n\363\362N\360\206rjL\352:\025L\340", 16);
memset((void*)0x200000000098, 0, 16);
memset((void*)0x2000000000ac, 0, 20);
syscall(__NR_ioctl, /*fd=*/r[0], /*cmd=*/0x8004587d, /*arg=*/0x200000000080ul);
// openat arguments: [
// fd: fd_dir (resource)
// file: nil
// flags: open_flags = 0x40441 (4 bytes)
// mode: open_mode = 0x106 (2 bytes)
// ]
// returns fd
syscall(__NR_openat, /*fd=*/0xffffff9c, /*file=*/0ul, /*flags=O_NOATIME|O_CREAT|O_APPEND|O_WRONLY*/0x40441, /*mode=S_IWOTH|S_IROTH|S_IRUSR*/0x106);
// syz_mount_image$msdos arguments: [
// fs: ptr[in, buffer] {
// buffer: {6d 73 64 6f 73 00} (length 0x6)
// }
// dir: ptr[in, buffer] {
// buffer: {2e 00} (length 0x2)
// }
// flags: mount_flags = 0x1a6a038 (8 bytes)
// opts: ptr[inout, array[ANYUNION]] {
// array[ANYUNION] {
// }
// }
// chdir: int8 = 0xb (1 bytes)
// size: len = 0x0 (8 bytes)
// img: ptr[in, buffer] {
// buffer: (compressed buffer with length 0x0)
// }
// ]
// returns fd_dir
memcpy((void*)0x200000000f40, "msdos\000", 6);
memcpy((void*)0x200000000f00, ".\000", 2);
syz_mount_image(/*fs=*/0x200000000f40, /*dir=*/0x200000000f00, /*flags=MS_I_VERSION|MS_PRIVATE|MS_UNBINDABLE|MS_SYNCHRONOUS|MS_STRICTATIME|MS_SILENT|0x202028*/0x1a6a038, /*opts=*/0x200000002cc0, /*chdir=*/0xb, /*size=*/0, /*img=*/0x200000000000);
return 0;
}
Reply: The bug report indicates a `WARN_ON(!new_inode)` being triggered in `btrfs_rename` (specifically in `fs/btrfs/inode.c`).
### Root Cause Analysis
The warning is triggered by the following code block in `btrfs_rename`:
```c
ret = btrfs_check_dir_item_collision(dest, new_dir->i_ino, &new_fname.disk_name);
if (ret) {
if (ret == -EEXIST) {
/* we shouldn't get
* eexist without a new_inode */
if (WARN_ON(!new_inode)) {
goto out_fscrypt_names;
}
} else {
/* maybe -EOVERFLOW */
goto out_fscrypt_names;
}
}
```
When a file is renamed, `btrfs_rename` checks if the destination name already exists in the directory b-tree by calling `btrfs_check_dir_item_collision`. If the exact name is found, it returns `-EEXIST`.
Normally, if the destination file exists, the VFS (Virtual File System) layer would have already found it during the path lookup phase (`lookup_one_qstr_excl`), and `new_inode` would point to the existing file's inode. In this case, `-EEXIST` is expected, the warning is bypassed, and the rename operation proceeds to overwrite the existing file.
However, if `new_inode` is `NULL`, it means the VFS lookup determined that the destination file does *not* exist (resulting in a negative dentry). If `btrfs_check_dir_item_collision` then finds the name in the b-tree, it reveals a structural inconsistency in the filesystem: a directory item exists for the name, but the corresponding inode item is missing.
When the VFS performs the lookup, `btrfs_lookup_dentry` finds the directory item but fails to find the inode item (`btrfs_iget` returns `-ENOENT`), causing it to return a negative dentry. Later, `btrfs_rename` finds the orphaned directory item and triggers the `WARN_ON`.
### Security Impact
This inconsistency can only be triggered by a corrupted or maliciously crafted BTRFS image.
- **Exploitable:** No. The bug is merely a `WARN_ON` that detects filesystem corruption. The unexpected condition is properly handled by the `goto out_fscrypt_names;` error path, which safely aborts the operation and returns `-EEXIST` to the user-space caller. There is no memory corruption, out-of-bounds access, or privilege escalation.
- **Denial of Service:** No. The warning does not halt or crash the system in production builds (unless `panic_on_warn` is explicitly enabled by the administrator, which is not the default). It simply returns an error.
- **Access Vectors:** Mounting a BTRFS filesystem requires `CAP_SYS_ADMIN` in the initial namespace (BTRFS does not support `FS_USERNS_MOUNT`). Therefore, unprivileged local users or users in a user namespace cannot trigger this bug directly. However, it can be triggered if a system is configured to auto-mount external media (e.g., a malicious USB drive plugged into a desktop machine).
In conclusion, this is a benign warning designed to catch filesystem corruption and gracefully error out, posing no significant security risk.
|
| 6/2 |
2026/06/01 17:16 |
llm |
expert |
0m
Model:
Tokens:
input: 19135
output: 38
thoughts: 159
Thoughts: **Analyzing a Btrfs Bug Report**
Okay, here's what I'm seeing: A syzkaller bug report has flagged a potential issue within the Btrfs filesystem code. The warnings specifically point to `fs/btrfs/inode.c`, with the critical line number being 8616. It looks like it's within the `btrfs_rename` function, or at least called by that function. The second warning gives a similar issue within `btrfs_rename2`, which I'm inferring is an extended variant of the `rename` call. I need to dive deeper. My next step is clearly to examine the code around that line β line 8616 in `inode.c`. I need to understand what's happening there to see if the warnings reveal the root cause or provide some critical clue.
|
| 7/2 |
2026/06/01 17:16 |
tool |
read-file |
0m
Args: map[File:fs/btrfs/inode.c FirstLine:8600 LineCount:50]
Results: map[Contents:8600: ret = fscrypt_setup_filename(old_dir, &old_dentry->d_name, 0, &old_fname);
8601: if (ret)
8602: return ret;
8603:
8604: ret = fscrypt_setup_filename(new_dir, &new_dentry->d_name, 0, &new_fname);
8605: if (ret) {
8606: fscrypt_free_filename(&old_fname);
8607: return ret;
8608: }
8609:
8610: /* check for collisions, even if the name isn't there */
8611: ret = btrfs_check_dir_item_collision(dest, new_dir->i_ino, &new_fname.disk_name);
8612: if (ret) {
8613: if (ret == -EEXIST) {
8614: /* we shouldn't get
8615: * eexist without a new_inode */
8616: if (WARN_ON(!new_inode)) {
8617: goto out_fscrypt_names;
8618: }
8619: } else {
8620: /* maybe -EOVERFLOW */
8621: goto out_fscrypt_names;
8622: }
8623: }
8624: ret = 0;
8625:
8626: /*
8627: * we're using rename to replace one file with another. Start IO on it
8628: * now so we don't add too much work to the end of the transaction
8629: */
8630: if (new_inode && S_ISREG(old_inode->i_mode) && new_inode->i_size)
8631: filemap_flush(old_inode->i_mapping);
8632:
8633: if (flags & RENAME_WHITEOUT) {
8634: whiteout_args.inode = new_whiteout_inode(idmap, old_dir);
8635: if (!whiteout_args.inode) {
8636: ret = -ENOMEM;
8637: goto out_fscrypt_names;
8638: }
8639: ret = btrfs_new_inode_prepare(&whiteout_args, &trans_num_items);
8640: if (ret)
8641: goto out_whiteout_inode;
8642: } else {
8643: /* 1 to update the old parent inode. */
8644: trans_num_items = 1;
8645: }
8646:
8647: if (old_ino == BTRFS_FIRST_FREE_OBJECTID) {
8648: /* Close the race window with snapshot create/destroy ioctl */
8649: down_read(&fs_info->subvol_sem);
]
|
| 8/2 |
2026/06/01 17:16 |
llm |
expert |
0m
Model:
Tokens:
input: 20335
output: 44
thoughts: 302
Thoughts: **Analyzing a Btrfs Rename Warning**
Okay, so I'm looking at this chunk of code from `btrfs_rename`, specifically this `WARN_ON(!new_inode)` check. It's triggered when `btrfs_check_dir_item_collision` returns `-EEXIST`, indicating a name collision, but critically, the `new_inode` pointer is NULL. This is the inode associated with the *destination* file in the rename operation.
Let me break this down. A `-EEXIST` from that collision check usually implies the destination file already *exists* in the filesystem and that the name already exists in the B-tree. However, `new_inode` being NULL suggests that at the VFS layer, we *didn't* have an existing inode for the destination. This is unexpected. It implies that the rename is occurring on a filename that already exists in the B-tree, but the VFS isn't aware of it. This could happen if the VFS cache is stale or if we're dealing with a negative dentry.
So, the potential scenarios here are: Either the B-tree contains an entry for the destination name, but the VFS cache doesn't, or there is a hash collision.
Now, a crucial question arises. Does `btrfs_check_dir_item_collision` check for *both* hash collisions *and* exact name collisions, or is it specifically looking for exact matches? I need to double-check the implementation of that function to fully understand the root cause of this warning. This distinction is vital for figuring out if we're dealing with a genuine filesystem inconsistency or a more subtle hashing issue. This could be a race condition, a bug in caching, or a legitimate collision that the VFS isn't handling correctly.
|
| 9/2 |
2026/06/01 17:16 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/btrfs/inode.c Name:btrfs_check_dir_item_collision]
Results: map[SourceCode: 230: int btrfs_check_dir_item_collision(struct btrfs_root *root, u64 dir_ino,
231: const struct fscrypt_str *name)
232: {
233: int ret;
234: struct btrfs_key key;
235: struct btrfs_dir_item *di;
236: int data_size;
237: struct extent_buffer *leaf;
238: int slot;
239: BTRFS_PATH_AUTO_FREE(path);
240:
241: path = btrfs_alloc_path();
242: if (!path)
243: return -ENOMEM;
244:
245: key.objectid = dir_ino;
246: key.type = BTRFS_DIR_ITEM_KEY;
247: key.offset = btrfs_name_hash(name->name, name->len);
248:
249: di = btrfs_lookup_match_dir(NULL, root, path, &key, name->name,
250: name->len, 0);
251: if (IS_ERR(di)) {
252: ret = PTR_ERR(di);
253: /* Nothing found, we're safe */
254: if (ret == -ENOENT)
255: return 0;
256: return ret;
257: }
258:
259: /* we found an item, look for our name in the item */
260: if (di) {
261: /* our exact name was found */
262: return -EEXIST;
263: }
264:
265: /* See if there is room in the item to insert this name. */
266: data_size = sizeof(*di) + name->len;
267: leaf = path->nodes[0];
268: slot = path->slots[0];
269: if (data_size + btrfs_item_size(leaf, slot) +
270: sizeof(struct btrfs_item) > BTRFS_LEAF_DATA_SIZE(root->fs_info)) {
271: return -EOVERFLOW;
272: }
273:
274: /* Plenty of insertion room. */
275: return 0;
276: }
SourceFile:fs/btrfs/dir-item.c]
|
| 10/2 |
2026/06/01 17:16 |
llm |
expert |
0m
Model:
Tokens:
input: 21797
output: 38
thoughts: 1211
Thoughts: **Delving into the Btrfs Rename Collision Issue**
Okay, so `btrfs_check_dir_item_collision` returns `-EEXIST` when a name *exactly* matches, which is expected. But if the exact name is already present, why isn't VFS aware of it? The clue is in the reproducer β it uses `fscrypt`!
Looking closer at the reproducer's code, I see the call to `ioctl` with `FS_IOC_REMOVE_ENCRYPTION_KEY`. Removing the key is what locks the directory or essentially encrypts the names. The reproducer does the rename *before* key removal, which seems odd initially. It appears to:
1. Mount the Btrfs image.
2. Open the directory (`.`).
3. Open `/sys/power/mem_sleep`.
4. Rename `./file1` to `.\002\000`.
5. Rename `./file1` to `.\002\000` again.
So the rename is happening *before* the key is removed. But if the image is mounted, it might already *have* some fscrypt state in it. Let's see what `btrfs_rename` does.
Within `btrfs_rename`, there's a call to `fscrypt_setup_filename` for both the old and new names. Then, the important part: `btrfs_check_dir_item_collision` is called using `new_fname.disk_name`. If `new_fname.disk_name` is a "no-key name", could it be colliding with something?
Hang on... if `new_inode` is `NULL`, it means VFS thinks the destination doesn't exist, *yet* `btrfs_check_dir_item_collision` finds it. Why would Btrfs find it, but VFS not? Could it be file system corruption?
Ah, that's right; the reproducer *mounts a crafted Btrfs image*! The `syz_mount_image` call reveals this. The image itself is deliberately malformed. This explains it.
Since the image is crafted, it could have a directory item for `.\002` but no corresponding inode, or some other inconsistency. It could be that VFS didn't find the directory entry because of a hash mismatch in the image. `btrfs_check_dir_item_collision` uses `btrfs_name_hash(name->name, name->len)`, so if the crafted image has a mismatched hash, that would explain it. But `btrfs_lookup_match_dir` searches for the exact name. So why does VFS fail to find the directory entry? I need to check `btrfs_lookup`.
|
| 11/2 |
2026/06/01 17:16 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/btrfs/inode.c Name:btrfs_lookup]
Results: map[SourceCode:6145: static struct dentry *btrfs_lookup(struct inode *dir, struct dentry *dentry,
6146: unsigned int flags)
6147: {
6148: struct inode *inode = btrfs_lookup_dentry(dir, dentry);
6149:
6150: if (inode == ERR_PTR(-ENOENT))
6151: inode = NULL;
6152: return d_splice_alias(inode, dentry);
6153: }
SourceFile:fs/btrfs/inode.c]
|
| 12/2 |
2026/06/01 17:16 |
llm |
expert |
0m
Model:
Tokens:
input: 23833
output: 41
thoughts: 14
Thoughts: Okay, let's dive into this. Here's my thought process on what we're looking at, summarized from the inside:
**Analyzing `btrfs_lookup_dentry`**
Alright, the first thing I'm doing is zeroing in on `btrfs_lookup_dentry`. This is where the rubber meets the road, as far as finding a directory entry goes. Given that, let's start with a mental rundown of what I *expect* to find going on inside this function, given my experience with Btrfs. This is where the core logic will handle the actual lookup within a Btrfs filesystem. Iβm guessing it involves traversing the B+tree structures, checking the metadata, validating the name, and ideally returning a pointer to the associated dentry. There might be some caching involved as well. I'll need to see how they handle symlinks, hardlinks and inodes in general, and if there are any specific locking mechanisms in place to guarantee concurrency safety. Time to dig in!
|
| 13/2 |
2026/06/01 17:16 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/btrfs/inode.c Name:btrfs_lookup_dentry]
Results: map[SourceCode:6062: struct inode *btrfs_lookup_dentry(struct inode *dir, struct dentry *dentry)
6063: {
6064: struct btrfs_fs_info *fs_info = inode_to_fs_info(dir);
6065: struct btrfs_inode *inode;
6066: struct btrfs_root *root = BTRFS_I(dir)->root;
6067: struct btrfs_root *sub_root = root;
6068: struct btrfs_key location = { 0 };
6069: u8 di_type = 0;
6070: int ret = 0;
6071:
6072: if (dentry->d_name.len > BTRFS_NAME_LEN)
6073: return ERR_PTR(-ENAMETOOLONG);
6074:
6075: ret = btrfs_inode_by_name(BTRFS_I(dir), dentry, &location, &di_type);
6076: if (ret < 0)
6077: return ERR_PTR(ret);
6078:
6079: if (location.type == BTRFS_INODE_ITEM_KEY) {
6080: inode = btrfs_iget(location.objectid, root);
6081: if (IS_ERR(inode))
6082: return ERR_CAST(inode);
6083:
6084: /* Do extra check against inode mode with di_type */
6085: if (unlikely(btrfs_inode_type(inode) != di_type)) {
6086: btrfs_crit(fs_info,
6087: "inode mode mismatch with dir: inode mode=0%o btrfs type=%u dir type=%u",
6088: inode->vfs_inode.i_mode, btrfs_inode_type(inode),
6089: di_type);
6090: iput(&inode->vfs_inode);
6091: return ERR_PTR(-EUCLEAN);
6092: }
6093: return &inode->vfs_inode;
6094: }
6095:
6096: ret = fixup_tree_root_location(fs_info, BTRFS_I(dir), dentry,
6097: &location, &sub_root);
6098: if (ret < 0) {
6099: if (ret != -ENOENT)
6100: inode = ERR_PTR(ret);
6101: else
6102: inode = new_simple_dir(dir, &location, root);
6103: } else {
6104: inode = btrfs_iget(location.objectid, sub_root);
6105: btrfs_put_root(sub_root);
6106:
6107: if (IS_ERR(inode))
6108: return ERR_CAST(inode);
6109:
6110: down_read(&fs_info->cleanup_work_sem);
6111: if (!sb_rdonly(inode->vfs_inode.i_sb))
6112: ret = btrfs_orphan_cleanup(sub_root);
6113: up_read(&fs_info->cleanup_work_sem);
6114: if (ret) {
6115: iput(&inode->vfs_inode);
6116: inode = ERR_PTR(ret);
6117: }
6118: }
6119:
6120: if (IS_ERR(inode))
6121: return ERR_CAST(inode);
6122:
6123: return &inode->vfs_inode;
6124: }
SourceFile:fs/btrfs/inode.c]
|
| 14/2 |
2026/06/01 17:16 |
llm |
expert |
0m
Model:
Tokens:
input: 25173
output: 42
thoughts: 15
Thoughts: Here's my attempt at summarizing the thought process, tailored for an expert audience:
**Diving into `btrfs_inode_by_name`**
Alright, let's take a look at `btrfs_inode_by_name`. Given my understanding of Btrfs' inode management and name lookups, I immediately want to consider a few key aspects of this function. I'm thinking about how it handles the internal data structures, specifically the extent tree and the name index, that are likely involved in translating a filename to an inode number within the filesystem.
I'm probably going to mentally trace the common paths; think, what kind of locking is employed during the process? Are we talking read-side, write-side, or something more nuanced? This should dictate how the function scales under concurrent access and potentially influences aspects of the design.
Is this operation sensitive to fragmentation? I'm curious whether the search has to hop around the disk. I have a sense that this function is key to performance for common filesystem operations such as listing or opening files by name.
More generally, I want to see if this `btrfs_inode_by_name` provides an optimization for filename lookups. Specifically, I'm thinking about how it integrates with the overall caching scheme for inodes and directory entries, and if there are any specific design choices to enhance lookups for common use cases.
|
| 15/2 |
2026/06/01 17:16 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/btrfs/inode.c Name:btrfs_inode_by_name]
Results: map[SourceCode:5752: /*
5753: * Return the key found in the dir entry in the location pointer, fill @type
5754: * with BTRFS_FT_*, and return 0.
5755: *
5756: * If no dir entries were found, returns -ENOENT.
5757: * If found a corrupted location in dir entry, returns -EUCLEAN.
5758: */
5759: static int btrfs_inode_by_name(struct btrfs_inode *dir, struct dentry *dentry,
5760: struct btrfs_key *location, u8 *type)
5761: {
5762: struct btrfs_dir_item *di;
5763: BTRFS_PATH_AUTO_FREE(path);
5764: struct btrfs_root *root = dir->root;
5765: int ret = 0;
5766: struct fscrypt_name fname;
5767:
5768: path = btrfs_alloc_path();
5769: if (!path)
5770: return -ENOMEM;
5771:
5772: ret = fscrypt_setup_filename(&dir->vfs_inode, &dentry->d_name, 1, &fname);
5773: if (ret < 0)
5774: return ret;
5775: /*
5776: * fscrypt_setup_filename() should never return a positive value, but
5777: * gcc on sparc/parisc thinks it can, so assert that doesn't happen.
5778: */
5779: ASSERT(ret == 0);
5780:
5781: /* This needs to handle no-key deletions later on */
5782:
5783: di = btrfs_lookup_dir_item(NULL, root, path, btrfs_ino(dir),
5784: &fname.disk_name, 0);
5785: if (IS_ERR_OR_NULL(di)) {
5786: ret = di ? PTR_ERR(di) : -ENOENT;
5787: goto out;
5788: }
5789:
5790: btrfs_dir_item_key_to_cpu(path->nodes[0], di, location);
5791: if (unlikely(location->type != BTRFS_INODE_ITEM_KEY &&
5792: location->type != BTRFS_ROOT_ITEM_KEY)) {
5793: ret = -EUCLEAN;
5794: btrfs_warn(root->fs_info,
5795: "%s gets something invalid in DIR_ITEM (name %s, directory ino %llu, location " BTRFS_KEY_FMT ")",
5796: __func__, fname.disk_name.name, btrfs_ino(dir),
5797: BTRFS_KEY_FMT_VALUE(location));
5798: }
5799: if (!ret)
5800: *type = btrfs_dir_ftype(path->nodes[0], di);
5801: out:
5802: fscrypt_free_filename(&fname);
5803: return ret;
5804: }
SourceFile:fs/btrfs/inode.c]
|
| 16/2 |
2026/06/01 17:16 |
llm |
expert |
0m
Model:
Tokens:
input: 26458
output: 40
thoughts: 122
Thoughts: **Analyzing Filesystem Encryption in Btrfs Rename Operations**
Okay, so I'm digging into how Btrfs handles encrypted filenames, specifically during rename operations. My initial observation is that `btrfs_inode_by_name` employs `fscrypt_setup_filename` with a `lookup` flag set to `1`. This `lookup=1` is interesting; it's the flag to tell the setup function to perform a lookup in a potentially encrypted directory.
Then, when looking at `btrfs_rename`, I see it calls the same function, `fscrypt_setup_filename`, but this time with `lookup=0`. This suggests that `btrfs_rename` is *not* intended to lookup the filename in the same encrypted way. This is a crucial distinction.
Now, my next step is to drill down into the inner workings of `fscrypt_setup_filename` itself. I need to understand precisely how its behavior changes based on this `lookup` flag. What are the key differences? What specific actions are taken when `lookup=1` versus when `lookup=0`? I need to understand how the encryption setup and the associated filename handling diverge under these different scenarios. This is key to ensuring that renames function correctly with encrypted filesystems.
|
| 17/2 |
2026/06/01 17:16 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/btrfs/inode.c Name:fscrypt_setup_filename]
Results: map[SourceCode: 319: /**
320: * fscrypt_setup_filename() - prepare to search a possibly encrypted directory
321: * @dir: the directory that will be searched
322: * @iname: the user-provided filename being searched for
323: * @lookup: 1 if we're allowed to proceed without the key because it's
324: * ->lookup() or we're finding the dir_entry for deletion; 0 if we cannot
325: * proceed without the key because we're going to create the dir_entry.
326: * @fname: the filename information to be filled in
327: *
328: * Given a user-provided filename @iname, this function sets @fname->disk_name
329: * to the name that would be stored in the on-disk directory entry, if possible.
330: * If the directory is unencrypted this is simply @iname. Else, if we have the
331: * directory's encryption key, then @iname is the plaintext, so we encrypt it to
332: * get the disk_name.
333: *
334: * Else, for keyless @lookup operations, @iname should be a no-key name, so we
335: * decode it to get the struct fscrypt_nokey_name. Non-@lookup operations will
336: * be impossible in this case, so we fail them with ENOKEY.
337: *
338: * If successful, fscrypt_free_filename() must be called later to clean up.
339: *
340: * Return: 0 on success, -errno on failure
341: */
342: int fscrypt_setup_filename(struct inode *dir, const struct qstr *iname,
343: int lookup, struct fscrypt_name *fname)
344: {
345: struct fscrypt_nokey_name *nokey_name;
346: int ret;
347:
348: memset(fname, 0, sizeof(struct fscrypt_name));
349: fname->usr_fname = iname;
350:
351: if (!IS_ENCRYPTED(dir) || fscrypt_is_dot_dotdot(iname)) {
352: fname->disk_name.name = (unsigned char *)iname->name;
353: fname->disk_name.len = iname->len;
354: return 0;
355: }
356: ret = fscrypt_get_encryption_info(dir, lookup);
357: if (ret)
358: return ret;
359:
360: if (fscrypt_has_encryption_key(dir)) {
361: if (!fscrypt_fname_encrypted_size(dir, iname->len, NAME_MAX,
362: &fname->crypto_buf.len))
363: return -ENAMETOOLONG;
364: fname->crypto_buf.name = kmalloc(fname->crypto_buf.len,
365: GFP_NOFS);
366: if (!fname->crypto_buf.name)
367: return -ENOMEM;
368:
369: ret = fscrypt_fname_encrypt(dir, iname, fname->crypto_buf.name,
370: fname->crypto_buf.len);
371: if (ret)
372: goto errout;
373: fname->disk_name.name = fname->crypto_buf.name;
374: fname->disk_name.len = fname->crypto_buf.len;
375: return 0;
376: }
377: if (!lookup)
378: return -ENOKEY;
379: fname->is_nokey_name = true;
380:
381: /*
382: * We don't have the key and we are doing a lookup; decode the
383: * user-supplied name
384: */
385:
386: if (iname->len > FSCRYPT_NOKEY_NAME_MAX_ENCODED)
387: return -ENOENT;
388:
389: fname->crypto_buf.name = kmalloc(FSCRYPT_NOKEY_NAME_MAX, GFP_KERNEL);
390: if (fname->crypto_buf.name == NULL)
391: return -ENOMEM;
392:
393: ret = base64_decode(iname->name, iname->len,
394: fname->crypto_buf.name, false, BASE64_URLSAFE);
395: if (ret < (int)offsetof(struct fscrypt_nokey_name, bytes[1]) ||
396: (ret > offsetof(struct fscrypt_nokey_name, sha256) &&
397: ret != FSCRYPT_NOKEY_NAME_MAX)) {
398: ret = -ENOENT;
399: goto errout;
400: }
401: fname->crypto_buf.len = ret;
402:
403: nokey_name = (void *)fname->crypto_buf.name;
404: fname->hash = nokey_name->dirhash[0];
405: fname->minor_hash = nokey_name->dirhash[1];
406: if (ret != FSCRYPT_NOKEY_NAME_MAX) {
407: /* The full ciphertext filename is available. */
408: fname->disk_name.name = nokey_name->bytes;
409: fname->disk_name.len =
410: ret - offsetof(struct fscrypt_nokey_name, bytes);
411: }
412: return 0;
413:
414: errout:
415: kfree(fname->crypto_buf.name);
416: return ret;
417: }
SourceFile:fs/crypto/fname.c]
|
| 18/2 |
2026/06/01 17:16 |
llm |
expert |
0m
Model:
Tokens:
input: 28604
output: 38
thoughts: 695
Thoughts: **Analyzing the Btrfs Rename Vulnerability**
Okay, let me break this down. My initial confusion stemmed from the interactions between `fscrypt_setup_filename`, `btrfs_lookup`, and `btrfs_rename`. The key piece here is how encryption interacts with name lookups and collisions. If `!lookup`, then `fscrypt_setup_filename` returns `-ENOKEY` if the key is absent. If the key *is* present, it encrypts the filename. If the key *is* present, both lookup and rename encrypt the name, resulting in the same `disk_name`. If the key isn't there, lookup decrypts the "no key" name, but rename still errors with `-ENOKEY`.
The puzzle was, why did `btrfs_rename` bypass the `fscrypt_setup_filename` error check but still hit a `-EEXIST` from `btrfs_check_dir_item_collision`? My hypothesis is: a crafted Btrfs image. I need to think about this in terms of the underlying data structures. If a malicious image contains a directory item matching the encrypted filename, but no corresponding inode item, a subtle but significant issue arises.
If there's no inode item, the `btrfs_lookup_dentry` should return either `ERR_PTR(-ENOENT)` or `NULL`. I remember seeing this pattern before, so let me quickly confirm by checking `btrfs_lookup_dentry`. Okay, here it is:
```c
ret = btrfs_inode_by_name(BTRFS_I(dir), dentry, &location, &di_type);
if (ret < 0)
return ERR_PTR(ret);
```
If `btrfs_inode_by_name` finds the directory item, then `ret` is `0` and it populates `location`. Next, `btrfs_lookup_dentry` calls:
```c
if (location.type == BTRFS_INODE_ITEM_KEY) {
inode = btrfs_iget(location.objectid, root);
if (IS_ERR(inode))
return ERR_CAST(inode);
```
Precisely! If `btrfs_iget` returns `-ENOENT` because the inode item is missing (in our crafted image), `btrfs_lookup_dentry` returns `ERR_PTR(-ENOENT)`. Then, going up, `btrfs_lookup` does:
```c
struct inode *inode = btrfs_lookup_dentry(dir, dentry);
if (inode == ERR_PTR(-ENOENT))
inode = NULL;
return d_splice_alias(inode, dentry);
```
So `btrfs_lookup` actually returns a negative dentry! This means the VFS considers the destination to *not* exist; `new_inode` is NULL. However, when `btrfs_rename` gets called, it calls `btrfs_check_dir_item_collision`.
`btrfs_check_dir_item_collision` searches for the directory item. And it *will* find it, because we crafted the image to include it. This means `btrfs_check_dir_item_collision` returns `-EEXIST`. The `btrfs_rename` function then sees this `-EEXIST` *and* `new_inode == NULL`, leading to the `WARN_ON(!new_inode)` condition, the crux of the issue.
Let me just double-check one more thing: `btrfs_iget` should return `-ENOENT` if the inode item is missing. That is what causes the entire flow to work like this! Okay, that's the key.
|
| 19/2 |
2026/06/01 17:17 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/btrfs/inode.c Name:btrfs_iget]
Results: map[SourceCode:5975: /*
5976: * Get an inode object given its inode number and corresponding root.
5977: */
5978: struct btrfs_inode *btrfs_iget(u64 ino, struct btrfs_root *root)
5979: {
5980: struct btrfs_inode *inode;
5981: struct btrfs_path *path;
5982: int ret;
5983:
5984: inode = btrfs_iget_locked(ino, root);
5985: if (!inode)
5986: return ERR_PTR(-ENOMEM);
5987:
5988: if (!(inode_state_read_once(&inode->vfs_inode) & I_NEW))
5989: return inode;
5990:
5991: path = btrfs_alloc_path();
5992: if (!path) {
5993: iget_failed(&inode->vfs_inode);
5994: return ERR_PTR(-ENOMEM);
5995: }
5996:
5997: ret = btrfs_read_locked_inode(inode, path);
5998: btrfs_free_path(path);
5999: if (ret)
6000: return ERR_PTR(ret);
6001:
6002: if (S_ISDIR(inode->vfs_inode.i_mode))
6003: inode->vfs_inode.i_opflags |= IOP_FASTPERM_MAY_EXEC;
6004: unlock_new_inode(&inode->vfs_inode);
6005: return inode;
6006: }
SourceFile:fs/btrfs/inode.c]
|
| 20/2 |
2026/06/01 17:17 |
llm |
expert |
0m
Model:
Tokens:
input: 30672
output: 42
thoughts: 15
Thoughts: Here's my attempt at summarizing the thought process, tailored for an expert audience:
**Analyzing `btrfs_read_locked_inode`**
Okay, let's dive into `btrfs_read_locked_inode`. My immediate concern is its place in the inode locking scheme. Given the name, I'm assuming this is a *read* operation, and that it's locking-related, hopefully not a full mutex but something more granular. I need to understand its relationship to the various inode flags and how this call interacts with any potential shared/exclusive locking protocols within Btrfs. Is it acquiring a read lock specifically, and if so, what level of atomicity are we talking about?
My primary concern, as always, is concurrency and deadlock. What's the potential for contention here? Is this function called frequently in a typical workload, and if so, are the lock acquisition and release paths optimized to minimize overhead? I'll need to check the surrounding code for how it handles lock acquisition failures β is there retry logic, or does it bail out gracefully, potentially propagating an error to the caller?
More specifically, I need to know:
* **What data structures are being protected?** Is it just the inode itself, or other related structures?
* **What are the calling conventions?** Are there assumptions about what locks are already held? Does it assume any specific context?
* **How does it interact with the journaling layer?** Is this operation within a transaction, or is it potentially dealing with uncommitted data?
* **How does it handle potential metadata updates while the inode is locked?** Specifically, how are the locks handled in combination with metadata tree updates?
* **What are the implications for performance?** What's the typical latency and throughput impact on file I/O?
I need to understand its role in a broader picture. I am going to see how it's used in the code to get an understanding of the overall scheme. This is all crucial for evaluating its correctness and performance characteristics. Let's see what the code has to say.
|
| 21/2 |
2026/06/01 17:17 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/btrfs/inode.c Name:btrfs_read_locked_inode]
Results: map[SourceCode:4039: /*
4040: * Read a locked inode from the btree into the in-memory inode and add it to
4041: * its root list/tree.
4042: *
4043: * On failure clean up the inode.
4044: */
4045: static int btrfs_read_locked_inode(struct btrfs_inode *inode, struct btrfs_path *path)
4046: {
4047: struct btrfs_root *root = inode->root;
4048: struct btrfs_fs_info *fs_info = root->fs_info;
4049: struct extent_buffer *leaf;
4050: struct btrfs_inode_item *inode_item;
4051: struct inode *vfs_inode = &inode->vfs_inode;
4052: struct btrfs_key location;
4053: unsigned long ptr;
4054: int maybe_acls;
4055: u32 rdev;
4056: int ret;
4057: bool filled = false;
4058: int first_xattr_slot;
4059:
4060: ret = btrfs_fill_inode(inode, &rdev);
4061: if (!ret)
4062: filled = true;
4063:
4064: ASSERT(path);
4065:
4066: btrfs_get_inode_key(inode, &location);
4067:
4068: ret = btrfs_lookup_inode(NULL, root, path, &location, 0);
4069: if (ret) {
4070: /*
4071: * ret > 0 can come from btrfs_search_slot called by
4072: * btrfs_lookup_inode(), this means the inode was not found.
4073: */
4074: if (ret > 0)
4075: ret = -ENOENT;
4076: goto out;
4077: }
4078:
4079: leaf = path->nodes[0];
4080:
4081: if (filled)
4082: goto cache_index;
4083:
4084: inode_item = btrfs_item_ptr(leaf, path->slots[0],
4085: struct btrfs_inode_item);
4086: vfs_inode->i_mode = btrfs_inode_mode(leaf, inode_item);
4087: set_nlink(vfs_inode, btrfs_inode_nlink(leaf, inode_item));
4088: i_uid_write(vfs_inode, btrfs_inode_uid(leaf, inode_item));
4089: i_gid_write(vfs_inode, btrfs_inode_gid(leaf, inode_item));
4090: btrfs_i_size_write(inode, btrfs_inode_size(leaf, inode_item));
4091:
4092: inode_set_atime(vfs_inode, btrfs_timespec_sec(leaf, &inode_item->atime),
4093: btrfs_timespec_nsec(leaf, &inode_item->atime));
4094:
4095: inode_set_mtime(vfs_inode, btrfs_timespec_sec(leaf, &inode_item->mtime),
4096: btrfs_timespec_nsec(leaf, &inode_item->mtime));
4097:
4098: inode_set_ctime(vfs_inode, btrfs_timespec_sec(leaf, &inode_item->ctime),
4099: btrfs_timespec_nsec(leaf, &inode_item->ctime));
4100:
4101: inode->i_otime_sec = btrfs_timespec_sec(leaf, &inode_item->otime);
4102: inode->i_otime_nsec = btrfs_timespec_nsec(leaf, &inode_item->otime);
4103:
4104: inode_set_bytes(vfs_inode, btrfs_inode_nbytes(leaf, inode_item));
4105: inode->generation = btrfs_inode_generation(leaf, inode_item);
4106: inode->last_trans = btrfs_inode_transid(leaf, inode_item);
4107:
4108: inode_set_iversion_queried(vfs_inode, btrfs_inode_sequence(leaf, inode_item));
4109: vfs_inode->i_generation = inode->generation;
4110: vfs_inode->i_rdev = 0;
4111: rdev = btrfs_inode_rdev(leaf, inode_item);
4112:
4113: if (S_ISDIR(vfs_inode->i_mode))
4114: inode->index_cnt = (u64)-1;
4115:
4116: btrfs_inode_split_flags(btrfs_inode_flags(leaf, inode_item),
4117: &inode->flags, &inode->ro_flags);
4118: btrfs_update_inode_mapping_flags(inode);
4119: btrfs_set_inode_mapping_order(inode);
4120:
4121: cache_index:
4122: /*
4123: * If we were modified in the current generation and evicted from memory
4124: * and then re-read we need to do a full sync since we don't have any
4125: * idea about which extents were modified before we were evicted from
4126: * cache.
4127: *
4128: * This is required for both inode re-read from disk and delayed inode
4129: * in the delayed_nodes xarray.
4130: */
4131: if (inode->last_trans == btrfs_get_fs_generation(fs_info))
4132: set_bit(BTRFS_INODE_NEEDS_FULL_SYNC, &inode->runtime_flags);
4133:
4134: /*
4135: * We don't persist the id of the transaction where an unlink operation
4136: * against the inode was last made. So here we assume the inode might
4137: * have been evicted, and therefore the exact value of last_unlink_trans
4138: * lost, and set it to last_trans to avoid metadata inconsistencies
4139: * between the inode and its parent if the inode is fsync'ed and the log
4140: * replayed. For example, in the scenario:
4141: *
4142: * touch mydir/foo
4143: * ln mydir/foo mydir/bar
4144: * sync
4145: * unlink mydir/bar
4146: * echo 2 > /proc/sys/vm/drop_caches # evicts inode
4147: * xfs_io -c fsync mydir/foo
4148: * <power failure>
4149: * mount fs, triggers fsync log replay
4150: *
4151: * We must make sure that when we fsync our inode foo we also log its
4152: * parent inode, otherwise after log replay the parent still has the
4153: * dentry with the "bar" name but our inode foo has a link count of 1
4154: * and doesn't have an inode ref with the name "bar" anymore.
4155: *
4156: * Setting last_unlink_trans to last_trans is a pessimistic approach,
4157: * but it guarantees correctness at the expense of occasional full
4158: * transaction commits on fsync if our inode is a directory, or if our
4159: * inode is not a directory, logging its parent unnecessarily.
4160: */
4161: inode->last_unlink_trans = inode->last_trans;
4162:
4163: /*
4164: * Same logic as for last_unlink_trans. We don't persist the generation
4165: * of the last transaction where this inode was used for a reflink
4166: * operation, so after eviction and reloading the inode we must be
4167: * pessimistic and assume the last transaction that modified the inode.
4168: */
4169: inode->last_reflink_trans = inode->last_trans;
4170:
4171: path->slots[0]++;
4172: if (vfs_inode->i_nlink != 1 ||
4173: path->slots[0] >= btrfs_header_nritems(leaf))
4174: goto cache_acl;
4175:
4176: btrfs_item_key_to_cpu(leaf, &location, path->slots[0]);
4177: if (location.objectid != btrfs_ino(inode))
4178: goto cache_acl;
4179:
4180: ptr = btrfs_item_ptr_offset(leaf, path->slots[0]);
4181: if (location.type == BTRFS_INODE_REF_KEY) {
4182: struct btrfs_inode_ref *ref;
4183:
4184: ref = (struct btrfs_inode_ref *)ptr;
4185: inode->dir_index = btrfs_inode_ref_index(leaf, ref);
4186: } else if (location.type == BTRFS_INODE_EXTREF_KEY) {
4187: struct btrfs_inode_extref *extref;
4188:
4189: extref = (struct btrfs_inode_extref *)ptr;
4190: inode->dir_index = btrfs_inode_extref_index(leaf, extref);
4191: }
4192: cache_acl:
4193: /*
4194: * try to precache a NULL acl entry for files that don't have
4195: * any xattrs or acls
4196: */
4197: maybe_acls = acls_after_inode_item(leaf, path->slots[0],
4198: btrfs_ino(inode), &first_xattr_slot);
4199: if (first_xattr_slot != -1) {
4200: path->slots[0] = first_xattr_slot;
4201: ret = btrfs_load_inode_props(inode, path);
4202: if (ret)
4203: btrfs_err(fs_info,
4204: "error loading props for ino %llu (root %llu): %d",
4205: btrfs_ino(inode), btrfs_root_id(root), ret);
4206: }
4207:
4208: /*
4209: * We don't need the path anymore, so release it to avoid holding a read
4210: * lock on a leaf while calling btrfs_init_file_extent_tree(), which can
4211: * allocate memory that triggers reclaim (GFP_KERNEL) and cause a locking
4212: * dependency.
4213: */
4214: btrfs_release_path(path);
4215:
4216: ret = btrfs_init_file_extent_tree(inode);
4217: if (ret)
4218: goto out;
4219: btrfs_inode_set_file_extent_range(inode, 0,
4220: round_up(i_size_read(vfs_inode), fs_info->sectorsize));
4221:
4222: if (!maybe_acls)
4223: cache_no_acl(vfs_inode);
4224:
4225: switch (vfs_inode->i_mode & S_IFMT) {
4226: case S_IFREG:
4227: vfs_inode->i_mapping->a_ops = &btrfs_aops;
4228: vfs_inode->i_fop = &btrfs_file_operations;
4229: vfs_inode->i_op = &btrfs_file_inode_operations;
4230: break;
4231: case S_IFDIR:
4232: vfs_inode->i_fop = &btrfs_dir_file_operations;
4233: vfs_inode->i_op = &btrfs_dir_inode_operations;
4234: break;
4235: case S_IFLNK:
4236: vfs_inode->i_op = &btrfs_symlink_inode_operations;
4237: inode_nohighmem(vfs_inode);
4238: vfs_inode->i_mapping->a_ops = &btrfs_aops;
4239: break;
4240: default:
4241: vfs_inode->i_op = &btrfs_special_inode_operations;
4242: init_special_inode(vfs_inode, vfs_inode->i_mode, rdev);
4243: break;
4244: }
4245:
4246: btrfs_sync_inode_flags_to_i_flags(inode);
4247:
4248: ret = btrfs_add_inode_to_root(inode, true);
4249: if (ret)
4250: goto out;
4251:
4252: return 0;
4253: out:
4254: /*
4255: * We may have a read locked leaf and iget_failed() triggers inode
4256: * eviction which needs to release the delayed inode and that needs
4257: * to lock the delayed inode's mutex. This can cause a ABBA deadlock
4258: * with a task running delayed items, as that require first locking
4259: * the delayed inode's mutex and then modifying its subvolume btree.
4260: * So release the path before iget_failed().
4261: */
4262: btrfs_release_path(path);
4263: iget_failed(vfs_inode);
4264: return ret;
4265: }
SourceFile:fs/btrfs/inode.c]
|
| 22/2 |
2026/06/01 17:17 |
llm |
expert |
0m
Model:
Tokens:
input: 35165
output: 44
thoughts: 977
Thoughts: **Analysis of a Potential Btrfs `WARN_ON` Trigger**
Okay, so here's the breakdown of this potential issue in Btrfs that I've been tracing. First, the core seems to stem from how the filesystem handles a missing inode item but an existing directory item within a crafted image, specifically with the name ".\002". We know that `btrfs_read_locked_inode` returns `-ENOENT` when `btrfs_lookup_inode` fails, and that a crafted image could cause `btrfs_lookup_dentry` to return `ERR_PTR(-ENOENT)`. This happens because `btrfs_iget` in `btrfs_lookup_dentry` also returns `-ENOENT` if the inode isn't found. This then results in `btrfs_lookup` returning a negative dentry (with `inode == NULL`).
`vfs_rename` is then called and detects a negative `new_dentry` so `new_inode` is `NULL`. `btrfs_rename2` then calls `btrfs_rename`. The crucial part is that `btrfs_rename` calls `btrfs_check_dir_item_collision`. Since we have a dir item, `btrfs_check_dir_item_collision` *does* find it and returns `-EEXIST`. Because the image is crafted in such a way, `new_inode` is still NULL at this point, which triggers the `WARN_ON(!new_inode)` in `btrfs_rename`.
So, it *is* a `WARN_ON` that can be triggered by a crafted image. But the key question: is this a critical bug? Not really. Examining the subsequent code, it jumps to `out_fscrypt_names` and ultimately just returns `-EEXIST` to the caller. No crashes, no memory corruption β just a warning. This is pretty important because it's just a warning.
Now, the follow-up question is: can this happen legitimately without a crafted image? Could a hash collision cause the same condition? `btrfs_check_dir_item_collision` explicitly checks for an *exact* name match, via `btrfs_lookup_match_dir`. If there *is* a hash collision, `btrfs_lookup_match_dir` wouldn't find the *exact* name and thus returns `NULL` or `-ENOENT`, which means no `-EEXIST`. So, based on this, a hash collision *shouldn't* trigger this issue. It seems like it's strictly a crafted-image-specific scenario, and while it's a `WARN_ON`, it doesn't appear to be a major vulnerability.
|
| 23/2 |
2026/06/01 17:17 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/btrfs/dir-item.c Name:btrfs_lookup_match_dir]
Results: map[SourceCode: 174: static struct btrfs_dir_item *btrfs_lookup_match_dir(
175: struct btrfs_trans_handle *trans,
176: struct btrfs_root *root, struct btrfs_path *path,
177: struct btrfs_key *key, const char *name,
178: int name_len, int mod)
179: {
180: const int ins_len = (mod < 0 ? -1 : 0);
181: const int cow = (mod != 0);
182: int ret;
183:
184: ret = btrfs_search_slot(trans, root, key, path, ins_len, cow);
185: if (ret < 0)
186: return ERR_PTR(ret);
187: if (ret > 0)
188: return ERR_PTR(-ENOENT);
189:
190: return btrfs_match_dir_item_name(path, name, name_len);
191: }
SourceFile:fs/btrfs/dir-item.c]
|
| 24/2 |
2026/06/01 17:17 |
llm |
expert |
0m
Model:
Tokens:
input: 37117
output: 46
thoughts: 14
Thoughts: Okay, here's my interpretation of that thought process, tailored for an expert audience, and written in the first person:
**Analyzing `btrfs_match_dir_item_name`**
Alright, let's break down this function, `btrfs_match_dir_item_name`. My initial thought is to dive right into its core purpose: determining if a directory entry's name matches a given target name within the Btrfs filesystem context. I'm immediately considering the performance implications β this likely gets called *a lot* during directory lookups, so even small inefficiencies can snowball.
I'm thinking about the various optimizations that *should* be in place. Case-sensitivity, or the lack thereof, would be a key consideration, along with whether any normalization is involved. Btrfs can handle a wide variety of names, so how does it cope with special characters, or long file names? That has to be accounted for. What about the encoding of names, UTF-8, etc.? Does it handle different name lengths efficiently? The choice of matching algorithm - is it a simple byte-by-byte comparison, or does it leverage something more sophisticated like a string hashing or a Boyer-Moore-Horspool?
I'm also pondering the potential for error conditions. What if the directory item is malformed? What edge cases is this function designed to handle β or deliberately not handle β regarding name length or invalid characters? I'm picturing how this interacts with the B-tree structure; are there any special considerations for indexing or searching the directory entries in the B-tree leaves? Any tricks to reduce the amount of data that needs to be loaded into memory for the comparison? How does it handle file names with extended attributes, if at all?
Finally, what's the broader picture? I'm thinking about the bigger context within the Btrfs code. Does this function interact with any other parts of the filesystem? Are there any upstream dependencies? Where does this fit in the larger performance picture for lookups? Are there any known vulnerabilities this function might be exposed to? I'm making a mental checklist of possible refactoring opportunities that could improve speed and reliability.
|
| 25/2 |
2026/06/01 17:17 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/btrfs/dir-item.c Name:btrfs_match_dir_item_name]
Results: map[SourceCode: 364: /*
365: * helper function to look at the directory item pointed to by 'path'
366: * this walks through all the entries in a dir item and finds one
367: * for a specific name.
368: */
369: struct btrfs_dir_item *btrfs_match_dir_item_name(const struct btrfs_path *path,
370: const char *name, int name_len)
371: {
372: struct btrfs_dir_item *dir_item;
373: unsigned long name_ptr;
374: u32 total_len;
375: u32 cur = 0;
376: u32 this_len;
377: struct extent_buffer *leaf;
378:
379: leaf = path->nodes[0];
380: dir_item = btrfs_item_ptr(leaf, path->slots[0], struct btrfs_dir_item);
381:
382: total_len = btrfs_item_size(leaf, path->slots[0]);
383: while (cur < total_len) {
384: this_len = sizeof(*dir_item) +
385: btrfs_dir_name_len(leaf, dir_item) +
386: btrfs_dir_data_len(leaf, dir_item);
387: name_ptr = (unsigned long)(dir_item + 1);
388:
389: if (btrfs_dir_name_len(leaf, dir_item) == name_len &&
390: memcmp_extent_buffer(leaf, name, name_ptr, name_len) == 0)
391: return dir_item;
392:
393: cur += this_len;
394: dir_item = (struct btrfs_dir_item *)((char *)dir_item +
395: this_len);
396: }
397: return NULL;
398: }
SourceFile:fs/btrfs/dir-item.c]
|
| 26/2 |
2026/06/01 17:17 |
llm |
expert |
0m
Model:
Tokens:
input: 38253
output: 34
thoughts: 1434
Thoughts: **Analyzing a Btrfs Rename Collision Scenario**
Okay, so I'm diving into this Btrfs rename issue. It looks like `btrfs_match_dir_item_name` returns `NULL` when there's no exact match on the name. Then, `btrfs_check_dir_item_collision` does a lookup and if it finds nothing, it's safe. But, if a name *is* found in the directory item, that exact name, `btrfs_check_dir_item_collision` should return `-EEXIST`. If the directory item is found (same hash), it then checks if there is *room* in that directory item for the new name.
So, the crux is how can the `btrfs_check_dir_item_collision` *find* the name (and return -EEXIST), yet the VFS thinks the *destination* doesn't exist (`new_inode` is NULL)? My initial thought, and what's probably correct, is that the directory item exists, but the inode item itself is missing. VFS finds the dir item, gets the inode number, and calls `btrfs_iget`. `btrfs_iget` fails, returning `-ENOENT`, because the inode item is not found, and thus `btrfs_lookup_dentry` returns an error, and the lookup finds nothing in VFS.
Now, if there is another error, like maybe `-ENOMEM`, or `-EUCLEAN` from `btrfs_iget`, then `vfs_rename` will fail earlier on. If `-EUCLEAN` is returned, because there is an inode mode mismatch, VFS fails early, and that's not the problem here. Therefore, the only way VFS returns `inode == NULL` and the dir item still exists (which implies that the exact match is done) is through the failure of `btrfs_iget` to find an inode item.
So, this looks like a corrupted Btrfs image, where a directory entry points to an inode that doesn't exist, or has been removed. That would require a crafted image. The good news is that this results in a `WARN_ON` which isn't a kernel crash, just an error return. It's not memory corruption or a privilege escalation, just an `-EEXIST` to the user, from what I can see.
Let's think about a race condition, just to be sure. What if thread A unlinks the destination file, while thread B is renaming to it? But, if thread B does the VFS lookup, *before* thread A removes the destination, `new_inode` won't be NULL. Then, thread A removes the inode entry and the dir entry. When thread B calls `btrfs_rename`, it won't find the name and there will be no conflict. No problem here.
However, if thread B does a lookup and finds *nothing*, which means `new_inode` is NULL, and then thread A creates that destination, and inserts both the inode *and* dir items, *then* thread B renames to the destination.
In this case, `btrfs_check_dir_item_collision` finds the dir item, because the directory item exists, it returns `-EEXIST`.
And `new_inode` will still be NULL (because VFS lookup was done before thread A created the file).
Would this trigger the warning? It will, however, let's see if VFS has some protections against this race.
|
| 27/2 |
2026/06/01 17:17 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/namei.c Name:vfs_rename]
Results: map[SourceCode:5878: /**
5879: * vfs_rename - rename a filesystem object
5880: * @rd: pointer to &struct renamedata info
5881: *
5882: * The caller must hold multiple mutexes--see lock_rename()).
5883: *
5884: * If vfs_rename discovers a delegation in need of breaking at either
5885: * the source or destination, it will return -EWOULDBLOCK and return a
5886: * reference to the inode in delegated_inode. The caller should then
5887: * break the delegation and retry. Because breaking a delegation may
5888: * take a long time, the caller should drop all locks before doing
5889: * so.
5890: *
5891: * Alternatively, a caller may pass NULL for delegated_inode. This may
5892: * be appropriate for callers that expect the underlying filesystem not
5893: * to be NFS exported.
5894: *
5895: * The worst of all namespace operations - renaming directory. "Perverted"
5896: * doesn't even start to describe it. Somebody in UCB had a heck of a trip...
5897: * Problems:
5898: *
5899: * a) we can get into loop creation.
5900: * b) race potential - two innocent renames can create a loop together.
5901: * That's where 4.4BSD screws up. Current fix: serialization on
5902: * sb->s_vfs_rename_mutex. We might be more accurate, but that's another
5903: * story.
5904: * c) we may have to lock up to _four_ objects - parents and victim (if it exists),
5905: * and source (if it's a non-directory or a subdirectory that moves to
5906: * different parent).
5907: * And that - after we got ->i_rwsem on parents (until then we don't know
5908: * whether the target exists). Solution: try to be smart with locking
5909: * order for inodes. We rely on the fact that tree topology may change
5910: * only under ->s_vfs_rename_mutex _and_ that parent of the object we
5911: * move will be locked. Thus we can rank directories by the tree
5912: * (ancestors first) and rank all non-directories after them.
5913: * That works since everybody except rename does "lock parent, lookup,
5914: * lock child" and rename is under ->s_vfs_rename_mutex.
5915: * HOWEVER, it relies on the assumption that any object with ->lookup()
5916: * has no more than 1 dentry. If "hybrid" objects will ever appear,
5917: * we'd better make sure that there's no link(2) for them.
5918: * d) conversion from fhandle to dentry may come in the wrong moment - when
5919: * we are removing the target. Solution: we will have to grab ->i_rwsem
5920: * in the fhandle_to_dentry code. [FIXME - current nfsfh.c relies on
5921: * ->i_rwsem on parents, which works but leads to some truly excessive
5922: * locking].
5923: */
5924: int vfs_rename(struct renamedata *rd)
5925: {
5926: int error;
5927: struct inode *old_dir = d_inode(rd->old_parent);
5928: struct inode *new_dir = d_inode(rd->new_parent);
5929: struct dentry *old_dentry = rd->old_dentry;
5930: struct dentry *new_dentry = rd->new_dentry;
5931: struct delegated_inode *delegated_inode = rd->delegated_inode;
5932: unsigned int flags = rd->flags;
5933: bool is_dir = d_is_dir(old_dentry);
5934: struct inode *source = old_dentry->d_inode;
5935: struct inode *target = new_dentry->d_inode;
5936: bool new_is_dir = false;
5937: unsigned max_links = new_dir->i_sb->s_max_links;
5938: struct name_snapshot old_name;
5939: bool lock_old_subdir, lock_new_subdir;
5940:
5941: if (source == target)
5942: return 0;
5943:
5944: error = may_delete_dentry(rd->mnt_idmap, old_dir, old_dentry, is_dir);
5945: if (error)
5946: return error;
5947:
5948: if (!target) {
5949: error = may_create_dentry(rd->mnt_idmap, new_dir, new_dentry);
5950: } else {
5951: new_is_dir = d_is_dir(new_dentry);
5952:
5953: if (!(flags & RENAME_EXCHANGE))
5954: error = may_delete_dentry(rd->mnt_idmap, new_dir,
5955: new_dentry, is_dir);
5956: else
5957: error = may_delete_dentry(rd->mnt_idmap, new_dir,
5958: new_dentry, new_is_dir);
5959: }
5960: if (error)
5961: return error;
5962:
5963: if (!old_dir->i_op->rename)
5964: return -EPERM;
5965:
5966: /*
5967: * If we are going to change the parent - check write permissions,
5968: * we'll need to flip '..'.
5969: */
5970: if (new_dir != old_dir) {
5971: if (is_dir) {
5972: error = inode_permission(rd->mnt_idmap, source,
5973: MAY_WRITE);
5974: if (error)
5975: return error;
5976: }
5977: if ((flags & RENAME_EXCHANGE) && new_is_dir) {
5978: error = inode_permission(rd->mnt_idmap, target,
5979: MAY_WRITE);
5980: if (error)
5981: return error;
5982: }
5983: }
5984:
5985: error = security_inode_rename(old_dir, old_dentry, new_dir, new_dentry,
5986: flags);
5987: if (error)
5988: return error;
5989:
5990: take_dentry_name_snapshot(&old_name, old_dentry);
5991: dget(new_dentry);
5992: /*
5993: * Lock children.
5994: * The source subdirectory needs to be locked on cross-directory
5995: * rename or cross-directory exchange since its parent changes.
5996: * The target subdirectory needs to be locked on cross-directory
5997: * exchange due to parent change and on any rename due to becoming
5998: * a victim.
5999: * Non-directories need locking in all cases (for NFS reasons);
6000: * they get locked after any subdirectories (in inode address order).
6001: *
6002: * NOTE: WE ONLY LOCK UNRELATED DIRECTORIES IN CROSS-DIRECTORY CASE.
6003: * NEVER, EVER DO THAT WITHOUT ->s_vfs_rename_mutex.
6004: */
6005: lock_old_subdir = new_dir != old_dir;
6006: lock_new_subdir = new_dir != old_dir || !(flags & RENAME_EXCHANGE);
6007: if (is_dir) {
6008: if (lock_old_subdir)
6009: inode_lock_nested(source, I_MUTEX_CHILD);
6010: if (target && (!new_is_dir || lock_new_subdir))
6011: inode_lock(target);
6012: } else if (new_is_dir) {
6013: if (lock_new_subdir)
6014: inode_lock_nested(target, I_MUTEX_CHILD);
6015: inode_lock(source);
6016: } else {
6017: lock_two_nondirectories(source, target);
6018: }
6019:
6020: error = -EPERM;
6021: if (IS_SWAPFILE(source) || (target && IS_SWAPFILE(target)))
6022: goto out;
6023:
6024: error = -EBUSY;
6025: if (is_local_mountpoint(old_dentry) || is_local_mountpoint(new_dentry))
6026: goto out;
6027:
6028: if (max_links && new_dir != old_dir) {
6029: error = -EMLINK;
6030: if (is_dir && !new_is_dir && new_dir->i_nlink >= max_links)
6031: goto out;
6032: if ((flags & RENAME_EXCHANGE) && !is_dir && new_is_dir &&
6033: old_dir->i_nlink >= max_links)
6034: goto out;
6035: }
6036: error = try_break_deleg(old_dir, delegated_inode);
6037: if (error)
6038: goto out;
6039: if (new_dir != old_dir) {
6040: error = try_break_deleg(new_dir, delegated_inode);
6041: if (error)
6042: goto out;
6043: }
6044: if (!is_dir) {
6045: error = try_break_deleg(source, delegated_inode);
6046: if (error)
6047: goto out;
6048: }
6049: if (target && !new_is_dir) {
6050: error = try_break_deleg(target, delegated_inode);
6051: if (error)
6052: goto out;
6053: }
6054: error = old_dir->i_op->rename(rd->mnt_idmap, old_dir, old_dentry,
6055: new_dir, new_dentry, flags);
6056: if (error)
6057: goto out;
6058:
6059: if (!(flags & RENAME_EXCHANGE) && target) {
6060: if (is_dir) {
6061: shrink_dcache_parent(new_dentry);
6062: target->i_flags |= S_DEAD;
6063: }
6064: dont_mount(new_dentry);
6065: detach_mounts(new_dentry);
6066: }
6067: if (!(old_dir->i_sb->s_type->fs_flags & FS_RENAME_DOES_D_MOVE)) {
6068: if (!(flags & RENAME_EXCHANGE))
6069: d_move(old_dentry, new_dentry);
6070: else
6071: d_exchange(old_dentry, new_dentry);
6072: }
6073: out:
6074: if (!is_dir || lock_old_subdir)
6075: inode_unlock(source);
6076: if (target && (!new_is_dir || lock_new_subdir))
6077: inode_unlock(target);
6078: dput(new_dentry);
6079: if (!error) {
6080: fsnotify_move(old_dir, new_dir, &old_name.name, is_dir,
6081: !(flags & RENAME_EXCHANGE) ? target : NULL, old_dentry);
6082: if (flags & RENAME_EXCHANGE) {
6083: fsnotify_move(new_dir, old_dir, &old_dentry->d_name,
6084: new_is_dir, NULL, new_dentry);
6085: }
6086: }
6087: release_dentry_name_snapshot(&old_name);
6088:
6089: return error;
6090: }
SourceFile:fs/namei.c]
|
| 28/2 |
2026/06/01 17:17 |
llm |
expert |
0m
Model:
Tokens:
input: 44096
output: 33
thoughts: 1344
Thoughts: **Analysis of a Btrfs Rename Warning**
Okay, so I'm looking at this `btrfs_rename` function, specifically a `WARN_ON` within it, and trying to understand the circumstances that trigger it. I've already established that `vfs_rename` locks `old_dir` and `new_dir`, so any concurrent access creating or deleting files within the destination directory is ruled out. This eliminates the race condition I initially considered.
The core of the issue boils down to a conflict: `btrfs_check_dir_item_collision` returning success, implying the presence of a directory item in the B-tree for the destination name, while simultaneously, `new_inode` being NULL. The standard VFS lookup, using `btrfs_iget`, failed to find the corresponding inode, and so that's why `new_inode` is NULL. This really screams *filesystem corruption*. It suggests the B-tree has a directory entry pointing to a non-existent inode. I need a specifically crafted Btrfs image to reach this state.
Let's explore other scenarios to verify this. What about encrypted filesystems? If the destination name is a "no-key name" (encrypted), and we hit a collision in `btrfs_check_dir_item_collision`, would this trigger the condition? No, because if the key isn't present, `fscrypt_setup_filename` returns `-ENOKEY`. The rename operation bails out early, preventing the collision check from even happening. So the key *must* be present for `btrfs_rename` to continue.
When the key is present, `fscrypt_setup_filename` encrypts the name. Then, `btrfs_check_dir_item_collision` searches for the encrypted name. If this name exists, and the `new_inode` is NULL, then that means the inode does not exist. Why doesn't it exist?
Going down a level, even if the directory *is* encrypted, VFS lookup *does* successfully retrieve the inode when the key is not given. When no key is provided, the VFS calls `fscrypt_setup_filename` with `lookup=1`. The plaintext name is used, and then converted to a hash. The function `btrfs_inode_by_name` will search for this hash, which will then return the inode. Then `btrfs_lookup_dentry` gets the inode.
Okay, so let's summarize the conditions that cause the `WARN_ON`:
* The bug is triggered when `btrfs_check_dir_item_collision` returns `-EEXIST` but `new_inode` is NULL.
* This happens when the B-tree contains a directory item for the destination name, but the corresponding inode item is missing.
* VFS lookup fails to find the inode (because `btrfs_iget` returns `-ENOENT`), so it creates a negative dentry.
* `btrfs_rename` then finds the directory item and returns `-EEXIST`.
* The `WARN_ON` is triggered.
* The kernel does not crash (unless `panic_on_warn` is set).
* The operation just returns `-EEXIST`.
* This requires mounting a crafted btrfs image.
Now, let's assess the implications. Is this exploitable? No, it's just a warning with an error return. No memory corruption, no privilege escalation. Is this a Denial-of-Service? Not in production, because it just prints a warning and returns an error. It doesn't crash or hang the system. If `panic_on_warn` is set, it becomes a DoS, however that is not the default. The "violation of assumptions" is gracefully handled by the code, so it doesn't lead to DoS.
Is this accessible from unprivileged processes? No. Mounting a Btrfs image requires `CAP_SYS_ADMIN` in the initial namespace.
Is this accessible from user namespaces? No. Btrfs can't be mounted in a user namespace.
|
| 29/2 |
2026/06/01 17:18 |
tool |
grepper |
0m
Error:
Args: map[Expression:FS_USERNS_MOUNT PathPrefix:fs/btrfs/]
Results:
|
| 30/2 |
2026/06/01 17:18 |
llm |
expert |
0m
Model:
Tokens:
input: 46381
output: 35
thoughts: 1857
Thoughts: **Analysis of a Btrfs Inconsistency Warning**
Okay, so Btrfs doesn't support `FS_USERNS_MOUNT`, so we can rule out user namespace mounts. No VM Guest or Host triggers, no Ethernet network triggers, and no other remote triggers. The only realistic trigger is a peripheral, specifically, a malicious USB drive with a crafted Btrfs image that gets auto-mounted. A malicious filesystem image is the key here.
But, I need to be sure there isn't another way. Could a sudden power loss cause this type of inconsistency? Btrfs is copy-on-write, it should be robust. When inserting a directory item, the inode item should be added in the same transaction. However, a bug in the journal replay or tree log replay *could* cause a problem. But, in that case, the worst that should happen is a warning and a return of `-EEXIST`. It shouldn't be a security vulnerability.
Let's dig deeper. What if `new_inode` isn't NULL, but it's a different inode? The warning is only triggered if `new_inode` *is* NULL.
I need to re-examine the reproducer. It mounts a Btrfs image, does some syscall noise that's likely syzkaller-generated, renames a file a couple of times, runs `quotactl` (irrelevant), and then calls `FS_IOC_REMOVE_ENCRYPTION_KEY`. The rename happens *before* removing the encryption key, but the image is already crafted. The reproducer is basically just mounting and renaming from a corrupted image.
Can `btrfs_rename` be reached with `new_inode == NULL` in another way? If `new_dentry` is negative, `new_inode` is NULL, which is normal when renaming to a non-existent file. In this case, `btrfs_check_dir_item_collision` should return `0`, but it's returning `-EEXIST`. That means the directory item exists, but the VFS thinks the file doesn't exist, meaning the inode is missing.
Could the VFS think a file doesn't exist due to caching, but the inode *is* on disk? If the VFS cache is evicted, `vfs_rename` calls `lookup`. This calls `btrfs_lookup`, reading the dir item and inode item. If both exist, it creates a positive dentry, and `new_inode` wouldn't be NULL. The only way is if `btrfs_lookup` returns a negative dentry, which it only does if the inode item (or dir item) isn't on disk. If the dir item isn't found, `btrfs_check_dir_item_collision` wouldn't find it either, so it won't return `-EEXIST`. So, the only way is if the directory item *exists* but the inode item does *not*. This means a structural inconsistency in the B-tree, that requires a crafted image or severe corruption.
The security impact is minimal. It's a warning from a crafted filesystem image, and returning an error is the correct handling.
Filling the results: Exploitability: False, Denial of Service: False, Unprivileged: False, UserNamespace: False, VMGuestTrigger: False, VMHostTrigger: False, NetworkTrigger: False, RemoteTrigger: False, PeripheralTrigger: True (because of the malicious image), FilesystemTrigger: True.
Wait, is a hash collision a possibility where `-EEXIST` could be returned by `btrfs_check_dir_item_collision`? Let's check the code:
`btrfs_lookup_match_dir` calls `btrfs_match_dir_item_name`, and it explicitly checks that `btrfs_dir_name_len == name_len` and that the *exact* name matches. If there's a hash collision, the exact name won't match, so it returns `NULL`. `btrfs_check_dir_item_collision` then checks if there's room, so it will *not* return `-EEXIST`. The bug strictly requires the *exact* directory name to exist, but the inode item doesn't exist.
Are there cases where `btrfs_lookup` might fail to find the inode even if the inode exists, like an `-ENOMEM` from `btrfs_iget`? If `btrfs_iget` returns `-ENOMEM`, then `btrfs_lookup_dentry` returns an error, which in turn returns an error from `btrfs_lookup`. That would cause a failure *before* the `vfs_rename` even takes place. Wait, `v
|
| 31/2 |
2026/06/01 17:18 |
tool |
codesearch-definition-source |
0m
Error:
Args: map[ContextFile:fs/namei.c Name:do_renameat2]
Results: map[SourceCode: SourceFile:]
|
| 32/2 |
2026/06/01 17:18 |
llm |
expert |
0m
Model:
Tokens:
input: 49319
output: 35
thoughts: 20
Thoughts: Here's my thought process, as if I were considering this myself:
**Investigating the File Renaming Functionality**
Okay, so I need to dig into the file renaming process, likely to understand a specific edge case or perhaps optimize performance. I'm going to start by looking into the kernel's implementation. My immediate thought is to check two key functions that handle this: `filename_renameat2` and `__do_sys_rename`. `filename_renameat2` feels like the likely primary entry point for rename operations with more modern features and the `at2` suffix hints at more advanced options. `__do_sys_rename` is another possibility; it's a more fundamental or older system call, and I need to make sure I consider both of them as the point of entry. It's a good starting point to verify the code paths, error handling, and file system interactions. Let's start there.
|
| 33/2 |
2026/06/01 17:18 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/namei.c Name:filename_renameat2]
Results: map[SourceCode:6093: int filename_renameat2(int olddfd, struct filename *from,
6094: int newdfd, struct filename *to, unsigned int flags)
6095: {
6096: struct renamedata rd;
6097: struct path old_path, new_path;
6098: struct qstr old_last, new_last;
6099: int old_type, new_type;
6100: struct delegated_inode delegated_inode = { };
6101: unsigned int lookup_flags = 0;
6102: bool should_retry = false;
6103: int error;
6104:
6105: if (flags & ~(RENAME_NOREPLACE | RENAME_EXCHANGE | RENAME_WHITEOUT))
6106: return -EINVAL;
6107:
6108: if ((flags & (RENAME_NOREPLACE | RENAME_WHITEOUT)) &&
6109: (flags & RENAME_EXCHANGE))
6110: return -EINVAL;
6111:
6112: retry:
6113: error = filename_parentat(olddfd, from, lookup_flags, &old_path,
6114: &old_last, &old_type);
6115: if (error)
6116: return error;
6117:
6118: error = filename_parentat(newdfd, to, lookup_flags, &new_path, &new_last,
6119: &new_type);
6120: if (error)
6121: goto exit1;
6122:
6123: error = -EXDEV;
6124: if (old_path.mnt != new_path.mnt)
6125: goto exit2;
6126:
6127: error = -EBUSY;
6128: if (old_type != LAST_NORM)
6129: goto exit2;
6130:
6131: if (flags & RENAME_NOREPLACE)
6132: error = -EEXIST;
6133: if (new_type != LAST_NORM)
6134: goto exit2;
6135:
6136: error = mnt_want_write(old_path.mnt);
6137: if (error)
6138: goto exit2;
6139:
6140: retry_deleg:
6141: rd.old_parent = old_path.dentry;
6142: rd.mnt_idmap = mnt_idmap(old_path.mnt);
6143: rd.new_parent = new_path.dentry;
6144: rd.delegated_inode = &delegated_inode;
6145: rd.flags = flags;
6146:
6147: error = __start_renaming(&rd, lookup_flags, &old_last, &new_last);
6148: if (error)
6149: goto exit_lock_rename;
6150:
6151: if (flags & RENAME_EXCHANGE) {
6152: if (!d_is_dir(rd.new_dentry)) {
6153: error = -ENOTDIR;
6154: if (new_last.name[new_last.len])
6155: goto exit_unlock;
6156: }
6157: }
6158: /* unless the source is a directory trailing slashes give -ENOTDIR */
6159: if (!d_is_dir(rd.old_dentry)) {
6160: error = -ENOTDIR;
6161: if (old_last.name[old_last.len])
6162: goto exit_unlock;
6163: if (!(flags & RENAME_EXCHANGE) && new_last.name[new_last.len])
6164: goto exit_unlock;
6165: }
6166:
6167: error = security_path_rename(&old_path, rd.old_dentry,
6168: &new_path, rd.new_dentry, flags);
6169: if (error)
6170: goto exit_unlock;
6171:
6172: error = vfs_rename(&rd);
6173: exit_unlock:
6174: end_renaming(&rd);
6175: exit_lock_rename:
6176: if (is_delegated(&delegated_inode)) {
6177: error = break_deleg_wait(&delegated_inode);
6178: if (!error)
6179: goto retry_deleg;
6180: }
6181: mnt_drop_write(old_path.mnt);
6182: exit2:
6183: if (retry_estale(error, lookup_flags))
6184: should_retry = true;
6185: path_put(&new_path);
6186: exit1:
6187: path_put(&old_path);
6188: if (should_retry) {
6189: should_retry = false;
6190: lookup_flags |= LOOKUP_REVAL;
6191: goto retry;
6192: }
6193: return error;
6194: }
SourceFile:fs/namei.c]
|
| 34/2 |
2026/06/01 17:18 |
llm |
expert |
0m
Model:
Tokens:
input: 51096
output: 35
thoughts: 11
Thoughts: Okay, here's my attempt at summarizing the thought process, as if it were my own, assuming I'm an expert in the field:
**Internal Workflow & the `__start_renaming` Function**
Alright, so here's the deal. When I'm working through this renaming process, the critical first step, the kickoff point, really hinges on `__start_renaming`. That function is the workhorse of finding the necessary information. It's the point where I initiate the whole lookup procedure. I'm essentially starting the navigation through my internal system, be that a database, a cache, a directory structure, or whatever the particular implementation relies upon. This is where I begin the search for the data I need.
|
| 35/2 |
2026/06/01 17:18 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:fs/namei.c Name:__start_renaming]
Results: map[SourceCode:3845: /**
3846: * __start_renaming - lookup and lock names for rename
3847: * @rd: rename data containing parents and flags, and
3848: * for receiving found dentries
3849: * @lookup_flags: extra flags to pass to ->lookup (e.g. LOOKUP_REVAL,
3850: * LOOKUP_NO_SYMLINKS etc).
3851: * @old_last: name of object in @rd.old_parent
3852: * @new_last: name of object in @rd.new_parent
3853: *
3854: * Look up two names and ensure locks are in place for
3855: * rename.
3856: *
3857: * On success the found dentries are stored in @rd.old_dentry,
3858: * @rd.new_dentry and an extra ref is taken on @rd.old_parent.
3859: * These references and the lock are dropped by end_renaming().
3860: *
3861: * The passed in qstrs must have the hash calculated, and no permission
3862: * checking is performed.
3863: *
3864: * Returns: zero or an error.
3865: */
3866: static int
3867: __start_renaming(struct renamedata *rd, int lookup_flags,
3868: struct qstr *old_last, struct qstr *new_last)
3869: {
3870: struct dentry *trap;
3871: struct dentry *d1, *d2;
3872: int target_flags = LOOKUP_RENAME_TARGET | LOOKUP_CREATE;
3873: int err;
3874:
3875: if (rd->flags & RENAME_EXCHANGE)
3876: target_flags = 0;
3877: if (rd->flags & RENAME_NOREPLACE)
3878: target_flags |= LOOKUP_EXCL;
3879:
3880: trap = lock_rename(rd->old_parent, rd->new_parent);
3881: if (IS_ERR(trap))
3882: return PTR_ERR(trap);
3883:
3884: d1 = lookup_one_qstr_excl(old_last, rd->old_parent,
3885: lookup_flags);
3886: err = PTR_ERR(d1);
3887: if (IS_ERR(d1))
3888: goto out_unlock;
3889:
3890: d2 = lookup_one_qstr_excl(new_last, rd->new_parent,
3891: lookup_flags | target_flags);
3892: err = PTR_ERR(d2);
3893: if (IS_ERR(d2))
3894: goto out_dput_d1;
3895:
3896: if (d1 == trap) {
3897: /* source is an ancestor of target */
3898: err = -EINVAL;
3899: goto out_dput_d2;
3900: }
3901:
3902: if (d2 == trap) {
3903: /* target is an ancestor of source */
3904: if (rd->flags & RENAME_EXCHANGE)
3905: err = -EINVAL;
3906: else
3907: err = -ENOTEMPTY;
3908: goto out_dput_d2;
3909: }
3910:
3911: rd->old_dentry = d1;
3912: rd->new_dentry = d2;
3913: dget(rd->old_parent);
3914: return 0;
3915:
3916: out_dput_d2:
3917: dput(d2);
3918: out_dput_d1:
3919: dput(d1);
3920: out_unlock:
3921: unlock_rename(rd->old_parent, rd->new_parent);
3922: return err;
3923: }
SourceFile:fs/namei.c]
|
| 36/2 |
2026/06/01 17:18 |
llm |
expert |
0m
Model:
Tokens:
input: 52513
output: 66
thoughts: 928
Thoughts: **Analysis of the Btrfs Rename Vulnerability**
Okay, so here's the breakdown of this issue. `lookup_one_qstr_excl` is the workhorse here, and it's clear that it bubbles up errors from `btrfs_lookup`. If `btrfs_lookup` craps out with something like `-ENOMEM`, then we immediately get an `ERR_PTR(-ENOMEM)` back. That error then cascades up through `__start_renaming`, `filename_renameat2`, and ultimately, `vfs_rename` never even gets called. Since `vfs_rename` isn't called, `btrfs_rename` is never called. Therefore, the only way `new_inode` can be `NULL` is if `btrfs_lookup` returns a negative dentry, meaning it returns `NULL` or `ERR_PTR(-ENOENT)`. This only happens if the inode item is missing in the first place, or perhaps if `btrfs_lookup` is failing for some other reason, like `-ENOMEM`, which we covered. The analysis holds, meaning this is a `WARN_ON` triggered by a crafted, malicious Btrfs image.
Alright, but let's be sure there's no other way this can happen. What if `new_inode` isn't `NULL`, but `btrfs_check_dir_item_collision` returns `-EEXIST`? Looking at the code:
```c
ret = btrfs_check_dir_item_collision(dest, new_dir->i_ino, &new_fname.disk_name);
if (ret) {
if (ret == -EEXIST) {
/* we shouldn't get
* eexist without a new_inode */
if (WARN_ON(!new_inode)) {
goto out_fscrypt_names;
}
} else {
```
If `new_inode` *isn't* `NULL`, it does *not* trigger the `WARN_ON`. No jump to `out_fscrypt_names`, it continues!
```c
ret = btrfs_check_dir_item_collision(dest, new_dir->i_ino, &new_fname.disk_name);
if (ret) {
if (ret == -EEXIST) {
/* we shouldn't get
* eexist without a new_inode */
if (WARN_ON(!new_inode)) {
goto out_fscrypt_names;
}
} else {
/* maybe -EOVERFLOW */
goto out_fscrypt_names;
}
}
ret = 0;
```
It actually sets `ret = 0` and proceeds. This is the intended behavior. If `ret == -EEXIST` and `new_inode` is *not* `NULL`, it means the destination file already exists. If the destination file exists, the directory entry MUST exist. This makes perfect sense; `btrfs_check_dir_item_collision` is called to check if there is room, or if the name already exists. `-EEXIST` is totally expected in this scenario because we will overwrite an existing entry.
Okay, let's step back and summarize. If `new_inode` is `NULL`, VFS thinks the name doesn't exist, but Btrfs finds it, an inconsistency! So, that `WARN_ON` is checking for this inconsistency. And this inconsistency can *only* be the result of a corrupted/crafted filesystem. This confirms everything.
Alright, so the dimensions:
* **Exploitable: False**
* **DenialOfService: False**
* **Unprivileged: False**
* **UserNamespace: False**
* **VMGuestTrigger: False**
* **VMHostTrigger: False**
* **NetworkTrigger: False**
* **RemoteTrigger: False**
* **PeripheralTrigger: True**
* **FilesystemTrigger: True**
With that, I'll call `set-results`.
|
| 37/2 |
2026/06/01 17:18 |
tool |
set-results |
0m
Args: map[DenialOfService:false Exploitable:false FilesystemTrigger:true NetworkTrigger:false PeripheralTrigger:true RemoteTrigger:false Unprivileged:false UserNamespace:false VMGuestTrigger:false VMHostTrigger:false]
Results: map[DenialOfService:false Exploitable:false FilesystemTrigger:true NetworkTrigger:false PeripheralTrigger:true RemoteTrigger:false Unprivileged:false UserNamespace:false VMGuestTrigger:false VMHostTrigger:false]
|
| 38/2 |
2026/06/01 17:18 |
llm |
expert |
0m
Model:
Tokens:
input: 54488
output: 765
thoughts: 90
Thoughts: Okay, here's my thought process as I craft the final response, tailored for an expert audience like you:
**Finalizing the Explanation**
Alright, the data's in, and I'm ready to finalize the explanation. My goal is to present a concise and accurate response to the problem. I need to make sure the root cause β this handcrafted btrfs image with a directory item but a missing inode item β is clearly articulated. I'll need to detail precisely how this specific structure triggers that `WARN_ON` message. And, importantly, I must emphasize that this doesnβt represent a security vulnerability. It's just a warning. The kernel handles it gracefully by returning an error, thus preventing any exploitable behavior. Finally, I'll reinforce that mounting a malicious filesystem is a prerequisite, which is why the `PeripheralTrigger` and `FilesystemTrigger` flags are set to true. My objective is clarity and completeness, leaving no room for misunderstanding.
|
| 39/1 |
2026/06/01 17:19 |
action |
format-explanation |
0m
Results: map[Explanation:The bug report indicates a `WARN_ON(!new_inode)` being triggered in
`btrfs_rename` (specifically in `fs/btrfs/inode.c`).
### Root Cause Analysis
The warning is triggered by the following code block in `btrfs_rename`:
```c
ret = btrfs_check_dir_item_collision(dest, new_dir->i_ino,
&new_fname.disk_name);
if (ret) {
if (ret == -EEXIST) {
/* we shouldn't get
* eexist without a new_inode */
if (WARN_ON(!new_inode)) {
goto out_fscrypt_names;
}
} else {
/* maybe -EOVERFLOW */
goto out_fscrypt_names;
}
}
```
When a file is renamed, `btrfs_rename` checks if the destination name already
exists in the directory b-tree by calling `btrfs_check_dir_item_collision`. If
the exact name is found, it returns `-EEXIST`.
Normally, if the destination file exists, the VFS (Virtual File System) layer
would have already found it during the path lookup phase
(`lookup_one_qstr_excl`), and `new_inode` would point to the existing file's
inode. In this case, `-EEXIST` is expected, the warning is bypassed, and the
rename operation proceeds to overwrite the existing file.
However, if `new_inode` is `NULL`, it means the VFS lookup determined that the
destination file does *not* exist (resulting in a negative dentry). If
`btrfs_check_dir_item_collision` then finds the name in the b-tree, it reveals a
structural inconsistency in the filesystem: a directory item exists for the
name, but the corresponding inode item is missing.
When the VFS performs the lookup, `btrfs_lookup_dentry` finds the directory item
but fails to find the inode item (`btrfs_iget` returns `-ENOENT`), causing it to
return a negative dentry. Later, `btrfs_rename` finds the orphaned directory
item and triggers the `WARN_ON`.
### Security Impact
This inconsistency can only be triggered by a corrupted or maliciously crafted
BTRFS image.
- **Exploitable:** No. The bug is merely a `WARN_ON` that detects filesystem
corruption. The unexpected condition is properly handled by the `goto
out_fscrypt_names;` error path, which safely aborts the operation and returns
`-EEXIST` to the user-space caller. There is no memory corruption, out-of-bounds
access, or privilege escalation.
- **Denial of Service:** No. The warning does not halt or crash the system in
production builds (unless `panic_on_warn` is explicitly enabled by the
administrator, which is not the default). It simply returns an error.
- **Access Vectors:** Mounting a BTRFS filesystem requires `CAP_SYS_ADMIN` in
the initial namespace (BTRFS does not support `FS_USERNS_MOUNT`). Therefore,
unprivileged local users or users in a user namespace cannot trigger this bug
directly. However, it can be triggered if a system is configured to auto-mount
external media (e.g., a malicious USB drive plugged into a desktop machine).
In conclusion, this is a benign warning designed to catch filesystem corruption
and gracefully error out, posing no significant security risk.]
|