From 95bdd7305cc4e99b29baf6e549849a0326e79df0 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 19 Oct 2026 23:22:51 -0500 Subject: [PATCH] Unroll the MD5 block function scr_md5_block ran its 64 steps as one loop that chose the round function, message index and rotation per step, and the update path called it once per block. scr_md5_blocks unrolls the steps with the message word, constant and rotation inlined, processes every whole block of an update in one call, and keeps each step's critical path to the round function of b, an add, the rotate and the add of b: a + m + K is summed first, G is written as (b & d) + (c & ~d) so that its c & ~d half joins that sum, and an empty asm statement keeps the compiler from moving the constant's add after the round function. MD5, HMAC-MD5 and md5-sha1 use it. Corpus 5775 compares MD5, md5-sha1 and HMAC-MD5 digests over every length from 0 to 520 bytes in five update sizes and over 1 MiB with Node; tests/harness/corpus-costs/ records its plain and sanitized costs. --- .../tests/corpus/5775-md5-lengths.ts.json | 6 + packages/runtime/src/scr_lib.c | 153 +++++++++++++----- tests/corpus/5775-md5-lengths.ts | 34 ++++ .../corpus-costs/5775-md5-lengths.ts.json | 1 + 4 files changed, 151 insertions(+), 43 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5775-md5-lengths.ts.json create mode 100644 tests/corpus/5775-md5-lengths.ts create mode 100644 tests/harness/corpus-costs/5775-md5-lengths.ts.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5775-md5-lengths.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5775-md5-lengths.ts.json new file mode 100644 index 000000000..84c394e4a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5775-md5-lengths.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5775-md5-lengths.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index 930d44818..ce7de9654 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -7263,45 +7263,112 @@ static ScrStr *scr_digest_encode(const unsigned char *d, size_t n, int enc) { /* ── MD5 (RFC 1321) ───────────────────────────────────────────────── */ -static const uint32_t scr_md5_k[64] = { - 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, - 0xa8304613, 0xfd469501, 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, - 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821, 0xf61e2562, 0xc040b340, - 0x265e5a51, 0xe9b6c7aa, 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8, - 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8, - 0x676f02d9, 0x8d2a4c8a, 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, - 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70, 0x289b7ec6, 0xeaa127fa, - 0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665, - 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, - 0xffeff47d, 0x85845dd1, 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, - 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391}; -static const unsigned char scr_md5_r[64] = { - 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, - 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, - 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, - 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21}; +/* The 64 steps are unrolled with their message word, constant and + * rotation inlined. a + m + K does not depend on the previous step, so + * each step's critical path is the round function of b, an add, the + * rotate and the add of b. G is written as (b & d) + (c & ~d) (the two + * terms share no bits) so that its c & ~d half joins that early sum. The + * empty asm statement keeps the compiler from moving the constant's add + * after the round function, which puts it back on the critical path. */ +#if (defined(__GNUC__) || defined(__clang__)) && !defined(__wasm__) +#define SCR_MD5_PIN(v) __asm__("" : "+r"(v)) +#else +#define SCR_MD5_PIN(v) ((void)0) +#endif +#define SCR_MD5_F(b, c, d) ((d) ^ ((b) & ((c) ^ (d)))) +#define SCR_MD5_H(b, c, d) ((b) ^ (c) ^ (d)) +#define SCR_MD5_I(b, c, d) ((c) ^ ((b) | ~(d))) +#define SCR_MD5_STEP(f, a, b, c, d, xk, s) \ + do { \ + (a) += (xk); \ + SCR_MD5_PIN(a); \ + (a) += f((b), (c), (d)); \ + (a) = ((a) << (s) | (a) >> (32 - (s))) + (b); \ + } while (0) +#define SCR_MD5_GSTEP(a, b, c, d, xk, s) \ + do { \ + (a) += ((c) & ~(d)) + (xk); \ + SCR_MD5_PIN(a); \ + (a) += (b) & (d); \ + (a) = ((a) << (s) | (a) >> (32 - (s))) + (b); \ + } while (0) -SCR_DIGEST_ALIGNED static void scr_md5_block(uint32_t h[4], const unsigned char *p) { - uint32_t m[16]; - for (int i = 0; i < 16; i++) { - m[i] = (uint32_t)p[i * 4] | ((uint32_t)p[i * 4 + 1] << 8) | - ((uint32_t)p[i * 4 + 2] << 16) | ((uint32_t)p[i * 4 + 3] << 24); - } +SCR_DIGEST_ALIGNED static void scr_md5_blocks(uint32_t h[4], const unsigned char *p, size_t blocks) { uint32_t a = h[0], b = h[1], c = h[2], d = h[3]; - for (int i = 0; i < 64; i++) { - uint32_t f, g; - if (i < 16) { f = (b & c) | (~b & d); g = (uint32_t)i; } - else if (i < 32) { f = (d & b) | (~d & c); g = (5u * i + 1) & 15; } - else if (i < 48) { f = b ^ c ^ d; g = (3u * i + 5) & 15; } - else { f = c ^ (b | ~d); g = (7u * i) & 15; } - uint32_t t = d; - d = c; - c = b; - uint32_t x = a + f + scr_md5_k[i] + m[g]; - b = b + ((x << scr_md5_r[i]) | (x >> (32 - scr_md5_r[i]))); - a = t; + for (; blocks != 0; blocks--, p += 64) { + uint32_t m[16]; + for (int i = 0; i < 16; i++) { + m[i] = (uint32_t)p[i * 4] | ((uint32_t)p[i * 4 + 1] << 8) | + ((uint32_t)p[i * 4 + 2] << 16) | ((uint32_t)p[i * 4 + 3] << 24); + } + uint32_t a0 = a, b0 = b, c0 = c, d0 = d; + SCR_MD5_STEP(SCR_MD5_F, a, b, c, d, m[0] + 0xd76aa478u, 7); + SCR_MD5_STEP(SCR_MD5_F, d, a, b, c, m[1] + 0xe8c7b756u, 12); + SCR_MD5_STEP(SCR_MD5_F, c, d, a, b, m[2] + 0x242070dbu, 17); + SCR_MD5_STEP(SCR_MD5_F, b, c, d, a, m[3] + 0xc1bdceeeu, 22); + SCR_MD5_STEP(SCR_MD5_F, a, b, c, d, m[4] + 0xf57c0fafu, 7); + SCR_MD5_STEP(SCR_MD5_F, d, a, b, c, m[5] + 0x4787c62au, 12); + SCR_MD5_STEP(SCR_MD5_F, c, d, a, b, m[6] + 0xa8304613u, 17); + SCR_MD5_STEP(SCR_MD5_F, b, c, d, a, m[7] + 0xfd469501u, 22); + SCR_MD5_STEP(SCR_MD5_F, a, b, c, d, m[8] + 0x698098d8u, 7); + SCR_MD5_STEP(SCR_MD5_F, d, a, b, c, m[9] + 0x8b44f7afu, 12); + SCR_MD5_STEP(SCR_MD5_F, c, d, a, b, m[10] + 0xffff5bb1u, 17); + SCR_MD5_STEP(SCR_MD5_F, b, c, d, a, m[11] + 0x895cd7beu, 22); + SCR_MD5_STEP(SCR_MD5_F, a, b, c, d, m[12] + 0x6b901122u, 7); + SCR_MD5_STEP(SCR_MD5_F, d, a, b, c, m[13] + 0xfd987193u, 12); + SCR_MD5_STEP(SCR_MD5_F, c, d, a, b, m[14] + 0xa679438eu, 17); + SCR_MD5_STEP(SCR_MD5_F, b, c, d, a, m[15] + 0x49b40821u, 22); + SCR_MD5_GSTEP(a, b, c, d, m[1] + 0xf61e2562u, 5); + SCR_MD5_GSTEP(d, a, b, c, m[6] + 0xc040b340u, 9); + SCR_MD5_GSTEP(c, d, a, b, m[11] + 0x265e5a51u, 14); + SCR_MD5_GSTEP(b, c, d, a, m[0] + 0xe9b6c7aau, 20); + SCR_MD5_GSTEP(a, b, c, d, m[5] + 0xd62f105du, 5); + SCR_MD5_GSTEP(d, a, b, c, m[10] + 0x02441453u, 9); + SCR_MD5_GSTEP(c, d, a, b, m[15] + 0xd8a1e681u, 14); + SCR_MD5_GSTEP(b, c, d, a, m[4] + 0xe7d3fbc8u, 20); + SCR_MD5_GSTEP(a, b, c, d, m[9] + 0x21e1cde6u, 5); + SCR_MD5_GSTEP(d, a, b, c, m[14] + 0xc33707d6u, 9); + SCR_MD5_GSTEP(c, d, a, b, m[3] + 0xf4d50d87u, 14); + SCR_MD5_GSTEP(b, c, d, a, m[8] + 0x455a14edu, 20); + SCR_MD5_GSTEP(a, b, c, d, m[13] + 0xa9e3e905u, 5); + SCR_MD5_GSTEP(d, a, b, c, m[2] + 0xfcefa3f8u, 9); + SCR_MD5_GSTEP(c, d, a, b, m[7] + 0x676f02d9u, 14); + SCR_MD5_GSTEP(b, c, d, a, m[12] + 0x8d2a4c8au, 20); + SCR_MD5_STEP(SCR_MD5_H, a, b, c, d, m[5] + 0xfffa3942u, 4); + SCR_MD5_STEP(SCR_MD5_H, d, a, b, c, m[8] + 0x8771f681u, 11); + SCR_MD5_STEP(SCR_MD5_H, c, d, a, b, m[11] + 0x6d9d6122u, 16); + SCR_MD5_STEP(SCR_MD5_H, b, c, d, a, m[14] + 0xfde5380cu, 23); + SCR_MD5_STEP(SCR_MD5_H, a, b, c, d, m[1] + 0xa4beea44u, 4); + SCR_MD5_STEP(SCR_MD5_H, d, a, b, c, m[4] + 0x4bdecfa9u, 11); + SCR_MD5_STEP(SCR_MD5_H, c, d, a, b, m[7] + 0xf6bb4b60u, 16); + SCR_MD5_STEP(SCR_MD5_H, b, c, d, a, m[10] + 0xbebfbc70u, 23); + SCR_MD5_STEP(SCR_MD5_H, a, b, c, d, m[13] + 0x289b7ec6u, 4); + SCR_MD5_STEP(SCR_MD5_H, d, a, b, c, m[0] + 0xeaa127fau, 11); + SCR_MD5_STEP(SCR_MD5_H, c, d, a, b, m[3] + 0xd4ef3085u, 16); + SCR_MD5_STEP(SCR_MD5_H, b, c, d, a, m[6] + 0x04881d05u, 23); + SCR_MD5_STEP(SCR_MD5_H, a, b, c, d, m[9] + 0xd9d4d039u, 4); + SCR_MD5_STEP(SCR_MD5_H, d, a, b, c, m[12] + 0xe6db99e5u, 11); + SCR_MD5_STEP(SCR_MD5_H, c, d, a, b, m[15] + 0x1fa27cf8u, 16); + SCR_MD5_STEP(SCR_MD5_H, b, c, d, a, m[2] + 0xc4ac5665u, 23); + SCR_MD5_STEP(SCR_MD5_I, a, b, c, d, m[0] + 0xf4292244u, 6); + SCR_MD5_STEP(SCR_MD5_I, d, a, b, c, m[7] + 0x432aff97u, 10); + SCR_MD5_STEP(SCR_MD5_I, c, d, a, b, m[14] + 0xab9423a7u, 15); + SCR_MD5_STEP(SCR_MD5_I, b, c, d, a, m[5] + 0xfc93a039u, 21); + SCR_MD5_STEP(SCR_MD5_I, a, b, c, d, m[12] + 0x655b59c3u, 6); + SCR_MD5_STEP(SCR_MD5_I, d, a, b, c, m[3] + 0x8f0ccc92u, 10); + SCR_MD5_STEP(SCR_MD5_I, c, d, a, b, m[10] + 0xffeff47du, 15); + SCR_MD5_STEP(SCR_MD5_I, b, c, d, a, m[1] + 0x85845dd1u, 21); + SCR_MD5_STEP(SCR_MD5_I, a, b, c, d, m[8] + 0x6fa87e4fu, 6); + SCR_MD5_STEP(SCR_MD5_I, d, a, b, c, m[15] + 0xfe2ce6e0u, 10); + SCR_MD5_STEP(SCR_MD5_I, c, d, a, b, m[6] + 0xa3014314u, 15); + SCR_MD5_STEP(SCR_MD5_I, b, c, d, a, m[13] + 0x4e0811a1u, 21); + SCR_MD5_STEP(SCR_MD5_I, a, b, c, d, m[4] + 0xf7537e82u, 6); + SCR_MD5_STEP(SCR_MD5_I, d, a, b, c, m[11] + 0xbd3af235u, 10); + SCR_MD5_STEP(SCR_MD5_I, c, d, a, b, m[2] + 0x2ad7d2bbu, 15); + SCR_MD5_STEP(SCR_MD5_I, b, c, d, a, m[9] + 0xeb86d391u, 21); + a += a0; b += b0; c += c0; d += d0; } - h[0] += a; h[1] += b; h[2] += c; h[3] += d; + h[0] = a; h[1] = b; h[2] = c; h[3] = d; } typedef struct ScrMd5Ctx { @@ -7329,14 +7396,15 @@ static void scr_md5_update(ScrMd5Ctx *ctx, const unsigned char *data, size_t len data += take; len -= take; if (ctx->tail_len == 64) { - scr_md5_block(ctx->h, ctx->tail); + scr_md5_blocks(ctx->h, ctx->tail, 1); ctx->tail_len = 0; } } - while (len >= 64) { - scr_md5_block(ctx->h, data); - data += 64; - len -= 64; + if (len >= 64) { + size_t blocks = len / 64; + scr_md5_blocks(ctx->h, data, blocks); + data += blocks * 64; + len -= blocks * 64; } if (len != 0) { memcpy(ctx->tail, data, len); @@ -7354,8 +7422,7 @@ static size_t scr_md5_final(const ScrMd5Ctx *source, unsigned char out[32]) { memset(tail + rem + 1, 0, pad - rem - 1 - 8); uint64_t bits = ctx.bytes * 8; for (int b = 0; b < 8; b++) tail[pad - 8 + b] = (unsigned char)(bits >> (8 * b)); - scr_md5_block(ctx.h, tail); - if (pad == 128) scr_md5_block(ctx.h, tail + 64); + scr_md5_blocks(ctx.h, tail, pad / 64); for (int j = 0; j < 4; j++) { for (int b = 0; b < 4; b++) out[j * 4 + b] = (unsigned char)(ctx.h[j] >> (8 * b)); } diff --git a/tests/corpus/5775-md5-lengths.ts b/tests/corpus/5775-md5-lengths.ts new file mode 100644 index 000000000..957491568 --- /dev/null +++ b/tests/corpus/5775-md5-lengths.ts @@ -0,0 +1,34 @@ +// MD5 and md5-sha1 over every length from 0 to 520 bytes, fed whole and +// in 1-, 7-, 64- and 100-byte updates, then over 1 MiB, and HMAC-MD5 +// over a few lengths. Each length's digests are folded into one running +// SHA-512 so the output stays short. +import { createHash, createHmac } from "node:crypto"; + +const data = Buffer.alloc(1 << 20); +let x = 0x9e3779b9; +for (let i = 0; i < data.length; i++) { + x ^= x << 13; + x ^= x >>> 17; + x ^= x << 5; + data[i] = x & 0xff; +} + +function digest(algorithm: string, input: Buffer, chunk: number): Buffer { + const hash = createHash(algorithm); + if (chunk === 0) hash.update(input); + else for (let at = 0; at < input.length; at += chunk) hash.update(input.subarray(at, at + chunk)); + return hash.digest(); +} + +for (const algorithm of ["md5", "md5-sha1"]) { + for (const chunk of [0, 1, 7, 64, 100]) { + const fold = createHash("sha512"); + for (let length = 0; length <= 520; length++) fold.update(digest(algorithm, data.subarray(0, length), chunk)); + console.log(algorithm, "chunk", chunk, fold.digest("hex").slice(0, 32)); + } + console.log(algorithm, "1 MiB", digest(algorithm, data, 0).toString("hex").slice(0, 32), digest(algorithm, data, 4096).toString("hex").slice(0, 32)); +} +console.log("abc", createHash("md5").update("abc").digest("hex")); +for (const length of [0, 1, 55, 56, 63, 64, 65, 119, 120, 1000]) { + console.log("hmac", length, createHmac("md5", "key").update(data.subarray(0, length)).digest("hex").slice(0, 32)); +} diff --git a/tests/harness/corpus-costs/5775-md5-lengths.ts.json b/tests/harness/corpus-costs/5775-md5-lengths.ts.json new file mode 100644 index 000000000..70f936cec --- /dev/null +++ b/tests/harness/corpus-costs/5775-md5-lengths.ts.json @@ -0,0 +1 @@ +{"plain":2850,"san":3660}