โฌข DragonFlyBSD Kernel Audit
DF-0819 / blkmap_lock_trace.c
โ† back to finding โ†“ download raw
/*
 * DF-0819 โ€” blkmap_lock leaked on hammer_bnew error paths
 *
 * BUG: In hammer_blockmap_alloc() and hammer_blockmap_reserve(), the
 * function acquires hammer_lock_ex(&hmp->blkmap_lock) then calls
 * hammer_bnew()/hammer_bnew_ext().  If that returns an error, the code
 * does `goto failed` which jumps PAST the hammer_unlock() โ€” the lock is
 * permanently leaked.  Since blkmap_lock serializes ALL HAMMER metadata
 * allocation, a leaked lock deadlocks the entire filesystem (every
 * subsequent hammer metadata op blocks forever in D-state, unkillable).
 *
 * This is a CODE-LEVEL analysis harness โ€” it prints the exact line ranges
 * proving the lock-acquire-without-matching-release.  The error path
 * (hammer_bnew failure) is effectively dead code under normal operation
 * because hammer_io_new() always returns 0, but the code pattern is
 * objectively wrong and is a latent deadlock waiting to happen.
 *
 * Run:   ./blkmap_lock_trace
 * (no kernel interaction โ€” pure static analysis printout)
 */
#include <stdio.h>

static const char *analysis =
"=================================================================\n"
"DF-0819: blkmap_lock LEAK on hammer_bnew error paths\n"
"=================================================================\n\n"
"FILE: sys/vfs/hammer/hammer_blockmap.c\n\n"
"--- BUG 1: hammer_blockmap_alloc() [line 95] ---\n\n"
"  285:  hammer_lock_ex(&hmp->blkmap_lock);   // LOCK ACQUIRED\n"
"  ...\n"
"  372:  if ((next_offset & HAMMER_BUFMASK) == 0) {\n"
"  373:      hammer_bnew_ext(trans->hmp, next_offset, bytes,\n"
"  374:                      errorp, &buffer3);\n"
"  375:      if (*errorp) {\n"
"  376:          result_offset = 0;\n"
"  377:          goto failed;                   // <<<< JUMPS PAST UNLOCK!\n"
"  378:      }\n"
"  379:  }\n"
"  ...\n"
"  392:  hammer_unlock(&hmp->blkmap_lock);     // UNLOCK (SKIPPED by goto)\n"
"  393: failed:                                 // <<<< LABEL IS PAST UNLOCK\n"
"  398:  if (buffer1) hammer_rel_buffer(buffer1, 0);\n"
"  ...\n"
"  405:  return(result_offset);\n\n"
"  PROOF: line 377 `goto failed` targets label at line 393,\n"
"         which is AFTER the unlock at line 392.\n"
"         The lock acquired at line 285 is NEVER released on this path.\n\n"
"--- BUG 2: hammer_blockmap_reserve() [line 419] ---\n\n"
"  546:  hammer_lock_ex(&hmp->blkmap_lock);   // LOCK ACQUIRED\n"
"  ...\n"
"  602:  if (bytes < HAMMER_BUFSIZE && (next_offset & HAMMER_BUFMASK) == 0) {\n"
"  603:      if (!vm_paging_min_dnc(HAMMER_BUFSIZE / PAGE_SIZE)) {\n"
"  604:          hammer_bnew(hmp, next_offset, errorp, &buffer3);\n"
"  605:          if (*errorp)\n"
"  606:              goto failed;               // <<<< JUMPS PAST UNLOCK!\n"
"  607:      }\n"
"  608:  }\n"
"  ...\n"
"  611:  hammer_unlock(&hmp->blkmap_lock);     // UNLOCK (SKIPPED by goto)\n"
"  613: failed:                                 // <<<< LABEL IS PAST UNLOCK\n"
"  ...\n"
"  623:  return(resv);\n\n"
"  PROOF: line 606 `goto failed` targets label at line 613,\n"
"         which is AFTER the unlock at line 611.\n"
"         The lock acquired at line 546 is NEVER released on this path.\n\n"
"--- IMPACT ---\n\n"
"  blkmap_lock serializes ALL HAMMER blockmap (metadata/data) allocation.\n"
"  A leaked lock means every subsequent hammer_blockmap_alloc/reserve/\n"
"  finalize/free/dedup call blocks forever in hammer_lock_ex() on\n"
"  blkmap_lock.  This is an unkillable D-state deadlock affecting the\n"
"  entire mounted HAMMER filesystem.  Impact: permanent DoS of the\n"
"  filesystem; all processes touching it hang in D-state.\n\n"
"--- TRIGGER REACHABILITY ---\n\n"
"  The error path requires hammer_bnew()/hammer_bnew_ext() to return\n"
"  a non-zero *errorp.  Tracing the call chain:\n"
"    hammer_bnew_ext -> _hammer_bread -> hammer_get_buffer ->\n"
"    hammer_load_buffer(isnew=1) -> hammer_io_new() -> return(0)\n"
"  hammer_io_new() (hammer_io.c:437) ALWAYS returns 0 โ€” it calls\n"
"  getblk() which either succeeds or panics (NULL deref at bp->b_ops).\n"
"  So under normal operation the error path is dead code.\n\n"
"  However, hammer_get_buffer() can set *errorp via hammer_get_volume()\n"
"  (ENOENT if zone2_offset decodes to a non-existent vol_no โ€” filesystem\n"
"  corruption) or hammer_load_volume() (I/O error on backing device).\n"
"  These are realistic on a corrupted/worn device or crafted image.\n\n"
"  CLASSIFICATION: latent bug โ€” real code defect, lock-leak pattern is\n"
"  objectively present. Would deadlock the filesystem if the error path\n"
"  is ever reached (corruption / I/O error / crafted image / future\n"
"  code changes adding real error returns to hammer_io_new).\n\n"
"--- FIX ---\n\n"
"  Add hammer_unlock(&hmp->blkmap_lock) before each `goto failed` that\n"
"  occurs after the lock is acquired (lines 377 and 606).  See fix.diff.\n"
"=================================================================\n";

int main(void)
{
    printf("%s", analysis);
    return 0;
}