Skip to content

Commit b80365b

Browse files
committed
perf(waf): bypass inactive request phases
1 parent a55ad44 commit b80365b

11 files changed

Lines changed: 138 additions & 43 deletions

File tree

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,8 @@ Defaults:
103103
- 5 measured batches;
104104
- 2 requests per batch;
105105
- 10 measured requests in total.
106-
- 2 consecutive unchanged JIT warmup requests;
107-
- at most 32 JIT warmup requests.
106+
- 12 consecutive unchanged JIT warmup requests;
107+
- at most 128 JIT warmup requests.
108108

109109
The output reports each mode's average latency, minimum/maximum batch averages, JIT compilation state, incremental WAF latency, and the JIT-to-interpreter ratio. Counts can be overridden without editing the test:
110110

@@ -119,6 +119,8 @@ cargo test --release --test perf -- --ignored --nocapture
119119

120120
Compilation latency is excluded. Both cases rebuild the simulated request context inside RSS for every measured request.
121121

122+
Set `WAF_PERF_AOT_DIAG=1` to report AOT compile latency, assert that the oversized ruleset selects `interpreter-boundary` lowering, and run interleaved AOT/JIT batches. The paired check fails when AOT exceeds JIT latency by more than 5%.
123+
122124
## Tests
123125

124126
Run the VM and real HTTP end-to-end tests:

rules/engine_bundle.rss

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -113,14 +113,14 @@ pub fn one(state: map<string>, base: string, name: string, text: [string], targe
113113
}
114114

115115
pub fn sqli_category_prefilter(state: map<string>) -> bool {
116-
let text: string = string_lower_ascii(
117-
ctx_get(&state, "query") + "\n" +
118-
ctx_get(&state, "request_body") + "\n" +
119-
ctx_get(&state, "args") + "\n" +
120-
ctx_get(&state, "cookies") + "\n" +
121-
ctx_get(&state, "request_headers")
122-
);
123-
re::match("(?i)(['\"`%\\[\\]{}|!^~*<>#]|\\b(?:select|union|insert|update|delete|drop|alter|create|truncate|replace|merge|from|where|having|group|order|limit|offset|join|into|values|set|call|exec|execute|declare|cast|convert|coalesce|case|when|then|else|end|sleep|benchmark|waitfor|delay|load_file|outfile|dumpfile|information_schema|mysql|sqlite|xor|regexp|rlike)\\b|0x[0-9a-f]|--|/\\*)", &text)
116+
re::match(
117+
"(?i)(['\"`%\\[\\]{}|!^~*<>#]|\\b(?:select|union|insert|update|delete|drop|alter|create|truncate|replace|merge|from|where|having|group|order|limit|offset|join|into|values|set|call|exec|execute|declare|cast|convert|coalesce|case|when|then|else|end|sleep|benchmark|waitfor|delay|load_file|outfile|dumpfile|information_schema|mysql|sqlite|xor|regexp|rlike)\\b|0x[0-9a-f]|--|/\\*)",
118+
&(ctx_get(&state, "query") + "\n" +
119+
ctx_get(&state, "request_body") + "\n" +
120+
ctx_get(&state, "args") + "\n" +
121+
ctx_get(&state, "cookies") + "\n" +
122+
ctx_get(&state, "request_headers"))
123+
)
124124
}
125125

126126
pub fn ctx_targets(state: map<string>, text: [string], target_count: int, id: int) -> [string] {

rules/engine_context.rss

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,14 @@ pub fn one(state: map<string>, base: string, name: string, text: [string], targe
5858
}
5959

6060
pub fn sqli_category_prefilter(state: map<string>) -> bool {
61-
let text: string = engine_text::lower(
62-
ctx_get(&state, "query") + "\n" +
63-
ctx_get(&state, "request_body") + "\n" +
64-
ctx_get(&state, "args") + "\n" +
65-
ctx_get(&state, "cookies") + "\n" +
66-
ctx_get(&state, "request_headers")
67-
);
68-
re::match("(?i)(['\"`%\\[\\]{}|!^~*<>#]|\\b(?:select|union|insert|update|delete|drop|alter|create|truncate|replace|merge|from|where|having|group|order|limit|offset|join|into|values|set|call|exec|execute|declare|cast|convert|coalesce|case|when|then|else|end|sleep|benchmark|waitfor|delay|load_file|outfile|dumpfile|information_schema|mysql|sqlite|xor|regexp|rlike)\\b|0x[0-9a-f]|--|/\\*)", &text)
61+
re::match(
62+
"(?i)(['\"`%\\[\\]{}|!^~*<>#]|\\b(?:select|union|insert|update|delete|drop|alter|create|truncate|replace|merge|from|where|having|group|order|limit|offset|join|into|values|set|call|exec|execute|declare|cast|convert|coalesce|case|when|then|else|end|sleep|benchmark|waitfor|delay|load_file|outfile|dumpfile|information_schema|mysql|sqlite|xor|regexp|rlike)\\b|0x[0-9a-f]|--|/\\*)",
63+
&(ctx_get(&state, "query") + "\n" +
64+
ctx_get(&state, "request_body") + "\n" +
65+
ctx_get(&state, "args") + "\n" +
66+
ctx_get(&state, "cookies") + "\n" +
67+
ctx_get(&state, "request_headers"))
68+
)
6969
}
7070

7171
pub fn ctx_targets(state: map<string>, text: [string], target_count: int, id: int) -> [string] {

rules/pd_edge_waf.rss

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -115,14 +115,14 @@ pub fn one(state: map<string>, base: string, name: string, text: [string], targe
115115
}
116116

117117
pub fn sqli_category_prefilter(state: map<string>) -> bool {
118-
let text: string = string_lower_ascii(
119-
ctx_get(&state, "query") + "\n" +
120-
ctx_get(&state, "request_body") + "\n" +
121-
ctx_get(&state, "args") + "\n" +
122-
ctx_get(&state, "cookies") + "\n" +
123-
ctx_get(&state, "request_headers")
124-
);
125-
re::match("(?i)(['\"`%\\[\\]{}|!^~*<>#]|\\b(?:select|union|insert|update|delete|drop|alter|create|truncate|replace|merge|from|where|having|group|order|limit|offset|join|into|values|set|call|exec|execute|declare|cast|convert|coalesce|case|when|then|else|end|sleep|benchmark|waitfor|delay|load_file|outfile|dumpfile|information_schema|mysql|sqlite|xor|regexp|rlike)\\b|0x[0-9a-f]|--|/\\*)", &text)
118+
re::match(
119+
"(?i)(['\"`%\\[\\]{}|!^~*<>#]|\\b(?:select|union|insert|update|delete|drop|alter|create|truncate|replace|merge|from|where|having|group|order|limit|offset|join|into|values|set|call|exec|execute|declare|cast|convert|coalesce|case|when|then|else|end|sleep|benchmark|waitfor|delay|load_file|outfile|dumpfile|information_schema|mysql|sqlite|xor|regexp|rlike)\\b|0x[0-9a-f]|--|/\\*)",
120+
&(ctx_get(&state, "query") + "\n" +
121+
ctx_get(&state, "request_body") + "\n" +
122+
ctx_get(&state, "args") + "\n" +
123+
ctx_get(&state, "cookies") + "\n" +
124+
ctx_get(&state, "request_headers"))
125+
)
126126
}
127127

128128
pub fn ctx_targets(state: map<string>, text: [string], target_count: int, id: int) -> [string] {
@@ -600,13 +600,15 @@ fn evaluate_request_942_application_attack_sqli_phase_2(next: map<string>) -> ma
600600
}
601601

602602
pub fn inspect_request(next: map<string>) -> map<string> {
603+
if !(category_enabled(&next, "request_911_method_enforcement") && ctx_get(&next, "tx.allowed_methods") != "" && !string_contains(" " + ctx_get(&next, "tx.allowed_methods") + " ", " " + ctx_get(&next, "method") + " ") || category_enabled(&next, "request_942_application_attack_sqli") && sqli_category_prefilter(&next)) => { ctx_set_phase(next, 2) } else => {
603604
next = ctx_set_phase(next, 1);
604605
if category_enabled(&next, "request_911_method_enforcement") && ctx_get(&next, "tx.allowed_methods") != "" && !string_contains(" " + ctx_get(&next, "tx.allowed_methods") + " ", " " + ctx_get(&next, "method") + " ") { next = evaluate_request_911_method_enforcement_phase_1(next); }
605606
if category_enabled(&next, "request_942_application_attack_sqli") && sqli_category_prefilter(&next) { next = evaluate_request_942_application_attack_sqli_phase_1(next); }
606607
next = ctx_set_phase(next, 2);
607608
if category_enabled(&next, "request_911_method_enforcement") && ctx_get(&next, "tx.allowed_methods") != "" && !string_contains(" " + ctx_get(&next, "tx.allowed_methods") + " ", " " + ctx_get(&next, "method") + " ") { next = evaluate_request_911_method_enforcement_phase_2(next); }
608609
if category_enabled(&next, "request_942_application_attack_sqli") && sqli_category_prefilter(&next) { next = evaluate_request_942_application_attack_sqli_phase_2(next); }
609610
next
611+
}
610612
}
611613

612614
pub fn inspect_response(next: map<string>) -> map<string> {

rules/ruleset.rss

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,13 +146,15 @@ fn evaluate_request_942_application_attack_sqli_phase_2(next: map<string>) -> ma
146146
}
147147

148148
pub fn inspect_request(next: map<string>) -> map<string> {
149+
if !(engine_bundle::category_enabled(&next, "request_911_method_enforcement") && engine_bundle::ctx_get(&next, "tx.allowed_methods") != "" && !string_contains(" " + engine_bundle::ctx_get(&next, "tx.allowed_methods") + " ", " " + engine_bundle::ctx_get(&next, "method") + " ") || engine_bundle::category_enabled(&next, "request_942_application_attack_sqli") && engine_bundle::sqli_category_prefilter(&next)) => { engine_bundle::ctx_set_phase(next, 2) } else => {
149150
next = engine_bundle::ctx_set_phase(next, 1);
150151
if engine_bundle::category_enabled(&next, "request_911_method_enforcement") && engine_bundle::ctx_get(&next, "tx.allowed_methods") != "" && !string_contains(" " + engine_bundle::ctx_get(&next, "tx.allowed_methods") + " ", " " + engine_bundle::ctx_get(&next, "method") + " ") { next = evaluate_request_911_method_enforcement_phase_1(next); }
151152
if engine_bundle::category_enabled(&next, "request_942_application_attack_sqli") && engine_bundle::sqli_category_prefilter(&next) { next = evaluate_request_942_application_attack_sqli_phase_1(next); }
152153
next = engine_bundle::ctx_set_phase(next, 2);
153154
if engine_bundle::category_enabled(&next, "request_911_method_enforcement") && engine_bundle::ctx_get(&next, "tx.allowed_methods") != "" && !string_contains(" " + engine_bundle::ctx_get(&next, "tx.allowed_methods") + " ", " " + engine_bundle::ctx_get(&next, "method") + " ") { next = evaluate_request_911_method_enforcement_phase_2(next); }
154155
if engine_bundle::category_enabled(&next, "request_942_application_attack_sqli") && engine_bundle::sqli_category_prefilter(&next) { next = evaluate_request_942_application_attack_sqli_phase_2(next); }
155156
next
157+
}
156158
}
157159

158160
pub fn inspect_response(next: map<string>) -> map<string> {

rules/ruleset_bundle.rss

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -113,14 +113,14 @@ pub fn one(state: map<string>, base: string, name: string, text: [string], targe
113113
}
114114

115115
pub fn sqli_category_prefilter(state: map<string>) -> bool {
116-
let text: string = string_lower_ascii(
117-
ctx_get(&state, "query") + "\n" +
118-
ctx_get(&state, "request_body") + "\n" +
119-
ctx_get(&state, "args") + "\n" +
120-
ctx_get(&state, "cookies") + "\n" +
121-
ctx_get(&state, "request_headers")
122-
);
123-
re::match("(?i)(['\"`%\\[\\]{}|!^~*<>#]|\\b(?:select|union|insert|update|delete|drop|alter|create|truncate|replace|merge|from|where|having|group|order|limit|offset|join|into|values|set|call|exec|execute|declare|cast|convert|coalesce|case|when|then|else|end|sleep|benchmark|waitfor|delay|load_file|outfile|dumpfile|information_schema|mysql|sqlite|xor|regexp|rlike)\\b|0x[0-9a-f]|--|/\\*)", &text)
116+
re::match(
117+
"(?i)(['\"`%\\[\\]{}|!^~*<>#]|\\b(?:select|union|insert|update|delete|drop|alter|create|truncate|replace|merge|from|where|having|group|order|limit|offset|join|into|values|set|call|exec|execute|declare|cast|convert|coalesce|case|when|then|else|end|sleep|benchmark|waitfor|delay|load_file|outfile|dumpfile|information_schema|mysql|sqlite|xor|regexp|rlike)\\b|0x[0-9a-f]|--|/\\*)",
118+
&(ctx_get(&state, "query") + "\n" +
119+
ctx_get(&state, "request_body") + "\n" +
120+
ctx_get(&state, "args") + "\n" +
121+
ctx_get(&state, "cookies") + "\n" +
122+
ctx_get(&state, "request_headers"))
123+
)
124124
}
125125

126126
pub fn ctx_targets(state: map<string>, text: [string], target_count: int, id: int) -> [string] {
@@ -598,13 +598,15 @@ fn evaluate_request_942_application_attack_sqli_phase_2(next: map<string>) -> ma
598598
}
599599

600600
pub fn inspect_request(next: map<string>) -> map<string> {
601+
if !(category_enabled(&next, "request_911_method_enforcement") && ctx_get(&next, "tx.allowed_methods") != "" && !string_contains(" " + ctx_get(&next, "tx.allowed_methods") + " ", " " + ctx_get(&next, "method") + " ") || category_enabled(&next, "request_942_application_attack_sqli") && sqli_category_prefilter(&next)) => { ctx_set_phase(next, 2) } else => {
601602
next = ctx_set_phase(next, 1);
602603
if category_enabled(&next, "request_911_method_enforcement") && ctx_get(&next, "tx.allowed_methods") != "" && !string_contains(" " + ctx_get(&next, "tx.allowed_methods") + " ", " " + ctx_get(&next, "method") + " ") { next = evaluate_request_911_method_enforcement_phase_1(next); }
603604
if category_enabled(&next, "request_942_application_attack_sqli") && sqli_category_prefilter(&next) { next = evaluate_request_942_application_attack_sqli_phase_1(next); }
604605
next = ctx_set_phase(next, 2);
605606
if category_enabled(&next, "request_911_method_enforcement") && ctx_get(&next, "tx.allowed_methods") != "" && !string_contains(" " + ctx_get(&next, "tx.allowed_methods") + " ", " " + ctx_get(&next, "method") + " ") { next = evaluate_request_911_method_enforcement_phase_2(next); }
606607
if category_enabled(&next, "request_942_application_attack_sqli") && sqli_category_prefilter(&next) { next = evaluate_request_942_application_attack_sqli_phase_2(next); }
607608
next
609+
}
608610
}
609611

610612
pub fn inspect_response(next: map<string>) -> map<string> {

tests/e2e.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,16 @@ async fn pd_edge_entrypoint_forwards_benign_and_blocks_attacks() {
4040
compiled.program.local_count <= 256,
4141
"entrypoint must fit the standard VM local-slot format"
4242
);
43+
assert!(
44+
compiled
45+
.program
46+
.root_callable_bindings
47+
.iter()
48+
.all(|binding| (binding.local_slot as usize) < compiled.program.local_count),
49+
"entrypoint root callable binding exceeds local frame: local_count={}, bindings={:?}",
50+
compiled.program.local_count,
51+
compiled.program.root_callable_bindings,
52+
);
4353
let program = encode_program(&compiled.program).expect("WAF bytecode should encode");
4454

4555
let upstream_hits = Arc::new(AtomicUsize::new(0));

tests/perf.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,12 @@ fn run_default_ruleset_perf() {
605605
println!("waf_call {line}");
606606
}
607607
}
608+
println!(
609+
"waf_program_shape local_count={} code_bytes={} constants={}",
610+
default_ruleset_program.local_count,
611+
default_ruleset_program.code.len(),
612+
default_ruleset_program.constants.len(),
613+
);
608614
let expected = Value::string("allow");
609615

610616
let baseline = measure_case(
@@ -670,9 +676,20 @@ fn run_default_ruleset_perf() {
670676
..JitConfig::default()
671677
},
672678
);
679+
let aot_compile_started = Instant::now();
673680
aot_vm
674681
.compile_aot()
675682
.expect("WAF AOT diagnostic should compile");
683+
let aot_compile_elapsed = aot_compile_started.elapsed();
684+
let aot_info = aot_vm.dump_aot_info();
685+
assert!(
686+
aot_info.contains("lowering=interpreter-boundary"),
687+
"oversized WAF AOT should select the boundary lowering: {aot_info}"
688+
);
689+
println!(
690+
"waf_aot_compile elapsed_ms={:.3} lowering=interpreter-boundary",
691+
aot_compile_elapsed.as_secs_f64() * 1_000.0,
692+
);
676693
let aot = measure_case(
677694
"default_ruleset_aot_diag",
678695
aot_vm,

tests/smoke.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,35 @@ assert(sqli_category_prefilter(&attack));
213213
assert_eq!(vm.stack().last(), Some(&vm::Value::string("ok")));
214214
}
215215

216+
#[test]
217+
fn benign_request_fast_path_preserves_final_request_state() {
218+
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
219+
let ruleset = std::fs::read_to_string(root.join("rules/ruleset_bundle.rss"))
220+
.expect("ruleset bundle should be readable");
221+
let source = format!(
222+
r#"{ruleset}
223+
assert((&inspect_request(new_state(
224+
"GET",
225+
"/products",
226+
"category=books&page=2",
227+
"HTTP/1.1",
228+
"192.0.2.10",
229+
{{ "host": "shop.example.test" }},
230+
{{ "category": "books", "page": "2" }},
231+
""
232+
)))["phase"] == "2");
233+
"ok";
234+
"#
235+
);
236+
let compiled = vm::compile_source(&source).expect("benign fast-path fixture should compile");
237+
let mut vm = vm::Vm::new(compiled.program);
238+
assert_eq!(
239+
vm.run().expect("benign fast-path fixture should run"),
240+
vm::VmStatus::Halted
241+
);
242+
assert_eq!(vm.stack().last(), Some(&vm::Value::string("ok")));
243+
}
244+
216245
#[test]
217246
fn enabled_ruleset_folds_common_exception_updates_into_rule_payloads() {
218247
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));

tools/convert_crs.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -650,11 +650,8 @@ def render_entry(
650650
lines.append(f" {call}")
651651
lines.extend([" next", "}", ""])
652652

653-
def append_enabled_call(source: str, phase: int) -> None:
653+
def category_condition(source: str) -> str:
654654
category = module_name(source)
655-
evaluator = evaluators.get((source, phase))
656-
if evaluator is None:
657-
return
658655
condition = f'engine_bundle::category_enabled(&next, "{category}")'
659656
if category == "request_911_method_enforcement":
660657
condition += (
@@ -665,6 +662,13 @@ def append_enabled_call(source: str, phase: int) -> None:
665662
)
666663
elif category == "request_942_application_attack_sqli":
667664
condition += " && engine_bundle::sqli_category_prefilter(&next)"
665+
return condition
666+
667+
def append_enabled_call(source: str, phase: int) -> None:
668+
evaluator = evaluators.get((source, phase))
669+
if evaluator is None:
670+
return
671+
condition = category_condition(source)
668672
lines.append(f" if {condition} {{ next = {evaluator}(next); }}")
669673

670674
lines.extend(
@@ -673,12 +677,27 @@ def append_enabled_call(source: str, phase: int) -> None:
673677
]
674678
)
675679

680+
request_conditions = [
681+
category_condition(source)
682+
for source in grouped
683+
if source.startswith("REQUEST-")
684+
and any((source, phase) in evaluators for phase in (1, 2))
685+
]
686+
if request_conditions:
687+
lines.append(
688+
" if !(" + " || ".join(request_conditions)
689+
+ ") => { engine_bundle::ctx_set_phase(next, 2) } else => {"
690+
)
691+
676692
for phase in (1, 2):
677693
lines.append(f" next = engine_bundle::ctx_set_phase(next, {phase});")
678694
for source in grouped:
679695
if source.startswith("REQUEST-"):
680696
append_enabled_call(source, phase)
681-
lines.extend([" next", "}", ""])
697+
lines.append(" next")
698+
if request_conditions:
699+
lines.append(" }")
700+
lines.extend(["}", ""])
682701

683702
lines.extend(
684703
[

0 commit comments

Comments
 (0)