Skip to content

Node.js built-in fetch fails inside nsjail with WebAssembly.instantiate() OOM, even after increasing --rlimit_as #262

@YangZxi

Description

@YangZxi

Problem

Running Node.js built-in fetch() inside nsjail fails with a Wasm OOM during undici initialization:

RangeError: WebAssembly.instantiate(): Out of memory: Cannot allocate Wasm memory for new instance
    at lazyllhttp (node:internal/deps/undici/undici:5829:32)

The failure path is:

  • Node.js built-in fetch
  • undici
  • lazyllhttp
  • WebAssembly.instantiate()

The same script works outside nsjail.

I reproduced this with a minimal Docker environment containing only:

  • Debian bookworm-slim
  • Node.js 22.19.0
  • nsjail 3.6

The sandbox configuration is roughly:

  • clone_newnet: false
  • clone_newuser/newns/newpid/newipc/newuts: true
  • bind mounts for /usr, /lib, /lib64, /proc, and basic DNS/SSL files
  • --keep_env
  • --disable_clone_newcgroup
  • --rlimit_as <value>

With lower RLIMIT_AS, Node may fail even earlier during V8 startup with:

Fatal process out of memory: Failed to reserve virtual memory for CodeRange

With a higher RLIMIT_AS value, Node starts, but fetch() still fails in WebAssembly.instantiate().

This looks like an interaction between nsjail memory/address-space limits and Node/undici Wasm initialization, rather than an application-level memory issue.

I would like to know whether this is:

  • expected behavior under nsjail
  • a known limitation with RLIMIT_AS
  • or a bug / unsupported case for this workload

This is a min reproducible environment version.
you just need run:

docker build -f "Dockerfile.nsjail" -t callit-nsjail-repro "." && docker run --rm --privileged -e RLIMIT_AS_MB=4096 callit-nsjail-repro bash -lc '/repro/run-fetch-in-nsjail.sh'
FROM debian:bookworm-slim

ARG NODE_VERSION=22.19.0
ARG NODE_ARCH=linux-x64
ARG NSJAIL_VERSION=3.6

WORKDIR /repro

ENV PATH=/usr/local/nodejs/bin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

RUN apt-get update \
    && apt-get install -y --no-install-recommends \
       autoconf \
       bison \
       ca-certificates \
       curl \
       flex \
       g++ \
       gcc \
       git \
       libnl-route-3-dev \
       libprotobuf-dev \
       libtool \
       make \
       pkg-config \
       protobuf-compiler \
       xz-utils \
    && rm -rf /var/lib/apt/lists/*

RUN mkdir -p /usr/local/nodejs \
    && curl -fsSL "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-${NODE_ARCH}.tar.xz" -o /tmp/node.tar.xz \
    && tar -xJf /tmp/node.tar.xz -C /usr/local/nodejs --strip-components=1 \
    && rm -f /tmp/node.tar.xz \
    && ln -sf /usr/local/nodejs/bin/node /usr/local/bin/node \
    && ln -sf /usr/local/nodejs/bin/npm /usr/local/bin/npm \
    && ln -sf /usr/local/nodejs/bin/npx /usr/local/bin/npx

RUN git clone --branch "${NSJAIL_VERSION}" --depth 1 https://github.com/google/nsjail.git /tmp/nsjail-src \
    && cd /tmp/nsjail-src \
    && make \
    && install -m 0755 /tmp/nsjail-src/nsjail /usr/local/bin/nsjail \
    && rm -rf /tmp/nsjail-src

RUN cat <<'EOF' > /repro/fetch-test.mjs
const targetUrl = process.argv[2] ?? 'https://example.com';

console.log(`[fetch-test] url=${targetUrl}`);

const response = await fetch(targetUrl);
const responseText = await response.text();

console.log(`[fetch-test] status=${response.status}`);
console.log(responseText.slice(0, 200));
EOF

RUN cat <<'EOF' > /repro/nsjail-node-fetch.cfg
name: "node-fetch-repro"
mode: ONCE
hostname: "nsjail-repro"
cwd: "/repro"
clone_newnet: false
clone_newuser: true
clone_newns: true
clone_newpid: true
clone_newipc: true
clone_newuts: true
uidmap {
  inside_id: "0"
  outside_id: ""
  count: 1
}
gidmap {
  inside_id: "0"
  outside_id: ""
  count: 1
}
mount {
  src: "/repro"
  dst: "/repro"
  is_bind: true
  rw: false
}
mount {
  src: "/usr"
  dst: "/usr"
  is_bind: true
  rw: false
}
mount {
  src: "/lib"
  dst: "/lib"
  is_bind: true
  rw: false
}
mount {
  src: "/lib64"
  dst: "/lib64"
  is_bind: true
  rw: false
}
mount {
  src: "/etc/resolv.conf"
  dst: "/etc/resolv.conf"
  is_bind: true
  rw: false
}
mount {
  src: "/etc/hosts"
  dst: "/etc/hosts"
  is_bind: true
  rw: false
}
mount {
  src: "/etc/nsswitch.conf"
  dst: "/etc/nsswitch.conf"
  is_bind: true
  rw: false
}
mount {
  src: "/etc/gai.conf"
  dst: "/etc/gai.conf"
  is_bind: true
  rw: false
}
mount {
  src: "/etc/ssl/certs"
  dst: "/etc/ssl/certs"
  is_bind: true
  rw: false
}
mount {
  src: "/etc/ca-certificates.conf"
  dst: "/etc/ca-certificates.conf"
  is_bind: true
  rw: false
}
mount {
  src: "/etc/ssl/openssl.cnf"
  dst: "/etc/ssl/openssl.cnf"
  is_bind: true
  rw: false
}
mount {
  dst: "/proc"
  fstype: "proc"
}
EOF

RUN cat <<'EOF' > /repro/run-fetch-in-nsjail.sh
#!/usr/bin/env bash
set -euo pipefail

TARGET_URL="${1:-https://example.com}"
RLIMIT_AS_MB="${RLIMIT_AS_MB:-4096}"

echo "[nsjail-repro] node=$(node --version)"
echo "[nsjail-repro] nsjail=$(nsjail --version 2>/dev/null || true)"
echo "[nsjail-repro] url=${TARGET_URL}"
echo "[nsjail-repro] rlimit_as_mb=${RLIMIT_AS_MB}"

exec nsjail \
  --config /repro/nsjail-node-fetch.cfg \
  --log_fd 1 \
  --keep_env \
  --disable_clone_newcgroup \
  --rlimit_as "${RLIMIT_AS_MB}" \
  -- /usr/local/bin/node /repro/fetch-test.mjs "${TARGET_URL}"
EOF

RUN chmod +x /repro/run-fetch-in-nsjail.sh

CMD ["bash"]

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions