npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

po6

v0.1.2

Published

Typed synchronous POSIX syscalls for Node.js on Linux: files, terminals, sockets, namespaces, ioctl, eventfd and errno handling

Readme

node-po6

Typed, synchronous POSIX syscalls for Node.js on Linux, written in TypeScript.

  • Errors are values. No function throws for a failing syscall. Each returns the errno, and once that is checked TypeScript knows the rest of the result is there.
  • Plain buffers. Data, addresses and option values are Uint8Arrays (Node.js Buffers work as well). po6 fills in struct msghdr and struct iovec for recvmsg() and sendmsg().
  • Checked against the C headers. The layouts of the structures follow the data model of the platform and are compared with the POSIX headers in the tests.
  • No native code. po6 performs syscalls and pins buffers with the functions you pass in. Use syscall-napi and buffer2address, or fakes in your tests.

Requirements

  • Linux
  • Node.js 24 or newer
  • syscall-napi to perform the syscalls and buffer2address to pin buffers, both available for x86_64 and arm64

Installation

npm install po6 syscall-napi buffer2address

Usage

Setup

Create the API once, e.g. in a module po6.ts, with the syscalls of syscall-napi, the buffer pinning of buffer2address and the ABI of the host. The ABI also provides the constants and errno values:

import { pinBuffer } from "buffer2address";
import { syscall, syscallNumbers } from "syscall-napi";
import {
  createKernelAbiFor,
  createLinuxKernelInterface,
  createPo6Api,
  hostAbi
} from "po6";

const kernelAbi = createKernelAbiFor({ machineAbi: hostAbi });

const po6 = createPo6Api({
  kernelInterface: createLinuxKernelInterface({ syscall, syscallNumbers }),
  kernelAbi,
  memory: { pinBuffer },
});

export {
  po6,
  kernelAbi
};

Reading and writing

import { po6 } from "./po6.ts";

const { errno, fd } = po6.eventfd({ initval: 0, flags: 0 });
if (errno !== undefined) {
  throw po6.createErrorFromErrno({ operation: "eventfd()", errno });
}

// the counter of an eventfd is a 64-bit integer in host byte order
const increment = new Uint8Array(new BigUint64Array([3n]).buffer);
const { errno: writeErrno } = po6.write({ fd, data: increment });
if (writeErrno !== undefined) {
  throw po6.createErrorFromErrno({ operation: "write()", errno: writeErrno });
}

const counter = new BigUint64Array(1);
const { errno: readErrno } = po6.read({ fd, buffer: new Uint8Array(counter.buffer) });
if (readErrno !== undefined) {
  throw po6.createErrorFromErrno({ operation: "read()", errno: readErrno });
}

console.log(counter[0]); // 3n

po6.close({ fd });

Sockets

import { kernelAbi, po6 } from "./po6.ts";

const AF_INET = 2;
const SOCK_DGRAM = 2;

// struct sockaddr_in: the family in host byte order (little endian on x86_64
// and arm64), port and address in network byte order
const sockaddrIn = ({ port, address }: { port: number, address: number[] }) => {
  const sockaddr = new Uint8Array(16);
  const view = new DataView(sockaddr.buffer);
  view.setUint16(0, AF_INET, true);
  view.setUint16(2, port);
  sockaddr.set(address, 4);
  return sockaddr;
};

const { errno: socketErrno, fd } = po6.socket({ domain: AF_INET, type: SOCK_DGRAM, protocol: 0 });
if (socketErrno !== undefined) {
  throw po6.createErrorFromErrno({ operation: "socket()", errno: socketErrno });
}

try {
  // port 0 lets the kernel pick a free port
  const { errno: bindErrno } = po6.bind({ fd, sockaddr: sockaddrIn({ port: 0, address: [127, 0, 0, 1] }) });
  if (bindErrno !== undefined) {
    throw po6.createErrorFromErrno({ operation: "bind()", errno: bindErrno });
  }

  const { errno: nameErrno, sockaddr } = po6.getsockname({ fd });
  if (nameErrno !== undefined) {
    throw po6.createErrorFromErrno({ operation: "getsockname()", errno: nameErrno });
  }

  // send a datagram to the socket itself
  const { errno: sendErrno } = po6.sendmsg({
    fd,
    data: new TextEncoder().encode("hello"),
    msghdr: { msg_name: sockaddr },
  });
  if (sendErrno !== undefined) {
    throw po6.createErrorFromErrno({ operation: "sendmsg()", errno: sendErrno });
  }

  const data = new Uint8Array(1500);
  const sender = new Uint8Array(16);

  const { errno: recvErrno, bytesReceived, msghdr } = po6.recvmsg({
    fd,
    data,
    msghdr: { msg_name: sender },
    flags: kernelAbi.constants.MSG_DONTWAIT,
  });
  if (recvErrno !== undefined) {
    throw po6.createErrorFromErrno({ operation: "recvmsg()", errno: recvErrno });
  }

  console.log(new TextDecoder().decode(data.subarray(0, bytesReceived))); // hello
  console.log(msghdr.msg_namelen); // 16, the length of the sender's address in sender
} finally {
  po6.close({ fd });
}

Terminals

import { kernelAbi, po6 } from "./po6.ts";

const { constants } = kernelAbi;

// O_NOCTTY keeps the terminal from becoming the controlling terminal of the
// process, O_NONBLOCK keeps reads and writes from blocking the event loop
const { errno, fd } = po6.open({
  pathname: "/dev/ttyUSB0",
  flags: constants.O_RDWR | constants.O_NOCTTY | constants.O_NONBLOCK | constants.O_CLOEXEC,
});
if (errno !== undefined) {
  throw po6.createErrorFromErrno({ operation: "open()", errno });
}

const { errno: getErrno, termios } = po6.tcgetattr({ fd });
if (getErrno !== undefined) {
  throw po6.createErrorFromErrno({ operation: "tcgetattr()", errno: getErrno });
}

const baudRate = 115200n;

const { errno: setErrno } = po6.tcsetattr({
  fd,
  // apply the settings once the pending output was sent, and discard the pending input
  optionalActions: constants.TCSAFLUSH,
  termios: {
    ...termios,
    // raw mode like cfmakeraw(): no conversions, line editing, echo or signals
    c_iflag: termios.c_iflag & ~(constants.IGNBRK | constants.BRKINT | constants.PARMRK | constants.ISTRIP |
      constants.INLCR | constants.IGNCR | constants.ICRNL | constants.IXON | constants.IXOFF | constants.IXANY),
    c_oflag: termios.c_oflag & ~constants.OPOST,
    c_lflag: termios.c_lflag & ~(constants.ECHO | constants.ECHONL | constants.ICANON | constants.ISIG | constants.IEXTEN),
    // 8 data bits, no parity, 1 stop bit, no hardware flow control, modem control lines
    // ignored, and the baud rate taken from c_ispeed and c_ospeed (BOTHER)
    c_cflag: (termios.c_cflag & ~(constants.CSIZE | constants.PARENB | constants.CSTOPB | constants.CRTSCTS |
      constants.CBAUD | constants.CIBAUD)) |
      constants.CS8 | constants.CREAD | constants.CLOCAL | constants.BOTHER | (constants.BOTHER << constants.IBSHIFT),
    c_ispeed: baudRate,
    c_ospeed: baudRate,
  },
});
if (setErrno !== undefined) {
  throw po6.createErrorFromErrno({ operation: "tcsetattr()", errno: setErrno });
}

// read() and write() the terminal once it is ready, e.g. as uv-poll reports

po6.close({ fd });

Network namespaces

unshare() and setns() move the calling thread into other namespaces, which needs CAP_SYS_ADMIN. For Node.js, this is the main thread. File descriptors and sockets stay in the namespace they were created in, also after the thread switched back. So enter a namespace, create the sockets, and leave it again, all synchronously, so no other JavaScript runs inside the namespace in between:

import net from "node:net";
import { kernelAbi, po6 } from "./po6.ts";

const { constants } = kernelAbi;

const throwOnErrno = ({ operation, errno }: { operation: string, errno: number | undefined }) => {
  if (errno !== undefined) {
    throw po6.createErrorFromErrno({ operation, errno });
  }
};

// a handle on the network namespace of the calling thread
const openNetns = () => {
  const { errno, fd } = po6.open({ pathname: "/proc/thread-self/ns/net", flags: constants.O_RDONLY | constants.O_CLOEXEC });
  throwOnErrno({ operation: "open()", errno });
  return fd as number;
};

// runs callback in the network namespace of fd and always switches back
const withNetns = <T>({ fd, callback }: { fd: number, callback: () => T }): T => {
  const original = openNetns();

  try {
    throwOnErrno({ operation: "setns()", errno: po6.setns({ fd, nstype: constants.CLONE_NEWNET }).errno });
    return callback();
  } finally {
    throwOnErrno({ operation: "setns()", errno: po6.setns({ fd: original, nstype: constants.CLONE_NEWNET }).errno });
    po6.close({ fd: original });
  }
};

// a new network namespace, which exists as long as netnsFd is open
const createNetns = () => {
  const original = openNetns();

  try {
    throwOnErrno({ operation: "unshare()", errno: po6.unshare({ flags: constants.CLONE_NEWNET }).errno });
    return openNetns();
  } finally {
    throwOnErrno({ operation: "setns()", errno: po6.setns({ fd: original, nstype: constants.CLONE_NEWNET }).errno });
    po6.close({ fd: original });
  }
};

const netnsFd = createNetns();

// a TCP socket listening on port 8080 in the namespace, its interfaces are set up separately, e.g. with node-rtnetlink
const listeningFd = withNetns({
  fd: netnsFd,
  callback: () => {
    const AF_INET = 2;
    const SOCK_STREAM = 1;

    const { errno, fd } = po6.socket({ domain: AF_INET, type: SOCK_STREAM, protocol: 0 });
    if (errno !== undefined) {
      throw po6.createErrorFromErrno({ operation: "socket()", errno });
    }

    // struct sockaddr_in for 0.0.0.0:8080, the port in network byte order
    const sockaddr = new Uint8Array(16);
    new DataView(sockaddr.buffer).setUint16(0, AF_INET, true);
    new DataView(sockaddr.buffer).setUint16(2, 8080);

    throwOnErrno({ operation: "bind()", errno: po6.bind({ fd, sockaddr }).errno });
    throwOnErrno({ operation: "listen()", errno: po6.listen({ fd, backlog: 128 }).errno });
    return fd;
  },
});

// Node.js accepts the connections, they belong to the namespace of the listening socket
net.createServer((connection) => {
  connection.end("hello from the namespace\n");
}).listen({ fd: listeningFd });

Creating a user namespace (CLONE_NEWUSER) fails with EINVAL, as Node.js runs several threads.

Handling errors

import { kernelAbi, po6 } from "./po6.ts";

const { constants, errnoCodes } = kernelAbi;

const fd = -1;

const { errno } = po6.recvmsg({ fd, data: new Uint8Array(1500), flags: constants.MSG_DONTWAIT });

if (errno === errnoCodes.EAGAIN) {
  // nothing to receive yet, wait until the socket is readable
} else if (errno !== undefined) {
  const error = po6.createErrorFromErrno({ operation: "recvmsg()", errno });
  console.log(error.message); // recvmsg() failed with EBADF: Bad file descriptor
  console.log(error.errno === errnoCodes.EBADF); // true
}

API overview

createPo6Api({ kernelInterface, kernelAbi, memory }) returns the functions below and createErrorFromErrno(). All of them take a single object of arguments. File descriptors are numbers, other integers like flags may be numbers or bigints.

| Function | Arguments | Result on success, besides errno: undefined | | --- | --- | --- | | socket() | domain, type, protocol | fd | | bind() | fd, sockaddr | | | listen() | fd, backlog | | | getsockname() | fd | sockaddr, a copy of the address | | sendmsg() | fd, data, optional msghdr (msg_name, msg_control, msg_flags), optional flags | bytesSent | | recvmsg() | fd, data, optional msghdr (msg_name, msg_control), optional flags | bytesReceived, msghdr (msg_namelen, msg_controllen, msg_flags) | | shutdown() | fd, how | | | getsockopt() | fd, level, optname, optval | optlen, the length the kernel wrote to optval | | setsockopt() | fd, level, optname, optval | | | ioctl() | fd, request, args (bigints and Uint8Arrays) | | | eventfd() | initval, flags | fd | | open() | pathname, flags, optional mode | fd | | openat() | dirfd, pathname, flags, optional mode | fd | | tcgetattr() | fd | termios | | tcsetattr() | fd, optionalActions (TCSANOW, TCSADRAIN, TCSAFLUSH), termios | | | tcflush() | fd, queueSelector (TCIFLUSH, TCOFLUSH, TCIOFLUSH) | | | tcdrain() | fd | | | read() | fd, buffer | bytesRead | | write() | fd, data | bytesWritten | | close() | fd | | | dup() | fd | fd, the new file descriptor | | unshare() | flags, the CLONE_NEW* types | | | setns() | fd, nstype, a CLONE_NEW* type or 0 for any | |

The termios functions use struct termios2 of the kernel. Its c_ispeed and c_ospeed hold the speeds as numbers, which apply with BOTHER in c_cflag, so any baud rate can be set, and c_cc has NCCS (19) entries.

recvmsg() and sendmsg() transfer data with a single struct iovec. recvmsg() writes the address of the sender to msg_name and ancillary data to msg_control, as far as they fit. ioctl() passes Uint8Array arguments as pointers to their data:

const FIONREAD = 0x541b;

const bytesAvailable = new Int32Array(1);
const { errno } = po6.ioctl({ fd, request: FIONREAD, args: [new Uint8Array(bytesAvailable.buffer)] });

Results and errors

Every result contains errno. It is undefined if the syscall succeeded and holds the errno if it failed, in which case the other fields of the result are undefined. The result types are exported as TReadResult, TRecvmsgResult and so on.

po6.createErrorFromErrno({ operation, errno }) turns an errno into an Error with a message like read() failed with EAGAIN: Resource temporarily unavailable and the errno attached (TErrorWithErrno). kernelAbi.errnoCodes maps the errno names to their values, including the aliases EWOULDBLOCK, EDEADLOCK and ENOTSUP.

The kernel ABI

createKernelAbiFor({ machineAbi }) describes how the kernel expects data: the layouts of the structures for the data model and byte order of the machine, the errno values in errnoCodes and the values in constants, which are

  • MSG_*, the flags of recvmsg() and sendmsg()
  • SHUT_RD, SHUT_WR and SHUT_RDWR for shutdown()
  • POLL*, the poll events
  • SOL_PACKET, PACKET_ADD_MEMBERSHIP, PACKET_DROP_MEMBERSHIP and PACKET_MR_PROMISC for packet sockets
  • O_*, the flags of open() and openat(), and AT_FDCWD. O_DIRECTORY, O_NOFOLLOW, O_DIRECT and O_LARGEFILE are left out, as arm and arm64 use other values for them than x86
  • CLONE_NEWNET, CLONE_NEWNS, CLONE_NEWUTS, CLONE_NEWIPC, CLONE_NEWUSER, CLONE_NEWPID, CLONE_NEWCGROUP and CLONE_NEWTIME, the namespace types
  • the termios flags of c_iflag, c_oflag, c_cflag and c_lflag, the indices of c_cc (V*) and NCCS, the arguments of tcsetattr() and tcflush(), and the ioctl requests TCGETS2, TCSETS2, TCSETSW2, TCSETSF2, TCSBRK and TCFLSH

hostAbi is the machine ABI Node.js runs on. The constants and errno values are the ones Linux uses on x86, arm and arm64.

Blocking

All functions perform their syscall synchronously and block until it returns, so a blocking recvmsg() on an empty socket blocks the whole event loop. Use non-blocking file descriptors or MSG_DONTWAIT, and wait for readiness in the event loop, e.g. with uv-poll. po6 has no poll() on purpose, as polling belongs into the event loop.

Buffers and file descriptors

po6 only uses buffers during the call. For recvmsg() and sendmsg() it pins data, msg_name and msg_control while the syscall runs, as struct msghdr refers to them by address. File descriptors are not tracked, so close the ones you create. File descriptors of Node.js, e.g. from fs.openSync(), can be used as well.

Development

npm ci
npm run build       # transpile to dist/
npm run type-check
npm run test        # mocha with c8, 100% coverage required
npm run lint

The functions in lib/functions/ get their dependencies passed in by createPo6Api() in lib/po6-api.ts:

  • lib/linux-kernel-interface.ts, the typed syscalls on top of a syscall function and the syscall numbers
  • lib/kernel-abi.ts, the layouts of the structures, the constants and the errno values for a machine ABI
  • lib/pinning.ts, pinning buffers to pass their addresses to the kernel

The unit tests run against a fake kernel and fake memory from lib/test-support/ and perform no syscalls. syscall-napi is a dev dependency only, to check that it provides all syscalls of the kernel interface. The layouts of the structures are compared with the POSIX headers by compiling C programs, and a spec builds the package to check its type declarations, so gcc and the C library headers are required.

Releases are published by pushing a tag like v0.1.0, which builds the package, merges package.npm.json into package.json and sets the version.

License

LGPL-2.1, see LICENSE.