denoland/deno · error

AAD length exceeds i32::MAX

Error message

AAD length exceeds i32::MAX

What it means

node:crypto in Deno implements chacha20-poly1305 with an EVP-based cipher that buffers AAD from setAAD() calls and flushes it to aws-lc right before the first encrypt/decrypt. EVP_CipherUpdate takes a C int length, so the buffered byte count must fit i32; this expect() panics when the cumulative AAD exceeds 2147483647 bytes (2 GiB). The panic originates in native code and crashes the process.

Source

Thrown at ext/node_crypto/cipher.rs:383

  fn set_aad(&mut self, aad: &[u8]) {
    self.aad_buf.extend_from_slice(aad);
  }

  /// Flush buffered AAD to EVP context. Called lazily before the first
  /// encrypt/decrypt so that multiple setAAD() calls are concatenated.
  fn flush_aad(&mut self) {
    if !self.aad_flushed {
      self.aad_flushed = true;
      if !self.aad_buf.is_empty() {
        // SAFETY: ctx is valid, aad_buf is a valid slice. Passing NULL
        // output tells EVP this is AAD, not plaintext/ciphertext.
        // Length is validated to fit in i32 before casting.
        unsafe {
          let aad_len: i32 = self
            .aad_buf
            .len()
            .try_into()
            .expect("AAD length exceeds i32::MAX");
          let mut outl: i32 = 0;
          let ret = aws_lc_sys::EVP_CipherUpdate(
            self.ctx,
            std::ptr::null_mut(),
            &mut outl,
            self.aad_buf.as_ptr(),
            aad_len,
          );
          assert_eq!(ret, 1, "EVP_CipherUpdate for AAD failed");
        }
      }
    }
  }

  fn encrypt(&mut self, input: &[u8], output: &mut [u8]) {
    assert!(output.len() >= input.len());
    self.flush_aad();
    // SAFETY: ctx is valid and initialized for encryption. output is

View on GitHub (pinned to 9ad36f7a2c)

Solutions

  1. Audit the call site: AAD is metadata (headers/associated data) and should be tiny; if it is gigabytes you almost certainly meant to pass the data to update() instead of setAAD().
  2. Cap total AAD: reject or chunk-by-redesign before the sum of setAAD() calls reaches 2^31-1 bytes.
  3. If genuinely huge associated data is required, hash it first and use the digest (fixed 32 bytes) as the AAD.
  4. Add a unit test asserting AAD size stays under a sane bound (e.g. 64 KiB).

Example fix

// before
cipher.setAAD(fileBytes);        // oops: whole 3 GiB file as AAD
cipher.update(header);           // flush_aad() -> panic

// after
cipher.setAAD(aadHeader);        // small associated data
cipher.update(fileBytes);        // payload goes through update()
Defensive patterns

Strategy: validation

Validate before calling

const MAX_I32 = 2 ** 31 - 1;
let aadTotal = 0;
function safeSetAAD(cipher, aad) {
  if (!Buffer.isBuffer(aad)) throw new TypeError("AAD must be a Buffer");
  if (aadTotal + aad.length > MAX_I32) {
    throw new Error(`total AAD ${aadTotal + aad.length} exceeds 2 GiB limit`);
  }
  cipher.setAAD(aad);
  aadTotal += aad.length;
}

Prevention

When it happens

Trigger: const c = crypto.createCipheriv("chacha20-poly1305", key, iv); c.setAAD(aad); ... where the total bytes passed to setAAD (possibly across multiple calls, since they are concatenated until the first update()) exceed 2 GiB, then the first c.update() triggers flush_aad() and the panic.

Common situations: Swapping arguments and accidentally passing the whole multi-gigabyte file to setAAD() instead of update(); piping an unbounded stream through setAAD in a loop; test harnesses that feed generated data into AAD without bounds. Real AEAD AAD is normally a few dozen bytes, so hitting this almost always indicates a logic bug rather than legitimate data.

Related errors


AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20). Data as JSON: /api/errors/a021688bf0b4157e. Report an issue: GitHub.