-
Notifications
You must be signed in to change notification settings - Fork 243
Expand file tree
/
Copy pathcmp_hints.c
More file actions
3529 lines (3242 loc) · 125 KB
/
Copy pathcmp_hints.c
File metadata and controls
3529 lines (3242 loc) · 125 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
/*
* KCOV comparison operand collection and hint pool management.
*
* Parses KCOV_TRACE_CMP trace buffers to extract constants that the
* kernel compared syscall-derived values against. These constants
* are stored in per-syscall in-memory pools and used during argument
* generation to produce values more likely to pass kernel validation.
*
* Buffer format (each record is 4 x u64):
* [0] type - KCOV_CMP_CONST | KCOV_CMP_SIZE(n)
* [1] arg1 - first comparison operand
* [2] arg2 - second comparison operand
* [3] ip - instruction pointer of the comparison
*
* Pool entries are keyed by (cmp_ip, value, size). Distinguishing on
* cmp_ip means the same constant compared at two different kernel
* sites occupies two slots rather than colliding -- the precision
* matters once a downstream consumer wants to attribute which site a
* hint came from. cmp_ip is the canonical (KASLR-stripped) address
* produced by kcov_canon_cmp_ip(), routed in at the top of the
* cmp_hints_collect() per-record loop; the bloom hash, the pool dedup
* key, and the persisted on-disk record all index by the same canonical
* value, so a KASLR reroll between save and warm-load does not alias
* every learned constant to a different (cmp_ip, value, size) tuple.
*
* When a pool fills, the entry with the lowest last_used generation
* is evicted (least-recently-inserted), so a fresh constant displaces
* stale long-tail noise instead of stomping a slot at random.
*/
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
#include "arch.h"
#include "child.h"
#include "cmp_hints.h"
#include "debug.h"
#include "deferred-free.h"
#include "fd.h"
#include "kcov.h"
#include "params.h"
#include "persist-util.h"
#include "random.h"
#include "rnd.h"
#include "shm.h"
#include "signals.h"
#include "stats_ring.h"
#include "strategy.h"
#include "struct_catalog.h"
#include "syscall.h"
#include "tables.h"
#include "trinity.h"
#include "utils.h"
#include "pids.h"
/* From uapi/linux/kcov.h. KCOV_CMP_SIZE(n) packs the operand-width
* index n in {0,1,2,3} into bits 1..2 of the type word; the actual
* operand width in bytes is (1U << n). */
#define KCOV_CMP_CONST (1U << 0)
#define KCOV_CMP_SIZE_SHIFT 1
#define KCOV_CMP_SIZE_MASK 3U
/* Words per comparison record in the trace buffer. */
#define WORDS_PER_CMP 4
struct cmp_hints_shared *cmp_hints_shm = NULL;
/*
* Per-syscall CMP-collection strip flags. When cmp_hints_strip[do32][nr]
* is true, cmp_hints_collect() returns immediately after the nr range
* check, bypassing the bloom + pool_add_locked path entirely for that
* syscall number. Indexed by [do32bit ? 1 : 0][nr] under biarch: a
* 32-bit syscall and a 64-bit syscall can share the same numeric nr
* but mean unrelated things, so a single-dimensional table would
* collaterally strip whichever sibling happens to live at the same
* slot. Uniarch builds only ever touch the [0] row. Targets are syscalls whose KCOV_TRACE_CMP records
* fire on task_struct / cred / ucounts / aio-table internal state set
* by prior syscalls or kernel init, not on values driven by the
* current syscall's argument surface -- the resulting pool entries
* are unreachable from any subsequent argument generator and only
* displace genuinely useful constants from the LRU eviction order.
*
* Per-record bump of cmp_hints_strip_skipped (mirroring the
* cmp_hints_bloom_skipped accounting) makes the avoided work
* observable; the stripped syscalls' pool[nr] entries continue to be
* served by cmp_hints_try_get() from anything they accumulated before
* the strip flag was set, so there is no consumer-side hole.
*/
static bool cmp_hints_strip[2][MAX_NR_SYSCALL];
/*
* Chaos-mode toggle. cmp_hints saturates after a warm-up period at
* roughly the per-syscall cap across the syscalls the fuzzer
* exercises, and substitutes kernel-blessed constants at the
* gen_undefined_arg injection point at >99% of pulls. Constants the
* kernel CMP'd against by definition passed the kernel's validation
* gates; the vast majority of WARN_ONs guard INVALID state (refcount
* underflow, mutually-exclusive flag combinations, etc.) -- so a
* hint-injected arg is biased AWAY from the args that trip WARNs.
*
* Periodically suppress hint injection so random-arg generation gets
* a fair shot at the invalid-combination space. Gate at the
* cmp_hints_try_get layer -- when chaos is active the function
* returns false (no hint), the caller falls through to its other
* arg-generation paths. Zero churn at the call site.
*
* Cadence: cmp_hints_chaos_tick() is called once per bandit window
* rotation from maybe_rotate_strategy(). Every CHAOS_WINDOW_MODULO'th
* window flips chaos_active for the duration of that window -- 1 in
* every 8 windows in the current default (12.5% of windows). Cheap:
* tick path is one fetch_add and one atomic store; hot-path gate is
* one atomic load. Modulo-of-counter rather than RNG so the cadence
* stays exactly predictable -- attribution work in follow-ups can
* line up WARN-fire deltas against the chaos schedule without
* sampling noise.
*/
#define CHAOS_WINDOW_MODULO 8
void cmp_hints_chaos_tick(void)
{
unsigned long n;
if (kcov_shm == NULL)
return;
n = __atomic_add_fetch(&kcov_shm->cmp_hints_chaos_window_count, 1UL,
__ATOMIC_RELAXED);
__atomic_store_n(&kcov_shm->cmp_hints_chaos_active,
(n % CHAOS_WINDOW_MODULO) == 0 ? 1u : 0u,
__ATOMIC_RELAXED);
}
bool cmp_hints_chaos_query(void)
{
if (kcov_shm == NULL)
return false;
return __atomic_load_n(&kcov_shm->cmp_hints_chaos_active,
__ATOMIC_RELAXED) != 0;
}
/*
* Mark each named syscall as cmp-collection-stripped. Names are
* resolved via search_syscall_table() against the active table set;
* under biarch both the 32-bit and 64-bit indices are flagged since
* cmp_hints_collect()'s nr argument comes from rec->nr at the call
* site (which uses whichever table the child ran against), and the
* same syscall name occupies different slots in each table.
*
* Unknown names log a warning and are skipped: the strip list is
* compiled in and a typo here would otherwise silently fail to take
* effect. NULL entries are tolerated so the strip-target array can
* carry a sentinel before any targets are populated.
*/
static void cmp_hints_strip_install(const char * const names[], unsigned int n)
{
unsigned int i;
for (i = 0; i < n; i++) {
const char *name = names[i];
bool found = false;
int nr;
if (name == NULL)
continue;
if (biarch == true) {
nr = search_syscall_table(syscalls_64bit,
max_nr_64bit_syscalls,
name);
if (nr >= 0 && (unsigned int)nr < MAX_NR_SYSCALL) {
cmp_hints_strip[0][nr] = true;
found = true;
}
nr = search_syscall_table(syscalls_32bit,
max_nr_32bit_syscalls,
name);
if (nr >= 0 && (unsigned int)nr < MAX_NR_SYSCALL) {
cmp_hints_strip[1][nr] = true;
found = true;
}
} else {
nr = search_syscall_table(syscalls,
max_nr_syscalls, name);
if (nr >= 0 && (unsigned int)nr < MAX_NR_SYSCALL) {
cmp_hints_strip[0][nr] = true;
found = true;
}
}
if (found == true)
output(0, "KCOV: CMP collection stripped for %s\n",
name);
else
output(0, "KCOV: cmp_hints strip target '%s' not found in syscall table\n",
name);
}
}
/*
* Compiled-in list of syscalls whose per-call CMP records are
* dominated by kernel-internal state unreachable from the syscall
* argument surface. See the cmp_hints_strip[] comment above for the
* semantics; per-target rationale follows.
*
* prctl -- the option dispatch reads task_struct / mm_struct /
* cred / signal_struct fields and compares them against
* compile-time constants in each PR_* arm. The option
* selector is one of trinity's syscall args, but every
* downstream comparand is kernel-internal state set by
* prior syscalls (or process init); the option value
* itself only feeds the dispatch switch, not any
* KCOV_CMP_CONST record.
* unshare -- the flags arg drives a switch over CLONE_* bits, but
* the comparisons KCOV traps fire inside the per-
* namespace clone paths against ucounts / user_ns /
* nsproxy state, none of which a single unshare() arg
* can move.
* io_setup -- the constants land on aio_ring_setup() validation of
* table state attached to the mm (existing ioctx count,
* pinned-page accounting), set by earlier io_setup /
* io_destroy calls on the same task; the nr_events arg
* only sizes the ring, it does not drive the compared
* fields.
*
* Each is a top-volume CMP-record producer whose entries can only
* displace constants from edge-producing syscalls in the same
* cmp_hints_try_get() namespace (per-nr pools, so no direct
* cross-contamination, but the global LRU and the bloom-reset cadence
* absorb the wasted work).
*/
static const char * const cmp_hints_strip_targets[] = {
"prctl",
"unshare",
"io_setup",
};
/*
* Auto-strip CMP collection for any syscall whose num_args == 0. With
* no syscall arguments at all, every KCOV_CMP record such a syscall
* emits is by construction unreachable from cmp_hints_try_get() -- the
* argument surface is empty, so no constant the kernel compares
* against can ever be steered by a subsequent generated arg. Pool
* entries from these syscalls only displace constants from
* arg-bearing syscalls in the LRU eviction order and waste
* bloom-reset cycles.
*
* Run after cmp_hints_strip_install() so the explicit per-rationale
* list above is in place first; the explicit set is independent (it
* strips arg-bearing syscalls whose comparisons fire on
* kernel-internal state) and is not a subset of this one. Emits a
* single count line rather than per-syscall output -- ~20+ matches
* would be log spam at fleet scale.
*/
static void cmp_hints_strip_no_arg_syscalls(void)
{
struct syscallentry *entry;
unsigned int i;
unsigned int count = 0;
if (biarch == true) {
for_each_64bit_syscall(i) {
if (i >= MAX_NR_SYSCALL)
break;
entry = syscalls_64bit[i].entry;
if (entry == NULL)
continue;
if (entry->num_args == 0 && !cmp_hints_strip[0][i]) {
cmp_hints_strip[0][i] = true;
count++;
}
}
for_each_32bit_syscall(i) {
if (i >= MAX_NR_SYSCALL)
break;
entry = syscalls_32bit[i].entry;
if (entry == NULL)
continue;
if (entry->num_args == 0 && !cmp_hints_strip[1][i]) {
cmp_hints_strip[1][i] = true;
count++;
}
}
} else {
for_each_syscall(i) {
if (i >= MAX_NR_SYSCALL)
break;
entry = syscalls[i].entry;
if (entry == NULL)
continue;
if (entry->num_args == 0 && !cmp_hints_strip[0][i]) {
cmp_hints_strip[0][i] = true;
count++;
}
}
}
output(0, "KCOV: CMP collection auto-stripped for %u zero-arg syscalls\n",
count);
}
void cmp_hints_init(void)
{
if (kcov_shm == NULL)
return;
/*
* Wild-write risk: a child syscall whose user-buffer arg aliases
* into a pool could let the kernel scribble into pool->entries[]
* (worst case: a duplicate slips past the linear-scan dedup, or a
* stale value is handed back as a hint -- not a crash) or into the
* lock byte (a stuck lock would deadlock subsequent
* cmp_hints_collect callers in that one syscall slot).
* Diagnostic-grade only.
*/
cmp_hints_shm = alloc_shared(sizeof(struct cmp_hints_shared));
memset(cmp_hints_shm, 0, sizeof(struct cmp_hints_shared));
/* Stamp the wild-write canaries flanking pool->entries[] in every
* (nr, arch) slot. These are runtime-only -- cmp_hints_load_file
* writes count/generation/entries/last_used_stamp and never touches
* canary_pre/canary_post, so a single init pass before warm-start
* is sufficient for the lifetime of the SHM. */
{
unsigned int nr, a;
for (nr = 0; nr < MAX_NR_SYSCALL; nr++) {
for (a = 0; a < 2; a++) {
struct cmp_hint_pool *pool =
&cmp_hints_shm->pools[nr][a];
pool->canary_lock_post = CMP_HINTS_POOL_CANARY;
pool->canary_pre = CMP_HINTS_POOL_CANARY;
pool->canary_post = CMP_HINTS_POOL_CANARY;
}
}
}
/* Field-pool canaries. Same triplet (lock_post / pre / post) as the
* per-syscall pools so a wild-write into either family lands in the
* same channel-attributed sentinels. */
{
unsigned int i;
for (i = 0; i < CMP_FIELD_POOL_BUCKETS; i++) {
struct cmp_field_pool *pool =
&cmp_hints_shm->field_pools[i];
pool->canary_lock_post = CMP_HINTS_POOL_CANARY;
pool->canary_pre = CMP_HINTS_POOL_CANARY;
pool->canary_post = CMP_HINTS_POOL_CANARY;
}
}
output(0, "KCOV: CMP hint pool allocated (%lu KB)\n",
(unsigned long) sizeof(struct cmp_hints_shared) / 1024);
cmp_hints_strip_install(cmp_hints_strip_targets,
ARRAY_SIZE(cmp_hints_strip_targets));
cmp_hints_strip_no_arg_syscalls();
cmp_hints_field_record_self_check();
}
static void pool_lock(struct cmp_hint_pool *pool)
{
if (cmp_hints_shm != NULL)
__atomic_fetch_add(&cmp_hints_shm->held_count, 1,
__ATOMIC_RELAXED);
lock(&pool->lock);
}
static void pool_unlock(struct cmp_hint_pool *pool)
{
unlock(&pool->lock);
if (cmp_hints_shm != NULL)
__atomic_sub_fetch(&cmp_hints_shm->held_count, 1,
__ATOMIC_RELAXED);
}
/*
* Wild-write gate for the cmp_hints SHM pool. Called by every reader
* (try_get lockless, pool_add_locked under lock) immediately after the
* count load. Bumps three independent kcov_shm counters so the
* post-mortem can attribute corruption to whichever channel hit:
*
* - cmp_hints_count_oob: pool->count exceeds the
* 16-slot cap, the only sign
* visible from the read path
* itself.
* - cmp_hints_canary_lock_post_corrupt: the sentinel between lock
* and count has been
* overwritten -- a wide write
* landed in the lock/count
* seam.
* - cmp_hints_canary_pre_corrupt: the sentinel between
* last_used_stamp and
* entries[] has been
* overwritten -- write
* reached the pool from the
* header side.
* - cmp_hints_canary_post_corrupt: the sentinel after entries[]
* has been overwritten --
* write reached the pool
* from the tail side.
*
* A narrow stomp that lands exactly on count (or exactly on any
* single header field) trips ONLY count_oob; the canaries flank the
* data but cannot detect a write that fits entirely between them.
* Non-zero canary deltas narrow the stomp's width and direction --
* useful for forensics, not load-bearing for the corruption-present
* decision. Canary loads are gated on the count check so the
* steady-state cost is one compare-against-16; the canary cache
* lines are only touched once a stomp has already occurred. Returns
* true when corruption is present so callers can treat the pool as
* advisory-empty.
*
* Per-pool latch (pool->corrupted) one-shots the counter bumps: the
* first observation records the channel, every subsequent call
* returns true from the latch check and skips both the count
* comparison and the canary probes. Without this, the batch loop in
* cmp_hints_flush_pending would multiply a single stomp event by up
* to CMP_HINTS_PENDING_BATCH bumps per cmp_hints_collect call and
* the count_oob counter would track "exposures" instead of "events".
*/
static bool cmp_hints_pool_corrupted(struct cmp_hint_pool *pool,
unsigned int observed_count)
{
if (__atomic_load_n(&pool->corrupted, __ATOMIC_RELAXED))
return true;
if (observed_count <= CMP_HINTS_PER_SYSCALL)
return false;
if (kcov_shm != NULL) {
__atomic_fetch_add(&kcov_shm->cmp_hints_count_oob, 1UL,
__ATOMIC_RELAXED);
if (pool->canary_lock_post != CMP_HINTS_POOL_CANARY)
__atomic_fetch_add(&kcov_shm->cmp_hints_canary_lock_post_corrupt,
1UL, __ATOMIC_RELAXED);
if (pool->canary_pre != CMP_HINTS_POOL_CANARY)
__atomic_fetch_add(&kcov_shm->cmp_hints_canary_pre_corrupt,
1UL, __ATOMIC_RELAXED);
if (pool->canary_post != CMP_HINTS_POOL_CANARY)
__atomic_fetch_add(&kcov_shm->cmp_hints_canary_post_corrupt,
1UL, __ATOMIC_RELAXED);
}
__atomic_store_n(&pool->corrupted, true, __ATOMIC_RELAXED);
return true;
}
/*
* Clamp wrapper for readers that need pool->count for accounting but
* do NOT index into entries[] (stats accumulators, strategy classifier).
* Returns 0 on a stomped pool so accounting code doesn't fold garbage
* counts into totals. Side-effects identical to cmp_hints_pool_corrupted:
* first observation of an OOB count records the channel via the kcov_shm
* counters and latches pool->corrupted, so these readers participate in
* the same one-shot accounting as the index-into-entries readers.
*
* Also closes a TOCTOU on the existing call sites that read .count
* twice in quick succession ('if (count > 0) total += count;'): a
* single atomic load drives both the test and the value used.
*/
unsigned int cmp_hints_pool_safe_count(struct cmp_hint_pool *pool)
{
unsigned int count = __atomic_load_n(&pool->count, __ATOMIC_RELAXED);
if (cmp_hints_pool_corrupted(pool, count))
return 0;
return count;
}
/*
* Insert (cmp_ip, val, size) into the entries[] array. Dedups via linear
* scan on the full (cmp_ip, value, size) key. When the pool is full,
* evicts the entry with the smallest last_used (least-recently-inserted)
* to make room. Duplicate hits refresh last_used so an actively-observed
* constant doesn't get evicted by transient long-tail noise. Caller must
* hold pool->lock.
*
* pool->generation is bumped ONLY on real content changes (fresh insert
* or evict-replace), never on dedup-refresh. That keeps the
* cmp_hints_total_generation() snapshot dirty-bit honest: a saturated
* pool whose hot tuples keep dedup-refreshing produces no generation
* delta and therefore no redundant snapshot save. The LRU clock that
* stamps each entry's last_used uses a separate per-pool counter
* (pool->last_used_stamp) which advances on every call — including
* dedup-refresh — so the eviction policy is unchanged from before this
* split: an actively-observed tuple still gets its stamp refreshed and
* is still the last thing the evictor will pick.
*/
static bool pool_add_locked(struct cmp_hint_pool *pool,
unsigned long cmp_ip,
unsigned long val,
unsigned int size)
{
unsigned int i, count = pool->count;
uint64_t stamp;
unsigned int victim;
uint64_t oldest;
/* SHM stomp from a fuzzed syscall arg has scribbled count past the
* 16-slot cap; the dedup loop below would walk off entries[]. Bail
* before mutating anything (including last_used_stamp). */
if (cmp_hints_pool_corrupted(pool, count))
return false;
stamp = ++pool->last_used_stamp;
for (i = 0; i < count; i++) {
struct cmp_hint_entry *e = &pool->entries[i];
if (e->value == val && e->cmp_ip == cmp_ip && e->size == size) {
e->last_used = stamp;
if (kcov_shm != NULL)
__atomic_fetch_add(&kcov_shm->cmp_hints_save_reject_dup,
1UL, __ATOMIC_RELAXED);
return false;
}
}
if (count < CMP_HINTS_PER_SYSCALL) {
struct cmp_hint_entry *e = &pool->entries[count];
e->value = val;
e->cmp_ip = cmp_ip;
e->size = size;
/* SHADOW feedback score starts at zero for a freshly inserted
* tuple. Dedup-refresh above keeps the existing score (same
* tuple, same identity); only this fresh-insert and the
* evict-replace below reset. */
e->wins = 0;
e->misses = 0;
e->last_used = stamp;
__atomic_fetch_add(&pool->generation, 1, __ATOMIC_RELAXED);
/*
* RELEASE-store count so a lockless reader in cmp_hints_try_get
* that observes the new count is guaranteed to also see the
* entries[] store above.
*/
__atomic_store_n(&pool->count, count + 1, __ATOMIC_RELEASE);
if (kcov_shm != NULL)
__atomic_fetch_add(&kcov_shm->cmp_hints_unique_inserts,
1UL, __ATOMIC_RELAXED);
return true;
}
victim = 0;
oldest = pool->entries[0].last_used;
for (i = 1; i < CMP_HINTS_PER_SYSCALL; i++) {
if (pool->entries[i].last_used < oldest) {
oldest = pool->entries[i].last_used;
victim = i;
}
}
pool->entries[victim].value = val;
pool->entries[victim].cmp_ip = cmp_ip;
pool->entries[victim].size = size;
/* Evict-replace: zero the SHADOW score so the displaced tuple's
* history does not bleed into the new identity that now lives in
* this slot. */
pool->entries[victim].wins = 0;
pool->entries[victim].misses = 0;
pool->entries[victim].last_used = stamp;
__atomic_fetch_add(&pool->generation, 1, __ATOMIC_RELAXED);
if (kcov_shm != NULL) {
__atomic_fetch_add(&kcov_shm->cmp_hints_unique_inserts, 1UL,
__ATOMIC_RELAXED);
/* Evict-replace pressure: a tuple displaced an older one
* because the per-syscall cap was full. The new entry won
* the slot (counted via cmp_hints_unique_inserts above) and
* the evicted entry is the silent loser; the cap counter
* tracks displacement events so a saturated pool is
* directly visible as cap > unique_inserts_delta over a
* window once the per-syscall pool tops out. */
__atomic_fetch_add(&kcov_shm->cmp_hints_save_reject_cap, 1UL,
__ATOMIC_RELAXED);
}
return true;
}
/*
* Per-child seen-bloom hashes over the (cmp_ip, val, size) tuple. Two
* independent splitmix64-style mixes -- the same shape the cmp_novelty
* bloom in strategy.c uses, kept local so the two hash families are
* free to drift if one turns out to need a different mixing constant
* for the load it actually sees. Indices are masked to
* CMP_HINTS_BLOOM_MASK so the bloom width can change without touching
* the hashes.
*/
static inline uint32_t cmp_hints_bloom_h1(unsigned long ip, unsigned long val,
unsigned int size)
{
uint64_t x = (uint64_t)ip
^ ((uint64_t)val * 0x9e3779b97f4a7c15ULL)
^ ((uint64_t)size << 13);
x ^= x >> 32;
x *= 0xbf58476d1ce4e5b9ULL;
x ^= x >> 27;
return (uint32_t)(x & CMP_HINTS_BLOOM_MASK);
}
static inline uint32_t cmp_hints_bloom_h2(unsigned long ip, unsigned long val,
unsigned int size)
{
uint64_t x = (uint64_t)val
^ ((uint64_t)ip * 0x94d049bb133111ebULL)
^ ((uint64_t)size * 0xff51afd7ed558ccdULL);
x ^= x >> 30;
x *= 0xc4ceb9fe1a85ec53ULL;
x ^= x >> 31;
return (uint32_t)(x & CMP_HINTS_BLOOM_MASK);
}
/*
* Test-and-set both bloom bits for the tuple. Returns true when both
* bits were already set -- the tuple has been seen within the current
* bloom window, so the caller can skip pool_add_locked. A miss on
* either bit returns false AND leaves both bits set, so the next
* encounter with the same tuple hits.
*/
static bool cmp_hints_bloom_check_and_set(struct cmp_hints_bloom *b,
unsigned long ip,
unsigned long val,
unsigned int size)
{
uint32_t i1 = cmp_hints_bloom_h1(ip, val, size);
uint32_t i2 = cmp_hints_bloom_h2(ip, val, size);
uint8_t m1 = (uint8_t)(1U << (i1 & 7));
uint8_t m2 = (uint8_t)(1U << (i2 & 7));
uint8_t *p1 = &b->bits[i1 >> 3];
uint8_t *p2 = &b->bits[i2 >> 3];
bool seen = ((*p1 & m1) != 0) && ((*p2 & m2) != 0);
*p1 |= m1;
*p2 |= m2;
return seen;
}
/*
* Per-call staging buffer for cmp_hints_collect's bloom-miss batch.
* Sized to balance: small enough that the worst-case 3KB stack
* footprint is comfortable in child context, large enough that the
* common case (a hot bloom yielding tens of misses per call) clears
* the loop with a single pool_lock cycle. Bursts that exceed the
* batch fall back to multiple flushes -- correct, just less optimal. */
#define CMP_HINTS_PENDING_BATCH 128
struct cmp_hints_pending {
unsigned long ip;
unsigned long val;
unsigned int size;
};
/*
* Recent-ring insert. Called for every
* pool_add_locked() return-true (fresh insert or evict-replace) under
* the durable pool's lock, so the only writer at any instant is the
* caller already holding pool->lock above; recent_pools[] needs no
* lock of its own. Lockless readers in cmp_hints_try_get_ex tolerate
* a torn (cmp_ip, value, size) triplet the same way the durable pool's
* lockless reader does -- hints are advisory.
*
* Ring write: overwrite the head slot, advance head modulo the cap.
* Count grows up to CMP_RECENT_PER_SYSCALL and then sticks; an insert
* over a populated slot bumps cmp_recent_evicts so the saturation
* point is observable. No dedup deliberately: "recent" semantics aren't
* diluted by collapsing a tuple the kernel saw twice into one slot.
*/
static void cmp_recent_insert(unsigned int nr, bool do32,
unsigned long cmp_ip, unsigned long val,
unsigned int size)
{
struct cmp_recent_pool *rp;
unsigned int head;
unsigned int count;
if (cmp_hints_shm == NULL || nr >= MAX_NR_SYSCALL)
return;
rp = &cmp_hints_shm->recent_pools[nr][do32 ? 1 : 0];
head = rp->head;
if (head >= CMP_RECENT_PER_SYSCALL)
head = 0; /* defensive: a stomp on head would index OOB */
count = rp->count;
if (kcov_shm != NULL) {
__atomic_fetch_add(&kcov_shm->cmp_recent_inserts, 1UL,
__ATOMIC_RELAXED);
if (count >= CMP_RECENT_PER_SYSCALL)
__atomic_fetch_add(&kcov_shm->cmp_recent_evicts, 1UL,
__ATOMIC_RELAXED);
}
rp->entries[head].value = val;
rp->entries[head].cmp_ip = cmp_ip;
rp->entries[head].size = size;
rp->entries[head].pad = 0;
head = (head + 1U) % CMP_RECENT_PER_SYSCALL;
__atomic_store_n(&rp->head, head, __ATOMIC_RELAXED);
if (count < CMP_RECENT_PER_SYSCALL)
__atomic_store_n(&rp->count, count + 1U, __ATOMIC_RELEASE);
}
static unsigned int cmp_hints_flush_pending(struct cmp_hint_pool *pool,
unsigned int nr, bool do32,
const struct cmp_hints_pending *batch,
unsigned int n)
{
unsigned int j;
unsigned int inserted = 0;
if (n == 0)
return 0;
/* Pre-lock fast path for already-latched-corrupted pools. Each
* pool_add_locked() below routes count through cmp_hints_pool_corrupted()
* and bails before any state mutation (including last_used_stamp)
* once the latch is set, so the lock acquire + N-iteration batch
* walk yields zero inserts and zero side effects on a latched pool
* -- pure overhead. The latched-corrupted branch in
* cmp_hints_pool_corrupted() is itself side-effect free (no
* counter bumps, no re-latch, no canary probes) by design, so
* skipping the walk does not lose any per-record accounting that
* the in-walk check would have produced. */
if (__atomic_load_n(&pool->corrupted, __ATOMIC_RELAXED))
return 0;
pool_lock(pool);
for (j = 0; j < n; j++) {
if (pool_add_locked(pool, batch[j].ip, batch[j].val,
batch[j].size)) {
inserted++;
/* Mirror every durable content change into the
* run-local recent ring. Under pool->lock so the
* recent insert has the same single-writer guarantee
* the durable insert has -- the only place that
* writes recent_pools[nr][do32] is this exact path. */
cmp_recent_insert(nr, do32, batch[j].ip, batch[j].val,
batch[j].size);
}
}
pool_unlock(pool);
return inserted;
}
/*
* Field-pool table lookup. splitmix64-style mix over the key tuple so
* (nr, do32, arg_idx, field_idx, size) variations spread evenly across
* buckets and the desc pointer's low bits don't dominate the index. The
* result is masked to CMP_FIELD_POOL_BUCKETS so the bucket count can
* change without touching the hash.
*/
static inline uint32_t cmp_field_pool_hash(const struct struct_desc *desc,
unsigned int nr, unsigned int do32,
unsigned int arg_idx,
unsigned int field_idx,
unsigned int size)
{
uint64_t x = (uint64_t)(uintptr_t) desc;
x ^= ((uint64_t) nr * 0x9e3779b97f4a7c15ULL);
x ^= ((uint64_t) arg_idx << 17);
x ^= ((uint64_t) field_idx * 0xbf58476d1ce4e5b9ULL);
x ^= ((uint64_t) size << 41);
x ^= ((uint64_t) do32 << 53);
x ^= x >> 30;
x *= 0xbf58476d1ce4e5b9ULL;
x ^= x >> 27;
return (uint32_t)(x & (CMP_FIELD_POOL_BUCKETS - 1U));
}
/* Same wild-write gate as cmp_hints_pool_corrupted() but for field pools.
* Independent latch + counter bumps so a stomp on a field pool is not
* folded into the per-syscall pool's corruption rate (the two paths
* write to different parts of cmp_hints_shm and pinpointing which one
* tripped narrows root-causing wild-write reports). */
static bool cmp_field_pool_corrupted(struct cmp_field_pool *pool,
unsigned int observed_count)
{
if (__atomic_load_n(&pool->corrupted, __ATOMIC_RELAXED))
return true;
if (observed_count <= CMP_HINTS_PER_SYSCALL)
return false;
if (kcov_shm != NULL) {
__atomic_fetch_add(&kcov_shm->cmp_hints_count_oob, 1UL,
__ATOMIC_RELAXED);
if (pool->canary_lock_post != CMP_HINTS_POOL_CANARY)
__atomic_fetch_add(&kcov_shm->cmp_hints_canary_lock_post_corrupt,
1UL, __ATOMIC_RELAXED);
if (pool->canary_pre != CMP_HINTS_POOL_CANARY)
__atomic_fetch_add(&kcov_shm->cmp_hints_canary_pre_corrupt,
1UL, __ATOMIC_RELAXED);
if (pool->canary_post != CMP_HINTS_POOL_CANARY)
__atomic_fetch_add(&kcov_shm->cmp_hints_canary_post_corrupt,
1UL, __ATOMIC_RELAXED);
}
__atomic_store_n(&pool->corrupted, true, __ATOMIC_RELAXED);
return true;
}
/* Mirror of pool_add_locked() for the field pool entries[] array. Same
* dedup / LRU-eviction discipline -- caller must hold pool->lock. */
static bool cmp_field_pool_insert_locked(struct cmp_field_pool *pool,
unsigned long cmp_ip,
unsigned long val,
unsigned int size)
{
unsigned int i, count = pool->count;
uint64_t stamp;
unsigned int victim;
uint64_t oldest;
if (cmp_field_pool_corrupted(pool, count))
return false;
stamp = ++pool->last_used_stamp;
for (i = 0; i < count; i++) {
struct cmp_hint_entry *e = &pool->entries[i];
if (e->value == val && e->cmp_ip == cmp_ip && e->size == size) {
e->last_used = stamp;
return false;
}
}
if (count < CMP_HINTS_PER_SYSCALL) {
struct cmp_hint_entry *e = &pool->entries[count];
e->value = val;
e->cmp_ip = cmp_ip;
e->size = size;
/* Field pools inherit the same fresh-insert / evict-replace
* SHADOW-score reset discipline as the per-syscall pool above;
* the score field is recording-only because the score-based
* feedback selection is shadow for both pools and does not
* steer pool selection yet. */
e->wins = 0;
e->misses = 0;
e->last_used = stamp;
__atomic_fetch_add(&pool->generation, 1, __ATOMIC_RELAXED);
__atomic_store_n(&pool->count, count + 1, __ATOMIC_RELEASE);
return true;
}
victim = 0;
oldest = pool->entries[0].last_used;
for (i = 1; i < CMP_HINTS_PER_SYSCALL; i++) {
if (pool->entries[i].last_used < oldest) {
oldest = pool->entries[i].last_used;
victim = i;
}
}
pool->entries[victim].value = val;
pool->entries[victim].cmp_ip = cmp_ip;
pool->entries[victim].size = size;
pool->entries[victim].wins = 0;
pool->entries[victim].misses = 0;
pool->entries[victim].last_used = stamp;
__atomic_fetch_add(&pool->generation, 1, __ATOMIC_RELAXED);
return true;
}
void cmp_hints_field_record(unsigned int nr, bool do32, unsigned int arg_idx,
const struct struct_desc *desc,
unsigned int field_idx, unsigned int size,
unsigned long val, unsigned long cmp_ip)
{
uint32_t h;
unsigned int probe;
unsigned int do32_idx = do32 ? 1U : 0U;
if (cmp_hints_shm == NULL || desc == NULL)
return;
if (nr >= MAX_NR_SYSCALL || arg_idx < 1 || arg_idx > 6)
return;
if (size != 1 && size != 2 && size != 4 && size != 8)
return;
h = cmp_field_pool_hash(desc, nr, do32_idx, arg_idx, field_idx, size);
for (probe = 0; probe < CMP_FIELD_POOL_PROBE_MAX; probe++) {
unsigned int idx = (h + probe) & (CMP_FIELD_POOL_BUCKETS - 1U);
struct cmp_field_pool *pool = &cmp_hints_shm->field_pools[idx];
const struct struct_desc *occ;
bool inserted;
/* ACQUIRE-load the occupancy gate so a non-NULL desc read is
* guaranteed to see the rest of the key the claimer published.
* NULL means empty -- candidate for our claim. */
occ = __atomic_load_n(&pool->key.desc, __ATOMIC_ACQUIRE);
if (occ != NULL && occ != desc)
continue;
lock(&pool->lock);
/* Re-read under lock so a racing claimer can't slip a different
* desc in between our ACQUIRE-load and lock acquire. */
occ = pool->key.desc;
if (occ == NULL) {
/* Claim: fill all key fields, then RELEASE-store desc
* last so a reader that ACQUIRE-loads desc sees a
* fully-populated key. */
pool->key.nr = (uint16_t) nr;
pool->key.do32 = (uint8_t) do32_idx;
pool->key.arg_idx = (uint8_t) arg_idx;
pool->key.field_idx = (uint16_t) field_idx;
pool->key.size = (uint8_t) size;
__atomic_store_n(&pool->key.desc, desc,
__ATOMIC_RELEASE);
} else if (occ != desc ||
pool->key.nr != (uint16_t) nr ||
pool->key.do32 != (uint8_t) do32_idx ||
pool->key.arg_idx != (uint8_t) arg_idx ||
pool->key.field_idx != (uint16_t) field_idx ||
pool->key.size != (uint8_t) size) {
/* Different key at this probe slot; keep walking. */
unlock(&pool->lock);
continue;
}
inserted = cmp_field_pool_insert_locked(pool, cmp_ip, val, size);
unlock(&pool->lock);
if (kcov_shm != NULL) {
__atomic_fetch_add(&kcov_shm->cmp_field_attribution_found,
1UL, __ATOMIC_RELAXED);
(void) inserted; /* dedup-refresh is a hit too */
}
return;
}
/* All probes filled with unrelated keys; advisory pool, drop. */
if (kcov_shm != NULL)
__atomic_fetch_add(&kcov_shm->cmp_field_attribution_pool_full,
1UL, __ATOMIC_RELAXED);
}
void cmp_hints_field_record_self_check(void)
{
/* Synthesise an insert against a sentinel desc pointer that can
* never collide with a real catalog entry (the catalog is an array
* of structs, never the address of the cmp_hints_shm itself), prove
* the counter bumps + the bucket claims, then clear the bucket back
* to empty so the live table starts clean. Runs at every fresh
* trinity startup so a regression in the recording path surfaces
* loudly here rather than hiding behind silent zero counters during
* a fuzz run.
*
* A sentinel address that is non-NULL, canonical-aligned, and
* stable for the lifetime of the process: the address of
* cmp_hints_shm itself. Cast through (const struct struct_desc *)
* for the key field type; we never deref it.
*/
const struct struct_desc *sentinel;
unsigned int idx;
unsigned int probe;
uint32_t h;
unsigned long before, after;
struct cmp_field_pool *claimed = NULL;
if (cmp_hints_shm == NULL || kcov_shm == NULL)
return;
sentinel = (const struct struct_desc *)(uintptr_t) cmp_hints_shm;
before = __atomic_load_n(&kcov_shm->cmp_field_attribution_found,
__ATOMIC_RELAXED);
cmp_hints_field_record(/*nr=*/0, /*do32=*/false, /*arg_idx=*/1,
sentinel, /*field_idx=*/0, /*size=*/8,
/*val=*/0x5a5a5a5a5a5a5a5aULL,
/*cmp_ip=*/0xc0ffee00c0ffee00ULL);
after = __atomic_load_n(&kcov_shm->cmp_field_attribution_found,
__ATOMIC_RELAXED);
if (after != before + 1)
BUG("cmp_hints: field-record self-check counter did not bump");
/* Locate the claimed bucket (linear probe from the same hash) and
* reset it so the live table starts empty. Walk the full probe
* window because a subsequent self-check that re-hashes the same
* sentinel must land on a freshly-empty slot, not the one we just
* filled. */
h = cmp_field_pool_hash(sentinel, 0, 0, 1, 0, 8);
for (probe = 0; probe < CMP_FIELD_POOL_PROBE_MAX; probe++) {
idx = (h + probe) & (CMP_FIELD_POOL_BUCKETS - 1U);
if (cmp_hints_shm->field_pools[idx].key.desc == sentinel) {
claimed = &cmp_hints_shm->field_pools[idx];
break;
}
}
if (claimed == NULL)
BUG("cmp_hints: field-record self-check could not locate claimed bucket");
/* Reset the claimed bucket back to empty. The unreachable() inside
* BUG() above makes the NULL branch terminal, but gcc's fortify
* memset-bounds check on -O2 still complains about deref through a
* possibly-NULL pointer; clear the entries[] in a hand loop so the
* checker sees the bounded indexing directly. */