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
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.