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

@cocalc/openat2

v0.1.1

Published

Linux openat2-based filesystem primitives for secure sandboxing

Readme

@cocalc/openat2

Linux-only napi-rs addon that exposes openat2-anchored filesystem operations for race-safe sandbox mutation.

Why this exists

In CoCalc safe mode, we need a strong guarantee:

  • filesystem operations for a project stay inside that project root
  • even if the project owner (or another process) is changing paths concurrently

The subtle failure mode is a classic race:

  1. We validate a string path (e.g. a/b/file.txt) and it looks safe.
  2. Before the actual mutation syscall runs, an attacker swaps an intermediate path component (or leaf) to a symlink.
  3. The mutation then lands outside the sandbox.

That validate(path) -> mutate(path) pattern is fundamentally fragile under concurrency, because validation and mutation happen at different times on a mutable namespace.

openat2 changes the model from string-based trust to descriptor-based trust:

  • we first open a root directory handle (dirfd) for the sandbox root
  • each operation resolves relative paths under that root via openat2
  • kernel-enforced resolve rules (RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS | RESOLVE_NO_MAGICLINKS) prevent escaping during resolution
  • we then mutate via *at syscalls (mkdirat, renameat, unlinkat, etc.) anchored to validated dirfds

This is closer to a capability model: possession of the root dirfd defines the authority boundary, and every derived operation stays constrained to that boundary. In practice, this removes dependence on ad-hoc deny/allow path filtering as the primary safety mechanism.

Why not just use Node fs + file descriptors?

  • File descriptors help for existing-file content I/O (read/write on an already opened inode), and we do use that pattern.
  • But many dangerous operations are path mutators (mkdir, rename, unlink, rmdir, chmod, utimes, create paths) that still require pathname resolution at operation time.
  • In plain Node, those mutators are path-based. You can pre-check with realpath/lstat, but that is still a user-space check followed by a later path syscall, so there is still a race window.
  • For create flows, there may be no target inode yet to pin with an fd. The critical security question is whether parent-chain resolution stayed inside the sandbox at the exact syscall boundary.
  • Node does not currently expose a complete openat2/*at capability API that lets us anchor all resolution to a sandbox dirfd with kernel-enforced constraints.

So fd-only hardening in Node is necessary but not sufficient: it meaningfully improves read/write safety, but it cannot fully eliminate TOCTOU escape classes for path-mutating operations. openat2 + *at is the piece that closes that remaining gap.

Tradeoffs:

  • implementation is more tedious than plain Node fs path calls
  • Linux-specific (openat2 is a Linux syscall)
  • existing path-oriented code needs adapter layers for migration

For our situation, that tradeoff is worth it: mutators become fail-closed under symlink/path-swap races, which is exactly the remaining hardening gap in backend sandbox safe mode.

Current API

import { SandboxRoot } from '@cocalc/openat2'

const root = new SandboxRoot('/srv/project')
root.mkdir('a/b', true)
root.rename('a/b/file.txt', 'a/b/file2.txt')
root.unlink('a/b/file2.txt')
const st = root.stat('a/b')

Methods implemented now:

  • mkdir(path, recursive?, mode?)
  • unlink(path)
  • rmdir(path)
  • rename(oldPath, newPath)
  • renameNoReplace(oldPath, newPath)
  • link(oldPath, newPath)
  • symlink(target, newPath)
  • chmod(path, mode)
  • truncate(path, len)
  • copyFile(src, dest, mode?)
  • rm(path, recursive?, force?)
  • utimes(path, atimeNs, mtimeNs)
  • stat(path)
  • openRead(path) -> fd
  • openWrite(path, create?, truncate?, append?, mode?) -> fd

openRead/openWrite return numeric file descriptors intended for high-frequency I/O paths in Node. The caller owns the descriptor and must close it.

Security model

  • Absolute paths are rejected.
  • .. traversal is rejected.
  • Symlink traversal is blocked by openat2 resolve flags.
  • Operations are anchored to a root dirfd opened once in constructor.

Build

pnpm install
pnpm build

Requirements:

  • Linux kernel with openat2 support (>=5.6)
  • Rust toolchain
  • Node 18+

Test

pnpm run test:rust

Packaging notes

This repository is configured to publish Linux prebuilt binaries for:

  • x86_64-unknown-linux-gnu
  • aarch64-unknown-linux-gnu

Runtime loading prefers:

  1. local cocalc_openat2.linux-*.node files (for CI/dev artifacts)
  2. optional npm packages (@cocalc/openat2-linux-x64-gnu / @cocalc/openat2-linux-arm64-gnu)
  3. local cocalc_openat2.node fallback for local development

Release automation

The workflow in .github/workflows/release.yml:

  1. builds both linux targets
  2. uploads .node artifacts
  3. on v* tags, downloads artifacts and publishes:
    • @cocalc/openat2-linux-x64-gnu
    • @cocalc/openat2-linux-arm64-gnu
    • @cocalc/openat2 (root package)

Publishing relies on the root prepublishOnly script:

pnpm run create-npm-dirs
napi prepublish -t npm --tagstyle npm --skip-gh-release