{"id":"CVE-2026-41197","aliases":["EUVD-2026-25156","GHSA-jj7c-x25r-r8r3"],"status":"published","title":"Brillig: Heap corruption in foreign call results with nested tuple arrays","description":"Noir is a Domain Specific Language for SNARK proving systems that is designed to use any ACIR compatible proving system, and Brillig is the bytecode ACIR uses for non-determinism. Noir programs can invoke external functions through foreign calls. When compiling to Brillig bytecode, the SSA instructions are processed block-by-block in `BrilligBlock::compile_block()`. When the compiler encounters an `Instruction::Call` with a `Value::ForeignFunction` target, it invokes `codegen_call()` in `brillig_call/code_gen_call.rs`, which dispatches to `convert_ssa_foreign_call()`. Before emitting the foreign call opcode, the compiler must pre-allocate memory for any array results the call will return. This happens through `allocate_external_call_results()`, which iterates over the result types. For `Type::Array` results, it delegates to `allocate_foreign_call_result_array()` to recursively allocate memory on the heap for nested arrays. The `BrilligArray` struct is the internal representation of a Noir array in Brillig IR. Its `size` field represents the semi-flattened size, the total number of memory slots the array occupies, accounting for the fact that composite types like tuples consume multiple slots per element. This size is computed by `compute_array_length()` in `brillig_block_variables.rs`. For the outer array, `allocate_external_call_results()` correctly uses `define_variable()`, which internally calls `allocate_value_with_type()`. This function applies the formula above, producing the correct semi-flattened size. However, for nested arrays, `allocate_foreign_call_result_array()` contains a bug. The pattern `Type::Array(_, nested_size)` discards the inner types with `_` and uses only `nested_size`, the semantic length of the nested array (the number of logical elements), not the semi-flattened size. For simple element types this works correctly, but for composite element types it under-allocates. Foreign calls returning nested arrays of tuples or other composite types corrupt the Brillig VM heap. Version 1.0.0-beta.19 fixes this issue.","severity":{"version":"4.0","score":9.3,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N","level":"critical","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-10","score":0.00395,"percentile":0.34058,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-131"],"published":"2026-04-21T20:16:09Z","modified":"2026-10-28T02:30:31Z","fixes":[{"ecosystem":"crates.io","package":"brillig","fixed_in":["1.0.0-beta.19"],"sources":["ghsa","osv"]}],"affected":[{"source":"ghsa","ecosystem":"crates.io","package":"brillig","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"1.0.0-beta.18"},{"fixed":"1.0.0-beta.19"}]}]},{"source":"osv","ecosystem":"crates.io","package":"brillig","ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"1.0.0-beta.19"}]}]}],"products":[{"vendor":"noir-lang","product":"noir"}],"references":[{"url":"https://github.com/noir-lang/noir/security/advisories/GHSA-jj7c-x25r-r8r3","tags":["x_refsource_CONFIRM","advisory","web"]},{"url":"https://github.com/noir-lang/noir/releases/tag/v1.0.0-beta.19","tags":["x_refsource_MISC","web"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-41197","tags":["advisory"]},{"url":"https://github.com/advisories/GHSA-jj7c-x25r-r8r3"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/41xxx/CVE-2026-41197.json","tags":["advisory"]},{"url":"https://github.com/noir-lang/noir","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-41197","title":"Brillig: Heap corruption in foreign call results with nested tuple arrays","description":"Noir is a Domain Specific Language for SNARK proving systems that is designed to use any ACIR compatible proving system, and Brillig is the bytecode ACIR uses for non-determinism. Noir programs can invoke external functions through foreign calls. When compiling to Brillig bytecode, the SSA instructions are processed block-by-block in `BrilligBlock::compile_block()`. When the compiler encounters an `Instruction::Call` with a `Value::ForeignFunction` target, it invokes `codegen_call()` in `brillig_call/code_gen_call.rs`, which dispatches to `convert_ssa_foreign_call()`. Before emitting the foreign call opcode, the compiler must pre-allocate memory for any array results the call will return. This happens through `allocate_external_call_results()`, which iterates over the result types. For `Type::Array` results, it delegates to `allocate_foreign_call_result_array()` to recursively allocate memory on the heap for nested arrays. The `BrilligArray` struct is the internal representation of a Noir array in Brillig IR. Its `size` field represents the semi-flattened size, the total number of memory slots the array occupies, accounting for the fact that composite types like tuples consume multiple slots per element. This size is computed by `compute_array_length()` in `brillig_block_variables.rs`. For the outer array, `allocate_external_call_results()` correctly uses `define_variable()`, which internally calls `allocate_value_with_type()`. This function applies the formula above, producing the correct semi-flattened size. However, for nested arrays, `allocate_foreign_call_result_array()` contains a bug. The pattern `Type::Array(_, nested_size)` discards the inner types with `_` and uses only `nested_size`, the semantic length of the nested array (the number of logical elements), not the semi-flattened size. For simple element types this works correctly, but for composite element types it under-allocates. Foreign calls returning nested arrays of tuples or other composite types corrupt the Brillig VM heap. Version 1.0.0-beta.19 fixes this issue.","status":"published","published":"2026-04-23T00:35:37.842Z","modified":"2026-04-25T03:55:38.608Z","cvss":[{"version":"4.0","score":9.3,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N","level":"critical","assessor":"cna"}],"cwes":["CWE-131"],"updated_at":"2026-11-02T13:41:08.498858Z"}],"euvd":[{"id":"EUVD-2026-25156","description":"Noir is a Domain Specific Language for SNARK proving systems that is designed to use any ACIR compatible proving system, and Brillig is the bytecode ACIR uses for non-determinism. Noir programs can invoke external functions through foreign calls. When compiling to Brillig bytecode, the SSA instructions are processed block-by-block in `BrilligBlock::compile_block()`. When the compiler encounters an `Instruction::Call` with a `Value::ForeignFunction` target, it invokes `codegen_call()` in `brillig_call/code_gen_call.rs`, which dispatches to `convert_ssa_foreign_call()`. Before emitting the foreign call opcode, the compiler must pre-allocate memory for any array results the call will return. This happens through `allocate_external_call_results()`, which iterates over the result types. For `Type::Array` results, it delegates to `allocate_foreign_call_result_array()` to recursively allocate memory on the heap for nested arrays. The `BrilligArray` struct is the internal representation of a Noir array in Brillig IR. Its `size` field represents the semi-flattened size, the total number of memory slots the array occupies, accounting for the fact that composite types like tuples consume multiple slots per element. This size is computed by `compute_array_length()` in `brillig_block_variables.rs`. For the outer array, `allocate_external_call_results()` correctly uses `define_variable()`, which internally calls `allocate_value_with_type()`. This function applies the formula above, producing the correct semi-flattened size. However, for nested arrays, `allocate_foreign_call_result_array()` contains a bug. The pattern `Type::Array(_, nested_size)` discards the inner types with `_` and uses only `nested_size`, the semantic length of the nested array (the number of logical elements), not the semi-flattened size. For simple element types this works correctly, but for composite element types it under-allocates. Foreign calls returning nested arrays of tuples or other composite types corrupt the Brillig VM heap. Version 1.0.0-beta.19 fixes this issue.","status":"published","published":"2026-04-23T00:35:37Z","modified":"2026-04-25T03:55:38Z","cvss":[{"version":"4.0","score":9.3,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N","level":"critical"}],"updated_at":"2026-11-02T19:24:18.224827Z"}],"ghsa":[{"id":"GHSA-jj7c-x25r-r8r3","title":"Brillig: Heap corruption in foreign call results with nested tuple arrays","description":"## Description\n\nNoir programs can invoke external functions through foreign calls. When compiling to Brillig bytecode, the SSA instructions are processed block-by-block in `BrilligBlock::compile_block()`. When the compiler encounters an `Instruction::Call` with a `Value::ForeignFunction` target, it invokes `codegen_call()` in `brillig_call/code_gen_call.rs`, which dispatches to `convert_ssa_foreign_call()`.\n\nBefore emitting the foreign call opcode, the compiler must pre-allocate memory for any array results the call will return. This happens through `allocate_external_call_results()`, which iterates over the result types. For `Type::Array` results, it delegates to `allocate_foreign_call_result_array()` to recursively allocate memory on the heap for nested arrays.\n\nThe `BrilligArray` struct is the internal representation of a Noir array in Brillig IR. Its `size` field represents the **semi-flattened size**, the total number of memory slots the array occupies, accounting for the fact that composite types like tuples consume multiple slots per element. This size is computed by `compute_array_length()` in `brillig_block_variables.rs`:\n\n```rust\npub(crate) fn compute_array_length(item_typ: \u0026CompositeType, elem_count: usize) -\u003e usize {\n    item_typ.len() * elem_count\n}\n```\n\nFor the **outer** array, `allocate_external_call_results()` correctly uses `define_variable()`, which internally calls `allocate_value_with_type()`. This function applies the formula above, producing the correct semi-flattened size.\n\nHowever, for **nested** arrays, `allocate_foreign_call_result_array()` contains a bug. When it encounters a nested `Type::Array(types, nested_size)`, it calls:\n\n```rust\nType::Array(_, nested_size) =\u003e { \n\tlet inner_array = self.brillig_context.allocate_brillig_array(*nested_size as usize);\n\t// ....\n}\n```\n\nThe pattern `Type::Array(_, nested_size)` discards the inner types with `_` and uses only `nested_size`, the **semantic length** of the nested array (the number of logical elements), not the semi-flattened size. For simple element types this works correctly, but for composite element types it under-allocates. Consider a nested array of type `[(u32, u32); 3]`:\n\n-   Semantic length: 3 (three tuples)\n-   Element size: 2 (each tuple has two fields)\n-   Required semi-flattened size: 6 memory slots\n    \n\nThe current code passes `3` to `allocate_brillig_array()`, which then calls `codegen_initialize_array()`. This function allocates `array.size + ARRAY_META_COUNT` slots, only 4 slots instead of the required 7 (6 data + 1 metadata). When the VM executes the foreign call and writes 6 values plus metadata, it overwrites adjacent heap memory.\n\n## Impact\n\nForeign calls returning nested arrays of tuples or other composite types corrupt the Brillig VM heap.\n\n## Recommendation\n\nMultiply the semantic length by the number of element types when allocating nested arrays. Extract the inner types from the pattern and replace the `nested_size` argument to `allocate_brillig_array()` with `types.len() * nested_size` to compute the semi-flattened size. Alternatively, reuse the existing `compute_array_length()` helper function to maintain consistency with outer array allocation.","status":"published","published":"2026-04-21T20:16:09Z","modified":"2026-04-27T16:29:20Z","cvss":[{"version":"4.0","score":9.3,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N","level":"critical"}],"cwes":["CWE-131"],"updated_at":"2026-11-02T14:39:58.897998Z"}],"nvd":[{"id":"CVE-2026-41197","description":"Noir is a Domain Specific Language for SNARK proving systems that is designed to use any ACIR compatible proving system, and Brillig is the bytecode ACIR uses for non-determinism. Noir programs can invoke external functions through foreign calls. When compiling to Brillig bytecode, the SSA instructions are processed block-by-block in `BrilligBlock::compile_block()`. When the compiler encounters an `Instruction::Call` with a `Value::ForeignFunction` target, it invokes `codegen_call()` in `brillig_call/code_gen_call.rs`, which dispatches to `convert_ssa_foreign_call()`. Before emitting the foreign call opcode, the compiler must pre-allocate memory for any array results the call will return. This happens through `allocate_external_call_results()`, which iterates over the result types. For `Type::Array` results, it delegates to `allocate_foreign_call_result_array()` to recursively allocate memory on the heap for nested arrays. The `BrilligArray` struct is the internal representation of a Noir array in Brillig IR. Its `size` field represents the semi-flattened size, the total number of memory slots the array occupies, accounting for the fact that composite types like tuples consume multiple slots per element. This size is computed by `compute_array_length()` in `brillig_block_variables.rs`. For the outer array, `allocate_external_call_results()` correctly uses `define_variable()`, which internally calls `allocate_value_with_type()`. This function applies the formula above, producing the correct semi-flattened size. However, for nested arrays, `allocate_foreign_call_result_array()` contains a bug. The pattern `Type::Array(_, nested_size)` discards the inner types with `_` and uses only `nested_size`, the semantic length of the nested array (the number of logical elements), not the semi-flattened size. For simple element types this works correctly, but for composite element types it under-allocates. Foreign calls returning nested arrays of tuples or other composite types corrupt the Brillig VM heap. Version 1.0.0-beta.19 fixes this issue.","status":"published","published":"2026-04-23T02:16:18.127Z","modified":"2026-06-17T10:46:18.79Z","cvss":[{"version":"4.0","score":9.3,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X","level":"critical","assessor":"secondary"}],"cwes":["CWE-131"],"updated_at":"2026-11-02T14:12:15.867234Z"}],"osv":[{"id":"CVE-2026-41197","title":"Brillig: Heap corruption in foreign call results with nested tuple arrays","description":"Noir is a Domain Specific Language for SNARK proving systems that is designed to use any ACIR compatible proving system, and Brillig is the bytecode ACIR uses for non-determinism. Noir programs can invoke external functions through foreign calls. When compiling to Brillig bytecode, the SSA instructions are processed block-by-block in `BrilligBlock::compile_block()`. When the compiler encounters an `Instruction::Call` with a `Value::ForeignFunction` target, it invokes `codegen_call()` in `brillig_call/code_gen_call.rs`, which dispatches to `convert_ssa_foreign_call()`. Before emitting the foreign call opcode, the compiler must pre-allocate memory for any array results the call will return. This happens through `allocate_external_call_results()`, which iterates over the result types. For `Type::Array` results, it delegates to `allocate_foreign_call_result_array()` to recursively allocate memory on the heap for nested arrays. The `BrilligArray` struct is the internal representation of a Noir array in Brillig IR. Its `size` field represents the semi-flattened size, the total number of memory slots the array occupies, accounting for the fact that composite types like tuples consume multiple slots per element. This size is computed by `compute_array_length()` in `brillig_block_variables.rs`. For the outer array, `allocate_external_call_results()` correctly uses `define_variable()`, which internally calls `allocate_value_with_type()`. This function applies the formula above, producing the correct semi-flattened size. However, for nested arrays, `allocate_foreign_call_result_array()` contains a bug. The pattern `Type::Array(_, nested_size)` discards the inner types with `_` and uses only `nested_size`, the semantic length of the nested array (the number of logical elements), not the semi-flattened size. For simple element types this works correctly, but for composite element types it under-allocates. Foreign calls returning nested arrays of tuples or other composite types corrupt the Brillig VM heap. Version 1.0.0-beta.19 fixes this issue.","status":"published","published":"2026-04-23T00:35:37.842Z","modified":"2026-10-28T02:30:31.754090854Z","cvss":[{"version":"4.0","score":9.3,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N","level":"critical"}],"cwes":["CWE-131"],"updated_at":"2026-11-02T15:45:03.764807Z"},{"id":"GHSA-jj7c-x25r-r8r3","title":"Brillig: Heap corruption in foreign call results with nested tuple arrays","description":"## Description\n\nNoir programs can invoke external functions through foreign calls. When compiling to Brillig bytecode, the SSA instructions are processed block-by-block in `BrilligBlock::compile_block()`. When the compiler encounters an `Instruction::Call` with a `Value::ForeignFunction` target, it invokes `codegen_call()` in `brillig_call/code_gen_call.rs`, which dispatches to `convert_ssa_foreign_call()`.\n\nBefore emitting the foreign call opcode, the compiler must pre-allocate memory for any array results the call will return. This happens through `allocate_external_call_results()`, which iterates over the result types. For `Type::Array` results, it delegates to `allocate_foreign_call_result_array()` to recursively allocate memory on the heap for nested arrays.\n\nThe `BrilligArray` struct is the internal representation of a Noir array in Brillig IR. Its `size` field represents the **semi-flattened size**, the total number of memory slots the array occupies, accounting for the fact that composite types like tuples consume multiple slots per element. This size is computed by `compute_array_length()` in `brillig_block_variables.rs`:\n\n```rust\npub(crate) fn compute_array_length(item_typ: \u0026CompositeType, elem_count: usize) -\u003e usize {\n    item_typ.len() * elem_count\n}\n```\n\nFor the **outer** array, `allocate_external_call_results()` correctly uses `define_variable()`, which internally calls `allocate_value_with_type()`. This function applies the formula above, producing the correct semi-flattened size.\n\nHowever, for **nested** arrays, `allocate_foreign_call_result_array()` contains a bug. When it encounters a nested `Type::Array(types, nested_size)`, it calls:\n\n```rust\nType::Array(_, nested_size) =\u003e { \n\tlet inner_array = self.brillig_context.allocate_brillig_array(*nested_size as usize);\n\t// ....\n}\n```\n\nThe pattern `Type::Array(_, nested_size)` discards the inner types with `_` and uses only `nested_size`, the **semantic length** of the nested array (the number of logical elements), not the semi-flattened size. For simple element types this works correctly, but for composite element types it under-allocates. Consider a nested array of type `[(u32, u32); 3]`:\n\n-   Semantic length: 3 (three tuples)\n-   Element size: 2 (each tuple has two fields)\n-   Required semi-flattened size: 6 memory slots\n    \n\nThe current code passes `3` to `allocate_brillig_array()`, which then calls `codegen_initialize_array()`. This function allocates `array.size + ARRAY_META_COUNT` slots, only 4 slots instead of the required 7 (6 data + 1 metadata). When the VM executes the foreign call and writes 6 values plus metadata, it overwrites adjacent heap memory.\n\n## Impact\n\nForeign calls returning nested arrays of tuples or other composite types corrupt the Brillig VM heap.\n\n## Recommendation\n\nMultiply the semantic length by the number of element types when allocating nested arrays. Extract the inner types from the pattern and replace the `nested_size` argument to `allocate_brillig_array()` with `types.len() * nested_size` to compute the semi-flattened size. Alternatively, reuse the existing `compute_array_length()` helper function to maintain consistency with outer array allocation.","status":"published","published":"2026-04-21T20:16:09Z","modified":"2026-05-05T16:10:00.908903Z","cvss":[{"version":"4.0","score":9.3,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N","level":"critical"}],"cwes":["CWE-131"],"updated_at":"2026-11-02T15:46:50.687609Z"}]}}
