Repository navigation
Closed
Labels
package-auditFound by the 2026 package audit: compiling real npm packages from source instead of native bindingsFound by the 2026 package audit: compiling real npm packages from source instead of native bindings
Description
Found by the package audit (compiling real npm packages from source instead of Perry's native bindings) on
Perry e6dcb62 (v0.5.1587), Linux x64. The Node-API host's napi_typeof classifies values with its own tag checks, which miss Perry's class-constructor representations. A compiled class value, or a class expression, is reported as number. JavaScript typeof on the same value correctly says function, and napi_new_instance on it works.
Reproduction
Needs a C compiler and Node's headers (include/node in the Node tarball). No npm packages.
addon.c
#include <node_api.h>
static napi_value TypeOf(napi_env env, napi_callback_info info) {
static const char *names[] = {"undefined", "null", "boolean", "number", "string",
"symbol", "object", "function", "external", "bigint"};
size_t argc = 1;
napi_value argv[1], out;
napi_valuetype type;
napi_get_cb_info(env, info, &argc, argv, NULL, NULL);
napi_typeof(env, argv[0], &type);
napi_create_string_utf8(env, names[type], NAPI_AUTO_LENGTH, &out);
return out;
}
static napi_value Construct(napi_env env, napi_callback_info info) {
size_t argc = 1;
napi_value argv[1], out;
napi_get_cb_info(env, info, &argc, argv, NULL, NULL);
if (napi_new_instance(env, argv[0], 0, NULL, &out) != napi_ok) napi_get_null(env, &out);
return out;
}
NAPI_MODULE_INIT() {
napi_value fn;
napi_create_function(env, "typeOf", NAPI_AUTO_LENGTH, TypeOf, NULL, &fn);
napi_set_named_property(env, exports, "typeOf", fn);
napi_create_function(env, "construct", NAPI_AUTO_LENGTH, Construct, NULL, &fn);
napi_set_named_property(env, exports, "construct", fn);
return exports;
}package.json
{
"name": "napi-typeof-class",
"private": true,
"type": "module",
"dependencies": { "demo-addon": "1.0.0" },
"perry": {
"compilePackages": ["demo-addon"],
"nativeAddons": ["demo-addon"],
"allow": { "compilePackages": ["*"] }
}
}node_modules/demo-addon/package.json
{ "name": "demo-addon", "version": "1.0.0", "main": "index.js" }node_modules/demo-addon/index.js
module.exports = require("./addon.node");main.ts
import addon from "demo-addon";
class Plain {}
class Tagged { tag = "made"; }
class MyError extends Error {}
const Expr = class {};
function decl() {}
for (const [name, value] of [
["class Plain", Plain], ["class Tagged", Tagged], ["class MyError extends Error", MyError],
["class expression", Expr], ["function decl", decl], ["arrow", () => 1],
["bound", decl.bind(null)], ["Map", Map], ["{}", {}],
] as [string, unknown][]) {
console.log(name.padEnd(28), typeof value, addon.typeOf(value));
}
const t: any = addon.construct(Tagged);
console.log("napi_new_instance(Tagged)", t instanceof Tagged, t?.tag);cc -shared -fPIC -I"$(dirname "$(dirname "$(command -v node)")")/include/node" addon.c \
-o node_modules/demo-addon/addon.node
node main.ts
PERRY_NO_AUTO_OPTIMIZE=1 perry compile main.ts -o app && ./appExpected (Node 26.5.1)
class Plain function function
class Tagged function function
class MyError extends Error function function
class expression function function
function decl function function
arrow function function
bound function function
Map function function
{} object object
napi_new_instance(Tagged) true made
Actual (Perry)
class Plain function number
class Tagged function number
class MyError extends Error function number
class expression function number
function decl function function
arrow function function
bound function function
Map function function
{} object object
napi_new_instance(Tagged) true made
The output is the same in no-auto and default auto-optimize builds.
Impact
- better-sqlite3 13.0.2 calls
addon.initialize(SqliteError, arrayFactory, …)inlib/database.js:47-48, whereSqliteErroris aclass. The addon checks that its first argument is a function. With the Node-API version gate of Node-API host rejects addons that declare Node-API 9 or 10 (argon2 0.45, better-sqlite3 13, sharp 0.35), though they import no symbol the host lacks #10456 bypassed for triage (binary patch), Perry fails there withTypeError: Expected first argument to be a function(fromcreateDatabase). This will block better-sqlite3 once Node-API host rejects addons that declare Node-API 9 or 10 (argon2 0.45, better-sqlite3 13, sharp 0.35), though they import no symbol the host lacks #10456 is fixed. - Any node-addon-api code doing
info[0].IsFunction()/Napi::Functioncasts, or any napi-rsJsFunctionargument check, rejects a class passed as a constructor or callback. The same applies tonapi_get_value_*paths that branch onnapi_typeof(inferred).
Notes
napi_typeof(crates/perry-runtime/src/node_api_host/values.rs:411-447) testsjs.is_number() || js.is_int32()before anything else and returnsFunctiononly forcrate::closure::is_closure_ptr. Perry's owntypeof(classify_value_typeof,crates/perry-runtime/src/builtins/arithmetic.rs:701) handles two more callable shapes that this misses:- a class ref encoded as
INT32_TAG | class_id, reported as function whencrate::object::is_class_id_registered(class_id)(arithmetic.rs:781-795); - a class object stamped
OBJECT_TYPE_CLASS, viacrate::object::is_class_object_ptr(arithmetic.rs:767-773).
A class ref therefore lands in theNumberarm. Suggested fix (inferred): derivenapi_typeoffromclassify_value_typeof, keeping the host'sExternalarm first, instead of duplicating the tag logic. Then auditnapi_get_value_int32/napi_get_value_double/napi_coerce_to_number(values.rs:335…), which would presumably accept a class ref as the numberclass_id(not verified).
- a class ref encoded as
napi_new_instance(functions.rs:307) andinstanceofalready handle class refs correctly, as the last line of the output shows, so only the classification is wrong.- Found while triaging Node-API host rejects addons that declare Node-API 9 or 10 (argon2 0.45, better-sqlite3 13, sharp 0.35), though they import no symbol the host lacks #10456. Related: runtime: Node-API (N-API) host for prebuilt napi addons — tracker #8523.
Metadata
Metadata
Assignees
Labels
package-auditFound by the 2026 package audit: compiling real npm packages from source instead of native bindingsFound by the 2026 package audit: compiling real npm packages from source instead of native bindings
Activity
docs(changelog): add fragment for #10456/#10461 Node-API v10 host fix
docs(changelog): add fragment for #10456/#10461 Node-API v10 host fix