fix(ssa_gen): Add constraint on slice length before popping#9323
fix(ssa_gen): Add constraint on slice length before popping#9323
Conversation
Changes to Brillig bytecode sizes
🧾 Summary (10% most significant diffs)
Full diff report 👇
|
Changes to number of Brillig opcodes executed
🧾 Summary (10% most significant diffs)
Full diff report 👇
|
|
Nice! I wonder if the constrain could be added to the SSA, before invoking those functions. Maybe that way optimizations could discard some constraints if they are known to hold, etc. |
|
Good question, I haven’t thought about that because it worked already for ACIR (albeit with slightly different message, talking about the array being 1 long but the index -1), and I saw there is an add_overflow_check in Brillig codegen. |
|
To be fair the compiler generated |
|
It could even be handled by the |
Ah, then there must be a reason for that. If we go the SSA route it will likely make things worse for ACIR, so I'd say the fix here is probably good. |
|
Actually there already are some checks inserted, but it says push and pop don't need them: noir/compiler/noirc_evaluator/src/ssa/ssa_gen/mod.rs Lines 1051 to 1083 in d35a767 |
|
And even without those two, getting an index does get SSA constraints: global G_A: [Field] = &[1, 2, 3];
fn main(i: u32) -> pub Field {
G_A[i]
}Perhaps @TomAFrench can tell us why this technique wasn't used for all intrinsic slice operations that can fail based on the size. |
|
This looks to simply be an oversight @aakoshh. ACIR appropriately fails as we check memory block sizes, but for Brillig we need to lay down an explicit check as otherwise it will attempt to look in the invalid memory slot. It would be better to have this check in SSA gen.
We can gate the check by runtime during code gen. |
To be clear, are you in favour of inserting a constraint in I haven't completely figured out how that initial SSA is generated from the integration test, where the simplifications are hidden, but inserting a constraint seems easy enough. |
Yes, as in general I prefer minimizing what code gen we do in Brillig/ACIR gen. When we have the check in SSA gen there is a chance that the check can be simplified out. |
Yeah, test coverage on slices tended to cover just the happy path so this was just omitted because it's not on the happy path and then we moved on to other features. |
|
Dumb question: This code: global G_A: [Field] = &[-282608142060723387702497189224219065418];
fn main() -> pub Field {
let (a, _) = G_A.pop_back();
let (_, f) = a.pop_back();
f
}generates this initial SSA: Why does it look like (Actually I have no idea what |
That subtraction comes from the The first |
|
Thanks. I thought it might be that, but didn't understand why we would do that in SSA when |
There was a problem hiding this comment.
⚠️ Performance Alert ⚠️
Possible performance regression was detected for benchmark 'Test Suite Duration'.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 1.20.
| Benchmark suite | Current: 3cc298c | Previous: e7fb7f3 | Ratio |
|---|---|---|---|
test_report_AztecProtocol_aztec-packages_noir-projects_noir-protocol-circuits_crates_private-kernel-lib |
2 s |
1 s |
2 |
This comment was automatically generated by workflow using github-action-benchmark.
CC: @TomAFrench
|
I did both. Making the decrement checked caused a panic with the call stack, which is fixed now. It's a bit unfortunate that the decrement check says "attempt to subtract with overflow", which masks the original "Index out of bounds" error. I was wondering if we need both checks:
|
If we have the intrinsic call check, which precedes the pop_back call itself, then we would prevent that overflow from occurring.
Good point. I say we just go with the intrinsic call check. |
|
Ah wait, something's not right, now it fails even for the first pop 👀 |
I meant if we chained them together, something like this: length1, slice1, elem1 = call slice_pop_back(length0, slice0)
length2, slice2, elem2 = call slice_pop_back(length1, slice1)turned into: length1 = unchecked_sub length0, 1 // what if length1 becomes u32::MAX here
constrain 0 < length1, "Index out of bounds" // and passes this constraint
length2, slice2, elem2 = call slice_pop_back(length1, slice0) // then gives invalid result |
|
So now this: global G_A: [Field] = &[-282608142060723387702497189224219065418];
fn main() -> pub Field {
let (a, f) = G_A.pop_back();
f
}generates: g0 = Field -282608142060723387702497189224219065418
g1 = u32 1
g2 = make_array [Field -282608142060723387702497189224219065418] : [Field]
brillig(inline) fn main f0 {
b0():
inc_rc g2
v4 = sub u32 0, u32 1 // src/main.nr:3:18
inc_rc g2 // src/main.nr:3:18
return Field -282608142060723387702497189224219065418
}and fails on the |
|
Maybe it's Got it, there was an extra decrement after all the type-elements were popped, that's where the |
I came to agree with this: the |
Automated pull of nightly from the [noir](https://github.com/noir-lang/noir) programming language, a dependency of Aztec. BEGIN_COMMIT_OVERRIDE fix: forbid self-referencing type aliases (noir-lang/noir#9103) chore: add a mem2reg test for when all references need to be invalidated (noir-lang/noir#9377) fix(ssa): Do not check ArrayGet/Set as unreachable for Brillig (noir-lang/noir#9376) chore: use SSA parser in all mem2reg tests (noir-lang/noir#9372) fix: trait where clause check fixes (noir-lang/noir#9369) fix: Correct doc comments for SSA passes (noir-lang/noir#9371) fix: prevent `SignedField::from(i128::MIN)` from crashing (noir-lang/noir#9366) fix: allow constants in the type-system to be negative (noir-lang/noir#9360) feat: show circuit output as a value of the program's return type (noir-lang/noir#9364) feat: add `FunctionDefinition::visibility` (noir-lang/noir#9363) chore(docs): Add example for `$crate` in docs (noir-lang/noir#9361) fix: Prevent accidental tuple sharing in comptime code (noir-lang/noir#9313) fix: perserve purities after SSA normalization (noir-lang/noir#9355) fix: modulo overflow in comptime (noir-lang/noir#9348) fix: handle short-syntax for trait constraints on trait generics (noir-lang/noir#9167) chore: enhance trait constraint comment (noir-lang/noir#9358) fix: replace implicitly added named generics with fresh type vars in check_trait_impl_where_clause_matches_trait_where_clause (noir-lang/noir#9352) fix: push definition trait constraints after trait item constraint (noir-lang/noir#9354) chore(ci): Update status of noir_json_parser (noir-lang/noir#9351) fix(ssa): Keep reference count increments for array set values (noir-lang/noir#9344) chore: remove unused `compile_workspace` (noir-lang/noir#9353) chore: try printing byte arrays as strings in the SSA interpreter (noir-lang/noir#9346) feat(lsp): allow opening noir stdlib files (noir-lang/noir#9339) fix: do u128 operations with u128, not i128 (noir-lang/noir#9345) chore(acir): ACIR parser error handling for blackbox inputs/outputs (noir-lang/noir#9342) fix: prevent invalid types in test/fuzz functions (noir-lang/noir#9343) chore(lsp): avoid redundant type checking (noir-lang/noir#9337) feat(acir): Parse ACIR memory and call opcodes (noir-lang/noir#9331) fix(ssa_gen): Add constraint on slice length before popping (noir-lang/noir#9323) chore: impl for u16 conversions (noir-lang/noir#9314) fix: substitute bindings in type before canonicalization (noir-lang/noir#9328) fix(ssa_interpreter): `push_back` and `pop_back` to slices with padding (noir-lang/noir#9320) fix: wildcard type should be allowed in lambda parameter types (noir-lang/noir#9325) chore: graceful handling of SIGPIPE (noir-lang/noir#9075) feat: return unsolvable opcode from `CircuitSimulator` (noir-lang/noir#8943) fix: allow nested fmtstr (noir-lang/noir#9309) feat: Initial ACIR parser (arithmetic exprs and black box functions) (noir-lang/noir#9316) fix(mem2reg): Register aliases when the `IfElse` result in a reference (noir-lang/noir#9305) fix: Make Ssa-gen use existing reference when compiling `&mut foo.bar.baz` (noir-lang/noir#9307) fix: top-level item in dependency isn't always visible (noir-lang/noir#9295) fix(ssa-interpreter): Return error if slice length is 0 during popping (noir-lang/noir#9308) chore: Release Noir(1.0.0-beta.9) (noir-lang/noir#9184) chore(LSP): simplify code lens request handling (noir-lang/noir#9279) chore: add regression tests for #6383 (noir-lang/noir#9302) fix: disallow `_` in signatures and struct members (noir-lang/noir#9301) fix: check associated types after validating where clause when looking up trait impls, plus some unification fixes (noir-lang/noir#9265) chore: Add fmtstr to coercions list (noir-lang/noir#9300) chore: Add a helper function `fmtstr::as_quoted_str` (noir-lang/noir#9293) chore(docs): Copy Type Coercions docs into v1.0.0-beta.8 versioned docs (noir-lang/noir#9298) feat: only inject "out of bounds" checks in brillig (noir-lang/noir#9200) END_COMMIT_OVERRIDE --------- Co-authored-by: AztecBot <tech@aztecprotocol.com> Co-authored-by: Maxim Vezenov <mvezenov@gmail.com>
Description
Problem*
Resolves #9266 and #9269
Summary*
constrain 1 <= slice_lengthin Brillig before calls toslice_pop_frontandslice_pop_back.Changesdecrement_slice_lengthin thesimplifymodule to use checkedsubin Brillig.simplify_slice_pop_backto stop decrementing the length beyond the number of elementsexecution_failuretests with both default and--force-brillig. In a follow-up PR we could potentially run these tests with the SSA interpreter as well.Additional Context
With this the integration test correctly fails:
Documentation*
Check one:
PR Checklist*
cargo fmton default settings.