kernel: make the crypto/content-addressing stack actually WASM-safe (32-bit ints)

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 <noreply@anthropic.com>
This commit is contained in:
2026-06-29 07:51:08 +00:00
parent 4df4de7f79
commit fce9e0c617
4 changed files with 121 additions and 77 deletions

View File

@@ -15,25 +15,29 @@ exception Cbor_error of string
let write_head buf major v = let write_head buf major v =
let m = major lsl 5 in 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 if v < 24 then
Buffer.add_char buf (Char.chr (m lor v)) Buffer.add_char buf (Char.chr (m lor v))
else if v < 0x100 then begin else if v < 0x100 then begin
Buffer.add_char buf (Char.chr (m lor 24)); Buffer.add_char buf (Char.chr (m lor 24)); put_be 1
Buffer.add_char buf (Char.chr v)
end else if v < 0x10000 then begin end else if v < 0x10000 then begin
Buffer.add_char buf (Char.chr (m lor 25)); Buffer.add_char buf (Char.chr (m lor 25)); put_be 2
Buffer.add_char buf (Char.chr ((v lsr 8) land 0xFF)); end else if Int64.compare v64 0x100000000L < 0 then begin
Buffer.add_char buf (Char.chr (v land 0xFF)) Buffer.add_char buf (Char.chr (m lor 26)); put_be 4
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
end else begin end else begin
Buffer.add_char buf (Char.chr (m lor 27)); Buffer.add_char buf (Char.chr (m lor 27)); put_be 8
for i = 7 downto 0 do
Buffer.add_char buf (Char.chr ((v lsr (8 * i)) land 0xFF))
done
end end
(* dag-cbor map key order: shorter key first, then bytewise. *) (* dag-cbor map key order: shorter key first, then bytewise. *)

View File

@@ -32,7 +32,11 @@ let base32_lower (s : string) : string =
while !bits >= 5 do while !bits >= 5 do
bits := !bits - 5; bits := !bits - 5;
Buffer.add_char buf b32_alpha.[(!acc lsr !bits) land 0x1f] 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 if !bits > 0 then
Buffer.add_char buf b32_alpha.[(!acc lsl (5 - !bits)) land 0x1f]; Buffer.add_char buf b32_alpha.[(!acc lsl (5 - !bits)) land 0x1f];
Buffer.contents buf Buffer.contents buf

View File

@@ -68,15 +68,22 @@ let sub (a : bn) (b : bn) : bn =
norm r norm r
let mul (a : bn) (b : bn) : bn = 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 la = Array.length a and lb = Array.length b in
let r = Array.make (la + lb) 0 in let r = Array.make (la + lb) 0 in
let maskL = Int64.of_int mask in
for i = 0 to la - 1 do 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 for j = 0 to lb - 1 do
let s = r.(i + j) + a.(i) * b.(j) + !carry in let s = Int64.add (Int64.add (Int64.of_int r.(i + j))
r.(i + j) <- s land mask; carry := s lsr bits (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; done;
r.(i + lb) <- r.(i + lb) + !carry r.(i + lb) <- r.(i + lb) + Int64.to_int !carry
done; done;
norm r norm r
@@ -109,12 +116,16 @@ let bn_mod (a : bn) (m : bn) : bn =
end end
let div_small (a : bn) (d : int) : bn = 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 la = Array.length a in
let q = Array.make la 0 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 for i = la - 1 downto 0 do
let cur = (!rem lsl bits) lor a.(i) in let cur = Int64.logor (Int64.shift_left !rem bits) (Int64.of_int a.(i)) in
q.(i) <- cur / d; rem := cur mod d q.(i) <- Int64.to_int (Int64.div cur dL);
rem := Int64.rem cur dL
done; done;
norm q norm q

View File

@@ -3,37 +3,40 @@
No C stubs, no external deps. Used by the fed-sx host primitives No C stubs, no external deps. Used by the fed-sx host primitives
[crypto-sha256] / [crypto-sha512]. Reference: FIPS 180-4. *) [crypto-sha256] / [crypto-sha512]. Reference: FIPS 180-4. *)
(* ---- SHA-256 (FIPS 180-4 §6.2). 32-bit words held in native int, (* ---- SHA-256 (FIPS 180-4 §6.2). 32-bit words via Int32, NOT native int.
masked to 32 bits after every arithmetic op. ---- *) 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
let mask32 = 0xFFFFFFFF 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 = [| let k256 = [|
0x428a2f98; 0x71374491; 0xb5c0fbcf; 0xe9b5dba5; 0x428a2f98l; 0x71374491l; 0xb5c0fbcfl; 0xe9b5dba5l;
0x3956c25b; 0x59f111f1; 0x923f82a4; 0xab1c5ed5; 0x3956c25bl; 0x59f111f1l; 0x923f82a4l; 0xab1c5ed5l;
0xd807aa98; 0x12835b01; 0x243185be; 0x550c7dc3; 0xd807aa98l; 0x12835b01l; 0x243185bel; 0x550c7dc3l;
0x72be5d74; 0x80deb1fe; 0x9bdc06a7; 0xc19bf174; 0x72be5d74l; 0x80deb1fel; 0x9bdc06a7l; 0xc19bf174l;
0xe49b69c1; 0xefbe4786; 0x0fc19dc6; 0x240ca1cc; 0xe49b69c1l; 0xefbe4786l; 0x0fc19dc6l; 0x240ca1ccl;
0x2de92c6f; 0x4a7484aa; 0x5cb0a9dc; 0x76f988da; 0x2de92c6fl; 0x4a7484aal; 0x5cb0a9dcl; 0x76f988dal;
0x983e5152; 0xa831c66d; 0xb00327c8; 0xbf597fc7; 0x983e5152l; 0xa831c66dl; 0xb00327c8l; 0xbf597fc7l;
0xc6e00bf3; 0xd5a79147; 0x06ca6351; 0x14292967; 0xc6e00bf3l; 0xd5a79147l; 0x06ca6351l; 0x14292967l;
0x27b70a85; 0x2e1b2138; 0x4d2c6dfc; 0x53380d13; 0x27b70a85l; 0x2e1b2138l; 0x4d2c6dfcl; 0x53380d13l;
0x650a7354; 0x766a0abb; 0x81c2c92e; 0x92722c85; 0x650a7354l; 0x766a0abbl; 0x81c2c92el; 0x92722c85l;
0xa2bfe8a1; 0xa81a664b; 0xc24b8b70; 0xc76c51a3; 0xa2bfe8a1l; 0xa81a664bl; 0xc24b8b70l; 0xc76c51a3l;
0xd192e819; 0xd6990624; 0xf40e3585; 0x106aa070; 0xd192e819l; 0xd6990624l; 0xf40e3585l; 0x106aa070l;
0x19a4c116; 0x1e376c08; 0x2748774c; 0x34b0bcb5; 0x19a4c116l; 0x1e376c08l; 0x2748774cl; 0x34b0bcb5l;
0x391c0cb3; 0x4ed8aa4a; 0x5b9cca4f; 0x682e6ff3; 0x391c0cb3l; 0x4ed8aa4al; 0x5b9cca4fl; 0x682e6ff3l;
0x748f82ee; 0x78a5636f; 0x84c87814; 0x8cc70208; 0x748f82eel; 0x78a5636fl; 0x84c87814l; 0x8cc70208l;
0x90befffa; 0xa4506ceb; 0xbef9a3f7; 0xc67178f2 |] 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 sha256_hex (msg : string) : string =
let h = [| 0x6a09e667; 0xbb67ae85; 0x3c6ef372; 0xa54ff53a; let h = [| 0x6a09e667l; 0xbb67ae85l; 0x3c6ef372l; 0xa54ff53al;
0x510e527f; 0x9b05688c; 0x1f83d9ab; 0x5be0cd19 |] in 0x510e527fl; 0x9b05688cl; 0x1f83d9abl; 0x5be0cd19l |] in
let len = String.length msg in let len = String.length msg in
(* Padded length: multiple of 64 bytes. *) (* Padded length: multiple of 64 bytes. *)
let bitlen = len * 8 in let bitlen = Int64.mul (Int64.of_int len) 8L in
let padlen = let padlen =
let r = (len + 1) mod 64 in let r = (len + 1) mod 64 in
if r <= 56 then 56 - r else 120 - r 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 let buf = Bytes.make total '\000' in
Bytes.blit_string msg 0 buf 0 len; Bytes.blit_string msg 0 buf 0 len;
Bytes.set buf len '\x80'; 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 for i = 0 to 7 do
Bytes.set buf (total - 1 - i) 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; 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 let nblocks = total / 64 in
for b = 0 to nblocks - 1 do for b = 0 to nblocks - 1 do
let base = b * 64 in let base = b * 64 in
for t = 0 to 15 do for t = 0 to 15 do
let o = base + t * 4 in let o = base + t * 4 in
w.(t) <- w.(t) <-
(Char.code (Bytes.get buf o) lsl 24) Int32.logor
lor (Char.code (Bytes.get buf (o + 1)) lsl 16) (Int32.logor
lor (Char.code (Bytes.get buf (o + 2)) lsl 8) (Int32.shift_left (byte o) 24)
lor (Char.code (Bytes.get buf (o + 3))) (Int32.shift_left (byte (o + 1)) 16))
(Int32.logor
(Int32.shift_left (byte (o + 2)) 8)
(byte (o + 3)))
done; done;
for t = 16 to 63 do for t = 16 to 63 do
let s0 = let s0 =
(rotr32 w.(t - 15) 7) lxor (rotr32 w.(t - 15) 18) Int32.logxor
lxor (w.(t - 15) lsr 3) in (Int32.logxor (rotr32 w.(t - 15) 7) (rotr32 w.(t - 15) 18))
(Int32.shift_right_logical w.(t - 15) 3) in
let s1 = let s1 =
(rotr32 w.(t - 2) 17) lxor (rotr32 w.(t - 2) 19) Int32.logxor
lxor (w.(t - 2) lsr 10) in (Int32.logxor (rotr32 w.(t - 2) 17) (rotr32 w.(t - 2) 19))
w.(t) <- (w.(t - 16) + s0 + w.(t - 7) + s1) land mask32 (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; done;
let a = ref h.(0) and bb = ref h.(1) and c = ref h.(2) 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 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 and g = ref h.(6) and hh = ref h.(7) in
for t = 0 to 63 do for t = 0 to 63 do
let s1 = let s1 =
(rotr32 !e 6) lxor (rotr32 !e 11) lxor (rotr32 !e 25) in Int32.logxor
let ch = (!e land !f) lxor ((lnot !e land mask32) land !g) in (Int32.logxor (rotr32 !e 6) (rotr32 !e 11)) (rotr32 !e 25) in
let t1 = (!hh + s1 + ch + k256.(t) + w.(t)) land mask32 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 = let s0 =
(rotr32 !a 2) lxor (rotr32 !a 13) lxor (rotr32 !a 22) in Int32.logxor
let maj = (!a land !bb) lxor (!a land !c) lxor (!bb land !c) in (Int32.logxor (rotr32 !a 2) (rotr32 !a 13)) (rotr32 !a 22) in
let t2 = (s0 + maj) land mask32 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; hh := !g; g := !f; f := !e;
e := (!d + t1) land mask32; e := Int32.add !d t1;
d := !c; c := !bb; bb := !a; d := !c; c := !bb; bb := !a;
a := (t1 + t2) land mask32 a := Int32.add t1 t2
done; done;
h.(0) <- (h.(0) + !a) land mask32; h.(0) <- Int32.add h.(0) !a;
h.(1) <- (h.(1) + !bb) land mask32; h.(1) <- Int32.add h.(1) !bb;
h.(2) <- (h.(2) + !c) land mask32; h.(2) <- Int32.add h.(2) !c;
h.(3) <- (h.(3) + !d) land mask32; h.(3) <- Int32.add h.(3) !d;
h.(4) <- (h.(4) + !e) land mask32; h.(4) <- Int32.add h.(4) !e;
h.(5) <- (h.(5) + !f) land mask32; h.(5) <- Int32.add h.(5) !f;
h.(6) <- (h.(6) + !g) land mask32; h.(6) <- Int32.add h.(6) !g;
h.(7) <- (h.(7) + !hh) land mask32 h.(7) <- Int32.add h.(7) !hh
done; done;
let out = Buffer.create 64 in 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 Buffer.contents out
(* ---- SHA-512 (FIPS 180-4 §6.4). 64-bit words via Int64. (* ---- SHA-512 (FIPS 180-4 §6.4). 64-bit words via Int64.
@@ -146,7 +168,7 @@ let sha512_hex (msg : string) : string =
0x510e527fade682d1L; 0x9b05688c2b3e6c1fL; 0x510e527fade682d1L; 0x9b05688c2b3e6c1fL;
0x1f83d9abfb41bd6bL; 0x5be0cd19137e2179L |] in 0x1f83d9abfb41bd6bL; 0x5be0cd19137e2179L |] in
let len = String.length msg 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. *) (* Pad to a multiple of 128 bytes; 16-byte big-endian length. *)
let padlen = let padlen =
let r = (len + 1) mod 128 in let r = (len + 1) mod 128 in
@@ -156,9 +178,12 @@ let sha512_hex (msg : string) : string =
let buf = Bytes.make total '\000' in let buf = Bytes.make total '\000' in
Bytes.blit_string msg 0 buf 0 len; Bytes.blit_string msg 0 buf 0 len;
Bytes.set buf len '\x80'; 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 for i = 0 to 7 do
Bytes.set buf (total - 1 - i) 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; done;
let w = Array.make 80 0L in let w = Array.make 80 0L in
let nblocks = total / 128 in let nblocks = total / 128 in