Repository navigation
Expand file tree
/
Copy pathrt_alloc_scan.awk
More file actions
214 lines (200 loc) · 6.41 KB
/
Copy pathrt_alloc_scan.awk
File metadata and controls
214 lines (200 loc) · 6.41 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
# SPDX-License-Identifier: GPL-3.0-or-later
# Copyright (c) 2026 Fábio Henrique de Lima Silva (fhl.bsb@gmail.com) All rights reserved.
#
# rt_alloc_scan.awk — fail-closed static scanner for RT-path allocations.
#
# Usage: awk -f utils/lib/rt_alloc_scan.awk <file.rs>...
#
# Flags any RT-hostile allocation pattern found on the real-time processing
# path of NAM-Plug's `src/clap/processor/`, excluding the documented off-RT
# sites:
# - test-only modules (`#[cfg(test)]` blocks); sibling `*_test.rs` files are
# excluded by the caller (repo convention: test modules included via
# `#[cfg(test)] #[path = "..."] mod`).
# - the off-RT lifecycle/helper functions in `processor/mod.rs`
# (`buffer_prealloc_error`, `panic_to_error`, `activate`, `deactivate`,
# `build_cab_sim_from_raw_samples`). `activate` is the documented ONLY
# allocation site (main thread, CLAP lifecycle); the panic helper only
# runs during exceptional unwinding (its `Box::leak` is the sole
# sanctioned leak).
# - `DryDelayLine::new` in `src/clap/processor/dsp/dry_delay.rs`: the
# `#[cold]` constructor that pre-allocates the two engine `DelayLine<f32>`
# rings. It is called only from `activate()`, i.e. the plugin-side
# continuation of the documented off-RT allocation site.
# - `Arc::clone` is a refcount bump, never an allocation — not flagged.
#
# Exit status: 0 when the scanned code is clean, 1 when any pattern matches
# (fail-closed). This is a structural guard that complements the dynamic
# heap-audit CI lane (`--features heap-audit`); it is intentionally
# conservative: any new heap-allocation API on the audio thread must be
# added to the pattern list below.
BEGIN {
# RT-hostile allocation patterns (POSIX ERE). Keep sorted by severity;
# only the first match per line is reported.
patterns[1] = "Box::new"
patterns[2] = "Box::leak"
patterns[3] = "Box::into_raw"
patterns[4] = "into_boxed_(str|slice)"
patterns[5] = "Vec::new"
patterns[6] = "Vec::with_capacity"
patterns[7] = "vec!"
patterns[8] = "with_capacity"
patterns[9] = "AlignedVec::new"
patterns[10] = "format!"
patterns[11] = "String::new"
patterns[12] = "to_string\\("
patterns[13] = "to_owned\\("
patterns[14] = "Arc::new"
patterns[15] = "Rc::new"
patterns[16] = "HashMap"
patterns[17] = "BTreeMap"
patterns[18] = "HashSet"
patterns[19] = "BTreeSet"
patterns[20] = "VecDeque"
patterns[21] = "collect\\(\\)"
patterns[22] = "Box<dyn"
N = 22
matches = ""
}
# Returns `line` with string literals, line comments and block comments
# replaced by blanks so brace counting is not corrupted by `{`/`}` inside
# format strings, doc comments, etc.
function stripped(line, out, i, n, c, in_str, in_line_cm, in_block_cm) {
out = ""
n = length(line)
in_str = 0
in_line_cm = 0
in_block_cm = 0
for (i = 1; i <= n; i++) {
c = substr(line, i, 1)
if (in_block_cm) {
if (c == "*" && substr(line, i + 1, 1) == "/") {
in_block_cm = 0
i++
}
out = out " "
continue
}
if (in_line_cm) {
out = out " "
continue
}
if (in_str) {
if (c == "\\") {
i++
out = out " "
continue
}
if (c == "\"") in_str = 0
out = out " "
continue
}
if (c == "\"") {
in_str = 1
out = out " "
continue
}
if (c == "/" && substr(line, i + 1, 1) == "/") {
in_line_cm = 1
out = out " "
continue
}
if (c == "/" && substr(line, i + 1, 1) == "*") {
in_block_cm = 1
i++
out = out " "
continue
}
out = out c
}
return out
}
# Counts `{`/`}` of a stripped line into the global `region_depth`, marking
# `seen_open` once the region's opening brace has been observed.
function depth_count(s, i, n, c) {
n = length(s)
for (i = 1; i <= n; i++) {
c = substr(s, i, 1)
if (c == "{") {
region_depth++
seen_open = 1
} else if (c == "}" && seen_open) {
region_depth--
}
}
}
function begin_region() {
skipping = 1
region_depth = 0
seen_open = 0
}
# Closes a skip region once its opening brace has been seen and the depth
# returns to zero.
function maybe_close_region() {
if (seen_open && region_depth <= 0) skipping = 0
}
function scan(line, i) {
for (i = 1; i <= N; i++) {
if (line ~ patterns[i]) {
matches = matches sprintf("%s:%d: %s\n %s\n", FILENAME, FNR, patterns[i], line)
return 1
}
}
return 0
}
FNR == 1 {
skipping = 0
pending_test = 0
region_depth = 0
seen_open = 0
}
{
s = stripped($0)
# --- #[cfg(test)] module blocks: skip until the closing brace ---
if (skipping == 0 && pending_test == 1) {
if ($0 ~ /^[ \t]*mod[ \t]+[A-Za-z_][A-Za-z0-9_]*[ \t]*(\{|[ \t]*$)/) {
begin_region()
depth_count(s)
maybe_close_region()
next
}
pending_test = 0
}
if (skipping == 0 && $0 ~ /^[ \t]*#\[cfg\(test\)\]/) {
pending_test = 1
next
}
# --- Whitelisted off-RT functions in processor/mod.rs ---
if (skipping == 0 &&
$0 ~ /^[ \t]*(pub(\(crate\)|\(super\))? )?fn (buffer_prealloc_error|panic_to_error|activate|deactivate|build_cab_sim_from_raw_samples)[ \t]*\(/) {
begin_region()
depth_count(s)
maybe_close_region()
next
}
# --- Whitelisted off-RT constructor in `dsp/dry_delay.rs` ---
# `DryDelayLine::new` composes the two engine delay-line rings; it is
# `#[cold]` and called only from `activate()`, so its
# `DelayLine::with_capacity` calls are off-RT by contract.
if (skipping == 0 && FILENAME ~ /dsp\/dry_delay\.rs$/ &&
$0 ~ /^[ \t]*(pub(\(crate\)|\(super\))? )?fn new[ \t]*\(/) {
begin_region()
depth_count(s)
maybe_close_region()
next
}
# --- Inside a skip region: only track braces ---
if (skipping == 1) {
depth_count(s)
maybe_close_region()
next
}
# --- Scan-eligible line ---
scan($0)
}
END {
if (matches != "") {
printf "%s", matches
exit 1
}
}