AphrodyBun GitHub

Runtime docs · Runtime · Interop & Tooling

Linux

Call Linux kernel interfaces directly from JavaScript with bun:linux

bun:linux exposes Linux kernel interfaces as synchronous functions: namespaces, mounts, cgroups, capabilities, pidfds, memfds, Landlock, sysctl, kernel modules and reboot. The module loads on first import, so it costs nothing at startup.

import linux from "bun:linux";

if (linux.isSupported) {
  console.log(linux.sysctl.get("kernel.ostype")); // "Linux"
}

On other platforms isSupported is false. Every function exists, and each call that reaches the kernel throws an error that says bun:linux is only available on Linux.


Errors

A failed kernel call throws a SystemError with code, errno, syscall and, when a path is involved, path.

try {
  linux.mount(null, "/mnt", "tmpfs");
} catch (error) {
  console.log(error.code, error.syscall); // "EPERM" "mount"
}

Invalid arguments throw TypeError or RangeError before any system call runs.


Constants

linux.constants holds the numeric values for CLONE_NEW*, MS_*, MNT_*, PR_*, CAP_*, RB_*, LANDLOCK_ACCESS_FS_*, MFD_* and the module, pidfd and kexec flags.


Namespaces and mounts

unshare and setns change the namespaces of the calling thread. Most calls need CAP_SYS_ADMIN.

const { constants } = linux;

linux.unshare(constants.CLONE_NEWNS);
linux.mount(null, "/", null, constants.MS_REC | constants.MS_PRIVATE);
linux.mount("tmpfs", "/mnt", "tmpfs", 0, "size=1m");
linux.umount("/mnt");

pivotRoot(newRoot, putOld) swaps the root of the mount namespace.


Control groups

linux.cgroup works on cgroup v2 under /sys/fs/cgroup. Paths with . or .. segments are rejected.

const path = linux.cgroup.create("my-job", {
  cpuMax: 0.5, // half a CPU
  memoryMax: 256 * 1024 * 1024,
  pidsMax: 64,
});

linux.cgroup.attach(path); // current process
console.log(linux.cgroup.read(path, "memory.current"));
console.log(linux.cgroup.current());

linux.cgroup.remove(path);

cpuMax also accepts a string such as "50000 100000". cpuWeight and ioWeight range from 1 to 10000.


Capabilities and prctl

const { effective, permitted } = linux.capabilities.get();
console.log((effective & (1n << BigInt(linux.constants.CAP_NET_ADMIN))) !== 0n);

linux.capabilities.dropBounding(linux.constants.CAP_SYS_MODULE);
linux.setNoNewPrivs();

Sets are bigint bit masks. prctl(option, arg2?, arg3?, arg4?, arg5?) calls prctl(2) directly.


pidfd

A pidfd names one process, so a signal can never reach a reused pid.

const child = Bun.spawn(["sleep", "60"]);
const fd = linux.pidfdOpen(child.pid);
linux.pidfdSendSignal(fd, "SIGKILL");
await child.exited;
require("node:fs").closeSync(fd);

pidfdGetfd(pidfd, targetFd) copies a descriptor from another process.


memfd

memfdCreate(name, flags?) returns the descriptor of an anonymous in-memory file. Use it with node:fs.

import fs from "node:fs";

const fd = linux.memfdCreate("scratch");
fs.writeSync(fd, "hello");
fs.closeSync(fd);

Landlock

landlock.restrictSelf confines the calling thread, and the processes it spawns, to a set of paths. The restriction is permanent.

if (linux.landlock.abiVersion() > 0) {
  linux.landlock.restrictSelf({
    readOnly: ["/usr", "/lib", "/etc"],
    readWrite: ["/tmp/work"],
    execute: ["/usr/bin"],
  });
}

abiVersion() returns 0 when Landlock is unavailable.


sysctl

linux.sysctl.get("net.ipv4.ip_forward");
linux.sysctl.set("net.ipv4.ip_forward", 1);

Names are dotted, such as kernel.ostype. Names with .. or / are rejected.


Kernel modules and power

These calls need CAP_SYS_MODULE or CAP_SYS_BOOT.

linux.finitModule("/lib/modules/dummy.ko", "numdummies=1");
linux.deleteModule("dummy");

linux.reboot(linux.constants.RB_POWER_OFF);

initModule loads an image from an ArrayBufferView. kexecFileLoad(kernelFd, initrdFd, cmdline, flags?) stages a kernel for RB_KEXEC.

reboot with RB_AUTOBOOT, RB_POWER_OFF, RB_HALT_SYSTEM or RB_KEXEC acts immediately.


io_uring

Bun does not use io_uring on Linux. ioUring.probe() only reports what the kernel allows.

const { supported, ops } = linux.ioUring.probe();

seccomp

seccomp.filter() builds a deny-list program for the running architecture (x64 or arm64). seccomp.setFilter() sets no_new_privs and installs it on the calling thread. The filter cannot be removed and child processes inherit it.

const deny = process.arch === "x64" ? [83, 258] : [34]; // mkdir, mkdirat
linux.seccomp.setFilter(linux.seccomp.filter({ deny, errno: 1 })); // EPERM

setFilter() also accepts any struct sock_filter array you build yourself.

perf_event

const { constants } = linux;
const fd = linux.perfEvent.open({
  type: constants.PERF_TYPE_SOFTWARE,
  config: constants.PERF_COUNT_SW_TASK_CLOCK,
  disabled: true,
});
linux.perfEvent.ioctl(fd, constants.PERF_EVENT_IOC_ENABLE);
work();
linux.perfEvent.ioctl(fd, constants.PERF_EVENT_IOC_DISABLE);
console.log(linux.perfEvent.read(fd)); // nanoseconds, as a bigint

The kernel.perf_event_paranoid sysctl decides what an unprivileged process may count.

eBPF

bpf creates maps, reads and writes their elements, loads programs, and pins and opens objects in bpffs. Keys and values are ArrayBufferViews, and their sizes are checked against the map's. Per-CPU maps are not supported. When progLoad() fails with a logSize, the error's log property holds the verifier output.

const fd = linux.bpf.mapCreate({ type: linux.constants.BPF_MAP_TYPE_HASH, keySize: 4, valueSize: 8, maxEntries: 64 });
linux.bpf.mapUpdate(fd, new Uint32Array([1]), new BigUint64Array([42n]));

netlink.request() sends one or more encoded messages to the kernel. It collects the replies until NLMSG_DONE or an ack arrives, and returns them parsed. A negative NLMSG_ERROR throws with its errno.

const { constants } = linux;
const links = linux.netlink.request(
  constants.NETLINK_ROUTE,
  linux.netlink.encode({
    type: constants.RTM_GETLINK,
    flags: constants.NLM_F_REQUEST | constants.NLM_F_DUMP,
    payload: new Uint8Array(16), // struct ifinfomsg
  }),
);

Reference

See packages/bun-types/linux.d.ts for the complete signatures, required privileges and errors.