@sproutboat/toolchain 0.4.21 → 0.4.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/package.json +1 -1
- package/src/acquire.test.ts +16 -1
- package/src/patch.ts +74 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# @sproutboat/toolchain
|
|
2
2
|
|
|
3
|
+
## 0.4.22
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 184fa15: Two more spec fixes for uuid (baronunread/sproutboat#238):
|
|
8
|
+
- **Unary `~`** now applies ToInt32 (wrapping modulo 2^32) instead of a saturating conversion. `~x` for `x >= 2^31`, the `~~x` truncation idiom and `~Infinity` were all wrong, which broke uuid's SHA-1.
|
|
9
|
+
- **Typed-array reads** with an index outside the array, or a negative or fractional index, return `undefined` instead of reading neighbouring memory. uuid's v5 UUIDs used to change from run to run because its SHA-1 reads past its buffer. The check applies to user code; Porffor's own precompiled builtins keep their reads.
|
|
10
|
+
- d03abed: `TypedArray.prototype.set(source)` without an offset copies to index 0 (part of baronunread/sproutboat#238). Porffor computed the offset as `Math.trunc(undefined)`, which is NaN, so a same-type copy wrote nothing where it should and went to the wrong place in memory instead. This dropped HMAC keys in @noble/hashes and killed the process after the first request. The offset now goes through `ToIntegerOrInfinity` as the spec says. @noble/hashes' SHA-256 and HMAC match Node and keep serving.
|
|
11
|
+
|
|
3
12
|
## 0.4.21
|
|
4
13
|
|
|
5
14
|
### Patch Changes
|
package/package.json
CHANGED
package/src/acquire.test.ts
CHANGED
|
@@ -40,6 +40,7 @@ async function fixture(): Promise<{ archive: string; sha256: string }> {
|
|
|
40
40
|
"compiler/builtins_precompiled.js": "fixture:precompiled\n",
|
|
41
41
|
"compiler/builtins/date.ts": "export const __ecma262_ParseDTSF = (string: string) => {\n let n: number = 0;\n let nInd: number = 0;\n\n const len: i32 = string.length;\n const endPtr: i32 = Porffor.IR.ptr(string) + len;\n let ptr: i32 = Porffor.IR.ptr(string);\n\n while (ptr <= endPtr) { // <= to include extra null byte to set last n\n const chr: i32 = Porffor.IR.loadU8(ptr++, 4);\n if (Porffor.fastAnd(chr >= 48, chr <= 57)) { // 0-9\n n *= 10;\n n += chr - 48;\n continue;\n }\n\n if (chr == 45) { // -\n if (Porffor.fastOr(ptr == Porffor.IR.ptr(string), nInd == 7)) n = -n;\n }\n\n if (n > 0) {\n if (nInd == 0) y = n;\n else if (nInd == 1) m = n - 1;\n else if (nInd == 2) dt = n;\n else if (nInd == 3) h = n;\n else if (nInd == 4) min = n;\n else if (nInd == 5) s = n;\n else if (nInd == 6) milli = n;\n else if (nInd == 7) tzHour = n;\n else if (nInd == 8) tzMin = n;\n\n n = 0;\n nInd++;\n }\n }\n\n h += tzHour;\n min += tzMin;\n};\n// RFC 7231 or Date.prototype.toString() parser\n",
|
|
42
42
|
"compiler/builtins/typedarray.js":
|
|
43
|
+
" offset = Math.trunc(offset);\n if (Porffor.fastOr(offset < 0, offset > len)) throw new RangeError('Offset out of bounds');\n" +
|
|
43
44
|
"export const __${name}_from = (arg: any, mapFn: any): ${name} => {\n" +
|
|
44
45
|
" const arr: any[] = Porffor.array.new(4);\n" +
|
|
45
46
|
" let len: i32 = 0;\n" +
|
|
@@ -121,7 +122,13 @@ async function fixture(): Promise<{ archive: string; sha256: string }> {
|
|
|
121
122
|
"compiler/builtins/json.ts": "// sb_json_utf16_v1\n",
|
|
122
123
|
"runtime/fetch-globals.js": "// sb_text_encoder_scalar_v1\n",
|
|
123
124
|
"compiler/codegen.js":
|
|
124
|
-
" stmt(scope, Store(ctype, addr, 4, ctype === 'f64' || ctype === 'f32' ? f : signed ? Convert(T.i32, f) : Convert(T.u32, f, 0)));\n"
|
|
125
|
+
" stmt(scope, Store(ctype, addr, 4, ctype === 'f64' || ctype === 'f32' ? f : signed ? Convert(T.i32, f) : Convert(T.u32, f, 0)));\n" +
|
|
126
|
+
" return Box(Convert(T.f64, Un('~', T.i32, Convert(T.i32, numValue(toNumeric())))), Const(T.i32, TYPES.number));\n" +
|
|
127
|
+
" const taGet = (ctype, size, signed = true) => () => {\n" +
|
|
128
|
+
" const loaded = Load(ctype, taAddr(size), 4);\n" +
|
|
129
|
+
" const f = ctype === 'f32' || ctype === 'f64' ? loaded : Convert(T.f64, loaded, signed ? CONVERT_SIGNED : 0);\n" +
|
|
130
|
+
" return Box(f, Const(T.i32, TYPES.number));\n" +
|
|
131
|
+
" };\n",
|
|
125
132
|
"compiler/builtins/string.ts":
|
|
126
133
|
"export const __Porffor_string_replaceAll = (str: any, searchValue: any, replaceValue: any) => {\n" +
|
|
127
134
|
" let out: any = __Porffor_string_emptyLike(str);\n let appendIndex: i32 = 0;\n let searchIndex: i32 = 0;\n let matched: boolean = false;\n" +
|
|
@@ -210,6 +217,14 @@ test("the default call (no url/expectedSha256 override) never touches the networ
|
|
|
210
217
|
},
|
|
211
218
|
});
|
|
212
219
|
expect(await readFile(join(dir, "runtime/index.js"), "utf8")).not.toBe("");
|
|
220
|
+
// #238: TypedArray.prototype.set(source) with no offset copies to index 0.
|
|
221
|
+
expect(await readFile(join(dir, "compiler/builtins/typedarray.js"), "utf8")).toContain(
|
|
222
|
+
"offset = ecma262.ToIntegerOrInfinity(offset);",
|
|
223
|
+
);
|
|
224
|
+
// #238: ~ wraps (ToInt32), and out-of-range typed-array reads are undefined.
|
|
225
|
+
const codegen = await readFile(join(dir, "compiler/codegen.js"), "utf8");
|
|
226
|
+
expect(codegen).toContain("Convert(T.i32, toUint32(scope, numValue(toNumeric())), CONVERT_RANGE_KNOWN | CONVERT_SIGNED)");
|
|
227
|
+
expect(codegen).toContain("Box(f, Const(T.i32, TYPES.number)), valUndefined());");
|
|
213
228
|
// #238: integer typed-array stores wrap modulo 2^n instead of saturating.
|
|
214
229
|
expect(await readFile(join(dir, "compiler/codegen.js"), "utf8")).toContain(
|
|
215
230
|
"signed ? Convert(T.i32, toUint32(scope, f), CONVERT_RANGE_KNOWN | CONVERT_SIGNED) : toUint32(scope, f)",
|
package/src/patch.ts
CHANGED
|
@@ -305,6 +305,58 @@ const TA_STORE_INJECT =
|
|
|
305
305
|
" // sproutboat #238: typed-array stores wrap modulo 2^n (ToUint32), not saturate.\n" +
|
|
306
306
|
" stmt(scope, Store(ctype, addr, 4, ctype === 'f64' || ctype === 'f32' ? f : signed ? Convert(T.i32, toUint32(scope, f), CONVERT_RANGE_KNOWN | CONVERT_SIGNED) : toUint32(scope, f)));";
|
|
307
307
|
const TA_STORE_MARKER = "sproutboat #238: typed-array stores wrap";
|
|
308
|
+
// baronunread/sproutboat#238: unary `~` had the same saturating conversion, so
|
|
309
|
+
// ~x for x >= 2^31 (or ~~x, the common truncation idiom) clamped to
|
|
310
|
+
// +/-2147483648, and ~Infinity gave -2147483648 instead of -1. uuid's SHA-1
|
|
311
|
+
// computes `x & y ^ ~x & z` on words above 2^31, so every v5 UUID was wrong.
|
|
312
|
+
const BITNOT_ANCHOR =
|
|
313
|
+
" return Box(Convert(T.f64, Un('~', T.i32, Convert(T.i32, numValue(toNumeric())))), Const(T.i32, TYPES.number));";
|
|
314
|
+
const BITNOT_INJECT =
|
|
315
|
+
" // sproutboat #238: ~ applies ToInt32 (wrap modulo 2^32), not a saturating cast.\n" +
|
|
316
|
+
" return Box(Convert(T.f64, Un('~', T.i32, Convert(T.i32, toUint32(scope, numValue(toNumeric())), CONVERT_RANGE_KNOWN | CONVERT_SIGNED))), Const(T.i32, TYPES.number));";
|
|
317
|
+
const BITNOT_MARKER = "sproutboat #238: ~ applies ToInt32";
|
|
318
|
+
// baronunread/sproutboat#238: typed-array element reads and writes had no
|
|
319
|
+
// bounds check. A read past the end returned whatever sat in the next
|
|
320
|
+
// allocation instead of undefined (uuid's SHA-1 reads past its buffer, so v5
|
|
321
|
+
// UUIDs changed run to run) and a[-1] / a[1.5] broke outright. Reuse the
|
|
322
|
+
// plain-array index check and compare against the typed array's length.
|
|
323
|
+
// Builtins keep the unchecked read: they index within bounds, and the check
|
|
324
|
+
// broke typed-array join/toString when compiled into them. Writes need the same
|
|
325
|
+
// guard, but builtins such as the constructor and fill() store elements before
|
|
326
|
+
// they set the length, so that is left for a follow-up.
|
|
327
|
+
const TA_GET_ANCHOR =
|
|
328
|
+
" const taGet = (ctype, size, signed = true) => () => {\n" +
|
|
329
|
+
" const loaded = Load(ctype, taAddr(size), 4);\n" +
|
|
330
|
+
" const f = ctype === 'f32' || ctype === 'f64' ? loaded : Convert(T.f64, loaded, signed ? CONVERT_SIGNED : 0);\n" +
|
|
331
|
+
" return Box(f, Const(T.i32, TYPES.number));\n" +
|
|
332
|
+
" };";
|
|
333
|
+
const TA_GET_INJECT =
|
|
334
|
+
" const taGet = (ctype, size, signed = true) => () => {\n" +
|
|
335
|
+
" // sproutboat #238: only a valid in-range integer index reads; anything else is undefined.\n" +
|
|
336
|
+
" // User code only: builtins (precompiled) index within bounds themselves and\n" +
|
|
337
|
+
" // typed-array join/toString misbehave with the extra check.\n" +
|
|
338
|
+
" if (globalThis.precompile) {\n" +
|
|
339
|
+
" const raw = Load(ctype, taAddr(size), 4);\n" +
|
|
340
|
+
" return Box(ctype === 'f32' || ctype === 'f64' ? raw : Convert(T.f64, raw, signed ? CONVERT_SIGNED : 0), Const(T.i32, TYPES.number));\n" +
|
|
341
|
+
" }\n" +
|
|
342
|
+
" const { idx, valid } = denseArrayIndexKey(scope, prop);\n" +
|
|
343
|
+
" const loaded = Load(ctype, Bin('+', T.u32, Load('u32', JvPtr(obj), 4), size === 1 ? idx : Bin('*', T.u32, idx, Const(T.u32, size))), 4);\n" +
|
|
344
|
+
" const f = ctype === 'f32' || ctype === 'f64' ? loaded : Convert(T.f64, loaded, signed ? CONVERT_SIGNED : 0);\n" +
|
|
345
|
+
" return Select(Bin('&&', T.i32, valid, Bin('<', T.i32, idx, Load('u32', JvPtr(obj), 0))),\n" +
|
|
346
|
+
" Box(f, Const(T.i32, TYPES.number)), valUndefined());\n" +
|
|
347
|
+
" };";
|
|
348
|
+
const TA_GET_MARKER = "sproutboat #238: only a valid in-range integer index reads";
|
|
349
|
+
|
|
350
|
+
// baronunread/sproutboat#238: TypedArray.prototype.set(source) with no offset
|
|
351
|
+
// ran `offset = Math.trunc(undefined)`, which is NaN. The same-type fast path
|
|
352
|
+
// then copied to `base + NaN * BYTES_PER_ELEMENT`: nothing landed where it
|
|
353
|
+
// should (noble's HMAC lost its key in `pad.set(key)`), and the write went
|
|
354
|
+
// somewhere else in memory. ECMA-262 uses ToIntegerOrInfinity, which maps
|
|
355
|
+
// undefined and NaN to 0; subarray right below already does.
|
|
356
|
+
const TA_SET_OFFSET_ANCHOR = " offset = Math.trunc(offset);\n if (Porffor.fastOr(offset < 0, offset > len)) throw new RangeError('Offset out of bounds');";
|
|
357
|
+
const TA_SET_OFFSET_INJECT =
|
|
358
|
+
" offset = ecma262.ToIntegerOrInfinity(offset); // sproutboat #238: undefined offset is 0, not NaN\n if (Porffor.fastOr(offset < 0, offset > len)) throw new RangeError('Offset out of bounds');";
|
|
359
|
+
const TA_SET_OFFSET_MARKER = "sproutboat #238: undefined offset is 0";
|
|
308
360
|
|
|
309
361
|
// Porffor's TypedArray.from only handles iterables. An array-like input such
|
|
310
362
|
// as { length: 16 } silently becomes an empty typed array, including HMAC
|
|
@@ -1126,11 +1178,19 @@ export async function patchReplaceAll(root: string): Promise<void> {
|
|
|
1126
1178
|
/** #238: modular typed-array stores. Before the precompile so builtins get it too. */
|
|
1127
1179
|
export async function patchTypedArrayStore(root: string): Promise<void> {
|
|
1128
1180
|
const file = resolve(root, "compiler/codegen.js");
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1181
|
+
let src = await readFile(file, "utf8");
|
|
1182
|
+
let changed = false;
|
|
1183
|
+
for (const [marker, anchor, inject, what] of [
|
|
1184
|
+
[TA_STORE_MARKER, TA_STORE_ANCHOR, TA_STORE_INJECT, "typed-array store conversion"],
|
|
1185
|
+
[BITNOT_MARKER, BITNOT_ANCHOR, BITNOT_INJECT, "bitwise NOT conversion"],
|
|
1186
|
+
[TA_GET_MARKER, TA_GET_ANCHOR, TA_GET_INJECT, "typed-array read bounds"],
|
|
1187
|
+
] as const) {
|
|
1188
|
+
if (src.includes(marker)) continue;
|
|
1189
|
+
if (!src.includes(anchor)) throw new Error(`could not patch Porffor's ${what}: anchor not found in ${file}`);
|
|
1190
|
+
src = src.replace(anchor, inject);
|
|
1191
|
+
changed = true;
|
|
1192
|
+
}
|
|
1193
|
+
if (changed) await writeFile(file, src);
|
|
1134
1194
|
}
|
|
1135
1195
|
|
|
1136
1196
|
export async function patchTypedArrayFrom(root: string): Promise<void> {
|
|
@@ -1139,8 +1199,14 @@ export async function patchTypedArrayFrom(root: string): Promise<void> {
|
|
|
1139
1199
|
if (!src.includes(TYPED_ARRAY_FROM_MARKER) && !src.includes(TYPED_ARRAY_FROM_ANCHOR)) {
|
|
1140
1200
|
throw new Error(`could not patch Porffor's TypedArray.from: anchor not found in ${file}`);
|
|
1141
1201
|
}
|
|
1142
|
-
|
|
1143
|
-
|
|
1202
|
+
let edited = src;
|
|
1203
|
+
if (!edited.includes(TYPED_ARRAY_FROM_MARKER)) edited = edited.replace(TYPED_ARRAY_FROM_ANCHOR, TYPED_ARRAY_FROM_INJECT);
|
|
1204
|
+
if (!edited.includes(TA_SET_OFFSET_MARKER)) {
|
|
1205
|
+
if (!edited.includes(TA_SET_OFFSET_ANCHOR))
|
|
1206
|
+
throw new Error(`could not patch Porffor's TypedArray.prototype.set offset: anchor not found in ${file}`);
|
|
1207
|
+
edited = edited.replace(TA_SET_OFFSET_ANCHOR, TA_SET_OFFSET_INJECT);
|
|
1208
|
+
}
|
|
1209
|
+
if (edited !== src) await writeFile(file, edited);
|
|
1144
1210
|
const table = resolve(root, "compiler/builtins_precompiled.js");
|
|
1145
1211
|
const before = await readFile(table);
|
|
1146
1212
|
// Porffor only writes the table when import.meta.url matches argv[1].
|
|
@@ -1157,7 +1223,7 @@ export async function patchTypedArrayFrom(root: string): Promise<void> {
|
|
|
1157
1223
|
new Response(child.stderr).text(),
|
|
1158
1224
|
]);
|
|
1159
1225
|
if (code !== 0) throw new Error(`could not precompile Porffor's TypedArray.from: ${stderr || stdout}`);
|
|
1160
|
-
if (
|
|
1226
|
+
if (edited !== src && before.equals(await readFile(table)))
|
|
1161
1227
|
throw new Error(`Porffor precompile did not update ${table}`);
|
|
1162
1228
|
}
|
|
1163
1229
|
|