1#!/bin/sh 2 3# 4# SPDX-License-Identifier: BSD-2-Clause 5# 6# Copyright (c) 2026 Maksym Sobolyev <sobomax@sippysoft.com> 7# 8# Redistribution and use in source and binary forms, with or without 9# modification, are permitted provided that the following conditions 10# are met: 11# 1. Redistributions of source code must retain the above copyright 12# notice, this list of conditions and the following disclaimer. 13# 2. Redistributions in binary form must reproduce the above copyright 14# notice, this list of conditions and the following disclaimer in the 15# documentation and/or other materials provided with the distribution. 16# 17# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 18# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 20# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 21# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 22# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 23# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 24# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 25# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 26# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 27# SUCH DAMAGE. 28# 29 30# Hunt for "panic: flush_newblk_dep: Bad newblk" (PR 297976). 31# 32# flush_pagedep_deps() drops the soft updates lock to obtain the vnode of a 33# newly created directory (MKDIR_BODY). If the mkdir dependency completes 34# in that window, the directory's allocdirect is retired and a lookup of its 35# first block by block number can return a stale allocindir left behind by a 36# previous owner of the same physical block that was relocated by 37# ffs_reallocblks() and freed. 38# 39# Three kinds of workers on a small SU (no journal) file system: 40# - "wal" writers grow two interleaved files past UFS_NDADDR so that 41# clusters referenced from an indirect block keep getting relocated, 42# freeing the old blocks while their dependencies are retained. 43# - "dir" workers fill a directory, unlink the trailing entries so the next 44# insert sets IN_ENDOFF, then mkdir subdirectories. ffs_vput_pair() 45# then truncates the parent and calls ffs_syncvnode(MNT_WAIT), which 46# is the path into flush_pagedep_deps() / flush_newblk_dep(). 47# - "sync" workers fsync() the parents and their subdirectories to 48# complete the mkdir dependencies while a flush is in progress. 49# 50# Several details are needed for a new directory to land on such a block: 51# directory blocks are allocated from the metadata area of the cylinder 52# group, so it is filled with placeholder directories first; fragments 53# are as large as blocks so that a directory block takes over a whole 54# freed block; each block the writers add must be on disk, with its 55# dependencies complete, before it is relocated, while the writers' 56# inodes stay unwritten: the writers put every new block on disk with a 57# partial O_DIRECT write before completing it, and fsync() of a small 58# helper file writes the cylinder group map; and the writers' inodes are 59# isolated in their own inode blocks so that no other fsync() writes them. 60# Keep the load light: dirty buffer pressure or dependency cleanups sync 61# every inode and retire the stale dependencies. 62# 63# The writers used to put their blocks on disk with fdatasync(), but that 64# writes the inode whenever the size or a block pointer changed once the 65# i_flag/i_flags mixup in ffs_syncvnode() is fixed, which retires the 66# stale dependencies at once. WALSYNC=fdatasync selects that old method, 67# which only works on kernels with the mixup; the default is "direct". 68# 69# Tunables: NWAL, NDIR, NSYNC, NFILES, NSUB, WALSIZE (bytes), 70# WALDELAY (us between 32k writes), WALSYNC (direct or fdatasync), 71# WALFSYNC (fsync the writers every N writes, 0 off), DIRDELAY (us between 72# mkdirs), NPAD (directories used to fill the metadata area of the 73# cylinder group), RUNTIME. 74# Set dtrace=1 to count how often the affected functions were entered and 75# how often a new directory reused a block with a stale dependency. The 76# latter is the precondition for the panic and can be observed on a fixed 77# kernel. 78# 79# Panics within a few minutes on 14.3 without the fix. 80 81. ../default.cfg 82[ `id -u` -ne 0 ] && echo "Must be root!" && exit 1 83 84dir=/tmp 85odir=`pwd` 86cd $dir 87sed '1,/^EOF/d' < $odir/$0 > $dir/mkdir_blkreuse.c 88mycc -o mkdir_blkreuse -Wall -Wextra -O0 -g mkdir_blkreuse.c || exit 1 89rm -f mkdir_blkreuse.c 90cd $odir 91 92runtime=`echo ${RUNTIME:-10m} | sed -e 's/m$/*60/' -e 's/s$//' | bc` 93export WALSYNC=${WALSYNC:-direct} 94case $WALSYNC in 95direct|fdatasync) 96 ;; 97*) 98 echo "WALSYNC must be direct or fdatasync" >&2 99 exit 1 100 ;; 101esac 102s=0 103set -e 104mount | grep "on $mntpoint " | grep -q /dev/md && umount -f $mntpoint 105[ -c /dev/md$mdstart ] && mdconfig -d -u $mdstart 106mdconfig -a -t swap -s 1g -u $mdstart 107# Fragment size equal to the block size makes every directory block a full 108# block allocation, so a new directory's block 0 can take over exactly the 109# block number a relocated cluster gave back, instead of a fragment carved 110# from a partially used block. SU defers inode frees; use the highest 111# inode density to limit "out of inodes". 112newfs -U -b 32768 -f 32768 -i 32768 md$mdstart > /dev/null 113mount -o noatime /dev/md$mdstart $mntpoint 114set +e 115 116if [ $dtrace ]; then 117 # flush_pagedep_deps() and flush_newblk_dep() are inlined; 118 # get_parent_vp() is only called from flush_pagedep_deps(). 119 # The second clause detects the precondition for the panic: block 0 120 # of a new directory is allocated while the newblk hash still holds a 121 # dependency of a previous owner of that block, an allocindir 122 # (D_ALLOCINDIR == 7) or an allocdirect (D_ALLOCDIRECT == 5) of 123 # another inode. A non-zero count means an unfixed kernel would 124 # have panicked. 125 cat > /tmp/mkdir_blkreuse.d <<'EOD' 126fbt::softdep_setup_mkdir:entry, 127fbt::get_parent_vp:entry, 128fbt::ffs_reallocblks:entry 129{ 130 @[probefunc] = count(); 131} 132 133fbt::softdep_setup_allocdirect:entry 134/args[1] == 0 && args[3] == 0 && (args[0]->i_mode & 0170000) == 0040000/ 135{ 136 this->ino = args[0]->i_number; 137 this->blk = args[2]; 138 this->sd = args[0]->i_ump->um_softdep; 139 this->nb = this->sd->sd_newblkhash[this->blk & 140 this->sd->sd_newblkhashsize].lh_first; 141 this->hit = 0; 142 this->hit += this->nb != NULL && this->nb->nb_newblkno == this->blk && 143 (this->nb->nb_list.wk_type == 7 || 144 (this->nb->nb_list.wk_type == 5 && 145 ((struct allocdirect *)this->nb)->ad_inodedep->id_ino != 146 this->ino)) ? 1 : 0; 147 this->nb = this->nb != NULL ? this->nb->nb_hash.le_next : NULL; 148 this->hit += this->nb != NULL && this->nb->nb_newblkno == this->blk && 149 (this->nb->nb_list.wk_type == 7 || 150 (this->nb->nb_list.wk_type == 5 && 151 ((struct allocdirect *)this->nb)->ad_inodedep->id_ino != 152 this->ino)) ? 1 : 0; 153 this->nb = this->nb != NULL ? this->nb->nb_hash.le_next : NULL; 154 this->hit += this->nb != NULL && this->nb->nb_newblkno == this->blk && 155 (this->nb->nb_list.wk_type == 7 || 156 (this->nb->nb_list.wk_type == 5 && 157 ((struct allocdirect *)this->nb)->ad_inodedep->id_ino != 158 this->ino)) ? 1 : 0; 159 this->nb = this->nb != NULL ? this->nb->nb_hash.le_next : NULL; 160 this->hit += this->nb != NULL && this->nb->nb_newblkno == this->blk && 161 (this->nb->nb_list.wk_type == 7 || 162 (this->nb->nb_list.wk_type == 5 && 163 ((struct allocdirect *)this->nb)->ad_inodedep->id_ino != 164 this->ino)) ? 1 : 0; 165 @["new directory block 0 allocated"] = count(); 166} 167 168fbt::softdep_setup_allocdirect:entry 169/this->hit != 0/ 170{ 171 @["new directory block 0 with stale dependency (would panic)"] = 172 count(); 173} 174EOD 175 dtrace -s /tmp/mkdir_blkreuse.d & 176 dpid=$! 177 sleep 2 178fi 179 180/tmp/mkdir_blkreuse $mntpoint $runtime || s=1 181 182if [ $dtrace ]; then 183 kill -s TERM $dpid 184 wait $dpid 185fi 186 187for i in `jot 6`; do 188 mount | grep -q "on $mntpoint " || break 189 umount $mntpoint && break || sleep 10 190 [ $i -eq 6 ] && 191 { echo FATAL; fstat -mf $mntpoint; exit 1; } 192done 193checkfs /dev/md$mdstart || s=2 194mdconfig -d -u $mdstart 195rm -f /tmp/mkdir_blkreuse /tmp/mkdir_blkreuse.d 196exit $s 197EOF 198#include <sys/param.h> 199#include <sys/stat.h> 200#include <sys/wait.h> 201 202#include <err.h> 203#include <errno.h> 204#include <fcntl.h> 205#include <signal.h> 206#include <stdio.h> 207#include <stdlib.h> 208#include <string.h> 209#include <time.h> 210#include <unistd.h> 211 212#define CHUNK 32768 213#define INOPAD 128 /* UFS2 inodes per 32k block */ 214 215static const char *path; 216static int nwal, ndir, nsync, nfiles, nsub, walfsync; 217static enum { WAL_DIRECT, WAL_FDATASYNC } walsync; 218static const char *walsyncname[] = { "direct", "fdatasync" }; 219static long walsize; 220static useconds_t waldelay, dirdelay; 221static volatile sig_atomic_t done; 222 223static int 224getenvint(const char *name, int def) 225{ 226 char *v; 227 228 if ((v = getenv(name)) == NULL || *v == '\0') 229 return (def); 230 return (atoi(v)); 231} 232 233static void 234handler(int sig __unused) 235{ 236 237 done = 1; 238} 239 240/* 241 * Inodes are handed out by a rotor, so surround a file with enough 242 * placeholder inodes that its inode block holds nothing anybody else will 243 * fsync(). An fsync() of an unrelated inode in the same block would 244 * write the writer's inode and retire the stale dependencies. 245 */ 246static int 247padded_create(const char *dir, const char *name, int *seq) 248{ 249 char pad[MAXPATHLEN]; 250 int fd, i; 251 252 for (i = 0; i < 2 * INOPAD + 1; i++) { 253 if (i == INOPAD) { 254 snprintf(pad, sizeof(pad), "%s/%s", dir, name); 255 if ((fd = open(pad, O_RDWR | O_CREAT | O_TRUNC, 256 0644)) == -1) 257 err(1, "open(%s)", pad); 258 continue; 259 } 260 snprintf(pad, sizeof(pad), "%s/pad.%d", dir, (*seq)++); 261 if (close(open(pad, O_CREAT | O_WRONLY, 0644)) == -1) 262 err(1, "creat(%s)", pad); 263 } 264 return (fd); 265} 266 267/* 268 * Create the writer's files before any other worker starts allocating 269 * inodes, so that the padding is not interleaved with their inodes. 270 */ 271static void 272wal_setup(int idx, int *fd, int *fdcg) 273{ 274 char dir[MAXPATHLEN]; 275 int seq; 276 277 snprintf(dir, sizeof(dir), "%s/w%d", path, idx); 278 if (mkdir(dir, 0755) == -1 && errno != EEXIST) 279 err(1, "mkdir(%s)", dir); 280 seq = 0; 281 fd[0] = padded_create(dir, "wal", &seq); 282 *fdcg = padded_create(dir, "cg", &seq); 283 fd[1] = padded_create(dir, "db", &seq); 284} 285 286/* 287 * Directory blocks are placed in the metadata area at the start of the 288 * cylinder group, file data after it. Fill the metadata area with 289 * placeholder directories so that the directories created later have to 290 * take the lowest free block of the data area, where the relocated 291 * clusters of the writers leave their holes. 292 */ 293static void 294metapad(int npad) 295{ 296 char dir[MAXPATHLEN]; 297 int i; 298 299 snprintf(dir, sizeof(dir), "%s/m", path); 300 if (mkdir(dir, 0755) == -1 && errno != EEXIST) 301 err(1, "mkdir(%s)", dir); 302 for (i = 0; i < npad; i++) { 303 snprintf(dir, sizeof(dir), "%s/m/%d", path, i); 304 if (mkdir(dir, 0755) == -1 && errno != EEXIST) 305 err(1, "mkdir(%s)", dir); 306 } 307} 308 309/* 310 * On an O_DIRECT descriptor, ffs_write() writes a partial block out right 311 * away with bawrite(), without the inode or the indirect block, while a 312 * write that completes a block goes to cluster_write() and so to 313 * ffs_reallocblks(). Write the first half of the block alone to get it on 314 * disk, then the second half to put it into the cluster. 315 */ 316static int 317wal_direct(int fd, char *buf, off_t off) 318{ 319 if (pwrite(fd, buf, CHUNK / 2, off) != CHUNK / 2 || 320 pwrite(fd, buf + CHUNK / 2, CHUNK / 2, off + CHUNK / 2) != 321 CHUNK / 2) 322 return (-1); 323 return (0); 324} 325 326/* 327 * Slowly grow two files in lockstep, like a database and its WAL, so that 328 * their blocks are interleaved and ffs_reallocblks() has to relocate the 329 * clusters referenced from the indirect blocks, freeing the old blocks for 330 * reuse. 331 * 332 * A relocated block only leaves its old allocindir behind if that 333 * dependency had already completed, and the leftover is only retained 334 * while the indirect block pointer in the inode is not yet on disk. 335 * Each new block is put on disk by wal_direct() before a later write 336 * extends the cluster and has it relocated, and fsync() of a small third 337 * file that just allocated a block in the same cylinder group writes that 338 * map synchronously. Together they complete the dependencies of the new 339 * blocks without writing the writers' inodes, which is exactly the state 340 * needed. The syncer then writes the indirect block, freeing the old 341 * blocks in a burst, and the inode pointer only becomes durable at the 342 * following flush of the device vnode. With WALSYNC=fdatasync, 343 * fdatasync() puts the blocks on disk instead, which leaves the inode 344 * alone only on kernels where ffs_syncvnode() tested IN_SIZEMOD and 345 * IN_IBLKDATA in i_flags. Never fsync() the two files unless asked to 346 * with WALFSYNC. 347 */ 348static void 349wal(int idx, int *fd, int fdcg) 350{ 351 char buf[CHUNK]; 352 long off; 353 int i; 354 355 memset(buf, idx + 1, sizeof(buf)); 356 if (walsync == WAL_DIRECT) 357 for (i = 0; i < 2; i++) 358 if (fcntl(fd[i], F_SETFL, 359 fcntl(fd[i], F_GETFL) | O_DIRECT) == -1) 360 err(1, "fcntl(O_DIRECT)"); 361 while (done == 0) { 362 for (i = 0; i < 2; i++) 363 if (ftruncate(fd[i], 0) == -1) 364 err(1, "ftruncate"); 365 for (off = 0; off < walsize && done == 0; off += CHUNK) { 366 for (i = 0; i < 2; i++) { 367 if (walsync == WAL_DIRECT && 368 wal_direct(fd[i], buf, off) == 0) 369 continue; 370 if (walsync != WAL_DIRECT && 371 write(fd[i], buf, sizeof(buf)) == 372 sizeof(buf)) 373 continue; 374 if (errno == ENOSPC) { 375 sleep(1); 376 break; 377 } 378 err(1, "write"); 379 } 380 if ((off / CHUNK) % 8 == 0 && ftruncate(fdcg, 0) == -1) 381 err(1, "ftruncate"); 382 if (pwrite(fdcg, buf, sizeof(buf), 383 ((off / CHUNK) % 8) * CHUNK) != sizeof(buf) && 384 errno != ENOSPC) 385 err(1, "pwrite"); 386 fsync(fdcg); 387 for (i = 0; i < 2; i++) { 388 if (walfsync != 0 && 389 (off / CHUNK) % walfsync == 0) 390 fsync(fd[i]); 391 else if (walsync == WAL_FDATASYNC) 392 fdatasync(fd[i]); 393 } 394 usleep(waldelay); 395 } 396 } 397 _exit(0); 398} 399 400/* 401 * Fill a directory, remove the entries at the end so the next insert leaves 402 * unused blocks after the last entry (IN_ENDOFF), then create new 403 * directories in it. ffs_vput_pair() truncates and syncs the parent. 404 */ 405static void 406dirwork(int idx) 407{ 408 char parent[MAXPATHLEN], name[MAXPATHLEN]; 409 int fd, i, keep; 410 411 snprintf(parent, sizeof(parent), "%s/p%d", path, idx); 412 if (mkdir(parent, 0755) == -1 && errno != EEXIST) 413 err(1, "mkdir(%s)", parent); 414 keep = nfiles / 4; 415 while (done == 0) { 416 for (i = 0; i < nfiles; i++) { 417 snprintf(name, sizeof(name), 418 "%s/f%08d-%0200d", parent, i, idx); 419 if ((fd = open(name, O_CREAT | O_WRONLY, 0644)) == 420 -1) { 421 if (errno == ENOSPC) 422 break; 423 err(1, "creat(%s)", name); 424 } 425 close(fd); 426 } 427 for (i = nfiles - 1; i >= keep; i--) { 428 snprintf(name, sizeof(name), 429 "%s/f%08d-%0200d", parent, i, idx); 430 if (unlink(name) == -1 && errno != ENOENT) 431 err(1, "unlink(%s)", name); 432 } 433 for (i = 0; i < nsub; i++) { 434 snprintf(name, sizeof(name), "%s/d%d", parent, i); 435 if (mkdir(name, 0755) == -1) { 436 if (errno == ENOSPC) 437 break; 438 if (errno != EEXIST) 439 err(1, "mkdir(%s)", name); 440 } 441 /* 442 * A trickle, not a flood: running out of inodes or 443 * dependencies triggers cleanups that sync every 444 * inode, retiring the stale dependencies. 445 */ 446 usleep(dirdelay); 447 } 448 for (i = 0; i < nsub; i++) { 449 snprintf(name, sizeof(name), "%s/d%d", parent, i); 450 if (rmdir(name) == -1 && errno != ENOENT) 451 err(1, "rmdir(%s)", name); 452 } 453 for (i = keep - 1; i >= 0; i--) { 454 snprintf(name, sizeof(name), 455 "%s/f%08d-%0200d", parent, i, idx); 456 if (unlink(name) == -1 && errno != ENOENT) 457 err(1, "unlink(%s)", name); 458 } 459 } 460 _exit(0); 461} 462 463/* 464 * Push the mkdir dependencies to completion while the dir workers are 465 * flushing their parents. 466 */ 467static void 468syncwork(int idx) 469{ 470 char name[MAXPATHLEN]; 471 int fd, i, j; 472 473 i = idx; 474 while (done == 0) { 475 i = (i + 1) % ndir; 476 snprintf(name, sizeof(name), "%s/p%d", path, i); 477 if ((fd = open(name, O_RDONLY | O_DIRECTORY)) != -1) { 478 fsync(fd); 479 close(fd); 480 } 481 for (j = 0; j < nsub; j += 7) { 482 snprintf(name, sizeof(name), "%s/p%d/d%d", path, 483 i, j); 484 if ((fd = open(name, O_RDONLY | O_DIRECTORY)) != 485 -1) { 486 fsync(fd); 487 close(fd); 488 } 489 } 490 usleep(1000); 491 } 492 _exit(0); 493} 494 495int 496main(int argc, char *argv[]) 497{ 498 pid_t *pids; 499 time_t start, runtime; 500 int e, i, n, npad, status, *walcg, *walfd; 501 502 if (argc != 3) { 503 fprintf(stderr, "Usage %s <path> <runtime seconds>\n", 504 argv[0]); 505 exit(1); 506 } 507 path = argv[1]; 508 runtime = atol(argv[2]); 509 nwal = getenvint("NWAL", 1); 510 ndir = getenvint("NDIR", 2); 511 nsync = getenvint("NSYNC", 1); 512 nfiles = getenvint("NFILES", 200); 513 nsub = getenvint("NSUB", 50); 514 walsize = getenvint("WALSIZE", 64 * 1024 * 1024); 515 waldelay = getenvint("WALDELAY", 40000); 516 walfsync = getenvint("WALFSYNC", 0); 517 dirdelay = getenvint("DIRDELAY", 2000); 518 npad = getenvint("NPAD", 3000); 519 walsync = WAL_DIRECT; 520 for (i = 0; i < (int)nitems(walsyncname); i++) 521 if (getenv("WALSYNC") != NULL && 522 strcmp(getenv("WALSYNC"), walsyncname[i]) == 0) 523 walsync = i; 524 if (nwal < 0 || ndir < 1 || nsync < 0 || nfiles < 8 || nsub < 1 || 525 walsize < CHUNK || walfsync < 0 || npad < 0) 526 errx(1, "bad tunables"); 527 fprintf(stderr, "wal=%d dir=%d sync=%d files=%d sub=%d walsize=%ld " 528 "waldelay=%uus walsync=%s walfsync=%d dirdelay=%uus npad=%d " 529 "runtime=%lds\n", 530 nwal, ndir, nsync, nfiles, nsub, walsize, (unsigned)waldelay, 531 walsyncname[walsync], walfsync, 532 (unsigned)dirdelay, npad, (long)runtime); 533 534 n = nwal + ndir + nsync; 535 if ((pids = calloc(n, sizeof(*pids))) == NULL || 536 (walfd = calloc(nwal + 1, 2 * sizeof(*walfd))) == NULL || 537 (walcg = calloc(nwal + 1, sizeof(*walcg))) == NULL) 538 err(1, "calloc"); 539 for (i = 0; i < nwal; i++) 540 wal_setup(i, &walfd[2 * i], &walcg[i]); 541 metapad(npad); 542 signal(SIGTERM, handler); 543 signal(SIGALRM, handler); 544 n = 0; 545 for (i = 0; i < nwal; i++) 546 if ((pids[n++] = fork()) == 0) 547 wal(i, &walfd[2 * i], walcg[i]); 548 for (i = 0; i < ndir; i++) 549 if ((pids[n++] = fork()) == 0) 550 dirwork(i); 551 for (i = 0; i < nsync; i++) 552 if ((pids[n++] = fork()) == 0) 553 syncwork(i); 554 for (i = 0; i < n; i++) 555 if (pids[i] == -1) 556 err(1, "fork()"); 557 558 start = time(NULL); 559 while (time(NULL) - start < runtime) { 560 sleep(1); 561 if (waitpid(-1, &status, WNOHANG) > 0 && status != 0) { 562 fprintf(stderr, "worker failed early\n"); 563 break; 564 } 565 } 566 for (i = 0; i < n; i++) 567 kill(pids[i], SIGTERM); 568 e = 0; 569 for (i = 0; i < n; i++) { 570 if (waitpid(pids[i], &status, 0) == -1) { 571 if (errno == ECHILD) 572 continue; 573 err(1, "waitpid(%d)", pids[i]); 574 } 575 if (status != 0) { 576 if (WIFSIGNALED(status)) 577 fprintf(stderr, "pid %d exit signal %d\n", 578 pids[i], WTERMSIG(status)); 579 e++; 580 } 581 } 582 583 return (e); 584} 585