#820·btrfs

Uninitialized variables and lifetime bugs: mknod (inode/subvol), mount_vol failure after VPB_MOUNTED, create_calc_threads typo, delayed notifications, balance chunk pointer

Author: xfcyhuangCreated Sep 4, 2026Updated Sep 4, 2026

Audit findings against master @ a0648190 (v1.10). Four independent bugs found by code audit; all verified against the current master source.

1. mknod: inode / subvol / dc used uninitialized

mknod (src/fsctl.c:3782) declares root* subvol; uint64_t inode; dir_child* dc; and calls:

c
Status = find_file_in_dir(&name, parfcb, &subvol, &inode, &dc, true);   // fsctl.c:3889
if (!NT_SUCCESS(Status) && Status != STATUS_OBJECT_NAME_NOT_FOUND) { ... }
if (NT_SUCCESS(Status)) { /* collision, bail out */ }

find_file_in_dir (src/create.c:182) only writes *subvol / *inode / *pdc when the name is actually found; every STATUS_OBJECT_NAME_NOT_FOUND path leaves them untouched — and NOT_FOUND is the normal outcome when creating a new file. Execution then continues and uses the uninitialized values:

  • src/fsctl.c:4050 — fcb->inode = inode; unconditionally overwrites the inode number carefully computed at fsctl.c:4019-4046 with whatever is on the stack. This path is reachable without any privilege: FSCTL_BTRFS_MKNOD with inode == 0 only requires FILE_ADD_FILE/FILE_ADD_SUBDIRECTORY on the parent directory (fsctl.c:3834-3838). It "works" whenever the stale stack slot happens to hold a sane value, and produces a wrong/duplicate inode number (metadata corruption) when it doesn't.
  • src/fsctl.c:4029 — check_inode_used(Vcb, subvol, bmn->inode, ...) passes the uninitialized subvol pointer. This variant needs SE_MANAGE_VOLUME_PRIVILEGE (checked at fsctl.c:3842 when bmn->inode != 0).

(The fcb->inode = inode; line predates the 2019 lock restructuring — it behaved the same way before.)

2. mount_vol: failure after VPB_MOUNTED tears down the Vcb under the running flush thread; create_calc_threads cleanup typo

mount_vol marks the volume mounted early (src/btrfs.c:5021-5031) and then starts the flush thread (btrfs.c:5037). Any later failure — e.g. create_calc_threads at btrfs.c:5043 or registry/other errors — does goto exit, and the exit path (btrfs.c:5066-5118) deletes every lookaside list and ERESOURCE (tree_lock, chunk_lock, ...), frees the root fcb/filerefs and devices, and calls IoDeleteDevice(NewDeviceObject) — while:

  • the VPB still has VPB_MOUNTED set and still points at the device object being deleted (it is never restored), and
  • the flush thread is still running against the now-deleted locks and freed structures (nothing signals or waits for it).

Separately, the create_calc_threads failure path has a copy-paste typo (src/btrfs.c:4142-4144):

c
for (j = 0; j < i; j++) {
    Vcb->calcthreads.threads[i].quit = true;   // should be threads[j]
}

so the already-started calc threads are never told to quit either — they keep spinning on a Vcb the caller is tearing down. In practice this needs PsCreateSystemThread or an allocation to fail during mount (e.g. under memory pressure), so it is a robustness bug rather than an attacker-controlled one.

3. Delayed change-notification work items hold a raw dir_child name pointer

queue_notification_fcb (src/btrfs.c:1660-1689) queues a DelayedWorkQueue item whose stream argument is a PUNICODE_STRING pointing into a dir_child. Several callers pass exactly that:

  • src/fileinfo.c:3400 and 3554 — queue_notification_fcb(fileref->parent, filter, FILE_ACTION_MODIFIED_STREAM, &fileref->dc->name);
  • src/create.c:3783 — queue_notification_fcb(fileref, ..., &dc->name);
  • src/create.c:3808 — queue_notification_fcb(fileref->parent, filter, FILE_ACTION_MODIFIED, &fileref->dc->name);

The work item only takes a reference on the fileref passed to it — not on the (child) fileref whose dc the name belongs to. If that stream/file is deleted before the work item runs, delete_fileref frees fileref->dc->name.Buffer and then dc itself (src/btrfs.c:2405-2407), and notification_work_itemsend_notification_fcb subsequently reads through the dangling pointer (use-after-free read of pool memory into FsRtlNotifyFilterReportChange).

4. balance: c dereferenced uninitialized when try_consolidation returns "success" without setting *newchunk

src/balance.c:3339-3363:

c
chunk* c;                                                        // uninitialized
Status = alloc_chunk(Vcb, Vcb->metadata_flags, &c, true);        // failure paths never write *pc
...
else if (Status == STATUS_DISK_FULL ...) {
    Status = try_consolidation(Vcb, Vcb->metadata_flags, &c);
    if (!NT_SUCCESS(Status)) { ... }
    else
        c->balance_num = Vcb->balance.balance_num;               // balance.c:3363 — writes through garbage pointer
}

try_consolidation has an early if (Vcb->balance.stopping) return STATUS_SUCCESS; (balance.c ~3112) that does not set *newchunk (only the normal completion at ~3138 does). So "disk full + balance stop requested" dereferences an uninitialized stack pointer. The same pattern repeats for the data and system chunk blocks just below (balance.c:3372+).

Suggested fixes: initialize subvol = NULL; inode = 0; dc = NULL; in mknod (or have find_file_in_dir zero its out-params on entry); set threads[j].quit; in mount_vol, on failure after the VPB was modified, restore the VPB, signal the flush thread to stop and wait for flush_thread_finished before deleting resources; take a reference on (or copy the name from) the dir_child in queue_notification_fcb; and make try_consolidation/alloc_chunk always set *newchunk (or c = NULL at declaration and check before use).