#36838·deno

crypto.generatePrimeSync(1) panics and aborts the process instead of throwing

Author: xia-chaoCreated Sep 14, 2026Updated Sep 14, 2026

crypto.generatePrimeSync(1) takes the whole Deno process down, while Node just throws an ordinary error. It takes one line to trigger.

Summary

Passing 1 to crypto.generatePrimeSync() makes Deno panic and kill the process. try/catch cannot stop it. Node returns a normal catchable error for the same input.

Reproduction

That's the whole thing:

import crypto from "node:crypto";

try {
  crypto.generatePrimeSync(1);
} catch (e) {
  console.log("caught:", e.code);   // never reached on Deno
}
console.log("SURVIVED");

What actually happens

runtime generatePrimeSync(1) process
Node v22.8.0 throws Error (bits too small) survives
Node v24.14.0 throws Error (bits too small) survives
Node v25.8.0 throws Error (bits too small) survives
Node v26.7.0 throws ERR_OSSL_BN_BITS_TOO_SMALL survives
Node v26.8.1 throws ERR_OSSL_BN_BITS_TOO_SMALL survives
Deno 2.9.6 panics, process dies exit code 1

I tried every Node from v22 through v26.8 and the behaviour is identical, so this is not a version difference — it is specific to Deno.

Screenshots: how it gets there

Below is me stepping through it with VS Code + CodeLLDB on a Deno built from source (not the release binary).

Step 1: breakpoint in gen_prime, with size = 1

The argument arrives as size = 1, safe = false, and add/rem are both empty, so the else branch is next.

Step 2: step in, and land on the line with no check at all

This is the problem. The two branches of the same function are not symmetric:

  • the branch on line 1643 calls generate_with_options(...)?it has a ? that propagates the error
  • the branch on line 1646 calls Prime::generate(size)nothing, it just goes through

size = 1 takes the second one.

Step 3: step in again, and stop on the dependency's panic!

This one is the clearest: the debugger stops on panic!("prime size must be at least 2-bit") inside the third-party num-bigint-dig crate, with bit_size = 1 sitting in the Variables panel.

Reading the Call Stack top-down makes the path obvious:

bigrand.rs:310      <- the panic! itself
primes.rs:18        <- Deno's Prime::generate, passes it down unchecked
lib.rs:1646         <- Deno's gen_prime, else branch, unchecked
lib.rs:1683         <- the op entry point

In other words: Deno never stopped the 1, it travelled all the way into the library, and the library panicked.

Step 4: the process is killed

Same script, same machine:

  • deno exit=1 — killed
  • node exit=0 — threw normally, process alive

And the ./ext/node_crypto/lib.rs:1646:8 in the backtrace is exactly the line the breakpoint was on in step 2.

Why this is worse than a normal crash

Deno's release profile sets panic = "abort" in Cargo.toml, so a Rust panic is not an exception — it kills the entire process. A caller's try/catch is useless and there is nothing to fall back on.

So any code that forwards an external value into generatePrimeSync() (for example passing a user-supplied primeLength straight through) can be taken down by a single 1.

Cause

gen_prime() in ext/node_crypto/lib.rs:

fn gen_prime(
  size: usize,
  safe: bool,
  add: Option<&[u8]>,
  rem: Option<&[u8]>,
) -> Result<Uint8Array, GeneratePrimeError> {
  if safe || add.is_some() || rem.is_some() {
    let prime = primes::Prime::generate_with_options(size, safe, add, rem)?;  // has ?
    Ok(prime.0.to_bytes_be().into())
  } else {
    Ok(primes::Prime::generate(size).0.to_bytes_be().into())  // no check
  }
}

The JS side validates with validateInt32(size, "size", 1), which only guarantees size >= 1. But num-bigint-dig underneath requires >= 2:

// num-bigint-dig-0.8.6/src/bigrand.rs
fn gen_prime(&mut self, bit_size: usize) -> BigUint {
    if bit_size < 2 {
        panic!("prime size must be at least 2-bit");
    }

The two ranges disagree, and size = 1 falls exactly in the gap: it passes the JS check (1 >= 1) and misses the lower-level requirement (1 < 2), so it panics.

For what it's worth, that else branch is what remained after PR #32618 added safe/add/rem support. That PR changed gen_prime to return a Result, but only added ? to the new branch; this pre-existing else branch was left untouched. That PR was itself about tightening boundary checks.

Why the tests did not catch it

parallel/test-crypto-prime.js is already listed in tests/node_compat/config.jsonc and is expected to pass. But its size cases are:

[-1, 0, 2 ** 31, 2 ** 31 + 1, 2 ** 32 - 1, 2 ** 32].forEach(...)

which skips 1. So this gap was never covered.

Suggested fix

Reject it before gen_prime() is reached, and return an error instead of letting it panic:

if size < 2 {
  return Err(GeneratePrimeError::BitsTooSmall);
}

Match Node's message for the target version (26.3.0 behaves as error:01800076:bignum routines::bits too small), and add 1 to that size array in the tests.

Environment

  • Deno: 2.9.6 (built from clone of main @ 83bb8d5780505d0b4f30c089680ce98b0e7a4770, with debug symbols)
  • OS: macOS aarch64
  • Node for comparison: v22.8.0 / v24.14.0 / v25.8.0 / v26.7.0 / v26.8.1

Aside: the same panic has a second path

While fixing this I ran the official crypto tests and found another place that hits the exact same panic, without going through generatePrimeSync:

crypto.generateKeyPairSync('dh', { primeLength: 0 })   // panics the same way

The backtrace goes through a different function:

bigrand.rs:310      panic!("prime size must be at least 2-bit")
primes.rs:18        Prime::generate
keys.rs:3465        dh_generate     <- calls Prime::generate directly, also unchecked
keys.rs:3509        op_node_generate_dh_key

keys.rs:3465 is this line:

.unwrap_or_else(|| Prime::generate(prime_len))

which is the same shape as the else branch in gen_prime — straight into Prime::generate with no check that prime_len is large enough.

The corresponding official test test-crypto-negative-zero.js currently fails. That failure predates my change and is not caused by it.

That said, I want to be precise rather than overstate it:

primeLength Node v22.8 v24.14 v25.8 v26.7 v26.8.1
0 throws throws throws ERR_OSSL_DH_MODULUS_TOO_SMALL same
1 throws throws throws same same
-0 crashes crashes crashes throws throws
  • For 0 and 1: Node has thrown a catchable error from v22 all the way to v26, while Deno panics — that one is Deno's own problem.
  • For -0: Node used to crash too before v26.7 (Assertion failed: IsAnyBufferSource), and fixed it later. Deno should align with the post-26.7 behaviour.

This is the same root cause as generatePrime (no lower-bound check before calling Prime::generate), but the fix is in a different file, so I kept it out of that PR.