-
Notifications
You must be signed in to change notification settings - Fork 243
Expand file tree
/
Copy pathrandom-syscall.c
More file actions
1389 lines (1249 loc) · 51.5 KB
/
random-syscall.c
File metadata and controls
1389 lines (1249 loc) · 51.5 KB
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
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
* Call a single random syscall with random args.
*/
#include <errno.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "arch.h" // biarch
#include "arg-decoder.h"
#include "child.h"
#include "cmp_hints.h"
#include "debug.h"
#include "edgepair.h"
#include "healer.h"
#include "kcov.h"
#include "locks.h"
#include "minicorpus.h"
#include "params.h"
#include "pids.h"
#include "pre_crash_ring.h"
#include "random.h"
#include "sequence.h"
#include "shm.h"
#include "signals.h"
#include "sanitise.h"
#include "stats.h"
#include "stats_ring.h"
#include "strategy.h"
#include "syscall.h"
#include "tables.h"
#include "trinity.h"
#include "utils.h"
/*
* This function decides if we're going to be doing a 32bit or 64bit syscall.
* There are various factors involved here, from whether we're on a 32-bit only arch
* to 'we asked to do a 32bit only syscall' and more.. Hairy.
*/
/*
* Biarch-only: pick which syscall table this call uses, refresh the
* caller's per-child active_syscalls pointer, and return do32. Uniarch
* builds bypass this entirely — child->active_syscalls is set once at
* init time to shm->active_syscalls and never re-evaluated.
*
* Non-static so STRATEGY_HEALER's picker can share the same per-call
* arch-pick decision the other set_syscall_nr_* variants already
* route through here, rather than re-implementing it (which would
* desync the picker's arch dim from the rest of the call dispatch).
*/
bool choose_syscall_table(struct childdata *child,
unsigned int *nr_syscalls_out)
{
bool do32 = false;
/* First, check that we have syscalls enabled in either table.
* Read the cached validity bits maintained by validate_syscall_table_*
* and the deactivate_syscall{32,64}() paths instead of re-running the
* walk on every pick. */
if (shm->valid_syscall_table_64 == false) {
use_64bit = false;
/* If no 64bit syscalls enabled, force 32bit. */
do32 = true;
}
if (shm->valid_syscall_table_32 == false)
use_32bit = false;
/* If both tables enabled, pick randomly. */
if ((use_64bit == true) && (use_32bit == true)) {
/* 10% possibility of a 32bit syscall */
if (ONE_IN(10))
do32 = true;
}
if (do32 == false) {
syscalls = syscalls_64bit;
child->active_syscalls = shm->active_syscalls64;
*nr_syscalls_out = max_nr_64bit_syscalls;
} else {
syscalls = syscalls_32bit;
child->active_syscalls = shm->active_syscalls32;
*nr_syscalls_out = max_nr_32bit_syscalls;
}
return do32;
}
/*
* Check if a syscall entry belongs to the target group.
* Used by group biasing to filter candidates.
*/
static bool syscall_in_group(unsigned int nr, bool do32, unsigned int target_group)
{
struct syscallentry *entry;
entry = get_syscall_entry(nr, do32);
if (entry == NULL)
return false;
return entry->group == target_group;
}
/*
* Pick the syscall to run under STRATEGY_HEURISTIC: uniform draw from
* active_syscalls, then layered biases — group affinity (70% prefer last
* group), kcov cold-skip (probabilistic), edgepair freshness (50% skip
* cold pairs). This is trinity's pre-rotation default behaviour.
*/
static bool set_syscall_nr_heuristic(struct syscallrecord *rec,
struct childdata *child)
{
struct syscallentry *entry;
unsigned int syscallnr;
int val;
bool do32;
unsigned int group_attempts = 0;
unsigned int kcov_attempts = 0;
unsigned int edgepair_attempts = 0;
unsigned int outer_attempts = 0;
unsigned int nr_syscalls;
/* Pick the syscall table once per call: in uniarch the result is
* a constant, and even in biarch the do32 dice rolls once per
* pick — re-rolling under the retry budget (up to 10 000 spins
* on a sparse table) burned ~5 cycles per iteration for nothing. */
if (biarch) {
do32 = choose_syscall_table(child, &nr_syscalls);
} else {
do32 = false;
nr_syscalls = max_nr_syscalls;
}
retry:
if (no_syscalls_enabled() == true) {
output(0, "[%d] No more syscalls enabled. Exiting\n", mypid());
__atomic_store_n(&shm->exit_reason, EXIT_NO_SYSCALLS_ENABLED, __ATOMIC_RELAXED);
return FAIL;
}
/* Bail if we have spent too many iterations failing to pick a
* usable syscall. Without this cap, a sparse active_syscalls
* table or a table dominated by EXPENSIVE syscalls (kept at
* 1-in-1000) can wedge the child in a tight retry loop. */
if (outer_attempts++ > 10000) {
output(0, "[%d] set_syscall_nr exceeded retry budget\n", mypid());
return FAIL;
}
syscallnr = rand() % nr_syscalls;
/* If we got a syscallnr which is not active repeat the attempt,
* since another child has switched that syscall off already.*/
val = child->active_syscalls[syscallnr];
if (val == 0)
goto retry;
syscallnr = val - 1;
if (validate_specific_syscall_silent(syscalls, syscallnr) == false) {
deactivate_syscall_locked(syscallnr, do32);
goto retry;
}
entry = get_syscall_entry(syscallnr, do32);
if (entry->flags & EXPENSIVE) {
if (!ONE_IN(1000))
goto retry;
}
/*
* Group biasing: when enabled and we have a previous group context,
* bias selection toward syscalls in the same group.
*
* 70% of the time: prefer same group as last call
* 25% of the time: accept any syscall (no bias)
* 5% of the time: accept any syscall (exploration)
*
* If we can't find a same-group syscall after 20 attempts,
* fall through and accept whatever we picked.
*/
if (group_bias && child->last_group != GROUP_NONE) {
unsigned int dice = rand() % 100;
if (dice < 70) {
/* Try to pick from same group */
if (!syscall_in_group(syscallnr, do32, child->last_group)) {
group_attempts++;
if (group_attempts < 20)
goto retry;
/* Gave up, accept this one. */
}
}
/* dice >= 70: accept any syscall */
}
/* Coverage-guided cold avoidance: if this syscall has stopped
* finding new edges, skip it with a probability that grows the
* staler it gets — a syscall stuck for one threshold-window gets
* the same 50% baseline as before, but one stuck for ten gets
* skipped 90% of the time.
*
* Suppressed inside a SR_PLATEAU_FORCE intervention when the
* random-rescue classifier has accumulated enough RRC_COLD_SKIP
* evidence to amplify that class: the rescues that have been
* carrying the fleet past the plateau are mostly cold-skipped
* syscalls, and structured replay means letting the heuristic
* actually pick them. Both gates checked because either alone
* is insufficient -- plateau_active without amplification means
* a different class won, and amplification cannot stay live
* after the plateau lifts (the orchestrator clears the field on
* its next non-intervention rotation). */
if (!plateau_rescue_bias_active_for(RRC_COLD_SKIP)) {
unsigned int skip_pct = kcov_syscall_cold_skip_pct(syscallnr);
if (skip_pct > 0 && (unsigned int)(rand() % 100) < skip_pct) {
kcov_attempts++;
if (kcov_attempts < 20)
goto retry;
}
}
/* Edge-pair sequence biasing: if we have a previous syscall,
* prefer pairs that have produced new edges before.
* Skip cold pairs (haven't found new edges recently) 50% of
* the time. Boost productive pairs by accepting them
* immediately when we'd otherwise retry. */
if (child->last_syscall_nr != EDGEPAIR_NO_PREV) {
if (edgepair_is_cold(child->last_syscall_nr, syscallnr) &&
RAND_BOOL()) {
edgepair_attempts++;
if (edgepair_attempts < 20)
goto retry;
}
}
/* critical section for shm updates. */
lock(&rec->lock);
rec->do32bit = do32;
rec->nr = syscallnr;
unlock(&rec->lock);
return true;
}
/*
* Anti-prior reject-retry budget. The accept gate's per-call rejection
* rate sits at 1 - 1/MAX_BOOST = 87.5% at the median; over a sparse
* active table this still resolves in a handful of retries on average,
* but a pathological mix (e.g. every active syscall sitting at the
* over-picked saturation point, accept = 1/MAX_BOOST^2) could push past
* the natural recovery budget. Bound at 64 so the inner loop never
* burns more than the per-iteration cost is worth; falling through
* means accepting whatever the picker happened to land on, which
* degrades anti-prior gracefully to uniform pick rather than wedging
* the syscall picker. Kept well below the outer 10000 budget so the
* gate cannot starve the rest of the validate / EXPENSIVE gates.
*/
#define ANTI_PRIOR_RETRY_CAP 64U
/*
* Pick the syscall to run under STRATEGY_RANDOM: uniform draw from
* active_syscalls with no further biasing. The "shake the dust off"
* pass — useless on its own, but exposes paths the heuristic biases
* systematically suppress (cold syscalls, productive-pair-only flow).
*
* Active_syscalls + EXPENSIVE + AVOID_SYSCALL gating remain because
* those are correctness gates, not selection biases — bypassing them
* just wastes iterations on calls we know we can't make.
*
* Anti-prior plateau intervention: during an SR_PLATEAU_FORCE window
* the orchestrator may have rotated into PIM_ANTI_PRIOR mode, in which
* case the per-candidate accept gate inverts the picker's learned
* per-syscall pick-rate distribution -- syscalls the bandit has been
* over-selecting get rejected at up to MAX_BOOST^2:1, low-count
* syscalls accept at full uniform rate. Outside the intervention the
* gate's atomic load short-circuits and the picker is the historical
* pure-uniform draw.
*/
bool set_syscall_nr_random(struct syscallrecord *rec,
struct childdata *child)
{
struct syscallentry *entry;
unsigned int syscallnr;
int val;
bool do32;
unsigned int outer_attempts = 0;
unsigned int nr_syscalls;
unsigned int anti_prior_attempts = 0;
bool anti_prior_on;
/* See the matching comment in set_syscall_nr_heuristic — the table
* pick is a per-call decision, not a per-retry one. */
if (biarch) {
do32 = choose_syscall_table(child, &nr_syscalls);
} else {
do32 = false;
nr_syscalls = max_nr_syscalls;
}
/* Latch the anti-prior mode once per pick so the per-retry inner
* loop reads a stable answer; a rotation that lands mid-pick is
* harmless either way (we either over-shoot one retry budget or
* under-shoot one) but caching avoids redoing the relaxed atomic
* load on every retry. */
anti_prior_on = plateau_anti_prior_active();
retry:
if (no_syscalls_enabled() == true) {
output(0, "[%d] No more syscalls enabled. Exiting\n", mypid());
__atomic_store_n(&shm->exit_reason, EXIT_NO_SYSCALLS_ENABLED, __ATOMIC_RELAXED);
return FAIL;
}
if (outer_attempts++ > 10000) {
output(0, "[%d] set_syscall_nr_random exceeded retry budget\n", mypid());
return FAIL;
}
syscallnr = rand() % nr_syscalls;
val = child->active_syscalls[syscallnr];
if (val == 0)
goto retry;
syscallnr = val - 1;
if (validate_specific_syscall_silent(syscalls, syscallnr) == false) {
deactivate_syscall_locked(syscallnr, do32);
goto retry;
}
entry = get_syscall_entry(syscallnr, do32);
if (entry->flags & EXPENSIVE) {
if (!ONE_IN(1000))
goto retry;
}
/* Anti-prior accept gate. Applied AFTER the active/validate/
* EXPENSIVE correctness gates so a rejected anti-prior candidate
* goes back through the uniform pick rather than burning the gate
* budget on disabled or AVOID-flagged syscalls. Bounded retry
* budget so an extreme distribution falls back to uniform instead
* of wedging the picker. */
if (anti_prior_on && !plateau_anti_prior_accept(syscallnr)) {
anti_prior_attempts++;
if (anti_prior_attempts < ANTI_PRIOR_RETRY_CAP)
goto retry;
/* Budget exhausted -- accept the current candidate and let
* the next pick re-roll. The intervention's per-window
* shape stays anti-prior on average even if individual
* picks fall through. */
}
lock(&rec->lock);
rec->do32bit = do32;
rec->nr = syscallnr;
unlock(&rec->lock);
return true;
}
/*
* Pick the syscall to run under STRATEGY_COVERAGE_FRONTIER: uniform draw
* from active_syscalls, then biased acceptance against the per-syscall
* frontier-edge weight via rejection sampling. Each candidate is
* accepted with probability (frontier_recent_count(nr) + 1) /
* (max_weight + 1); the +1 keeps cold syscalls from starving completely
* and lets the strategy still drive forward when no syscall has
* produced a frontier edge in the last K windows (every weight is 1, so
* acceptance reduces to uniform).
*
* max_weight is read once at the top of the function from the cached
* shm->frontier_max_weight_cached so the bias mass stays stable across
* the inner retry loop, and so concurrent kcov_collect-driven bumps to
* frontier_history during the pick don't perturb the acceptance
* probability mid-call. The cache is recomputed authoritatively on
* each window rotation by frontier_window_advance() and ratcheted
* upward on new-edge bumps by frontier_record_new_edge(), turning what
* used to be an O(MAX_NR_SYSCALL) walk per pick into a single RELAXED
* load.
*
* The validate / EXPENSIVE / AVOID_SYSCALL retry budget mirrors the
* other set_syscall_nr_* variants because those are correctness gates,
* not selection biases.
*/
static bool set_syscall_nr_coverage_frontier(struct syscallrecord *rec,
struct childdata *child)
{
struct syscallentry *entry;
unsigned int syscallnr;
unsigned int val;
bool do32;
unsigned int outer_attempts = 0;
unsigned int nr_syscalls;
unsigned long max_weight;
if (biarch) {
do32 = choose_syscall_table(child, &nr_syscalls);
} else {
do32 = false;
nr_syscalls = max_nr_syscalls;
}
max_weight = __atomic_load_n(&shm->frontier_max_weight_cached,
__ATOMIC_RELAXED);
retry:
if (no_syscalls_enabled() == true) {
output(0, "[%d] No more syscalls enabled. Exiting\n", mypid());
__atomic_store_n(&shm->exit_reason, EXIT_NO_SYSCALLS_ENABLED, __ATOMIC_RELAXED);
return FAIL;
}
if (outer_attempts++ > 10000) {
output(0, "[%d] set_syscall_nr_coverage_frontier exceeded retry budget\n", mypid());
return FAIL;
}
syscallnr = rand() % nr_syscalls;
val = child->active_syscalls[syscallnr];
if (val == 0)
goto retry;
syscallnr = val - 1;
if (validate_specific_syscall_silent(syscalls, syscallnr) == false) {
deactivate_syscall_locked(syscallnr, do32);
goto retry;
}
entry = get_syscall_entry(syscallnr, do32);
if (entry->flags & EXPENSIVE) {
if (!ONE_IN(1000))
goto retry;
}
/* Frontier-weighted acceptance. When max_weight is 0 (no syscall
* has registered a frontier edge in the last FRONTIER_DECAY_WINDOWS
* rotations -- typical at run start or in a heavily-explored
* codebase where everything has plateaued), the (w+1)/(max+1) ratio
* is 1 for every candidate and the acceptance gate is bypassed,
* degenerating gracefully to uniform pick. */
if (max_weight > 0) {
unsigned long w = frontier_recent_count(syscallnr);
unsigned long denom = max_weight + 1UL;
unsigned long roll = (unsigned long)rand() % denom;
if (roll >= w + 1UL)
goto retry;
}
lock(&rec->lock);
rec->do32bit = do32;
rec->nr = syscallnr;
unlock(&rec->lock);
__atomic_fetch_add(&shm->stats.frontier_strategy_picks, 1UL,
__ATOMIC_RELAXED);
return true;
}
/*
* Dispatch syscall selection through the active strategy's picker.
* Reads shm->current_strategy with relaxed atomic, then snapshots the
* chosen arm into child->strategy_at_pick so the post-syscall reward
* attribution sites credit the arm that actually picked the syscall --
* not whichever arm happens to be current_strategy by the time the
* syscall returns. Without the stamp, a rotation that lands mid-call
* (especially common on long or blocking syscalls) would misattribute
* the reward and contaminate the bandit's learning signal. Out-of-range
* guard preserves correctness even if a wild write into shm corrupts
* the strategy index.
*/
static bool set_syscall_nr(struct syscallrecord *rec, struct childdata *child)
{
int strat;
/* Explorer-pool children bypass the bandit's current pick and run
* STRATEGY_RANDOM unconditionally -- including when the bandit has
* picked STRATEGY_COVERAGE_FRONTIER. The pool is the always-on
* uniform baseline that lets the bandit's reward signal stay honest
* even when its winning arm goes stale. Skip the strategy_at_pick
* stamp too: explorer contributions are filtered out of the bandit's
* per-arm reward counters in the post-syscall path on is_explorer
* alone, and leaving the -1 sentinel here makes that intent explicit
* if a future reader forgets the is_explorer gate. */
if (child->is_explorer) {
__atomic_fetch_add(&shm->stats.strategy_explorer_picks, 1UL,
__ATOMIC_RELAXED);
/* Explorer-pool exposure: explorers always run STRATEGY_RANDOM
* regardless of the bandit's pick. Bump strategy_picks for
* RANDOM directly (strategy_at_pick stays at the -1 sentinel
* so the post-syscall PC/CMP reward attribution still skips
* explorers as before). strategy_bandit_pool_ops is NOT
* bumped here -- it is a bandit-pool-only sub-counter so the
* operator can derive the explorer contribution per arm. */
__atomic_fetch_add(&shm->strategy_picks[STRATEGY_RANDOM], 1UL,
__ATOMIC_RELAXED);
return set_syscall_nr_random(rec, child);
}
/* ACQUIRE pairs with the RELEASE store on current_strategy in
* maybe_rotate_strategy below. Without it, a child observing the
* new strategy id is not guaranteed to also see the companion
* fields (current_selection_reason, plateau_rescue_amplified_class,
* plateau_intervention_mode_current) the orchestrator published
* just before the rotation -- the gates downstream that consult
* those fields would mis-fire under weak memory. */
strat = __atomic_load_n(&shm->current_strategy, __ATOMIC_ACQUIRE);
if (strat < 0 || strat >= NR_STRATEGIES)
strat = STRATEGY_HEURISTIC;
/* Stamp the picked arm before dispatching so the post-syscall PC
* and CMP reward sites read a stable value even if shm->current_
* strategy rotates mid-call. Written exactly once per pick on the
* bandit-pool path; explorers (handled above) leave the -1 sentinel
* from clean_childdata in place. */
child->strategy_at_pick = strat;
/* Bandit-pool exposure: bump both the wide picks counter and the
* bandit-pool-only sub-counter so post-run analysis can separate
* bandit dispatches from explorer dispatches per arm. Bumped
* before the picker-specific set_syscall_nr_* call -- a FAIL from
* that path still counts as a pick attributed to this arm; the
* matching strategy_completed_calls bump in dispatch_step lets
* the operator read off the per-arm dispatch success rate. */
__atomic_fetch_add(&shm->strategy_picks[strat], 1UL, __ATOMIC_RELAXED);
__atomic_fetch_add(&shm->strategy_bandit_pool_ops[strat], 1UL,
__ATOMIC_RELAXED);
switch (strat) {
case STRATEGY_HEURISTIC:
return set_syscall_nr_heuristic(rec, child);
case STRATEGY_RANDOM:
return set_syscall_nr_random(rec, child);
case STRATEGY_COVERAGE_FRONTIER:
return set_syscall_nr_coverage_frontier(rec, child);
case STRATEGY_HEALER:
return set_syscall_nr_healer(rec, child);
default:
__builtin_unreachable();
}
}
/*
* Probability (in percent) that, when a substitute retval is offered by
* the sequence-chain executor, one randomly-chosen arg slot is overwritten
* with it. Exposed here (rather than in sequence.c) because the substitution
* itself happens between argument generation and dispatch, which lives in
* this file. Tunable independently of the chain length distribution.
*/
#define CHAIN_SUBST_PCT 30
/*
* Substituting the previous syscall's return value (almost always a
* small integer — fd, retval, error code) into a pointer-typed arg
* slot produces a wild pointer. The rendering path then SEGVs in
* printf("%s", small_int) → strlen(0x402), or the kernel deref'es a
* wild address, depending on which slot got stomped. Restrict
* substitution to slots whose argtype legitimately accepts a numeric
* value.
*/
static bool argtype_accepts_numeric_substitute(enum argtype t)
{
switch (t) {
case ARG_UNDEFINED:
case ARG_FD:
case ARG_LEN:
case ARG_MODE_T:
case ARG_PID:
case ARG_KEY_SERIAL:
case ARG_TIMERID:
case ARG_AIO_CTX:
case ARG_SEM_ID:
case ARG_MSG_ID:
case ARG_SYSV_SHM:
case ARG_RANGE:
case ARG_OP:
case ARG_LIST:
case ARG_CPU:
case ARG_NUMA_NODE:
case ARG_IOVECLEN:
case ARG_SOCKADDRLEN:
case ARG_STRUCT_SIZE:
case ARG_FD_BPF_BTF:
case ARG_FD_BPF_LINK:
case ARG_FD_BPF_MAP:
case ARG_FD_BPF_PROG:
case ARG_FD_EPOLL:
case ARG_FD_EVENTFD:
case ARG_FD_FANOTIFY:
case ARG_FD_FS_CTX:
case ARG_FD_INOTIFY:
case ARG_FD_IO_URING:
case ARG_FD_LANDLOCK:
case ARG_FD_MEMFD:
case ARG_FD_MOUNT:
case ARG_FD_MQ:
case ARG_FD_PERF:
case ARG_FD_PIDFD:
case ARG_FD_PIPE:
case ARG_FD_SIGNALFD:
case ARG_FD_SOCKET:
case ARG_FD_TIMERFD:
return true;
case ARG_ADDRESS:
case ARG_NON_NULL_ADDRESS:
case ARG_PATHNAME:
case ARG_IOVEC:
case ARG_SOCKADDR:
case ARG_MMAP:
case ARG_SOCKETINFO:
case ARG_STRUCT_PTR_IN:
case ARG_STRUCT_PTR_OUT:
return false;
}
return false;
}
/*
* Build the numeric-substitute slot bitmap for entry's argtype[] table.
* Called once per syscallentry at table-init time from
* copy_syscall_table() in tables.c; the cached mask in
* entry->numeric_substitute_mask then drives apply_chain_substitution()
* below without re-walking argtype[] or re-running the 23-case
* argtype_accepts_numeric_substitute() switch on every chain step.
* Bit k (k=0..5) set means slot (k+1) accepts a numeric substitute.
*/
uint8_t compute_numeric_substitute_mask(const struct syscallentry *entry)
{
uint8_t mask = 0;
unsigned int i;
if (entry == NULL)
return 0;
for (i = 0; i < entry->num_args && i < 6; i++) {
if (argtype_accepts_numeric_substitute(entry->argtype[i]))
mask |= (uint8_t)(1u << i);
}
return mask;
}
/*
* Apply Phase 1 retval substitution to rec in place. Used by both the
* fresh-args path (random_syscall_step) and the corpus-replay path
* (replay_syscall_step) so the chain semantics — substituted args reach
* the kernel and show up in the trace — are identical regardless of
* where the args came from. No-op when no substitute is offered, the
* dice roll comes up against, the syscall takes zero args, or no arg
* slot has a numeric-substitute-compatible argtype.
*/
static void apply_chain_substitution(struct syscallrecord *rec,
struct syscallentry *entry,
bool have_substitute,
unsigned long substitute_retval)
{
unsigned int draw, slot;
uint8_t mask;
if (!have_substitute)
return;
if (entry == NULL || entry->num_args == 0)
return;
if ((unsigned int)(rand() % 100) >= CHAIN_SUBST_PCT)
return;
mask = entry->numeric_substitute_mask;
if (mask == 0)
return;
if (substitute_retval == (unsigned long)mainpid) {
unsigned int i;
for (i = 0; i < entry->num_args && i < 6; i++) {
if (entry->argtype[i] == ARG_PID)
mask &= (uint8_t)~(1u << i);
}
if (mask == 0)
return;
}
/*
* Pick a slot via __builtin_ctz of a masked random draw against
* the precomputed mask. When the masked draw lands on zero (no
* eligible slot bit overlapped this iteration's random bits) fall
* back to the mask itself so __builtin_ctz still picks the
* lowest-numbered eligible slot. Same slots remain eligible as
* the old per-call safe_slots[] walk — only the dispatch
* mechanism changes.
*/
draw = (unsigned int)rand() & mask;
if (draw == 0)
draw = mask;
slot = (unsigned int)__builtin_ctz(draw) + 1;
switch (slot) {
case 1: rec->a1 = substitute_retval; break;
case 2: rec->a2 = substitute_retval; break;
case 3: rec->a3 = substitute_retval; break;
case 4: rec->a4 = substitute_retval; break;
case 5: rec->a5 = substitute_retval; break;
case 6: rec->a6 = substitute_retval; break;
}
if (minicorpus_shm != NULL)
__atomic_fetch_add(&minicorpus_shm->chain_substitution_count,
1, __ATOMIC_RELAXED);
}
/*
* Check the rotation boundary and, if crossed, atomically claim the
* switch and update shm->current_strategy to whatever the configured
* picker (round-robin or UCB1 bandit, see strategy.h) selects next.
*
* The rotation clock is shm_published->fleet_op_count, which mirrors
* the parent-private fleet op_count (every child contributes ticks at
* the same rate, including non-syscall alt-ops). A child that observes
* (op_count - syscalls_at_last_switch) >= STRATEGY_WINDOW tries to CAS
* syscalls_at_last_switch forward to the current op_count; the CAS
* winner performs the switch and emits the
* stats line, the losers fall through and continue with the new strategy
* on their next syscall pick.
*
* Per-strategy attribution: the just-finished window's call-count delta
* is pc_edge_calls_by_strategy[prev] - pc_edge_calls_at_window_start and
* the parallel real bucket-count delta is pc_edge_count_by_strategy[prev]
* - pc_edge_count_at_window_start. After the switch, both *at_window_start
* snapshots are reseeded from the new strategy's current cumulative
* counters, so the next switch will compute the deltas correctly even if
* other strategies' counters are bumped during the grace period.
*/
static void maybe_rotate_strategy(void)
{
unsigned long now;
unsigned long last;
int prev, next;
int prev_reason_raw;
enum strategy_selection_reason prev_reason;
enum strategy_selection_reason next_reason = SR_NORMAL_UCB;
unsigned long calls_now, calls_in_window;
unsigned long edges_now, edges_in_window;
unsigned long syscalls_in_window;
unsigned long cmp_now, cmp_in_window;
/* Read fleet op_count off the parent-published mirror page; the
* canonical aggregate is parent-private and not visible to children.
* The mirror is republished once per parent main_loop iteration so
* a stale read here only delays the rotation by drain cadence. */
now = (shm_published != NULL)
? __atomic_load_n(&shm_published->fleet_op_count, __ATOMIC_RELAXED)
: 0;
last = __atomic_load_n(&shm->syscalls_at_last_switch, __ATOMIC_RELAXED);
if (now - last < STRATEGY_WINDOW)
return;
if (!__atomic_compare_exchange_n(&shm->syscalls_at_last_switch,
&last, now,
false,
__ATOMIC_RELAXED, __ATOMIC_RELAXED))
return;
prev = __atomic_load_n(&shm->current_strategy, __ATOMIC_RELAXED);
if (prev < 0 || prev >= NR_STRATEGIES)
prev = STRATEGY_HEURISTIC;
/* Selection reason for the just-finished window -- the intervention
* orchestrator stamped this when it picked prev. Treated as raw int
* across the shm boundary and re-validated here so a wild write
* landing on the field falls back to the "policy chose this" path
* rather than skipping the learner update spuriously. */
prev_reason_raw = __atomic_load_n(&shm->current_selection_reason,
__ATOMIC_RELAXED);
switch (prev_reason_raw) {
case SR_NORMAL_UCB:
case SR_ROUND_ROBIN:
case SR_COLD_START:
case SR_PLATEAU_FORCE:
prev_reason = (enum strategy_selection_reason)prev_reason_raw;
break;
default:
prev_reason = SR_NORMAL_UCB;
break;
}
calls_now = __atomic_load_n(&shm->pc_edge_calls_by_strategy[prev],
__ATOMIC_RELAXED);
calls_in_window = calls_now - shm->pc_edge_calls_at_window_start;
edges_now = __atomic_load_n(&shm->pc_edge_count_by_strategy[prev],
__ATOMIC_RELAXED);
edges_in_window = edges_now - shm->pc_edge_count_at_window_start;
syscalls_in_window = now - last;
/* CMP-novelty delta: number of comparison constants the active arm
* exposed for the first time within CMP_NOVELTY_DECAY_WINDOWS this
* window. Folded into the bandit reward by bandit_record_pull as
* a 0.25-weight secondary signal so an arm whose PC growth has
* plateaued but whose validation surface is still mutating doesn't
* lose to a noisier arm on PC delta alone. */
cmp_now = __atomic_load_n(&shm->bandit_cmp_new_constants[prev],
__ATOMIC_RELAXED);
cmp_in_window = cmp_now - shm->bandit_cmp_at_window_start;
/* Feed the just-finished window into the bandit before asking
* the picker to choose the next arm, so UCB1 sees up-to-date
* pulls/reward when scoring. The learner consumes the call-count
* delta as today; the real bucket-count delta is recorded into the
* parallel diagnostic reward series so the operator can compare the
* two reward shapes without changing the learner's behaviour.
* Round-robin mode ignores the counters but the bookkeeping is
* harmless and lets the end-of-run summary print pulls under either
* picker.
*
* Called on EVERY window including SR_PLATEAU_FORCE. The
* per-arm-per-reason bucketing inside bandit_record_pull captures
* every cohort (forced included) so dump-side analysis can split
* each arm's exposure by selection path, while the learner-facing
* update (bandit_pulls[] / bandit_reward_calls[] / EMA) skips
* SR_PLATEAU_FORCE internally to keep the UCB scorer's view of
* RANDOM uncontaminated. All other bookkeeping
* (bandit_window_count tick, frontier ring advance, window-start
* snapshot reseed) runs unconditionally -- those are coverage-side
* structures and must stay aligned with the rotation cadence. */
bandit_record_pull(prev, prev_reason, calls_in_window,
edges_in_window, cmp_in_window);
/* Tick the rotation counter so bandit_cmp_observe()'s per-syscall
* bloom decay sees the new window index on subsequent calls.
* Bumped after bandit_record_pull so a concurrent observer racing
* the rotation either sees the old (still-valid) window or the
* fresh one — both attribute correctly. */
__atomic_fetch_add(&shm->bandit_window_count, 1UL, __ATOMIC_RELAXED);
/* Roll the per-syscall frontier-edge ring forward and zero the new
* slot so it represents only edges discovered in the upcoming
* window. Same K-window decay horizon as the CMP-novelty bloom
* above. */
frontier_window_advance();
next = select_next_strategy(prev, &next_reason);
if (next < 0 || next >= NR_STRATEGIES) {
next = (prev + 1) % NR_STRATEGIES;
next_reason = SR_ROUND_ROBIN;
}
shm->pc_edge_calls_at_window_start =
__atomic_load_n(&shm->pc_edge_calls_by_strategy[next],
__ATOMIC_RELAXED);
shm->pc_edge_count_at_window_start =
__atomic_load_n(&shm->pc_edge_count_by_strategy[next],
__ATOMIC_RELAXED);
shm->bandit_cmp_at_window_start =
__atomic_load_n(&shm->bandit_cmp_new_constants[next],
__ATOMIC_RELAXED);
/* Publish the selection reason BEFORE current_strategy: the RELEASE
* store on current_strategy below pairs with the picker's and the
* plateau gates' ACQUIRE loads of current_strategy, making the
* reason and the companion plateau fields (published earlier under
* RELAXED in select_next_strategy) visible to any child that
* observes the new strategy. */
__atomic_store_n(&shm->current_selection_reason,
(int)next_reason, __ATOMIC_RELAXED);
__atomic_store_n(&shm->current_strategy, next, __ATOMIC_RELEASE);
output(0, "strategy: switched to %s (%d) [%s] (prev %s (%d) [%s]: edge_calls=%lu, edge_count=%lu, syscalls=%lu, cmp_novel=%lu%s)\n",
strategy_name(next), next,
strategy_selection_reason_name(next_reason),
strategy_name(prev), prev,
strategy_selection_reason_name(prev_reason),
calls_in_window, edges_in_window, syscalls_in_window,
cmp_in_window,
prev_reason == SR_PLATEAU_FORCE ?
", learner-update skipped" : "");
}
/*
* Dispatch a fully-prepared syscallrecord and run the per-call
* post-dispatch bookkeeping: kcov collection / cmp-hint collection,
* edge-pair recording, mutator-attribution commit, mini-corpus save,
* trace output, fd-ring update, group/last_syscall_nr tracking.
*
* Caller has already populated rec->nr, rec->do32bit, rec->a1..a6, the
* postbuffer is already cleared, and any chain substitution has been
* applied. The two callers (random_syscall_step and replay_syscall_step)
* differ only in how they got the args into rec; everything from
* output_syscall_prefix forward is shared.
*/
static bool dispatch_step(struct childdata *child, struct syscallentry *entry,
bool *found_new)
{
struct syscallrecord *rec = &child->syscall;
bool new_edges;
unsigned long new_edge_count = 0;
/* Stamp the resolved entry on the rec so .sanitise / .post handlers
* (and helpers like this_syscallname()) can reach it without
* re-running get_syscall_entry(nr, do32bit) on every probe. */
rec->entry = entry;
output_syscall_prefix(rec, entry);
/* PC mode: per-child kcov fd collects edge coverage, optionally
* via KCOV_REMOTE_ENABLE to also pick up softirq / threaded-irq /
* kthread coverage triggered by this syscall. CMP mode: per-child
* cmp fd collects comparison-operand records that feed the
* cmp_hints pool. Mode is fixed at child init; remote_mode is
* only meaningful in PC mode (KCOV_REMOTE_ENABLE applies to the PC
* fd, not the cmp fd).
*
* Sample rate is per-syscall: calls whose interesting kernel work
* is deferred to kthreads / workqueues / softirqs (netlink async
* delivery, io_uring workers, BPF attach, mount workqueues, cgroup
* migration, namespace setup) are flagged with KCOV_REMOTE_HEAVY
* and sampled at the heavier 1-in-KCOV_REMOTE_RATIO_HEAVY rate so
* those deferred-work edges don't get stuck cold; everything else
* uses the default 1-in-KCOV_REMOTE_RATIO trickle. */
if (child->kcov.mode == KCOV_MODE_PC) {
unsigned int remote_reciprocal =
(entry->flags & KCOV_REMOTE_HEAVY) ?
KCOV_REMOTE_RATIO_HEAVY : KCOV_REMOTE_RATIO;
child->kcov.remote_mode = child->kcov.remote_capable &&
ONE_IN(remote_reciprocal);
} else {
child->kcov.remote_mode = false;
}
do_syscall(rec, entry, &child->kcov, child);
/* kcov_collect() returns the real per-call bucket-edge count via the
* out-param alongside its bool found_new return. Diff-ing the global
* kcov_shm->edges_found around the call would race other children's
* concurrent increments and over-attribute their edges to this
* syscall; the per-call count is the authoritative number.
*
* CMP-mode children contribute zero PC edges (their PC fd is never
* enabled), so new_edge_count stays 0 and the per-strategy edge
* attribution block below naturally skips on its `new_edge_count > 0`
* gate. HEALER observation, frontier ring updates, and bandit
* reward attribution also skip cleanly via `if (new_edges)`
* further down -- CMP-mode children deliberately don't contribute
* to those PC-edge concepts.
*
* CMP-source corpus saves are the exception: kcov_collect_cmp
* returns the per-call count of bloom-novel KCOV_CMP_CONST
* comparisons, captured here in new_cmp. Under a PC-edge plateau
* (cmp_rising_pc_flat) that count is the only available novelty
* signal -- the save gate below widens to `new_edges || new_cmp
* > 0` so the corpus can still grow and mutator wins can still be
* credited, breaking the self-reinforcing
* PC-plateau->no-saves->no-mutator-wins loop. See
* investigations/corpus-mutator-zero-wins-2026-05-20 for the full
* analysis. */
unsigned long new_cmp = 0;
if (child->kcov.mode == KCOV_MODE_PC) {
new_edges = kcov_collect(&child->kcov, rec->nr, &new_edge_count);
} else {
new_cmp = kcov_collect_cmp(&child->kcov, rec->nr,
child->is_explorer,
child->strategy_at_pick);
new_edges = false;
}
/* Per-syscall new-edge attribution split by strategy pool. Skipped
* when the call produced no new edges (the dump only consumes the
* positive delta side) and when rec->nr falls outside the table.
* Biarch attribution follows the same raw-rec->nr indexing the
* existing kcov_shm->per_syscall_edges array uses; the dump iterates
* only the active 64-bit table when biarch, so 32-bit calls are
* effectively ignored there as they are everywhere else. */
if (new_edge_count > 0 && rec->nr < MAX_NR_SYSCALL) {
unsigned long *bucket = child->is_explorer
? shm->stats.edges_per_syscall_explorer
: shm->stats.edges_per_syscall_bandit;
__atomic_fetch_add(&bucket[rec->nr], new_edge_count,
__ATOMIC_RELAXED);
}
/* Surface this step's new-coverage signal to the chain executor
* (when called via run_sequence_chain). */
if (found_new != NULL)
*found_new = new_edges;
/* Record the (prev, curr) syscall pair for sequence coverage. */
if (child->last_syscall_nr != EDGEPAIR_NO_PREV)
edgepair_record(child, child->last_syscall_nr, rec->nr, new_edges);
/* CMP-bloom novelty is an equivalent corpus-save / mutator-win
* signal alongside PC-edge novelty. Under a PC-edge plateau the
* PC-only gate fires for ~0% of calls; the OR-with-CMP gate keeps
* the corpus growing on arg neighbourhoods that exercise new
* compile-time-constant comparisons (the cmp_rising_pc_flat
* frontier). PC-edge-specific bookkeeping below (HEALER, frontier