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_context allocation (ctx) is never freed.
  • If the uname kstrdup succeeded before aname failed, ctx->session_opts.uname is 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) = 31
  • s_blocksize_bits = 31
  • 1 << 31 — shifting a signed int value 1 left 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;