Vulnerability Report: fs/9p/vfs_super.c
Most Serious: Kernel Heap Memory Leak in v9fs_init_fs_context Error Path
File: fs/9p/vfs_super.c
Lines: 307–355
Severity: High (Kernel memory exhaustion / DoS)
Description
v9fs_init_fs_context sets fc->fs_private = ctx after both kstrdup calls succeed. If either allocation fails, the function jumps to the error: label — but fc->fs_private has never been written. The cleanup callback v9fs_free_fc is registered in fc->ops->free, but fc->ops is also set after the kstrdups:
// fs/9p/vfs_super.c:319-354
ctx->session_opts.uname = kstrdup(V9FS_DEFUSER, GFP_KERNEL);
if (!ctx->session_opts.uname)
goto error; // <-- jumps here
ctx->session_opts.aname = kstrdup(V9FS_DEFANAME, GFP_KERNEL);
if (!ctx->session_opts.aname)
goto error; // <-- or here
...
fc->ops = &v9fs_context_ops; // never reached on error
fc->fs_private = ctx; // never reached on error
return 0;
error:
fc->need_free = 1;
return -ENOMEM;
v9fs_free_fc (line 283) starts with:
struct v9fs_context *ctx = fc->fs_private;
if (!ctx)
return; // always taken — ctx is leaked
The kernel’s put_fs_context path only invokes fc->ops->free when fc->ops != NULL. Since fc->ops is not set on the error path, even the guard in put_fs_context (fc->need_free && fc->ops && fc->ops->free) evaluates to false. The result:
- The
v9fs_contextallocation (ctx) is never freed. - If the
unamekstrdup succeeded beforeanamefailed,ctx->session_opts.unameis also never freed.
Impact
An unprivileged local user with CAP_SYS_ADMIN (or in a user namespace that allows mounting) can trigger repeated 9p mount attempts under artificial memory pressure (e.g., using userfaultfd or resource limits to cause kstrdup to fail). Each failed attempt leaks at least sizeof(v9fs_context) bytes of kernel slab memory. Sufficient repetition exhausts kernel heap, causing a system-wide denial of service.
Root Cause
fc->fs_private and fc->ops are assigned too late — only after all infallible allocations have succeeded. The error label does not have access to ctx for cleanup, and no explicit kfree(ctx) is present.
Fix
Move fc->fs_private = ctx immediately after kzalloc succeeds, so the existing v9fs_free_fc callback can handle partial-initialization cleanup:
ctx = kzalloc(sizeof(*ctx), GFP_KERNEL);
if (!ctx)
return -ENOMEM;
fc->ops = &v9fs_context_ops; // register early
fc->fs_private = ctx; // allow free callback to find ctx
ctx->session_opts.uname = kstrdup(V9FS_DEFUSER, GFP_KERNEL);
if (!ctx->session_opts.uname)
return -ENOMEM; // v9fs_free_fc will clean up ctx
...
Secondary: Undefined Behavior in v9fs_fill_super Block Size Calculation
File: fs/9p/vfs_super.c
Lines: 40–41
Severity: Medium (implementation-defined / UB, unlikely to be exploitable directly)
Description
// line 40-41
sb->s_blocksize_bits = fls(v9ses->maxdata - 1);
sb->s_blocksize = 1 << sb->s_blocksize_bits;
maxdata is unsigned int. The validated upper bound for the 9p negotiated msize is INT_MAX (line 346 of v9fs.c), giving a maximum maxdata of INT_MAX - P9_IOHDRSZ = 2147483623. Then:
fls(2147483622) = 31s_blocksize_bits = 311 << 31— shifting a signedintvalue1left by 31 bits sets the sign bit, which is undefined behavior under the C standard (C11 §6.5.7).
On x86-64 GCC this evaluates to 0x80000000 in practice, but UB allows a compiler to produce any value (including zero, which would cascade into divide-by-zero in block I/O paths).
Fix
Use an unsigned shift: sb->s_blocksize = 1UL << sb->s_blocksize_bits;