Assessment reports>UPA circuits circuitId hash function change>Low findings>Range check in ,add_slice_at_offset
Category: Coding Mistakes

Range check in add_slice_at_offset

Low Severity
Informational Impact
Low Likelihood

Description

In circuits/src/keccak/multivar.rs, the function KeccakMultiVarHasher::absorb_var checks that the length of the input fits in MAX_BIT_SIZE bits but nothing stricter:

assert!(input.len() < 1 << MAX_BIT_SIZE, "input too long");

So a length of (1 << MAX_BIT_SIZE) - 1 is in principle possible.

Then, in add_slice_at_offset, there is the following snippet:

// Require `len <= padded input length`
let padded_input_len = source.len();
let padded_input_len_plus_one =
    ctx.load_constant(F::from(padded_input_len as u64 + 1));
range.range_check(ctx, len, MAX_BIT_SIZE);
range.check_less_than(ctx, len, padded_input_len_plus_one, MAX_BIT_SIZE);

Here, source is one of the input vectors from absorb_var, so padded_input_len can in principle have value (1 << MAX_BIT_SIZE) - 1, and hence padded_input_len_plus_one can have value 1 << MAX_BIT_SIZE. This is a value that does not fit in MAX_BIT_SIZE bits, which is an assumption required for the soundness of check_less_than, however.

Impact

The range check

range.check_less_than(ctx, len, padded_input_len_plus_one, MAX_BIT_SIZE);

is unsound in the case of inputs of the very specific padded length for a variable length input of (1 << MAX_BIT_SIZE) - 1. Note that a typical attacker cannot control this padded length, which is determined at verification-key--generation time. It seems very unlikely that the circuit will be instantiated with a variable length input of this length on accident, in particular due to MAX_BIT_SIZE being 32, so that this would amount to a quite large variable-length input.

Assuming a variable-length input with padded length (1 << MAX_BIT_SIZE) - 1, we cannot assume that the above range check constrains len to be smaller than padded_input_len_plus_one = 1 << MAX_BIT_SIZE. This is as the assumption that padded_input_len_plus_one fits in MAX_BIT_SIZE bits fails. However, as len is potentially able to be chosen by the attacker, that it fits in MAX_BIT_SIZE bits is checked with an explicit range check in the line right before:

range.range_check(ctx, len, MAX_BIT_SIZE);

This ensures that len < (1 << MAX_BIT_SIZE). Thus, the intended inequality len < padded_input_len_plus_one still holds.

Recommendations

Due to the reasoning outlined in the impact section, the code is not vulnerable as is, due to the separate range check on len. However, for best practice, we recommend to use a slightly stricter check on the padded input length in the function KeccakMultiVarHasher::absorb_var:

-assert!(input.len() < 1 << MAX_BIT_SIZE, "input too long");
+assert!(input.len() < (1 << MAX_BIT_SIZE) - 1, "input too long");

This will ensure that padded_input_len_plus_one is at most MAX_BIT_SIZE bits wide.

Remediation

This issue has been acknowledged by Nebra, and a fix was implemented in commit 3d3567ad.

Zellic © 2025Back to top ↑