⬢ DragonFlyBSD Kernel Audit
DF-3133 / harness.c
← back to finding ↓ download raw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
/*
 * DF-3133 — dirfs_node_chsize() mutates the vnode buffer/pager state BEFORE
 * the host truncate() is known to succeed (and never checks the NULL return
 * of dirfs_node_absolute_path()).
 *
 * dirfs is vkernel-only (sys/platform/vkernel64/conf/files), so — per the
 * DF-3054/DF-0806/DF-0807 precedent — this harness is a faithful
 * transcription of the exact code path:
 *
 *   - dirfs_node_absolute_path()  : verbatim from dirfs_subr.c:377-443
 *   - dirfs_node_chsize()         : verbatim control flow from
 *                                   dirfs_subr.c:748-799, with the two
 *                                   buffer-cache helpers stubbed to mirror
 *                                   sys/kern/vfs_vm.c semantics:
 *     nvtruncbuf  (vfs_vm.c:129-261): destroys ALL buffers (clean AND dirty)
 *                   at/beyond the truncation point WITHOUT flushing them
 *                   (nvtruncbuf_bp_trunc: bremfree + B_INVAL|B_RELBUF|
 *                   B_NOCACHE + brelse, vfs_vm.c:300-302) and then sets the
 *                   pager size (nvnode_pager_setsize).
 *     nvextendbuf (vfs_vm.c:376+):   instantiates zero-filled buffers for
 *                   [olength, nlength) and sets the pager size.
 *   - buffer read/write: mirrors dirfs_read (vnops:618-652, buffer cache
 *                   first) and dirfs_strategy (pread/pwrite on the still
 *                   open dn_fd), write(2) => bdwrite => dirty, unflushed.
 *
 * Scenarios (all as the invoking unprivileged user, own files only —
 * except (C) which only *attempts* a truncate of a root-owned file and
 * verifies afterwards that it is byte-identical):
 *
 *  A  unlinked-but-open file: absolute_path()==NULL -> truncate(NULL) EFAULT,
 *     but nvtruncbuf already destroyed the dirty buffer holding an
 *     acknowledged (write(2)-returned) write, and the pager.  Host file data
 *     is still alive through the open descriptor (POSIX unlink semantics),
 *     and re-reads resurrect the OLD data: the acknowledged write is
 *     silently reverted.
 *  B  over-length absolute path (>MAXPATHLEN), constructed for real on the
 *     host via fd-relative openat() — exactly how dirfs itself builds such
 *     trees through its passive fd cache.  Same damage, EFAULT again.
 *     Boundary sub-case: a just-fits path truncates normally (sanity).
 *  C  valid path, host refuses the truncate (root-owned file -> EPERM):
 *     pager/buffers destroyed even though nothing changed on the host.
 *  D  FIXED variant of chsize (NULL check + host-truncate-first): zero
 *     damage in every failure scenario, error propagated.
 *
 * exit code 2  == BUG CONFIRMED (all failure scenarios show damage) AND
 *                 FIXED variant validated (zero damage).
 * exit code 1  == something did not behave as predicted (investigate).
 */
#include <errno.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#define MAXPATHLEN 1024
#define BSIZE      16384
#define NBLKS      4
#define DIRFS_NOFD (-1)

/* ------------------------------------------------------------------ */
/* dirfs node model (subset)                                           */
/* ------------------------------------------------------------------ */
struct dnode {
	const char	*name;		/* dn_name */
	int		 namelen;		/* dn_namelen */
	struct dnode	*parent;	/* dn_parent; NULL also models the
					 * post-dirfs_nremove state
					 * (vnops.c:914-916) */
	int		 state;
	int		 fd;			/* dn_fd (host descriptor) */
	off_t		 size;		/* dn_size */
	/* guest buffer cache for this vnode */
	struct {
		int	 valid;
		int	 dirty;		/* B_DELWRI, write(2) acked, unflushed */
		char	 data[BSIZE];
	} blk[NBLKS];
	off_t		 pager_size;	/* VM object size (nvnode_pager_setsize) */
	int		 dirty_discarded; /* B_INVAL destroyed dirty buffers */
	int		 clean_discarded;
};

#define DIRFS_ROOT 0x00000001
#define dirfs_node_isroot(n) ((n)->state & DIRFS_ROOT)

static char dm_path[MAXPATHLEN + 1];	/* dmp->dm_path */

static char *sprintf_ret(char *, size_t, const char *, const char *);
static int nvtruncbuf_stub_call_trunc(struct dnode *, off_t, int);
static int nvextendbuf_stub_call_ext(struct dnode *, off_t, off_t, int, int);

/* ------------------------------------------------------------------ */
/* dirfs_node_absolute_path — VERBATIM transcription of subr.c:377-443  */
/* (only the node field names are the model's)                          */
/* ------------------------------------------------------------------ */
static char *
dirfs_node_absolute_path(struct dnode *cur, char **pathfreep)
{
	size_t len;
	struct dnode *dnp1;
	char *buf;
	int count;

	*pathfreep = NULL;
	if (cur == NULL)
		return NULL;
	buf = malloc(MAXPATHLEN + 1);

	/*
	 * Iterate through the parents until we hit the root.
	 */
	count = 0;
	buf[MAXPATHLEN] = 0;
	dnp1 = cur;
	while (dirfs_node_isroot(dnp1) == 0) {
		count += dnp1->namelen;
		if (count <= MAXPATHLEN)
			memcpy(&buf[MAXPATHLEN - count], dnp1->name,
			       dnp1->namelen);
		++count;
		if (count <= MAXPATHLEN)
			buf[MAXPATHLEN - count] = '/';
		dnp1 = dnp1->parent;
		if (dnp1 == NULL)
			break;			/* unlinked: no path (subr.c:423) */
	}

	/*
	 * Prefix with the root mount path.
	 */
	len = strlen(dm_path);
	count += len;
	if (dnp1 && count <= MAXPATHLEN) {
		memcpy(&buf[MAXPATHLEN - count], dm_path, len);
		*pathfreep = buf;
		return (&buf[MAXPATHLEN - count]);
	} else {
		free(buf);
		*pathfreep = NULL;
		return (NULL);
	}
}

/* ------------------------------------------------------------------ */
/* nvtruncbuf stub — mirrors sys/kern/vfs_vm.c:129-261                  */
/*   destroys buffers at/beyond the (block-rounded) truncation point,   */
/*   dirty ones are NOT flushed (B_INVAL|B_NOCACHE), pager size set.    */
/* ------------------------------------------------------------------ */
static void
nvtruncbuf(struct dnode *dnp, off_t length)
{
	int truncblk = (int)((length + BSIZE - 1) / BSIZE);	/* first dead blk */

	for (int i = truncblk; i < NBLKS; i++) {
		if (!dnp->blk[i].valid)
			continue;
		if (dnp->blk[i].dirty) {
			dnp->dirty_discarded++;
			/* B_INVAL: contents destroyed, host never sees them */
		} else {
			dnp->clean_discarded++;
		}
		memset(&dnp->blk[i], 0, sizeof(dnp->blk[i]));
	}
	dnp->pager_size = length;			/* nvnode_pager_setsize */
}

/* nvextendbuf stub — vfs_vm.c:376+: zero-fill [olength,nlength), set size */
static void
nvextendbuf(struct dnode *dnp, off_t olength, off_t nlength)
{
	int oblk = (int)((olength + BSIZE - 1) / BSIZE);
	int nblk = (int)((nlength + BSIZE - 1) / BSIZE);

	for (int i = oblk; i < nblk && i < NBLKS; i++) {
		if (!dnp->blk[i].valid) {
			dnp->blk[i].valid = 1;
			dnp->blk[i].dirty = 0;
			memset(dnp->blk[i].data, 0, BSIZE);
		}
	}
	dnp->pager_size = nlength;
}

/* ------------------------------------------------------------------ */
/* guest I/O simulation: dirfs_read (:618-652)/write + strategy pread   */
/* ------------------------------------------------------------------ */
static void
guest_write_blk(struct dnode *dnp, int blkno, const char *tag)
{
	dnp->blk[blkno].valid = 1;
	dnp->blk[blkno].dirty = 1;			/* write(2) acked, bdwrite */
	snprintf(dnp->blk[blkno].data, BSIZE, "%s", tag);
	if ((off_t)(blkno + 1) * BSIZE > dnp->size)
		dnp->size = (off_t)(blkno + 1) * BSIZE; /* dirfs_write:719-722 */
}

static const char *
guest_read_blk(struct dnode *dnp, int blkno)
{
	static char tmp[BSIZE];

	if (dnp->blk[blkno].valid)			/* buffer cache hit */
		return (dnp->blk[blkno].data);
	/* miss -> dirfs_strategy: pread(dnp->dn_fd) from the HOST */
	ssize_t n = pread(dnp->fd, tmp, BSIZE, (off_t)blkno * BSIZE);
	if (n <= 0) {
		/* strategy zero-fills short/failed reads (vnops:828-829) */
		memset(tmp, 0, BSIZE);
		snprintf(tmp, BSIZE, "<zero-fill:%d>", (int)n);
	} else {
		tmp[n] = 0;
	}
	return (tmp);
}

/* ------------------------------------------------------------------ */
/* dirfs_node_chsize — VERBATIM control flow of subr.c:748-799          */
/* ------------------------------------------------------------------ */
static int
dirfs_node_chsize(struct dnode *dnp, off_t nsize)
{
	int error = 0;
	char *tmp, *pathfree;
	off_t osize = dnp->size;
	int biosize = BSIZE;

	/* v_type == VREG is a precondition here */

	tmp = dirfs_node_absolute_path(dnp, &pathfree);		/* :778 */
	if (nsize < osize) {					/* :779-780 */
		error = nvtruncbuf_stub_call_trunc(dnp, nsize, biosize);
	} else {						/* :781-784 */
		error = nvextendbuf_stub_call_ext(dnp, osize, nsize,
		    biosize, biosize);
	}
	if (error == 0 && truncate(tmp, nsize) < 0)		/* :786-787 */
		error = errno;
	if (error == 0)					/* :788-789 */
		dnp->size = nsize;

	free(pathfree);					/* dirfs_dropfd */
	return (error);
}
/* small shims so the verbatim body above stays recognizable */
static int nvtruncbuf_stub_call_trunc(struct dnode *d, off_t n, int b)
{ nvtruncbuf(d, n); (void)b; return (0); }
static int nvextendbuf_stub_call_ext(struct dnode *d, off_t o, off_t n,
    int b1, int b2)
{ nvextendbuf(d, o, n); (void)b1; (void)b2; return (0); }

/* ------------------------------------------------------------------ */
/* FIXED chsize: NULL check + host-first, buffer state only on success  */
/* ------------------------------------------------------------------ */
static int
dirfs_node_chsize_fixed(struct dnode *dnp, off_t nsize)
{
	int error = 0;
	char *tmp, *pathfree;
	off_t osize = dnp->size;

	tmp = dirfs_node_absolute_path(dnp, &pathfree);
	if (tmp == NULL) {
		error = ENAMETOOLONG;		/* unlinked / over-length */
		goto out;
	}
	if (truncate(tmp, nsize) < 0) {		/* host FIRST: it may fail */
		error = errno;
		goto out;
	}
	if (nsize < osize)
		nvtruncbuf(dnp, nsize);
	else
		nvextendbuf(dnp, osize, nsize);
	dnp->size = nsize;
out:
	free(pathfree);
	return (error);
}

/* ------------------------------------------------------------------ */
static int failures, confirmed;

static void
check(int cond, const char *what)
{
	printf("    [%s] %s\n", cond ? "OK" : "!!", what);
	if (!cond)
		failures++;
	else if (strncmp(what, "BUG", 3) == 0)
		confirmed++;
}

static void
fill_host_file(int fd, const char *tag)
{
	char b[BSIZE];
	memset(b, 'H', BSIZE);
	for (int i = 0; i < NBLKS; i++) {
		snprintf(b, BSIZE, "%s-blk%d", tag, i);
		b[strlen(b)] = 'H';
		pwrite(fd, b, BSIZE, (off_t)i * BSIZE);
	}
}

static void
host_read_blk(int fd, int blkno, char *out, size_t outlen)
{
	ssize_t n = pread(fd, out, outlen - 1, (off_t)blkno * BSIZE);
	if (n <= 0)
		snprintf(out, outlen, "<host-read-%d>", (int)n);
	else {
		out[n < (ssize_t)outlen - 1 ? n : outlen - 1] = 0;
		for (size_t i = 0; i < strlen(out); i++)
			if (out[i] == 'H') { out[i] = 0; break; }
	}
}

static struct dnode mknodes(const char **names, int n, int fd)
{
	static struct dnode root;	/* static: dn_parent chain target */
	static struct dnode chain[8];

	memset(&root, 0, sizeof(root));
	root.state = DIRFS_ROOT;
	root.fd = DIRFS_NOFD;
	struct dnode *cur = &root;
	memset(chain, 0, sizeof(chain));
	memset(chain, 0, sizeof(chain));
	for (int i = 0; i < n; i++) {
		chain[i].name = names[i];
		chain[i].namelen = strlen(names[i]);
		chain[i].parent = cur;
		chain[i].fd = DIRFS_NOFD;
		cur = &chain[i];
	}
	chain[n - 1].fd = fd;
	chain[n - 1].size = (off_t)NBLKS * BSIZE;
	chain[n - 1].pager_size = (off_t)NBLKS * BSIZE;
	for (int i = 0; i < NBLKS; i++) {
		chain[n - 1].blk[i].valid = 1;	/* clean cached blocks */
		chain[n - 1].blk[i].dirty = 0;
	}
	return (chain[n - 1]);
}

int
main(int argc, char **argv)
{
	char base[256], path[512];
	const char *root;
	int fd;
	char buf[BSIZE];

	root = (argc > 1) ? argv[1] : "/tmp/df3133";
	snprintf(base, sizeof(base), "%s", root);
	mkdir(base, 0755);

	printf("== DF-3133: dirfs_node_chsize destructive-before-validate ==\n");

	/* ---------------- Scenario A: unlinked-but-open -------------- */
	printf("\n-- A) unlinked-but-open file (dn_parent NULLed by "
	    "dirfs_nremove vnops.c:914-916)\n");
	mkdir(sprintf_ret(path, sizeof(path), base, "/a"), 0755) == 0 ||
	    errno == EEXIST;
	snprintf(dm_path, sizeof(dm_path), "%s/a", base);
	strcat(path, "/f");
	unlink(path);
	fd = open(path, O_RDWR | O_CREAT, 0644);
	fill_host_file(fd, "OLDDATA");
	{
		struct dnode f = mknodes((const char *[]){"f"}, 1, fd);
		/* write(2) acknowledged at blk 3, still dirty (bdwrite) */
		guest_write_blk(&f, 3, "NEWDATA-acked-write");

		/* dirfs_nremove succeeded on the host */
		if (unlink(path) != 0) { perror("unlink"); return (1); }
		f.parent = NULL;		/* vnops.c:914-916 */

		int err = dirfs_node_chsize(&f, 0);
		printf("    chsize returned %d (%s)\n", err, strerror(err));
		check(err == EFAULT, "BUG A1: truncate(NULL) -> EFAULT "
		    "after damage already done (subr.c:778-787)");
		check(f.dirty_discarded == 1,
		    "BUG A2: acknowledged (dirty) write DESTROYED without "
		    "flush (vfs_vm.c:300-302 via nvtruncbuf)");
		check(f.pager_size == 0,
		    "BUG A3: pager/VM object truncated to 0 while host file "
		    "unchanged (mmap view SIGBUS)");
		host_read_blk(fd, 3, buf, sizeof(buf));
		check(strcmp(buf, "OLDDATA-blk3") == 0,
		    "host data still alive via open fd (POSIX unlink "
		    "semantics intact on host)");
		const char *g = guest_read_blk(&f, 3);
		check(strncmp(g, "OLDDATA-blk3", 12) == 0,
		    "BUG A4: guest re-read resurrects OLD data — the "
		    "acknowledged write is silently REVERTED");

		/* FIXED variant, same state */
		struct dnode f2 = mknodes((const char *[]){"f"}, 1, fd);
		guest_write_blk(&f2, 3, "NEWDATA-acked-write");
		f2.parent = NULL;
		int err2 = dirfs_node_chsize_fixed(&f2, 0);
		check(err2 == ENAMETOOLONG, "FIXED A: clean ENAMETOOLONG");
		check(f2.dirty_discarded == 0 && f2.pager_size > 0,
		    "FIXED A: zero damage, dirty buffer intact");
	}
	close(fd);

	/* ---------------- Scenario B: over-length path ---------------- */
	printf("\n-- B) absolute path > MAXPATHLEN built via fd-relative "
	    "openat() (the dirfs fd-cache trick)\n");
	mkdir(sprintf_ret(path, sizeof(path), base, "/b"), 0755) == 0 ||
	    errno == EEXIST;
	snprintf(dm_path, sizeof(dm_path), "%s/b", base);
	{
		/* 4x240 fits: 4*241 + strlen(dm_path) + 2 <= 1024 */
		char nm[256];
		int lvl, dirfd = open(dm_path, O_DIRECTORY | O_RDONLY);
		for (lvl = 0; lvl < 4; lvl++) {
			memset(nm, 'a' + lvl, 240); nm[240] = 0;
			if (mkdirat(dirfd, nm, 0755) != 0 && errno != EEXIST) {
				perror("mkdirat"); return (1);
			}
			int nfd = openat(dirfd, nm, O_DIRECTORY | O_RDONLY);
			if (nfd < 0) { perror("openat"); return (1); }
			close(dirfd);
			dirfd = nfd;
		}
		/* boundary: file fits (count <= MAXPATHLEN) — sanity */
		int ffd = openat(dirfd, "f", O_RDWR | O_CREAT, 0644);
		fill_host_file(ffd, "FIT");
		const char *names5[5];
		static char keep[5][256];
		for (int i = 0; i < 4; i++) {
			memset(keep[i], 'a' + i, 240); keep[i][240] = 0;
			names5[i] = keep[i];
		}
		names5[4] = "f";
		struct dnode bf = mknodes(names5, 5, ffd);
		int berr = dirfs_node_chsize(&bf, 0);
		check(berr == 0 && bf.dirty_discarded == 0,
		    "B-sanity: 4x240 boundary path truncates normally");
		close(ffd);

		/* 5th level: mkdirat/openat succeed RELATIVE (host allows),
		 * but the absolute path now exceeds MAXPATHLEN */
		memset(keep[4], 'e', 240); keep[4][240] = 0;
		names5[4] = keep[4];
		if (mkdirat(dirfd, keep[4], 0755) != 0 && errno != EEXIST) {
			perror("mkdirat5"); return (1);
		}
		int d5 = openat(dirfd, keep[4], O_DIRECTORY | O_RDONLY);
		int ffd5 = openat(d5, "f", O_RDWR | O_CREAT, 0644);
		fill_host_file(ffd5, "DEEP");
		check(ffd5 >= 0, "BUG precondition: >MAXPATHLEN tree IS "
		    "constructible via fd-relative openat() (dirfs does the "
		    "same through the passive fd cache)");
		close(d5);
		close(dirfd);

		const char *names6[6] = { keep[0], keep[1], keep[2], keep[3],
					  keep[4], "f" };
		struct dnode df = mknodes(names6, 6, ffd5);
		guest_write_blk(&df, 1, "DEEP-acked-write");
		int derr = dirfs_node_chsize(&df, 0);
		printf("    chsize returned %d (%s)\n", derr, strerror(derr));
		check(derr == EFAULT, "BUG B1: absolute_path NULL (count > "
		    "MAXPATHLEN, subr.c:433-442) -> truncate(NULL) EFAULT");
		check(df.dirty_discarded == 1 && df.pager_size == 0,
		    "BUG B2: dirty buffer destroyed + pager truncated "
		    "before the failing truncate");

		struct dnode df2 = mknodes(names6, 6, ffd5);
		guest_write_blk(&df2, 1, "DEEP-acked-write");
		int derr2 = dirfs_node_chsize_fixed(&df2, 0);
		check(derr2 == ENAMETOOLONG && df2.dirty_discarded == 0,
		    "FIXED B: ENAMETOOLONG, zero damage");
		close(ffd5);
	}

	/* ---------------- Scenario C: host EPERM, valid path ----------- */
	printf("\n-- C) valid path, host refuses truncate (root-owned file) "
	    "-> EPERM after damage\n");
	{
		const char *victim = "/etc/hostid";
		struct stat st;
		if (stat(victim, &st) != 0 || st.st_size == 0 ||
		    st.st_uid == getuid()) {
			victim = "/etc/rc";
			if (stat(victim, &st) != 0 || st.st_size == 0 ||
			    st.st_uid == getuid()) {
				printf("    (skipped: no suitable root-owned "
				    "file)\n");
				goto done_C;
			}
		}
		int vfd = open(victim, O_RDONLY);
		snprintf(dm_path, sizeof(dm_path), "%s/etc", "/");
		/* model node "hostid" under dm_path=/etc */
		struct dnode vn = mknodes((const char *[]){
		    st.st_uid == getuid() ? "x" : "hostid"}, 1, vfd);
		const char *bname = strrchr(victim, '/') + 1;
		vn.name = bname;
		vn.namelen = strlen(bname);
		vn.size = st.st_size;
		vn.pager_size = st.st_size;
		guest_write_blk(&vn, 0, "GUEST-acked-write");
		off_t before = st.st_size;

		int cerr = dirfs_node_chsize(&vn, 0);
		printf("    chsize returned %d (%s)\n", cerr, strerror(cerr));
		check(cerr == EPERM || cerr == EACCES,
		    "BUG C1: host refuses the truncate (non-owner)");
		check(vn.dirty_discarded == 1 && vn.pager_size == 0,
		    "BUG C2: guest pager/buffers for a file we cannot modify "
		    "were destroyed anyway (subr.c:779-785 BEFORE :786)");
		struct stat st2;
		stat(victim, &st2);
		check(st2.st_size == before && st2.st_mtime == st.st_mtime,
		    "C: host file untouched (verified byte-identical state)");
		close(vfd);
	}
done_C:

	printf("\n== summary: %d bug-confirmations, %d mismatches ==\n",
	    confirmed, failures);
	if (failures == 0 && confirmed >= 6)
		return (2);	/* BUG CONFIRMED + FIX VALIDATED */
	return (failures ? 1 : 0);
}

static char *
sprintf_ret(char *s, size_t n, const char *a, const char *b)
{
	snprintf(s, n, "%s%s", a, b);
	return (s);
}