xref: /freebsd/tools/test/stress2/misc/mkdir_blkreuse.sh (revision a127039dd0c24bc18b3513322d22fd8fee6724eb)
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