Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/api/schema.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1707,6 +1707,9 @@ pub const Api = struct {
serve_hmr: ?bool = null,
serve_define: ?StringMap = null,

// from --no-addons. null == true
allow_addons: ?bool = null,

bunfig_path: []const u8,

pub fn decode(reader: anytype) anyerror!TransformOptions {
Expand Down
5 changes: 5 additions & 0 deletions src/bun.js/VirtualMachine.zig
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ comptime {
@export(&setEntryPointEvalResultCJS, .{ .name = "Bun__VM__setEntryPointEvalResultCJS" });
@export(&specifierIsEvalEntryPoint, .{ .name = "Bun__VM__specifierIsEvalEntryPoint" });
@export(&string_allocation_limit, .{ .name = "Bun__stringSyntheticAllocationLimit" });
@export(&allowAddons, .{ .name = "Bun__VM__allowAddons" });
}

global: *JSGlobalObject,
Expand Down Expand Up @@ -192,6 +193,10 @@ pub const OnUnhandledRejection = fn (*VirtualMachine, globalObject: *JSGlobalObj

pub const OnException = fn (*ZigException) void;

pub fn allowAddons(this: *VirtualMachine) callconv(.c) bool {
return if (this.transpiler.options.transform_options.allow_addons) |allow_addons| allow_addons else true;
}

pub fn initRequestBodyValue(this: *VirtualMachine, body: JSC.WebCore.Body.Value) !*Body.Value.HiveRef {
return .init(body, &this.body_value_hive_allocator);
}
Expand Down
6 changes: 6 additions & 0 deletions src/bun.js/bindings/BunProcess.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,7 @@ static char* toFileURI(std::span<const char> span)
extern "C" size_t Bun__process_dlopen_count;

extern "C" void CrashHandler__setDlOpenAction(const char* action);
extern "C" bool Bun__VM__allowAddons(void* vm);

JSC_DEFINE_HOST_FUNCTION(Process_functionDlopen, (JSC::JSGlobalObject * globalObject_, JSC::CallFrame* callFrame))
{
Expand All @@ -367,6 +368,11 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionDlopen, (JSC::JSGlobalObject * globalOb
auto scope = DECLARE_THROW_SCOPE(JSC::getVM(globalObject));
auto& vm = JSC::getVM(globalObject);

if (!Bun__VM__allowAddons(globalObject->bunVM())) {
JSC::throwTypeError(globalObject, scope, "Cannot load native addon because loading addons is disabled."_s);
return {};
}

auto argCount = callFrame->argumentCount();
if (argCount < 2) {
JSC::throwTypeError(globalObject, scope, "dlopen requires 2 arguments"_s);
Expand Down
6 changes: 6 additions & 0 deletions src/bun.js/bindings/ErrorCode.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1411,6 +1411,12 @@ JSC::EncodedJSValue INVALID_THIS(JSC::ThrowScope& scope, JSC::JSGlobalObject* gl
return {};
}

JSC::EncodedJSValue DLOPEN_DISABLED(JSC::ThrowScope& scope, JSC::JSGlobalObject* globalObject, ASCIILiteral message)
{
scope.throwException(globalObject, createError(globalObject, ErrorCode::ERR_DLOPEN_DISABLED, message));
return {};
}

} // namespace ERR

static JSC::JSValue ERR_INVALID_ARG_TYPE(JSC::ThrowScope& scope, JSC::JSGlobalObject* globalObject, JSValue arg0, JSValue arg1, JSValue arg2)
Expand Down
1 change: 1 addition & 0 deletions src/bun.js/bindings/ErrorCode.h
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ JSC::EncodedJSValue MISSING_OPTION(JSC::ThrowScope&, JSC::JSGlobalObject*, ASCII
JSC::EncodedJSValue INVALID_MIME_SYNTAX(JSC::ThrowScope&, JSC::JSGlobalObject*, const String& part, const String& input, int position);
JSC::EncodedJSValue CLOSED_MESSAGE_PORT(JSC::ThrowScope&, JSC::JSGlobalObject*);
JSC::EncodedJSValue INVALID_THIS(JSC::ThrowScope& scope, JSC::JSGlobalObject* globalObject, ASCIILiteral expectedType);
JSC::EncodedJSValue DLOPEN_DISABLED(JSC::ThrowScope&, JSC::JSGlobalObject*, ASCIILiteral message);

// URL

Expand Down
39 changes: 38 additions & 1 deletion src/bun.js/web_worker.zig
Original file line number Diff line number Diff line change
Expand Up @@ -280,10 +280,47 @@ pub const WebWorker = struct {
assert(this.status.load(.acquire) == .start);
assert(this.vm == null);

var transform_options = this.parent.transpiler.options.transform_options;

if (this.execArgv) |exec_argv| parse_new_args: {
var new_args: std.ArrayList([]const u8) = try .initCapacity(bun.default_allocator, exec_argv.len);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we should convert the string impls to [] const []const u8 so we don't need to hold onto them. process.execArgv will cache the result, so this way we don't hold onto them in two places

Copy link
Member Author

@dylan-conway dylan-conway May 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nvm, not doing this. parsing the new execArgv will only happen once

defer {
for (new_args.items) |arg| {
bun.default_allocator.free(arg);
}
new_args.deinit();
}

for (exec_argv) |arg| {
try new_args.append(arg.toOwnedSliceZ(bun.default_allocator));
}

var diag: bun.clap.Diagnostic = .{};
var iter: bun.clap.args.SliceIterator = .init(new_args.items);

var args = bun.clap.parseEx(bun.clap.Help, bun.CLI.Command.Tag.RunCommand.params(), &iter, .{
.diagnostic = &diag,
.allocator = bun.default_allocator,

// just one for executable
.stop_after_positional_at = 1,
}) catch {
// ignore param parsing errors
break :parse_new_args;
};
defer args.deinit();

// override the existing even if it was set
transform_options.allow_addons = !args.flag("--no-addons");

// TODO: currently this only checks for --no-addons. I think
// this should go through most flags and update the options.
}

this.arena = try bun.MimallocArena.init();
var vm = try JSC.VirtualMachine.initWorker(this, .{
.allocator = this.arena.?.allocator(),
.args = this.parent.transpiler.options.transform_options,
.args = transform_options,
.store_fd = this.store_fd,
.graph = this.parent.standalone_module_graph,
});
Expand Down
7 changes: 7 additions & 0 deletions src/cli.zig
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ pub const Arguments = struct {
clap.parseParam("--title <STR> Set the process title") catch unreachable,
clap.parseParam("--zero-fill-buffers Boolean to force Buffer.allocUnsafe(size) to be zero-filled.") catch unreachable,
clap.parseParam("--redis-preconnect Preconnect to $REDIS_URL at startup") catch unreachable,
clap.parseParam("--no-addons Throw an error if process.dlopen is called, and disable export condition \"node-addon\"") catch unreachable,
};

const auto_or_run_params = [_]ParamType{
Expand Down Expand Up @@ -715,6 +716,12 @@ pub const Arguments = struct {
ctx.runtime_options.redis_preconnect = true;
}

if (args.flag("--no-addons")) {
// used for disabling process.dlopen and
// for disabling export condition "node-addons"
opts.allow_addons = false;
}

if (args.option("--port")) |port_str| {
if (comptime cmd == .RunAsNodeCommand) {
// TODO: prevent `node --port <script>` from working
Expand Down
20 changes: 13 additions & 7 deletions src/deps/zig-clap/clap/args.zig
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,21 @@ pub const ExampleArgIterator = struct {
pub const SliceIterator = struct {
const Error = error{};

args: []const []const u8,
index: usize = 0,
remain: []const []const u8,

pub fn next(iter: *SliceIterator) Error!?[]const u8 {
if (iter.args.len <= iter.index)
return null;
pub fn init(args: []const []const u8) SliceIterator {
return .{
.remain = args,
};
}

defer iter.index += 1;
return iter.args[iter.index];
pub fn next(iter: *SliceIterator) ?[]const u8 {
if (iter.remain.len > 0) {
const res = iter.remain[0];
iter.remain = iter.remain[1..];
return res;
}
return null;
}
};

Expand Down
33 changes: 28 additions & 5 deletions src/options.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1122,7 +1122,7 @@ pub const ESMConditions = struct {
require: ConditionsMap,
style: ConditionsMap,

pub fn init(allocator: std.mem.Allocator, defaults: []const string) !ESMConditions {
pub fn init(allocator: std.mem.Allocator, defaults: []const string) bun.OOM!ESMConditions {
var default_condition_amp = ConditionsMap.init(allocator);

var import_condition_map = ConditionsMap.init(allocator);
Expand Down Expand Up @@ -1176,7 +1176,7 @@ pub const ESMConditions = struct {
};
}

pub fn appendSlice(self: *ESMConditions, conditions: []const string) !void {
pub fn appendSlice(self: *ESMConditions, conditions: []const string) bun.OOM!void {
try self.default.ensureUnusedCapacity(conditions.len);
try self.import.ensureUnusedCapacity(conditions.len);
try self.require.ensureUnusedCapacity(conditions.len);
Expand All @@ -1189,6 +1189,13 @@ pub const ESMConditions = struct {
self.style.putAssumeCapacity(condition, {});
}
}

pub fn append(self: *ESMConditions, condition: string) bun.OOM!void {
self.default.putAssumeCapacity(condition, {});
self.import.putAssumeCapacity(condition, {});
self.require.putAssumeCapacity(condition, {});
self.style.putAssumeCapacity(condition, {});
}
};

pub const JSX = struct {
Expand Down Expand Up @@ -1993,10 +2000,26 @@ pub const BundleOptions = struct {
opts.main_fields = Target.DefaultMainFields.get(opts.target);
}

opts.conditions = try ESMConditions.init(allocator, opts.target.defaultConditions());
{
// conditions:
// 1. defaults
// 2. node-addons
// 3. user conditions
opts.conditions = try ESMConditions.init(allocator, opts.target.defaultConditions());

dont_append_node_addons: {
if (transform.allow_addons) |allow_addons| {
if (!allow_addons) {
break :dont_append_node_addons;
}
}

try opts.conditions.append("node-addons");
}

if (transform.conditions.len > 0) {
opts.conditions.appendSlice(transform.conditions) catch bun.outOfMemory();
if (transform.conditions.len > 0) {
try opts.conditions.appendSlice(transform.conditions);
}
}

switch (opts.target) {
Expand Down
17 changes: 17 additions & 0 deletions test/js/node/no-addons.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { test, expect } from "bun:test";
import { spawnSync } from "bun";
import { bunExe, bunEnv as env } from "harness";

test("--no-addons throws an error on process.dlopen", () => {
const { stdout, stderr, exitCode } = spawnSync({
cmd: [bunExe(), "--no-addons", "-p", "process.dlopen()"],
env,
stdout: "pipe",
stderr: "pipe",
});
const err = stderr.toString();
const out = stdout.toString();
expect(exitCode).toBe(1);
expect(out).toBeEmpty();
expect(err).toContain("Cannot load native addon because loading addons is disabled");
});
29 changes: 29 additions & 0 deletions test/js/node/test/parallel/test-no-addons-resolution-condition.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
'use strict';

const common = require('../common');
const fixtures = require('../common/fixtures');
const { Worker, isMainThread, parentPort } = require('worker_threads');
const assert = require('assert');
const { createRequire } = require('module');

const loadFixture = createRequire(fixtures.path('node_modules'));

if (isMainThread) {
const tests = [[], ['--no-addons']];

for (const execArgv of tests) {
const worker = new Worker(__filename, { execArgv });

worker.on('message', common.mustCall((message) => {
if (execArgv.length === 0) {
assert.strictEqual(message, 'using native addons');
} else {
assert.strictEqual(message, 'not using native addons');
}
}));
}

} else {
const message = loadFixture('pkgexports/no-addons');
parentPort.postMessage(message);
}