@sproutboat/toolchain 0.4.16 → 0.4.17

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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # @sproutboat/toolchain
2
2
 
3
+ ## 0.4.17
4
+
5
+ ### Patch Changes
6
+
7
+ - 2cda218: Fix async handlers failing permanently with `TypeError: Cannot get property of null` after a few requests (baronunread/sproutboat#168). Porffor represents `null` as an object-typed value with pointer 0, so a promise resolving to `null` went through the `then` probe and looked `then` up at memory offset 0. Once `Date.prototype.toISOString` had written there, the probe matched, read `null.then` and rejected, and every later async function returning `null` failed the same way. A patch to `compiler/builtins/promise.ts` keeps `null` out of the probe.
8
+
3
9
  ## 0.4.16
4
10
 
5
11
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sproutboat/toolchain",
3
- "version": "0.4.16",
3
+ "version": "0.4.17",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {
@@ -119,6 +119,7 @@ async function fixture(): Promise<{ archive: string; sha256: string }> {
119
119
  "compiler/builtins/promise.ts":
120
120
  "export const __Porffor_promise_resolve = (value: any, promise: any): void => {\n" +
121
121
  " if (Porffor.type(value) == Porffor.TYPES.object) {\n" +
122
+ " // cheap prototype-chain probe for 'then' before the expensive Get below, does not invoke getters\n" +
122
123
  " const thenHash: i32 = __Porffor_object_hash('then');\n" +
123
124
  " let probe: any = value;\n" +
124
125
  " while (Porffor.type(probe) == Porffor.TYPES.object) {\n" +
@@ -197,6 +198,10 @@ test("the default call (no url/expectedSha256 override) never touches the networ
197
198
  },
198
199
  });
199
200
  expect(await readFile(join(dir, "runtime/index.js"), "utf8")).not.toBe("");
201
+ // #168: the real vendored source takes the null guard on the `then` probe.
202
+ expect(await readFile(join(dir, "compiler/builtins/promise.ts"), "utf8")).toContain(
203
+ "if (Porffor.type(value) == Porffor.TYPES.object && value != null) {",
204
+ );
200
205
  });
201
206
 
202
207
  test("a corrupted warm cache is replaced from the verified archive", async () => {
package/src/patch.ts CHANGED
@@ -192,6 +192,21 @@ const CFLAGS_INJECT =
192
192
  " ...(process.env.SB_EXTRA_CFLAGS ? process.env.SB_EXTRA_CFLAGS.split(' ').filter(Boolean) : []),\n";
193
193
  const CFLAGS_MARKER = "SB_EXTRA_CFLAGS";
194
194
 
195
+ // baronunread/sproutboat#168: Porffor represents null as an object-typed value
196
+ // with pointer 0, so __Porffor_promise_resolve's cheap `then` probe treated a
197
+ // promise resolving to null as an object and looked `then` up at memory offset
198
+ // 0. Once something (Date.prototype.toISOString's scratch writes, in practice)
199
+ // left bytes there that hash-match, the probe "found" a then, read null.then,
200
+ // and rejected with "Cannot get property of null". Every later async function
201
+ // resolving to null then failed, permanently. Only non-null objects can be
202
+ // thenables (ECMA-262 Promise Resolve Functions step 8), so exclude null.
203
+ const PROMISE_NULL_ANCHOR =
204
+ " if (Porffor.type(value) == Porffor.TYPES.object) {\n // cheap prototype-chain probe for 'then'";
205
+ const PROMISE_NULL_INJECT =
206
+ " // sproutboat #168: null is object-typed with pointer 0, never a thenable.\n" +
207
+ " if (Porffor.type(value) == Porffor.TYPES.object && value != null) {\n // cheap prototype-chain probe for 'then'";
208
+ const PROMISE_NULL_MARKER = "sproutboat #168: null is object-typed";
209
+
195
210
  // Porffor's TypedArray.from only handles iterables. An array-like input such
196
211
  // as { length: 16 } silently becomes an empty typed array, including HMAC
197
212
  // keys made with Uint8Array.from({ length: 16 }, mapFn). The compiler uses a
@@ -977,6 +992,16 @@ export async function patchDateParser(root: string): Promise<void> {
977
992
  await writeFile(file, src.slice(0, start) + parser.replace(DATE_PARSER_ANCHOR, DATE_PARSER_INJECT) + src.slice(end));
978
993
  }
979
994
 
995
+ /** #168: keep a promise resolving to null out of the `then` probe. Precompiled by patchTypedArrayFrom. */
996
+ export async function patchPromiseResolveNull(root: string): Promise<void> {
997
+ const file = resolve(root, "compiler/builtins/promise.ts");
998
+ const src = await readFile(file, "utf8");
999
+ if (src.includes(PROMISE_NULL_MARKER)) return;
1000
+ if (!src.includes(PROMISE_NULL_ANCHOR))
1001
+ throw new Error(`could not patch Porffor's promise resolve for null: anchor not found in ${file}`);
1002
+ await writeFile(file, src.replace(PROMISE_NULL_ANCHOR, PROMISE_NULL_INJECT));
1003
+ }
1004
+
980
1005
  export async function patchTypedArrayFrom(root: string): Promise<void> {
981
1006
  const file = resolve(root, "compiler/builtins/typedarray.js");
982
1007
  const src = await readFile(file, "utf8");
@@ -1052,6 +1077,9 @@ export async function ensurePorfforPatched(root: string): Promise<void> {
1052
1077
  await patchUwebsockets(root);
1053
1078
  await patchRenderJs(root);
1054
1079
  await patchDateParser(root);
1080
+ await patchPromiseResolveNull(root);
1081
+ // Last of the builtin edits: its precompile picks up the date and promise
1082
+ // patches above too.
1055
1083
  await patchTypedArrayFrom(root);
1056
1084
  await patchUnicode(root);
1057
1085
  done.add(root);