Uninitialized variables and lifetime bugs: mknod (inode/subvol), mount_vol failure after VPB_MOUNTED, create_calc_threads typo, delayed notifications, balance chunk pointer
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:
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_MKNODwithinode == 0only requiresFILE_ADD_FILE/FILE_ADD_SUBDIRECTORYon 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 uninitializedsubvolpointer. This variant needsSE_MANAGE_VOLUME_PRIVILEGE(checked at fsctl.c:3842 whenbmn->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_MOUNTEDset 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):
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_item → send_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:
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).
Source: maharmstone/btrfs