Skip to content

Adopt sha2 0.11 in build_support and bound the font download #312

Description

@lodyai

Summary

sha2 is pinned at 0.10.9 here. When it moves to 0.11 (Dependabot will propose it), the
build support crate stops compiling: the font checksum is rendered with {:x}, which 0.11
removes.

This one also has an unbounded network read worth fixing at the same time, since the code is
already being touched.

Affected site

  • build_support/src/font.rs:172let actual = format!("{digest:x}");

Hardening: bound the response body

fetch_font_data() currently does:

let bytes = resp
    .bytes()
    .with_context(|| format!("reading response body from {FONT_URL}"))?;

resp.bytes() buffers the entire response into memory with no cap, before the checksum is
checked. A hostile or misbehaving origin at that URL can therefore drive build-time memory use
arbitrarily high. Since this runs in a build script, the failure lands on every developer and
CI job.

Cap it explicitly. With reqwest's blocking API:

use std::io::Read;

/// A Fira Sans regular TTF is ~400 KiB; this leaves generous headroom while
/// bounding what a hostile origin can make the build allocate.
const MAX_FONT_BYTES: u64 = 8 * 1024 * 1024;

let mut reader = resp.take(MAX_FONT_BYTES + 1);
let mut bytes = Vec::new();
reader
    .read_to_end(&mut bytes)
    .with_context(|| format!("reading response body from {FONT_URL}"))?;
if bytes.len() as u64 > MAX_FONT_BYTES {
    return Err(anyhow!(
        "font exceeds the maximum of {MAX_FONT_BYTES} bytes"
    ));
}

Reading MAX + 1 and then checking is what distinguishes "exactly at the cap" from
"truncated silently at the cap" — a plain take(MAX) cannot tell those apart.

What does not apply here

The expected digest is a compile-time constant (FONT_SHA256), not a fetched .sha256
sidecar. So the sidecar-specific hardening from the whitaker migration — validating the token
is 64 hex digits, lowercasing before comparison, capping the sidecar read — is not relevant
to this repo. Likewise, tmp.persist(&font_path) already gives you an atomic publish, so the
partial-artefact cleanup work does not apply either.

The comparison at line 173 is already constant-to-constant lowercase, so it stays as is.

The fix

sha2 0.11 returns hybrid_array::Array<u8, _> from finalize()/digest(). That type
derefs to [u8] but does not implement core::fmt::LowerHex, so {:x} no longer
compiles. Render the digest explicitly instead.

If this crate already depends on hex, hex::encode(digest) is sufficient. Otherwise add a
small crate-internal encoder — no new dependency needed:

/// Encode `bytes` as a lowercase hexadecimal string.
///
/// Every byte renders as exactly two digits, including leading zeroes, so the
/// output is always twice the input length.
#[must_use]
fn to_lower_hex(bytes: &[u8]) -> String {
    const HEX_DIGITS: &[u8; 16] = b"0123456789abcdef";
    let mut hex = String::with_capacity(bytes.len() * 2);
    for &byte in bytes {
        hex.push(char::from(HEX_DIGITS[usize::from(byte >> 4)]));
        hex.push(char::from(HEX_DIGITS[usize::from(byte & 0x0f)]));
    }
    hex
}

Call sites become to_lower_hex(&hasher.finalize()) — the & coerces Array to &[u8]
via Deref.

Worth a bounded test over the whole u8 range: each byte must render as exactly two
lowercase hex digits that parse back to the original value. That catches leading-zero bugs
a couple of example vectors would miss.

Also in this family

sha2 0.11 does not implement std::io::Write either, so io::copy(&mut reader, &mut hasher)
stops compiling. I did not find that pattern in this repo, but if one appears, use a bounded
buffered read loop or a small io::Write adapter newtype that forwards to hasher.update().

The sibling RustCrypto crates move in lockstep and carry the same break — sha1/sha3/md-5
go to 0.11, and hmac/hkdf/pbkdf2 go to 0.13.

Validation

  • cargo fmt --all -- --check
  • cargo clippy --workspace --all-targets --all-features -- -D warnings
  • cargo test (or the repo's make test)

Reference

Worked example: leynos/whitaker#296, which migrated the installer crate and added the
hardening described above. installer/src/hex.rs there is the encoder; the digest call sites
are in installer/src/dependency_binaries/install/checksum.rs.

Raised from an estate-wide survey of sha2 exposure. Sites were identified statically
(manifest and source inspection); they have not been compiled against 0.11.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingmediumRoadmap items to schedule within the current quarter. Clear scope, normal review cycles.testingTest coverage, test infrastructure, and verification tooling work.

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions