Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
484b133
fix: skip heavy recursion tests in debug builds (#22446)
AztecBot Apr 9, 2026
02ed704
Merge branch 'next' into merge-train/barretenberg
Apr 9, 2026
04a5e8f
Merge branch 'next' into merge-train/barretenberg
Apr 9, 2026
6ce591d
Merge branch 'next' into merge-train/barretenberg
Apr 10, 2026
5e23bc0
Merge branch 'next' into merge-train/barretenberg
Apr 10, 2026
993d369
fix: add clear error for unsatisfiable ACIR AssertZero opcode (#22417)
AztecBot Apr 10, 2026
c20855c
Merge branch 'next' into merge-train/barretenberg
Apr 10, 2026
a91a851
feat: enforce accumulator_not_empty = 0 at ECCVM lagrange_first row (…
notnotraju Apr 10, 2026
d0385aa
fix: skip heavy recursion tests in debug builds, keep one for asserti…
AztecBot Apr 10, 2026
46a06da
Merge branch 'next' into merge-train/barretenberg
Apr 10, 2026
417047a
Merge branch 'next' into merge-train/barretenberg
Apr 10, 2026
aebfa92
Merge branch 'next' into merge-train/barretenberg
Apr 10, 2026
3178ffc
fix: external audit fixes for Pedersen (#22434)
nishatkoti Apr 10, 2026
855ca16
chore!: fix BASE off-by-one in create_small_range_constraint in theta…
nishatkoti Apr 10, 2026
bc37900
fix: external audit fixes for Keccak (#22436)
nishatkoti Apr 10, 2026
ae42cd1
fix: external audit fixes for BLAKE (#22443)
nishatkoti Apr 10, 2026
b3e284c
chore: misc hash gadget updates (#22452)
ledwards2225 Apr 10, 2026
9173c83
Merge branch 'next' into merge-train/barretenberg
Apr 10, 2026
3f57f29
Merge branch 'next' into merge-train/barretenberg
Apr 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions barretenberg/cpp/bootstrap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -245,16 +245,21 @@ function test_cmds_native {
awk '/^[a-zA-Z]/ {suite=$1} /^[ ]/ {print suite$1}' | \
grep -v 'DISABLED_' | \
while read -r test; do
# Skip heavy recursion tests in debug builds — they take 400-600s and the same
# code paths are already exercised (with assertions) by faster tests in the suite.
# Keep WithoutPredicate/1.GenerateVKFromConstraints (241s) so that the debug-only
# native_verification_debug path in honk_recursion_constraint.cpp is still exercised.
if [[ "$native_preset" == *debug* ]] && [[ "$test" =~ ^(HonkRecursionConstraintTest|ChonkRecursionConstraintTest|AvmRecursionInnerCircuitTests) ]]; then
if [[ "$test" != "HonkRecursionConstraintTestWithoutPredicate/1.GenerateVKFromConstraints" ]]; then
continue
fi
fi
local prefix=$hash
# A little extra resource for these tests.
# IPARecursiveTests fails with 2 threads.
if [[ "$test" =~ ^(AcirAvmRecursionConstraint|ChonkKernelCapacity|AvmRecursiveTests|IPARecursiveTests|HonkRecursionConstraintTest|ChonkRecursionConstraintTest) ]]; then
prefix="$prefix:CPUS=4:MEM=8g"
fi
# These tests routinely take 400-600s in debug builds; bump from the 600s default.
if [[ "$test" =~ ^(HonkRecursionConstraintTest|ChonkRecursionConstraintTest) ]]; then
prefix="$prefix:TIMEOUT=900s"
fi
echo -e "$prefix barretenberg/cpp/scripts/run_test.sh $bin_name $test"
done || (echo "Failed to list tests in $bin" && exit 1)
done
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ script_path="$root/barretenberg/cpp/scripts/test_chonk_standalone_vks_havent_cha
# - Generate a hash for versioning: sha256sum bb-chonk-inputs.tar.gz
# - Upload the compressed results: aws s3 cp bb-chonk-inputs.tar.gz s3://aztec-ci-artifacts/protocol/bb-chonk-inputs-[hash(0:8)].tar.gz
# Note: In case of the "Test suite failed to run ... Unexpected token 'with' " error, need to run: docker pull aztecprotocol/build:3.0
pinned_short_hash="50947760"
pinned_short_hash="d519f639"
pinned_chonk_inputs_url="https://aztec-ci-artifacts.s3.us-east-2.amazonaws.com/protocol/bb-chonk-inputs-${pinned_short_hash}.tar.gz"

function update_pinned_hash_in_script {
Expand Down Expand Up @@ -66,7 +66,7 @@ function check_circuit_vks {
bb_check_args+=(--disable_asserts)
fi

output=$($bb "${bb_check_args[@]}") || exit_code=$?
output=$($bb "${bb_check_args[@]}" 2>&1) || exit_code=$?

if [[ $exit_code -ne 0 ]]; then
# Check if this is actually a VK change
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ enum blake3s_constant {
BLAKE3_MAX_DEPTH = 54
};

using key_array = std::array<uint32_t, BLAKE3_KEY_LEN>;
using key_array = std::array<uint32_t, BLAKE3_KEY_LEN / sizeof(uint32_t)>;
using block_array = std::array<uint8_t, BLAKE3_BLOCK_LEN>;
using state_array = std::array<uint32_t, 16>;
using out_array = std::array<uint8_t, BLAKE3_OUT_LEN>;
Expand Down
9 changes: 7 additions & 2 deletions barretenberg/cpp/src/barretenberg/crypto/blake3s/blake3s.tcc
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ struct output_t {
constexpr output_t make_output(const key_array& input_cv, const uint8_t* block, uint8_t block_len, uint8_t flags)
{
output_t ret;
for (size_t i = 0; i < (BLAKE3_OUT_LEN >> 2); ++i) {
for (size_t i = 0; i < ret.input_cv.size(); ++i) {
ret.input_cv[i] = input_cv[i];
}
for (size_t i = 0; i < BLAKE3_BLOCK_LEN; i++) {
Expand All @@ -193,7 +193,7 @@ constexpr output_t make_output(const key_array& input_cv, const uint8_t* block,

constexpr void blake3_hasher_init(blake3_hasher* self)
{
for (size_t i = 0; i < (BLAKE3_KEY_LEN >> 2); ++i) {
for (size_t i = 0; i < IV.size(); ++i) {
self->key[i] = IV[i];
self->cv[i] = IV[i];
}
Expand Down Expand Up @@ -260,6 +260,11 @@ std::vector<uint8_t> blake3s(std::vector<uint8_t> const& input)

constexpr std::array<uint8_t, BLAKE3_OUT_LEN> blake3s_constexpr(const uint8_t* input, const size_t input_size)
{
if (std::is_constant_evaluated()) {
if (input_size > 1024U) {
__builtin_trap();
}
}
blake3_hasher hasher;
blake3_hasher_init(&hasher);
blake3_hasher_update(&hasher, input, input_size);
Expand Down
27 changes: 0 additions & 27 deletions barretenberg/cpp/src/barretenberg/crypto/keccak/keccak.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -110,30 +110,3 @@ struct keccak256 ethash_keccak256(const uint8_t* data, size_t size) NOEXCEPT
keccak(hash.word64s, 256, data, size);
return hash;
}

struct keccak256 hash_field_elements(const uint64_t* limbs, size_t num_elements)
{
std::vector<uint8_t> input_buffer(num_elements * KECCAK256_OUTPUT_BYTES);

for (size_t i = 0; i < num_elements; ++i) {
for (size_t j = 0; j < KECCAK256_OUTPUT_WORDS; ++j) {
uint64_t word = (limbs[i * KECCAK256_OUTPUT_WORDS + j]);
size_t idx = i * 32 + j * 8;
input_buffer[idx] = (uint8_t)((word >> 56) & 0xff);
input_buffer[idx + 1] = (uint8_t)((word >> 48) & 0xff);
input_buffer[idx + 2] = (uint8_t)((word >> 40) & 0xff);
input_buffer[idx + 3] = (uint8_t)((word >> 32) & 0xff);
input_buffer[idx + 4] = (uint8_t)((word >> 24) & 0xff);
input_buffer[idx + 5] = (uint8_t)((word >> 16) & 0xff);
input_buffer[idx + 6] = (uint8_t)((word >> 8) & 0xff);
input_buffer[idx + 7] = (uint8_t)(word & 0xff);
}
}

return ethash_keccak256(input_buffer.data(), num_elements * KECCAK256_OUTPUT_BYTES);
}

struct keccak256 hash_field_element(const uint64_t* limb)
{
return hash_field_elements(limb, 1);
}
4 changes: 0 additions & 4 deletions barretenberg/cpp/src/barretenberg/crypto/keccak/keccak.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,6 @@ void ethash_keccakf1600(uint64_t state[KECCAKF1600_LANES]) NOEXCEPT;

struct keccak256 ethash_keccak256(const uint8_t* data, size_t size) NOEXCEPT;

struct keccak256 hash_field_elements(const uint64_t* limbs, size_t num_elements);

struct keccak256 hash_field_element(const uint64_t* limb);

namespace bb::crypto {
/**
* @brief A wrapper class used to construct `KeccakTranscript`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ std::vector<typename Curve::BaseField> pedersen_hash_base<Curve>::convert_buffer
template <typename Curve>
typename Curve::BaseField pedersen_hash_base<Curve>::hash(const std::vector<Fq>& inputs, const GeneratorContext context)
{
if (inputs.empty()) {
throw_or_abort("pedersen hash: empty input");
}

Element result = length_generator * Fr(inputs.size());
return (result + pedersen_commitment_base<Curve>::commit_native(inputs, context)).normalize().x;
}
Expand All @@ -88,6 +92,10 @@ template <typename Curve>
typename Curve::BaseField pedersen_hash_base<Curve>::hash_buffer(const std::vector<uint8_t>& input,
const GeneratorContext context)
{
if (input.empty()) {
throw_or_abort("pedersen hash_buffer: empty input");
}

std::vector<Fq> converted = convert_buffer(input);

if (converted.size() < 2) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,26 @@ TEST(Pedersen, Hash32Bytes)

EXPECT_EQ(got, expected);
}
// Verifies that hashing an empty input throws an exception
TEST(Pedersen, HashRejectsEmptyInput)
{
EXPECT_THROW(
{
auto result = pedersen_hash::hash({});
static_cast<void>(result);
},
std::runtime_error);
}

// Verifies that hashing an empty input throws an exception
TEST(Pedersen, HashBufferRejectsEmptyInput)
{
EXPECT_THROW(
{
auto result = pedersen_hash::hash_buffer({});
static_cast<void>(result);
},
std::runtime_error);
}

} // namespace bb::crypto
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,7 @@ TYPED_TEST(AcirFormatTests, ExpressionWithOnlyConstantTermFails)
.return_values = {},
};

EXPECT_THROW_WITH_MESSAGE(circuit_serde_to_acir_format(circuit),
"split_into_mul_quad_gates: resulted in zero gates.");
EXPECT_THROW_WITH_MESSAGE(circuit_serde_to_acir_format(circuit), "circuit is unsatisfiable");
}

TYPED_TEST(AcirFormatTests, ExpressionWithCancellingCoefficientsFails)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
/**
* @file acir_null_deref.test.cpp
* @brief Exploit and fix tests for null shared_ptr dereference in ACIR deserialization.
*
* Demonstrates that crafted ACIR bytecode containing msgpack NIL values for
* shared_ptr<array<T,N>> fields would produce a null pointer dereference in
* acir_to_constraint_buf.cpp, and that the fix (rejecting NIL in conv_fld_from_kvmap
* and conv_fld_from_array) prevents the crash.
*
* Attack vector: An attacker crafts raw ACIR bytecode (bypassing the Noir compiler)
* containing a BlackBoxFuncCall opcode where a fixed-size array field is encoded as
* msgpack NIL (0xc0). Without the fix, the AztecProtocol/msgpack-c fork silently
* converts NIL to a null shared_ptr, which is then dereferenced unconditionally.
* With the fix, deserialization rejects NIL for required fields and throws.
*/

#include <gtest/gtest.h>
#include <memory>
#include <regex>
#include <vector>

#include "acir_to_constraint_buf.hpp"
#include "barretenberg/common/assert.hpp"
#include "barretenberg/serialize/msgpack_impl.hpp"
#include "serde/acir.hpp"

using namespace acir_format;

class AcirNullDerefTest : public ::testing::Test {};

/**
* Helper: build a ProgramWithoutBrillig wire format by manually packing with msgpack.
* We can't use ProgramWithoutBrillig::msgpack_pack because std::monostate lacks a pack adaptor.
* Instead, we pack the structure by hand: ARRAY[ARRAY[circuit], NIL].
*/
static std::vector<uint8_t> serialize_program_with_circuit(const Acir::Circuit& circuit)
{
msgpack::sbuffer sbuf;
msgpack::packer<msgpack::sbuffer> packer(sbuf);

// ProgramWithoutBrillig = ARRAY[functions, unconstrained_functions]
packer.pack_array(2);

// functions = ARRAY[circuit]
packer.pack_array(1);
packer.pack(circuit);

// unconstrained_functions = NIL (std::monostate — ignored by ProgramWithoutBrillig::msgpack_unpack)
packer.pack_nil();

// Prepend format marker 0x03 (FORMAT_MSGPACK_COMPACT)
std::vector<uint8_t> buf;
buf.reserve(1 + sbuf.size());
buf.push_back(0x03);
buf.insert(buf.end(), sbuf.data(), sbuf.data() + sbuf.size());
return buf;
}

// ============================================================================
// Exploit tests: These construct null shared_ptr directly in the struct,
// bypassing deserialization. They prove the dereference sites are unguarded.
// ============================================================================

TEST_F(AcirNullDerefTest, AES128Encrypt_NullIV_DirectCircuit_Crashes)
{
Acir::BlackBoxFuncCall::AES128Encrypt aes_op{
.inputs = {},
.iv = nullptr,
.key = nullptr,
.outputs = {},
};

Acir::BlackBoxFuncCall bbfc;
bbfc.value = aes_op;

Acir::Circuit circuit{
.opcodes = { Acir::Opcode{ Acir::Opcode::BlackBoxFuncCall{ .value = bbfc } } },
.public_parameters = {},
.return_values = {},
};

// Dereferences nullptr at acir_to_constraint_buf.cpp:636: *arg.iv
EXPECT_DEATH(circuit_serde_to_acir_format(circuit), "");
}

TEST_F(AcirNullDerefTest, Keccakf1600_NullInputs_DirectCircuit_Crashes)
{
Acir::BlackBoxFuncCall::Keccakf1600 keccak_op{
.inputs = nullptr,
.outputs = nullptr,
};

Acir::BlackBoxFuncCall bbfc;
bbfc.value = keccak_op;

Acir::Circuit circuit{
.opcodes = { Acir::Opcode{ Acir::Opcode::BlackBoxFuncCall{ .value = bbfc } } },
.public_parameters = {},
.return_values = {},
};

// Dereferences nullptr at acir_to_constraint_buf.cpp:716: *arg.inputs
EXPECT_DEATH(circuit_serde_to_acir_format(circuit), "");
}

TEST_F(AcirNullDerefTest, Sha256Compression_NullInputs_DirectCircuit_Crashes)
{
Acir::BlackBoxFuncCall::Sha256Compression sha_op{
.inputs = nullptr,
.hash_values = nullptr,
.outputs = nullptr,
};

Acir::BlackBoxFuncCall bbfc;
bbfc.value = sha_op;

Acir::Circuit circuit{
.opcodes = { Acir::Opcode{ Acir::Opcode::BlackBoxFuncCall{ .value = bbfc } } },
.public_parameters = {},
.return_values = {},
};

// Dereferences nullptr at acir_to_constraint_buf.cpp:644: *arg.inputs
EXPECT_DEATH(circuit_serde_to_acir_format(circuit), "");
}

// ============================================================================
// Fix tests: These go through the deserialization path (raw bytes or msgpack
// roundtrip). The fix in conv_fld_from_array rejects NIL before it can become
// a null shared_ptr, throwing instead of crashing.
// ============================================================================

TEST_F(AcirNullDerefTest, AES128Encrypt_NullIV_FromBytes_ThrowsAfterFix)
{
Acir::BlackBoxFuncCall::AES128Encrypt aes_op{
.inputs = {},
.iv = nullptr, // Serialized as NIL (0xc0)
.key = nullptr,
.outputs = {},
};

Acir::BlackBoxFuncCall bbfc;
bbfc.value = aes_op;

Acir::Circuit circuit{
.opcodes = { Acir::Opcode{ Acir::Opcode::BlackBoxFuncCall{ .value = bbfc } } },
.public_parameters = {},
.return_values = {},
};

auto buf = serialize_program_with_circuit(circuit);

// Verify the buffer contains NIL byte (0xc0) from the null shared_ptr
size_t nil_count = 0;
for (uint8_t b : buf) {
if (b == 0xc0) {
nil_count++;
}
}
ASSERT_GE(nil_count, 2U) << "Buffer must contain at least 2 NIL (0xc0) bytes for null iv and key";

// After fix: deserialization rejects NIL for required fields → throws instead of SIGSEGV
EXPECT_THROW_WITH_MESSAGE(circuit_buf_to_acir_format(std::move(buf)), "nil value for required field");
}

TEST_F(AcirNullDerefTest, NullSharedPtr_RejectedByMsgpackRoundtrip)
{
// After the fix, a null shared_ptr encoded as NIL is rejected during deserialization.
// conv_fld_from_array detects NIL at the array slot and throws before converting.
Acir::BlackBoxFuncCall::AES128Encrypt original{
.inputs = {},
.iv = nullptr,
.key = nullptr,
.outputs = {},
};

// Serialize (null shared_ptr → NIL byte 0xc0)
msgpack::sbuffer sbuf;
msgpack::pack(sbuf, original);

// Deserialize — should throw because iv slot is NIL
auto oh = msgpack::unpack(sbuf.data(), sbuf.size());
Acir::BlackBoxFuncCall::AES128Encrypt deserialized;
EXPECT_THROW_WITH_MESSAGE(oh.get().convert(deserialized), "nil value for required field");
}
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,17 @@ void assert_zero_to_quad_constraints(Acir::Opcode::AssertZero const& arg, AcirFo
};

auto linear_terms = process_linear_terms(arg.value);

// Check for unsatisfiable constraint: no variables but a non-zero constant means the circuit requires
// `constant == 0` which can never be satisfied.
if (arg.value.mul_terms.empty() && linear_terms.empty()) {
fr constant = from_buffer_with_bound_checks(arg.value.q_c);
BB_ASSERT_EQ(constant,
fr::zero(),
"circuit is unsatisfiable. An AssertZero opcode contains no variables but has a non-zero "
"constant, which can never equal zero.");
}

bool is_single_gate = is_single_arithmetic_gate(arg.value, linear_terms);
std::vector<mul_quad_<fr>> mul_quads = split_into_mul_quad_gates(arg.value, linear_terms);

Expand Down
Loading
Loading