Skip to content

Public APIs perform unsafe work before validating available input lengths #1212

Description

@fegge

Several public API entry points receive caller-controlled lengths that are
sufficient to reject malformed input, but they perform memory access or
allocator-controlled work before validating those lengths. Attackers who
control message lengths, signature lengths, context lengths, or low-level
external-mu mode lengths can trigger out-of-bounds access, undefined behavior,
null pointer dereferences, or allocator-dependent error paths before the API
rejects the request.

The signed-message API is the highest-impact instance. It receives mlen, then
uses MLDSA_CRYPTO_BYTES + mlen in indexed memory accesses before checking
whether that addition fits in size_t or whether the implementation supports
such a large input. A malformed mlen near SIZE_MAX can make the output index
wrap and write outside the caller-provided output buffer.

  for (i = 0; i < mlen; ++i)
  __loop__(
    assigns(i, object_whole(sm))
    invariant(i <= mlen)
    decreases(mlen - i)
  )
  {
    sm[MLDSA_CRYPTO_BYTES + mlen - 1 - i] = m[mlen - 1 - i];
  }
  ret = mld_sign_signature(sm, smlen, sm + MLDSA_CRYPTO_BYTES, mlen, ctx,
                           ctxlen, sk, context);

Figure X.1: The signed-message API uses mlen in output indexing before checking length arithmetic (mldsa-native/mldsa/src/sign.c#L1156-L1166)

The low-level signing and verification APIs also receive an explicit mlen
argument in external-mu mode. Their public contracts state that mlen must
equal MLDSA_CRHBYTES when externalmu != 0, but the implementation copies
MLDSA_CRHBYTES bytes from m without first enforcing that mode-specific
length rule.

  else
  {
    /* mu has been provided directly (external-mu variant; line 6 done by the
     * caller in a separate cryptographic module). */
    mld_memcpy(mu, m, MLDSA_CRHBYTES);
  }

Figure X.2: The external-mu signing path copies 64 bytes before enforcing mlen == MLDSA_CRHBYTES (mldsa-native/mldsa/src/sign.c#L948-L953)

  else
  {
    /* mu has been provided directly */
    mld_memcpy(mu, m, MLDSA_CRHBYTES);
  }

Figure X.3: The external-mu verification path has the same unchecked fixed-size copy (mldsa-native/mldsa/src/sign.c#L1235-L1239)

Other retained cases have the same property: the API has the length needed to
reject the input before the risky operation. Detached signing allocates scratch
buffers before checking ctxlen <= 255, detached verification allocates all
verifier scratch buffers before checking siglen == MLDSA_CRYPTO_BYTES, and
the public prefix helper documents ctx as nullable but copies from it whenever
ctxlen > 0.

  MLD_ALLOC(pre, uint8_t, MLD_DOMAIN_SEPARATION_MAX_BYTES, context);
  MLD_ALLOC(rnd, uint8_t, MLDSA_RNDBYTES, context);

  if (pre == NULL || rnd == NULL)
  {
    ret = MLD_ERR_OUT_OF_MEMORY;
    goto cleanup;
  }

  /* Prepare domain separation prefix for pure ML-DSA */
  pre_len = mld_prepare_domain_separation_prefix(pre, NULL, 0, ctx, ctxlen,
                                                 MLD_PREHASH_NONE);

Figure X.4: Detached signing allocates before rejecting overlong contexts (mldsa-native/mldsa/src/sign.c#L1055-L1070)

  MLD_ALLOC(buf, uint8_t, (MLDSA_K * MLDSA_POLYW1_PACKEDBYTES), context);
  MLD_ALLOC(mu, uint8_t, MLDSA_CRHBYTES, context);
  MLD_ALLOC(c, uint8_t, MLDSA_CTILDEBYTES, context);
  MLD_ALLOC(c2, uint8_t, MLDSA_CTILDEBYTES, context);
  MLD_ALLOC(z, mld_polyvecl, 1, context);
  MLD_ALLOC(cp, mld_poly, 1, context);
  MLD_ALLOC(mat, mld_polymat, 1, context);
  MLD_ALLOC(w1, mld_poly, 1, context);
  MLD_ALLOC(tmp, mld_poly, 1, context);

  if (buf == NULL || mu == NULL || c == NULL || c2 == NULL || z == NULL ||
      cp == NULL || mat == NULL || w1 == NULL || tmp == NULL)
  {
    ret = MLD_ERR_OUT_OF_MEMORY;
    goto cleanup;
  }

  if (siglen != MLDSA_CRYPTO_BYTES)

Figure X.5: Verification allocates scratch buffers before checking the caller-provided signature length (mldsa-native/mldsa/src/sign.c#L1190-L1208)

  /* Common prefix: 0x00/0x01 || ctxlen || ctx */
  prefix[0] = (hashalg == MLD_PREHASH_NONE) ? 0 : 1;
  prefix[1] = (uint8_t)ctxlen;
  if (ctxlen > 0)
  {
    mld_memcpy(prefix + 2, ctx, ctxlen);
  }

Figure X.6: The prefix helper copies from ctx whenever ctxlen is nonzero (mldsa-native/mldsa/src/sign.c#L1624-L1629)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions