From fce9e0c617069856d6d3e9b95f39977ebc912c8b Mon Sep 17 00:00:00 2001 From: giles Date: Mon, 29 Jun 2026 07:51:08 +0000 Subject: [PATCH] kernel: make the crypto/content-addressing stack actually WASM-safe (32-bit ints) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The kernel's sha2/cbor/cid/ed25519 modules were labelled 'WASM-safe' but assumed 63-bit native int. On the web targets — js_of_ocaml (32-bit int) and wasm_of_ocaml (31-bit int) — they truncated, producing wrong digests/CIDs and a Char.chr crash at kernel INIT (ed25519 precomputes sqrtm1 + base_point at module load, driving the base-2^26 bignum). This is why a freshly-built browser kernel crashed on boot while the stale committed artifact (older toolchain) still ran. Fixes (all verified bit-identical to the 63-bit native build, conformance 271/271): - sx_sha2: SHA-256 round words via Int32 (were native int + land 0xFFFFFFFF, which is a no-op on 31-bit and overflows the constants); both SHA-256/512 length-encoding via Int64 shifts (native "lsr 32" is shift-mod-32 on js, which leaked the length byte into a higher word). NIST vectors pass native/js/wasm. - sx_cbor: write_head width selection + byte emission via Int64 (the 0x100000000 literal truncated to 0 on js, sending small ints to the 8-byte branch; and "v lsr (8*i)" with i>=4 was shift-mod-32). - sx_cid: base32_lower keeps acc bounded to the unconsumed low bits (it grew 8 bits/byte and overflowed). cid_from_sx now matches native<->js exactly. - sx_ed25519: bignum mul accumulates in Int64 (26x26=52-bit products overflow); div_small running remainder in Int64 (rem<<26 ~= 2^34). This was the boot gate — the browser kernel now boots (SxKernel live, crypto-sha256 correct on js). Co-Authored-By: Claude Opus 4.8 --- hosts/ocaml/lib/sx_cbor.ml | 32 ++++---- hosts/ocaml/lib/sx_cid.ml | 6 +- hosts/ocaml/lib/sx_ed25519.ml | 25 +++++-- hosts/ocaml/lib/sx_sha2.ml | 135 ++++++++++++++++++++-------------- 4 files changed, 121 insertions(+), 77 deletions(-) diff --git a/hosts/ocaml/lib/sx_cbor.ml b/hosts/ocaml/lib/sx_cbor.ml index b4ec7ba1..97affef6 100644 --- a/hosts/ocaml/lib/sx_cbor.ml +++ b/hosts/ocaml/lib/sx_cbor.ml @@ -15,25 +15,29 @@ exception Cbor_error of string let write_head buf major v = let m = major lsl 5 in + (* Width selection + big-endian byte emission via Int64, so the web targets + compute identically to native: on js_of_ocaml [int] is 32-bit, so the + literal 0x100000000 (2^32) truncates to 0 (sending small values to the + 8-byte branch) and [v lsr (8*i)] with i>=4 is shift-mod-32. Int64 has the + full 64-bit width and well-defined shifts on every target. *) + let v64 = Int64.of_int v in + let put_be nbytes = + for i = nbytes - 1 downto 0 do + Buffer.add_char buf + (Char.chr (Int64.to_int + (Int64.logand (Int64.shift_right_logical v64 (8 * i)) 0xFFL))) + done + in if v < 24 then Buffer.add_char buf (Char.chr (m lor v)) else if v < 0x100 then begin - Buffer.add_char buf (Char.chr (m lor 24)); - Buffer.add_char buf (Char.chr v) + Buffer.add_char buf (Char.chr (m lor 24)); put_be 1 end else if v < 0x10000 then begin - Buffer.add_char buf (Char.chr (m lor 25)); - Buffer.add_char buf (Char.chr ((v lsr 8) land 0xFF)); - Buffer.add_char buf (Char.chr (v land 0xFF)) - end else if v < 0x100000000 then begin - Buffer.add_char buf (Char.chr (m lor 26)); - for i = 3 downto 0 do - Buffer.add_char buf (Char.chr ((v lsr (8 * i)) land 0xFF)) - done + Buffer.add_char buf (Char.chr (m lor 25)); put_be 2 + end else if Int64.compare v64 0x100000000L < 0 then begin + Buffer.add_char buf (Char.chr (m lor 26)); put_be 4 end else begin - Buffer.add_char buf (Char.chr (m lor 27)); - for i = 7 downto 0 do - Buffer.add_char buf (Char.chr ((v lsr (8 * i)) land 0xFF)) - done + Buffer.add_char buf (Char.chr (m lor 27)); put_be 8 end (* dag-cbor map key order: shorter key first, then bytewise. *) diff --git a/hosts/ocaml/lib/sx_cid.ml b/hosts/ocaml/lib/sx_cid.ml index 380fef01..d31a5932 100644 --- a/hosts/ocaml/lib/sx_cid.ml +++ b/hosts/ocaml/lib/sx_cid.ml @@ -32,7 +32,11 @@ let base32_lower (s : string) : string = while !bits >= 5 do bits := !bits - 5; Buffer.add_char buf b32_alpha.[(!acc lsr !bits) land 0x1f] - done) s; + done; + (* Keep only the unconsumed low [bits] bits, so [acc] stays tiny (< 2^13). + Without this it grows by 8 bits per byte and overflows native [int] on + the 32-bit web targets, corrupting the emitted symbols. *) + acc := !acc land ((1 lsl !bits) - 1)) s; if !bits > 0 then Buffer.add_char buf b32_alpha.[(!acc lsl (5 - !bits)) land 0x1f]; Buffer.contents buf diff --git a/hosts/ocaml/lib/sx_ed25519.ml b/hosts/ocaml/lib/sx_ed25519.ml index 0b7a42bc..1a929a6e 100644 --- a/hosts/ocaml/lib/sx_ed25519.ml +++ b/hosts/ocaml/lib/sx_ed25519.ml @@ -68,15 +68,22 @@ let sub (a : bn) (b : bn) : bn = norm r let mul (a : bn) (b : bn) : bn = + (* Accumulate in Int64: a limb product is 26+26 = 52 bits, which overflows the + web targets' int (32-bit js_of_ocaml / 31-bit wasm_of_ocaml). Int64 is a + real 64-bit type on every target, so the carries are exact. *) let la = Array.length a and lb = Array.length b in let r = Array.make (la + lb) 0 in + let maskL = Int64.of_int mask in for i = 0 to la - 1 do - let carry = ref 0 in + let carry = ref 0L in + let ai = Int64.of_int a.(i) in for j = 0 to lb - 1 do - let s = r.(i + j) + a.(i) * b.(j) + !carry in - r.(i + j) <- s land mask; carry := s lsr bits + let s = Int64.add (Int64.add (Int64.of_int r.(i + j)) + (Int64.mul ai (Int64.of_int b.(j)))) !carry in + r.(i + j) <- Int64.to_int (Int64.logand s maskL); + carry := Int64.shift_right_logical s bits done; - r.(i + lb) <- r.(i + lb) + !carry + r.(i + lb) <- r.(i + lb) + Int64.to_int !carry done; norm r @@ -109,12 +116,16 @@ let bn_mod (a : bn) (m : bn) : bn = end let div_small (a : bn) (d : int) : bn = + (* [rem lsl bits] reaches ~2^34 (rem < d <= 256, bits = 26), past the web + targets' int width — accumulate the running remainder in Int64. *) let la = Array.length a in let q = Array.make la 0 in - let rem = ref 0 in + let rem = ref 0L in + let dL = Int64.of_int d in for i = la - 1 downto 0 do - let cur = (!rem lsl bits) lor a.(i) in - q.(i) <- cur / d; rem := cur mod d + let cur = Int64.logor (Int64.shift_left !rem bits) (Int64.of_int a.(i)) in + q.(i) <- Int64.to_int (Int64.div cur dL); + rem := Int64.rem cur dL done; norm q diff --git a/hosts/ocaml/lib/sx_sha2.ml b/hosts/ocaml/lib/sx_sha2.ml index 1ea6b8f8..2eb43e38 100644 --- a/hosts/ocaml/lib/sx_sha2.ml +++ b/hosts/ocaml/lib/sx_sha2.ml @@ -3,37 +3,40 @@ No C stubs, no external deps. Used by the fed-sx host primitives [crypto-sha256] / [crypto-sha512]. Reference: FIPS 180-4. *) -(* ---- SHA-256 (FIPS 180-4 §6.2). 32-bit words held in native int, - masked to 32 bits after every arithmetic op. ---- *) - -let mask32 = 0xFFFFFFFF +(* ---- SHA-256 (FIPS 180-4 §6.2). 32-bit words via Int32, NOT native int. + On the web targets the kernel is compiled by js_of_ocaml (32-bit int) and + wasm_of_ocaml (31-bit int), where native [int] silently truncates the 32-bit + round words — producing WRONG digests (and, downstream, bad CIDs and a + Char.chr crash at kernel init). Int32 has well-defined wrap-around mod 2^32 on + every target, so this matches the 63-bit native build exactly. ---- *) let k256 = [| - 0x428a2f98; 0x71374491; 0xb5c0fbcf; 0xe9b5dba5; - 0x3956c25b; 0x59f111f1; 0x923f82a4; 0xab1c5ed5; - 0xd807aa98; 0x12835b01; 0x243185be; 0x550c7dc3; - 0x72be5d74; 0x80deb1fe; 0x9bdc06a7; 0xc19bf174; - 0xe49b69c1; 0xefbe4786; 0x0fc19dc6; 0x240ca1cc; - 0x2de92c6f; 0x4a7484aa; 0x5cb0a9dc; 0x76f988da; - 0x983e5152; 0xa831c66d; 0xb00327c8; 0xbf597fc7; - 0xc6e00bf3; 0xd5a79147; 0x06ca6351; 0x14292967; - 0x27b70a85; 0x2e1b2138; 0x4d2c6dfc; 0x53380d13; - 0x650a7354; 0x766a0abb; 0x81c2c92e; 0x92722c85; - 0xa2bfe8a1; 0xa81a664b; 0xc24b8b70; 0xc76c51a3; - 0xd192e819; 0xd6990624; 0xf40e3585; 0x106aa070; - 0x19a4c116; 0x1e376c08; 0x2748774c; 0x34b0bcb5; - 0x391c0cb3; 0x4ed8aa4a; 0x5b9cca4f; 0x682e6ff3; - 0x748f82ee; 0x78a5636f; 0x84c87814; 0x8cc70208; - 0x90befffa; 0xa4506ceb; 0xbef9a3f7; 0xc67178f2 |] + 0x428a2f98l; 0x71374491l; 0xb5c0fbcfl; 0xe9b5dba5l; + 0x3956c25bl; 0x59f111f1l; 0x923f82a4l; 0xab1c5ed5l; + 0xd807aa98l; 0x12835b01l; 0x243185bel; 0x550c7dc3l; + 0x72be5d74l; 0x80deb1fel; 0x9bdc06a7l; 0xc19bf174l; + 0xe49b69c1l; 0xefbe4786l; 0x0fc19dc6l; 0x240ca1ccl; + 0x2de92c6fl; 0x4a7484aal; 0x5cb0a9dcl; 0x76f988dal; + 0x983e5152l; 0xa831c66dl; 0xb00327c8l; 0xbf597fc7l; + 0xc6e00bf3l; 0xd5a79147l; 0x06ca6351l; 0x14292967l; + 0x27b70a85l; 0x2e1b2138l; 0x4d2c6dfcl; 0x53380d13l; + 0x650a7354l; 0x766a0abbl; 0x81c2c92el; 0x92722c85l; + 0xa2bfe8a1l; 0xa81a664bl; 0xc24b8b70l; 0xc76c51a3l; + 0xd192e819l; 0xd6990624l; 0xf40e3585l; 0x106aa070l; + 0x19a4c116l; 0x1e376c08l; 0x2748774cl; 0x34b0bcb5l; + 0x391c0cb3l; 0x4ed8aa4al; 0x5b9cca4fl; 0x682e6ff3l; + 0x748f82eel; 0x78a5636fl; 0x84c87814l; 0x8cc70208l; + 0x90befffal; 0xa4506cebl; 0xbef9a3f7l; 0xc67178f2l |] -let rotr32 x n = ((x lsr n) lor (x lsl (32 - n))) land mask32 +let rotr32 (x : int32) (n : int) : int32 = + Int32.logor (Int32.shift_right_logical x n) (Int32.shift_left x (32 - n)) let sha256_hex (msg : string) : string = - let h = [| 0x6a09e667; 0xbb67ae85; 0x3c6ef372; 0xa54ff53a; - 0x510e527f; 0x9b05688c; 0x1f83d9ab; 0x5be0cd19 |] in + let h = [| 0x6a09e667l; 0xbb67ae85l; 0x3c6ef372l; 0xa54ff53al; + 0x510e527fl; 0x9b05688cl; 0x1f83d9abl; 0x5be0cd19l |] in let len = String.length msg in (* Padded length: multiple of 64 bytes. *) - let bitlen = len * 8 in + let bitlen = Int64.mul (Int64.of_int len) 8L in let padlen = let r = (len + 1) mod 64 in if r <= 56 then 56 - r else 120 - r @@ -42,60 +45,79 @@ let sha256_hex (msg : string) : string = let buf = Bytes.make total '\000' in Bytes.blit_string msg 0 buf 0 len; Bytes.set buf len '\x80'; - (* 64-bit big-endian bit length (we cap at OCaml int range). *) + (* 64-bit big-endian bit length. Int64 shifts so the high bytes (shift >= 32) + are correct on the 32-bit web targets — native int `lsr 32` is shift-mod-32 + on js_of_ocaml and would leak the low length byte into a higher word. *) for i = 0 to 7 do Bytes.set buf (total - 1 - i) - (Char.chr ((bitlen lsr (8 * i)) land 0xFF)) + (Char.chr (Int64.to_int + (Int64.logand (Int64.shift_right_logical bitlen (8 * i)) 0xFFL))) done; - let w = Array.make 64 0 in + let byte i = Int32.of_int (Char.code (Bytes.get buf i)) in + let w = Array.make 64 0l in let nblocks = total / 64 in for b = 0 to nblocks - 1 do let base = b * 64 in for t = 0 to 15 do let o = base + t * 4 in w.(t) <- - (Char.code (Bytes.get buf o) lsl 24) - lor (Char.code (Bytes.get buf (o + 1)) lsl 16) - lor (Char.code (Bytes.get buf (o + 2)) lsl 8) - lor (Char.code (Bytes.get buf (o + 3))) + Int32.logor + (Int32.logor + (Int32.shift_left (byte o) 24) + (Int32.shift_left (byte (o + 1)) 16)) + (Int32.logor + (Int32.shift_left (byte (o + 2)) 8) + (byte (o + 3))) done; for t = 16 to 63 do let s0 = - (rotr32 w.(t - 15) 7) lxor (rotr32 w.(t - 15) 18) - lxor (w.(t - 15) lsr 3) in + Int32.logxor + (Int32.logxor (rotr32 w.(t - 15) 7) (rotr32 w.(t - 15) 18)) + (Int32.shift_right_logical w.(t - 15) 3) in let s1 = - (rotr32 w.(t - 2) 17) lxor (rotr32 w.(t - 2) 19) - lxor (w.(t - 2) lsr 10) in - w.(t) <- (w.(t - 16) + s0 + w.(t - 7) + s1) land mask32 + Int32.logxor + (Int32.logxor (rotr32 w.(t - 2) 17) (rotr32 w.(t - 2) 19)) + (Int32.shift_right_logical w.(t - 2) 10) in + w.(t) <- + Int32.add (Int32.add w.(t - 16) s0) (Int32.add w.(t - 7) s1) done; let a = ref h.(0) and bb = ref h.(1) and c = ref h.(2) and d = ref h.(3) and e = ref h.(4) and f = ref h.(5) and g = ref h.(6) and hh = ref h.(7) in for t = 0 to 63 do let s1 = - (rotr32 !e 6) lxor (rotr32 !e 11) lxor (rotr32 !e 25) in - let ch = (!e land !f) lxor ((lnot !e land mask32) land !g) in - let t1 = (!hh + s1 + ch + k256.(t) + w.(t)) land mask32 in + Int32.logxor + (Int32.logxor (rotr32 !e 6) (rotr32 !e 11)) (rotr32 !e 25) in + let ch = + Int32.logxor (Int32.logand !e !f) + (Int32.logand (Int32.lognot !e) !g) in + let t1 = + Int32.add + (Int32.add (Int32.add !hh s1) (Int32.add ch k256.(t))) w.(t) in let s0 = - (rotr32 !a 2) lxor (rotr32 !a 13) lxor (rotr32 !a 22) in - let maj = (!a land !bb) lxor (!a land !c) lxor (!bb land !c) in - let t2 = (s0 + maj) land mask32 in + Int32.logxor + (Int32.logxor (rotr32 !a 2) (rotr32 !a 13)) (rotr32 !a 22) in + let maj = + Int32.logxor + (Int32.logxor (Int32.logand !a !bb) (Int32.logand !a !c)) + (Int32.logand !bb !c) in + let t2 = Int32.add s0 maj in hh := !g; g := !f; f := !e; - e := (!d + t1) land mask32; + e := Int32.add !d t1; d := !c; c := !bb; bb := !a; - a := (t1 + t2) land mask32 + a := Int32.add t1 t2 done; - h.(0) <- (h.(0) + !a) land mask32; - h.(1) <- (h.(1) + !bb) land mask32; - h.(2) <- (h.(2) + !c) land mask32; - h.(3) <- (h.(3) + !d) land mask32; - h.(4) <- (h.(4) + !e) land mask32; - h.(5) <- (h.(5) + !f) land mask32; - h.(6) <- (h.(6) + !g) land mask32; - h.(7) <- (h.(7) + !hh) land mask32 + h.(0) <- Int32.add h.(0) !a; + h.(1) <- Int32.add h.(1) !bb; + h.(2) <- Int32.add h.(2) !c; + h.(3) <- Int32.add h.(3) !d; + h.(4) <- Int32.add h.(4) !e; + h.(5) <- Int32.add h.(5) !f; + h.(6) <- Int32.add h.(6) !g; + h.(7) <- Int32.add h.(7) !hh done; let out = Buffer.create 64 in - Array.iter (fun x -> Buffer.add_string out (Printf.sprintf "%08x" x)) h; + Array.iter (fun x -> Buffer.add_string out (Printf.sprintf "%08lx" x)) h; Buffer.contents out (* ---- SHA-512 (FIPS 180-4 §6.4). 64-bit words via Int64. @@ -146,7 +168,7 @@ let sha512_hex (msg : string) : string = 0x510e527fade682d1L; 0x9b05688c2b3e6c1fL; 0x1f83d9abfb41bd6bL; 0x5be0cd19137e2179L |] in let len = String.length msg in - let bitlen = len * 8 in + let bitlen = Int64.mul (Int64.of_int len) 8L in (* Pad to a multiple of 128 bytes; 16-byte big-endian length. *) let padlen = let r = (len + 1) mod 128 in @@ -156,9 +178,12 @@ let sha512_hex (msg : string) : string = let buf = Bytes.make total '\000' in Bytes.blit_string msg 0 buf 0 len; Bytes.set buf len '\x80'; + (* Low 64 bits of the bit length (high 64 stay 0). Int64 shifts so the bytes + at shift >= 32 are correct on the 32-bit web targets (js shift-mod-32). *) for i = 0 to 7 do Bytes.set buf (total - 1 - i) - (Char.chr ((bitlen lsr (8 * i)) land 0xFF)) + (Char.chr (Int64.to_int + (Int64.logand (Int64.shift_right_logical bitlen (8 * i)) 0xFFL))) done; let w = Array.make 80 0L in let nblocks = total / 128 in