From 4af48440d7090b3e32b1e122af70108505b05ea0 Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Sat, 10 Oct 2026 12:36:50 -0600 Subject: [PATCH 1/5] Document how native addons load on pointer-compression Node runtimes On a V8 pointer-compression runtime, Harper refuses a native addon that links V8's C++ API unless it was built for pointer compression (HarperFast/harper#3140). Describe the three ways an addon loads, the IncompatibleNativeAddonError a refused one raises, and the JavaScript fallback of optional accelerators. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011m4iSgKNWY8toCDWmuoh9a Dispatch-Task: harper-pc-native-addon-guard --- reference/components/module-loading.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/reference/components/module-loading.md b/reference/components/module-loading.md index 1d54fc4a..a9ce5ca5 100644 --- a/reference/components/module-loading.md +++ b/reference/components/module-loading.md @@ -148,6 +148,27 @@ Like `allowedDirectory`, this applies to imports the application module loader h Allowlisting `child_process` still yields Harper's constrained substitute under the VM loaders, not Node's unmodified module. +## Native Addons on Pointer-Compression Runtimes + + + +Harper can run on a Node.js build with V8 pointer compression enabled (built with `--experimental-enable-pointer-compression`, so that `process.config.variables.v8_enable_pointer_compression` is `1`), which uses less JavaScript heap per thread. Pointer compression changes V8's C++ ABI without changing Node.js's module version. A native addon (`.node` file) that calls V8's C++ API directly, rather than Node-API, and was built for a standard Node.js therefore loads and then crashes the whole process the first time it is used. + +On a pointer-compression runtime, Harper checks every native addon before it loads, in every thread, including addons that applications and their dependencies load. It refuses one that uses V8's C++ API unless the addon was built for pointer compression. An addon loads when any of these holds: + +- It uses only [Node-API](https://nodejs.org/api/n-api.html), which is ABI-stable across the flag. Most current addons are Node-API. +- node-gyp compiled it under this Node.js, for example with `npm rebuild --build-from-source`, and it is loaded from that package's `build/` directory. A binary that is copied elsewhere after building, as node-pre-gyp does, needs the marker below instead. +- Its package directory contains a `.pointer-compression-build` file. This marks binaries that were built for pointer compression by other means. + +A refused addon fails to load with an `IncompatibleNativeAddonError`. The error names the file and its package, and lists the fixes: + +- a Node-API version of the package; +- a build compiled for pointer compression; +- a rebuild from source under the running Node.js; +- a standard Node.js runtime. + +The error carries `code: 'ERR_DLOPEN_FAILED'`, the code Node.js uses for an addon it cannot load. So packages that treat their native accelerator as optional, such as `cbor-x` and `msgpackr`, fall back to their JavaScript implementation. The check inspects Linux (ELF) binaries; addons in other formats are left to Node.js. On a standard Node.js build nothing changes. + ## Choosing a Mode For most applications the default is the right choice, and the settings on this page are worth changing only in response to a concrete problem. From 169dad674cfff8cd53c19dea66ada46842845eae Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Sat, 10 Oct 2026 12:45:50 -0600 Subject: [PATCH 2/5] Scope the pointer-compression addon check precisely and add it to the 5.4 notes Name the NODE_OPTIONS preload exception, the strictly-newer config.gypi condition, the marker's whole-package scope and the refusal of an unreadable ELF symbol table, and list the change in the 5.4 release notes. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011m4iSgKNWY8toCDWmuoh9a Dispatch-Task: harper-pc-native-addon-guard --- reference/components/module-loading.md | 20 +++++++++++++++----- release-notes/v5-lincoln/5.4.md | 14 ++++++++++++++ 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/reference/components/module-loading.md b/reference/components/module-loading.md index a9ce5ca5..bad16594 100644 --- a/reference/components/module-loading.md +++ b/reference/components/module-loading.md @@ -154,11 +154,15 @@ Allowlisting `child_process` still yields Harper's constrained substitute under Harper can run on a Node.js build with V8 pointer compression enabled (built with `--experimental-enable-pointer-compression`, so that `process.config.variables.v8_enable_pointer_compression` is `1`), which uses less JavaScript heap per thread. Pointer compression changes V8's C++ ABI without changing Node.js's module version. A native addon (`.node` file) that calls V8's C++ API directly, rather than Node-API, and was built for a standard Node.js therefore loads and then crashes the whole process the first time it is used. -On a pointer-compression runtime, Harper checks every native addon before it loads, in every thread, including addons that applications and their dependencies load. It refuses one that uses V8's C++ API unless the addon was built for pointer compression. An addon loads when any of these holds: +On a pointer-compression runtime, Harper checks native addons before they load, in every thread. That covers addons loaded by applications, by their dependencies, and by [`threads.preload` and `threads.preloadRequire`](../configuration/options.md#threads) modules. Harper refuses an addon that links V8's C++ API unless it was built for pointer compression. A 64-bit ELF addon whose symbol table cannot be read is refused too, unless one of the last two rules below admits it. -- It uses only [Node-API](https://nodejs.org/api/n-api.html), which is ABI-stable across the flag. Most current addons are Node-API. -- node-gyp compiled it under this Node.js, for example with `npm rebuild --build-from-source`, and it is loaded from that package's `build/` directory. A binary that is copied elsewhere after building, as node-pre-gyp does, needs the marker below instead. -- Its package directory contains a `.pointer-compression-build` file. This marks binaries that were built for pointer compression by other means. +An addon loads when any of these holds: + +- It uses only [Node-API](https://nodejs.org/api/n-api.html), which is ABI-stable across the flag: it imports no V8 C++ symbols. Most current addons are Node-API. +- node-gyp compiled it under this Node.js, for example with `npm rebuild --build-from-source`, and it is loaded from that package's `build/` directory. That build's `config.gypi` must record `"v8_enable_pointer_compression": 1`, and the binary must be newer than it. Two cases need the marker below instead: + - a binary copied elsewhere after building, as node-pre-gyp does; + - a build whose files all share one modification time, such as a reproducible build with clamped timestamps. +- Its package directory contains a `.pointer-compression-build` file. The marker admits every native binary in the package without inspecting it, so add it only when all of them were built for pointer compression. A refused addon fails to load with an `IncompatibleNativeAddonError`. The error names the file and its package, and lists the fixes: @@ -167,7 +171,13 @@ A refused addon fails to load with an `IncompatibleNativeAddonError`. The error - a rebuild from source under the running Node.js; - a standard Node.js runtime. -The error carries `code: 'ERR_DLOPEN_FAILED'`, the code Node.js uses for an addon it cannot load. So packages that treat their native accelerator as optional, such as `cbor-x` and `msgpackr`, fall back to their JavaScript implementation. The check inspects Linux (ELF) binaries; addons in other formats are left to Node.js. On a standard Node.js build nothing changes. +The error carries `code: 'ERR_DLOPEN_FAILED'`, the code Node.js uses for an addon it cannot load. So packages that treat their native accelerator as optional, such as `cbor-x` and `msgpackr`, fall back to their JavaScript implementation. + +The check has three limits: + +- It inspects Linux (ELF) binaries only; addons in other formats are left to Node.js. +- Modules preloaded through `NODE_OPTIONS` run before Harper starts, so their addons are not checked. Load such modules with `threads.preload` or `threads.preloadRequire` instead. +- On a standard Node.js build, nothing changes. ## Choosing a Mode diff --git a/release-notes/v5-lincoln/5.4.md b/release-notes/v5-lincoln/5.4.md index 32120367..2fb26b8a 100644 --- a/release-notes/v5-lincoln/5.4.md +++ b/release-notes/v5-lincoln/5.4.md @@ -23,3 +23,17 @@ A durable MQTT session now resumes through the same checked replay. When a recon ### Certifying a Release in a Canary Worker A `deploy_component` with `restart: true` or `restart: "rolling"` now checks its release before rolling it out. The first worker started on the release, the canary, is held out of traffic until it reports whether the deployed component loaded, and the rollout goes on only if it did. Each later replacement is checked the same way. When the node that received the deploy rejects the release, the deploy fails with `400`. The release it replaced is made live again, or, with nothing to go back to, the component fails closed on that node. A new `certification` field in the response reports the outcome, and says when a restarting deploy went out unchecked (`uncertified` or `unavailable`, as for a `package: file:` deploy). With `"rolling"`, each other node then certifies the release with a canary of its own, and a node that rejects it fails the deploy's job, not the deploy: poll its `restartJobId` with `get_job` for the outcome. A node running a version before 5.4.0 activates without a canary. A deploy that restarts nothing is not test-loaded. If its release throws when it loads, `get_status` reports the failure. See [Certifying a release in a canary worker](/reference/v5/operations-api/operations#certifying-a-release-in-a-canary-worker). + +## Native Addons + +### Pointer-Compression Runtimes Refuse Incompatible Native Addons + +On a Node.js build with V8 pointer compression, Harper now checks native addons before they load, in every thread. It refuses an addon that links V8's C++ API built for the standard ABI, which would otherwise load and then crash the process on first use. The refusal is an `IncompatibleNativeAddonError` that names the file, its package and the fixes. + +These still load as before: + +- Node-API addons; +- addons built by node-gyp under the running Node.js; +- packages marked with `.pointer-compression-build`. + +Standard Node.js builds are unchanged. See [Native Addons on Pointer-Compression Runtimes](/reference/v5/components/module-loading#native-addons-on-pointer-compression-runtimes). From 8da4eeab9a380cf60b6226b419aaa7d559652778 Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Sat, 10 Oct 2026 12:50:48 -0600 Subject: [PATCH 3/5] Keep the 5.4 summary's node-gyp condition aligned with the reference rules Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011m4iSgKNWY8toCDWmuoh9a Dispatch-Task: harper-pc-native-addon-guard --- release-notes/v5-lincoln/5.4.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release-notes/v5-lincoln/5.4.md b/release-notes/v5-lincoln/5.4.md index 2fb26b8a..823416a7 100644 --- a/release-notes/v5-lincoln/5.4.md +++ b/release-notes/v5-lincoln/5.4.md @@ -33,7 +33,7 @@ On a Node.js build with V8 pointer compression, Harper now checks native addons These still load as before: - Node-API addons; -- addons built by node-gyp under the running Node.js; +- addons that node-gyp built under the running Node.js and that load from the package's `build/` directory; - packages marked with `.pointer-compression-build`. Standard Node.js builds are unchanged. See [Native Addons on Pointer-Compression Runtimes](/reference/v5/components/module-loading#native-addons-on-pointer-compression-runtimes). From 643e6f520e6dd43fe1f0499fc5555b864b54fa77 Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Sat, 10 Oct 2026 12:58:58 -0600 Subject: [PATCH 4/5] State each pointer-compression check limit in its own sentence Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011m4iSgKNWY8toCDWmuoh9a Dispatch-Task: harper-pc-native-addon-guard --- reference/components/module-loading.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/reference/components/module-loading.md b/reference/components/module-loading.md index bad16594..5b889002 100644 --- a/reference/components/module-loading.md +++ b/reference/components/module-loading.md @@ -162,7 +162,7 @@ An addon loads when any of these holds: - node-gyp compiled it under this Node.js, for example with `npm rebuild --build-from-source`, and it is loaded from that package's `build/` directory. That build's `config.gypi` must record `"v8_enable_pointer_compression": 1`, and the binary must be newer than it. Two cases need the marker below instead: - a binary copied elsewhere after building, as node-pre-gyp does; - a build whose files all share one modification time, such as a reproducible build with clamped timestamps. -- Its package directory contains a `.pointer-compression-build` file. The marker admits every native binary in the package without inspecting it, so add it only when all of them were built for pointer compression. +- Its package directory contains a `.pointer-compression-build` file. The marker admits every native binary in the package without inspecting it. Add this marker only when all of them were built for pointer compression. A refused addon fails to load with an `IncompatibleNativeAddonError`. The error names the file and its package, and lists the fixes: @@ -175,8 +175,8 @@ The error carries `code: 'ERR_DLOPEN_FAILED'`, the code Node.js uses for an addo The check has three limits: -- It inspects Linux (ELF) binaries only; addons in other formats are left to Node.js. -- Modules preloaded through `NODE_OPTIONS` run before Harper starts, so their addons are not checked. Load such modules with `threads.preload` or `threads.preloadRequire` instead. +- It inspects Linux (ELF) binaries only. Addons in other formats are left to Node.js and are not checked. +- Modules preloaded through `NODE_OPTIONS` run before Harper starts. Their addons are not checked. Load such modules with `threads.preload` or `threads.preloadRequire` instead. - On a standard Node.js build, nothing changes. ## Choosing a Mode From 5c7fd4f62fa25d7f999f1e29c2308787fc44980f Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Mon, 12 Oct 2026 19:34:18 -0600 Subject: [PATCH 5/5] Document pointer compression defaults for Free and Hobby clusters Co-Authored-By: OpenAI Codex --- reference/components/module-loading.md | 2 +- release-notes/v5-lincoln/5.4.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/reference/components/module-loading.md b/reference/components/module-loading.md index 5b889002..31e4faa9 100644 --- a/reference/components/module-loading.md +++ b/reference/components/module-loading.md @@ -152,7 +152,7 @@ Allowlisting `child_process` still yields Harper's constrained substitute under -Harper can run on a Node.js build with V8 pointer compression enabled (built with `--experimental-enable-pointer-compression`, so that `process.config.variables.v8_enable_pointer_compression` is `1`), which uses less JavaScript heap per thread. Pointer compression changes V8's C++ ABI without changing Node.js's module version. A native addon (`.node` file) that calls V8's C++ API directly, rather than Node-API, and was built for a standard Node.js therefore loads and then crashes the whole process the first time it is used. +Harper can run on a Node.js build with V8 pointer compression enabled (built with `--experimental-enable-pointer-compression`, so that `process.config.variables.v8_enable_pointer_compression` is `1`), which uses less JavaScript heap per thread. Free and Hobby tier clusters on Harper Fabric use pointer-compression runtimes by default. Pointer compression changes V8's C++ ABI without changing Node.js's module version. A native addon (`.node` file) that calls V8's C++ API directly, rather than Node-API, and was built for a standard Node.js therefore loads and then crashes the whole process the first time it is used. On a pointer-compression runtime, Harper checks native addons before they load, in every thread. That covers addons loaded by applications, by their dependencies, and by [`threads.preload` and `threads.preloadRequire`](../configuration/options.md#threads) modules. Harper refuses an addon that links V8's C++ API unless it was built for pointer compression. A 64-bit ELF addon whose symbol table cannot be read is refused too, unless one of the last two rules below admits it. diff --git a/release-notes/v5-lincoln/5.4.md b/release-notes/v5-lincoln/5.4.md index 823416a7..63febd7b 100644 --- a/release-notes/v5-lincoln/5.4.md +++ b/release-notes/v5-lincoln/5.4.md @@ -28,7 +28,7 @@ A `deploy_component` with `restart: true` or `restart: "rolling"` now checks its ### Pointer-Compression Runtimes Refuse Incompatible Native Addons -On a Node.js build with V8 pointer compression, Harper now checks native addons before they load, in every thread. It refuses an addon that links V8's C++ API built for the standard ABI, which would otherwise load and then crash the process on first use. The refusal is an `IncompatibleNativeAddonError` that names the file, its package and the fixes. +Free and Hobby tier clusters on Harper Fabric use pointer-compression runtimes by default. On a Node.js build with V8 pointer compression, Harper now checks native addons before they load, in every thread. It refuses an addon that links V8's C++ API built for the standard ABI, which would otherwise load and then crash the process on first use. The refusal is an `IncompatibleNativeAddonError` that names the file, its package and the fixes. These still load as before: