
I investigated the execution environment of Lambda Web Functions
This page has been translated by machine translation. View original
Introduction
AWS Lambda Web Functions, which became GA in October 2026, allocates 2 GB of memory and 2 vCPUs to a single execution environment.
In this article, I examined the CPU generation and OpenSSL benchmark performance within the range observable from the execution environment. I also compared it with traditional Lambda (Event Functions), Lambda MicroVMs, and EC2 c6g, c7g, and c8g instances.
What Was Tested
For testing, I used a diagnostic app running on Node.js 24 (arm64). It reads /proc and /sys from fixed endpoints, executes commands on an allowlist, tests writes to /tmp, and returns results as JSON. It does not retrieve environment variables. Diagnostic data was collected from a single execution environment in the Tokyo region. The code is included at the end of the article.
Execution Environment Specs
The information observable from the Lambda Web Functions execution environment is as follows.
CPU and Memory
| Item | Value |
|---|---|
| Node.js | v24.21.0 (arm64) |
| Kernel | 6.18.51-120.163.amzn2023.aarch64.microvm |
| OS | Amazon Linux 2023.12.20260930 |
| vCPU | 2 (os.availableParallelism(), the count from /proc/cpuinfo, and Cpus_allowed_list all show 2 CPUs) |
| CPU | CPU part 0xd40, CPU variant 0x1, BogoMIPS 2100.00 |
| Memory (official docs) | 2 GB |
| Memory (/proc/meminfo MemTotal) | 2460024 kB (approx. 2.35 GiB). os.totalmem() is 2519064576 bytes, swap is 0 |
| os.cpus() model | unknown |
While the official memory allocation is 2 GB, /proc/meminfo in the execution environment shows MemTotal as approximately 2.35 GiB.
Permissions and Sandbox
The permission-related lines from /proc/self/status:
Uid: 993 993 993 993
Gid: 990 990 990 990
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 0000000000000000
CapAmb: 0000000000000000
NoNewPrivs: 1
Seccomp: 2
Seccomp_filters: 8
The app runs as a non-root regular user, and execution of dmesg was denied (described later). With ps -eo pid,ppid,user,comm, PID 1 was root's init and PID 2 was sbx_user…'s node. With lsns, I confirmed 8 namespaces: time, user, net, mnt, uts, ipc, pid, and cgroup.
Filesystem and /tmp
The df -h output from the execution environment:
Filesystem Size Used Avail Use% Mounted on
/dev/vde 1.6G 1.1G 436M 71% /
tmpfs 64M 0 64M 0% /dev
/dev/vdc 3.8M 3.8M 0 100% /var/task
/dev/vdd 525M 11M 503M 3% /tmp
/dev/root 9.8G 854M 8.9G 9% /etc/passwd
/dev/vdb 1.5G 1.3M 1.5G 1% /etc/resolv.conf
/ was mounted as ext4 with ro,nosuid,nodev, and /var/task was mounted as squashfs with ro,nosuid,nodev. /tmp was ext4 with rw,relatime and without noexec. /tmp is readable and writable, and writes from the app succeeded. /dev/shm does not exist and returns ENOENT when accessed.
Writing 64 MiB at a time to /tmp resulted in ENOSPC when the file reached 526,385,152 bytes (approximately 502 MiB). The available space before writing was 527,192,064 bytes, and it recovered to the same value after deleting the file. statfs reported the filesystem capacity as 525 MiB, but the practical usable amount for temporary files is approximately 502 MiB.
Local files such as those in /tmp may persist while the execution environment is reused. However, the lifetime of the execution environment is not guaranteed.
/proc and /sys
| Path | Result |
|---|---|
| /sys/class/dmi/id | ENOENT |
| /sys/hypervisor | ENOENT |
| /sys/fs/cgroup | ENOENT |
| /proc/self/cgroup | readable |
| /proc/self/mountinfo | readable (7 mounts, no sysfs) |
| /proc/cpuinfo, /proc/meminfo, /proc/self/status, /proc/self/limits | readable |
| /proc/cmdline | EACCES |
cgroup CPU and memory limits, DMI, and hypervisor information could not be obtained from these paths. CPU count and the memory information visible from the execution environment could be retrieved from /proc/cpuinfo, /proc/meminfo, and Node.js's os module.
External Commands
Collecting and Bundling Binaries
Of the commands used for diagnostics, only df, uname, and coreutils were present in the execution environment by default. All other commands were collected from an AL2023 arm64 container (public.ecr.aws/amazonlinux/amazonlinux:2023, linux/arm64), and the binaries and shared libraries were bundled into bin/ and lib/ of the deployment package. The bundled versions were util-linux 2.37.4, procps-ng 3.3.17, and coreutils 8.32.
Saved as collect-binaries.sh:
#!/bin/bash
# Collect binaries and dependent libraries for util-linux / procps-ng / coreutils into bin/ lib/ inside an AL2023 (linux/arm64) container.
set -euo pipefail
OUT="${1:-/out}"
mkdir -p "$OUT/bin" "$OUT/lib"
dnf install -y -q util-linux procps-ng openssl >/dev/null 2>&1 || dnf install -y util-linux procps-ng openssl
cat /etc/os-release | head -3
# df / uname are shebang scripts pointing to the multi-call binary (/usr/bin/coreutils), so collect the main binary
for c in lscpu lsblk lsmem findmnt lsns dmesg free ps coreutils openssl; do
p=$(command -v "$c" || true)
if [ -z "$p" ]; then echo "MISSING $c"; continue; fi
cp -L "$p" "$OUT/bin/$c"
{ ldd "$p" 2>&1 || true; } | awk '/=> \//{print $3}' | while read -r so; do cp -Ln "$so" "$OUT/lib/" 2>/dev/null || true; done
echo "OK $c $p"
done
rpm -q openssl openssl-libs util-linux procps-ng coreutils-single
# Place the loader itself in lib/
cp -L /lib/ld-linux-aarch64.so.1 "$OUT/lib/" 2>/dev/null || cp -L /lib64/ld-linux-aarch64.so.1 "$OUT/lib/"
ls -l "$OUT/bin" "$OUT/lib"
[ -n "${HOST_UID:-}" ] && chown -R "$HOST_UID:${HOST_GID:-$HOST_UID}" "$OUT" || true
Collection was performed with the following command. bin/ and lib/ were output to diag-bin.
docker run --rm --platform linux/arm64 -e HOST_UID=$(id -u) -e HOST_GID=$(id -g) -v "$PWD:/work" -v "$PWD/diag-bin:/out" public.ecr.aws/amazonlinux/amazonlinux:2023 bash /work/collect-binaries.sh /out
df and uname were shebang scripts whose actual bodies were the multi-call binary coreutils. I bundled coreutils and called it with --coreutils-prog=<name>. All other commands were executed in the following form, passing the library path to the bundled loader:
<DST>/lib/ld-linux-aarch64.so.1 --library-path <DST>/lib <DST>/bin/<cmd>
In the diagnostic app, <DST> is set to /tmp/diag-bin.
The 24 files in the zipped bin/ and lib/ were all extracted to /var/task with mode 755 intact. /var/task is not noexec, and /var/task/bin/lscpu could be executed directly. lscpu exited with code 1 with the same error whether run directly, via the loader, or after copying to /tmp. The following table shows the results when copied to /tmp and run via the loader.
Execution Results
| Command | Result of bundled version |
|---|---|
| lscpu | exit 1: failed to determine number of CPUs: /sys/devices/system/cpu/possible: No such file or directory |
| lsblk | exit 1: failed to access sysfs directory: /sys/dev/block: No such file or directory |
| lsmem | exit 1: cannot open /sys/devices/system/memory: No such file or directory |
| findmnt | exit 0 |
| lsns | exit 0 |
| dmesg | exit 1: read kernel buffer failed: Operation not permitted |
| free -m | exit 0 |
| ps | exit 0 |
| df -h | exit 0 (identical output to /usr/bin/df in the execution environment) |
| uname -a | exit 0 (identical output to /usr/bin/uname in the execution environment) |
The failures were lscpu, lsblk, and lsmem, which read /sys, and dmesg, which requires elevated permissions. findmnt, lsns, free, and ps, which only need /proc, worked when bundled. /usr/bin in the execution environment had 175 entries, and the target commands other than df, uname, and coreutils were not found in /usr/bin, /bin, /usr/sbin, /sbin, or /usr/local/bin.
CPU Generation and Performance
Graviton3-Generation CPU
In the Linux kernel CPU part definitions, 0xd40 corresponds to Neoverse V1. In the AWS Graviton technical guide, the compile option for Graviton3(E) is -mcpu=neoverse-v1, Graviton2 is neoverse-n1 (0xd0c), and Graviton4 is neoverse-v2 (0xd4f).
CPU identification information comparing Web Function with EC2 c6g.large, c7g.large, and c8g.large:
| Item | c6g.large | c7g.large | c8g.large | Web Function |
|---|---|---|---|---|
| CPU part | 0xd0c | 0xd40 | 0xd4f | 0xd40 |
| CPU variant / revision | 0x3 / 1 | 0x1 / 1 | 0x0 / 1 | 0x1 / 1 |
| BogoMIPS | 243.75 | 2100.00 | 2000.00 | 2100.00 |
| lscpu BIOS Model name | AWS Graviton2 | AWS Graviton3 | AWS Graviton4 | — (lscpu fails due to missing /sys) |
| SVE-related Features | none | sve, svebf16, svei8mm (no sve2) | sve2, sveaes, svesha3, etc. | sve, svebf16, svei8mm (no sve2) |
| OPENSSL_armcap | 0xbd | 0x13ffd | 0x179fd | 0x13ffd |
The CPU part, CPU variant, BogoMIPS, Features, and OPENSSL_armcap of Web Function all matched c7g.large. It has SVE-related features not present in Graviton2, but lacks SVE2 which Graviton4 has. From this, it can be inferred that Web Function uses a Graviton3-generation CPU. However, Graviton3 and Graviton3E cannot be distinguished by CPU part alone. Also, since the model name string could not be obtained in Web Function, this judgment is based on matching values.
OpenSSL Speed Comparison
Using the bundled openssl 3.5.8, I ran openssl speed with the same arguments on Web Function (one execution environment) and three EC2 instances (one each). The same version was installed on EC2 via dnf install and run from AL2023 in ap-northeast-1a via SSM Run Command. The measurement command format is as follows:
openssl speed -seconds 2 -bytes {64,16384} [-multi 2] -evp {aes-128-gcm,aes-256-gcm,sha256,sha512,sha3-256,chacha20-poly1305}
openssl speed -seconds 2 [-multi 2] {rsa2048,ecdsap256,ecdhx25519}
Of the 17 items measured with a single process, here are 4 representative ones. Each value is the median of 3 rounds.
| Item | Web Function | c6g.large | c7g.large | c8g.large |
|---|---|---|---|---|
| sha256 64B (kB/s) | 287,673 | 225,301 | 287,460 | 367,102 |
| aes-128-gcm 16KB (kB/s) | 5,949,915 | 2,393,760 | 5,287,231 | 6,333,563 |
| rsa2048 sign/s | 1,032 | 294 | 1,045 | 1,187 |
| ecdsap256 sign/s | 41,629 | 18,754 | 41,980 | 48,936 |
Web Function results were close to c7g.large in most items.
Next, the throughput of 2 processes (-multi 2) is shown as a multiplier relative to 1 process. Aggregated from 51 measurements of 17 items × 3 rounds.
| Environment | Median | Min–Max |
|---|---|---|
| Web Function | 1.84 | 1.809–1.856 |
| c6g.large | 2.000 | 1.954–2.016 |
| c7g.large | 1.997 | 1.861–2.022 |
| c8g.large | 1.998 | 1.801–2.003 |
All three EC2 instances were around 2.00x, while Web Function was 1.84x even with 2 vCPUs.
Comparison with Lambda MicroVMs and Traditional Lambda
I ran the same diagnostics on Web Functions, Lambda MicroVMs, and traditional Lambda. MicroVMs were configured with a baseline of 4 GB / 2 vCPU, and traditional Lambda was configured with arm64 / 3,538 MB.
| Item | Web Functions | Lambda MicroVMs | Traditional Lambda |
|---|---|---|---|
| Visible CPU count | 2 | 8 (peak) | 3 |
| MemTotal | 2460024 kB | 16437604 kB (peak) | 4510668 kB |
| Execution user and permissions | uid 993, CapEff 0, 8 Seccomp filters | uid 0, 14 CapEff capabilities, Seccomp 0 | uid 993, CapEff 0, 6 Seccomp filters |
| /sys | absent | sysfs (read-only). No DMI or hypervisor | absent |
| lscpu, lsblk, lsmem | exit 1 | exit 0 | exit 1 |
| dmesg | denied | exit 0 | denied |
| /tmp | ENOSPC at approx. 502 MiB | could write up to 1 GiB (test limit) | ENOSPC at approx. 495 MiB (default 512 MB) |
| CPU part | 0xd40 | 0xd40 (no SVE features, 1 instance) | 0xd0c and 0xd40 |
| openssl speed scaling with 2 processes | 1.84 | 2.00 | 1.69 |
MicroVMs, measured in a single execution environment, ran as root and could access /sys, so lscpu, lsblk, lsmem, and dmesg all succeeded. openssl speed scaled to 7.996x with 8 processes (median of 51 measurements).
Traditional Lambda had the same uid, capabilities, /sys visibility, and command success/failure behavior as Web Functions. The visible CPU count was 3, but the total openssl speed throughput plateaued at around 1.7x: 1.686x with 2 processes, 1.698x with 3 processes, and 1.696x with 4 processes. Even with the same configuration, traditional Lambda execution environments were split between Graviton2 and Graviton3. The Graviton3 execution environment was used for OpenSSL performance measurements.
Estimated Hourly Cost
These are estimated values calculated from listed unit prices, not actual billed amounts. The assumptions are as follows:
- Unit prices: Tokyo region, Arm, USD, using values from the Price List API effective 2026-10-01. Only compute charges are included.
- CPU utilization: CPU time used ÷ (2 vCPUs × 3,600 seconds). Assumes a 2 vCPU configuration running continuously for 1 hour.
- Web Functions: Calculated assuming billing for vCPU usage time and memory usage. Since memory usage was not measured, 1 GB is assumed.
- MicroVMs: Calculated within the baseline of 4 GB / 2 vCPU.
| CPU Utilization | EC2 c7g.large | Web Functions (1 GB memory consumed) | Lambda MicroVMs (4 GB / 2 vCPU) |
|---|---|---|---|
| 100% | $0.0910 | $0.2475 | $0.2936 |
| 75% | $0.0910 | $0.1895 | $0.2936 |
| 50% | $0.0910 | $0.1314 | $0.2936 |
| 25% | $0.0910 | $0.0734 | $0.2936 |
When utilization falls below approximately 33%, the estimated cost for Web Functions drops below EC2.
Calculation formulas
- Web Functions
vCPU 7,200 vCPU-seconds × $0.0000322421 = $0.2321
Memory 3,600 GB-seconds × $0.0000042688 = $0.0154
Total (100% utilization) = $0.2475 / hour
Utilization at which cost equals EC2
($0.0910 − $0.0154) ÷ $0.2321 = 0.326 (32.6%)
- Traditional Lambda (3,538 MB memory) run for 1 hour ($0.1658)
3,538 MB ÷ 1,024 = 3.455 GB
3.455 GB × 3,600 seconds = 12,438 GB-seconds
12,438 GB-seconds × $0.0000133334 = $0.1658
Summary
This verification targets a single execution environment of Lambda Web Functions. The confirmed environment runs on Amazon Linux 2023 with a 2 vCPU configuration, and from the CPU identification information and benchmark results, we can infer that a Graviton3 generation CPU is being used.
While this is not a comparison with matched conditions such as memory, web servers with many wait times for DB or external API responses may benefit from billing based on CPU usage.
Compared to conventional Lambda, there are currently limitations on available runtimes and configurations. On the other hand, it can be a strong option for web workloads with low CPU usage, so please give it a try.
Diagnostic app code (app-src/diag.ts)
import http from 'node:http';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { spawnSync } from 'node:child_process';
// Diagnostic app for a Lambda Web Function. Read-only except for /tmp.
// - Only fixed paths and a fixed command allowlist are used. No request data reaches a path or a command.
// - Environment variables and /proc/self/environ are never read or returned.
const port = Number(process.env.LOCAL_PORT ?? 3000);
const HOSTNAME = os.hostname();
const SECRET_NAME = /serial|uuid/i; // values of these files are replaced by their length
type FileResult = { path: string; ok: boolean; bytes?: number; text?: string; truncated?: boolean; error?: string };
const readCapped = (p: string, cap = 16384): FileResult => {
let fd: number | undefined;
try {
fd = fs.openSync(p, 'r');
const buf = Buffer.alloc(cap + 1);
const n = fs.readSync(fd, buf, 0, cap + 1, null);
const truncated = n > cap;
const text = buf.subarray(0, Math.min(n, cap)).toString('utf8');
if (SECRET_NAME.test(path.basename(p))) return { path: p, ok: true, bytes: n, text: `<redacted by app: ${n} bytes>` };
return { path: p, ok: true, bytes: n, text, truncated };
} catch (e) {
return { path: p, ok: false, error: (e as NodeJS.ErrnoException).code ?? String(e) };
} finally {
if (fd !== undefined) try { fs.closeSync(fd); } catch { /* ignore */ }
}
};
// Read the regular files directly under `dir`; list (but do not read) sub-entries. Optionally recurse.
const dumpDir = (dir: string, depth: number, readLimit = 200): unknown => {
let names: string[];
try { names = fs.readdirSync(dir).sort(); } catch (e) { return { dir, ok: false, error: (e as NodeJS.ErrnoException).code ?? String(e) }; }
const files: FileResult[] = [];
const subdirs: string[] = [];
const links: Record<string, string> = {};
const children: unknown[] = [];
for (const name of names) {
const full = path.join(dir, name);
let st: fs.Stats;
try { st = fs.lstatSync(full); } catch (e) { files.push({ path: full, ok: false, error: (e as NodeJS.ErrnoException).code ?? String(e) }); continue; }
if (st.isSymbolicLink()) { try { links[name] = fs.readlinkSync(full); } catch { links[name] = '?'; } continue; }
if (st.isDirectory()) {
subdirs.push(name);
if (depth > 0) children.push(dumpDir(full, depth - 1, readLimit));
continue;
}
if (files.length < readLimit) files.push(readCapped(full, 4096));
}
return { dir, ok: true, subdirs, links, files, children };
};
const statfs = (p: string) => {
try {
const s = fs.statfsSync(p, { bigint: true });
const bs = s.bsize;
return {
path: p, type: `0x${s.type.toString(16)}`, bsize: Number(bs), blocks: Number(s.blocks), bfree: Number(s.bfree), bavail: Number(s.bavail),
files: Number(s.files), ffree: Number(s.ffree),
totalBytes: Number(s.blocks * bs), freeBytes: Number(s.bfree * bs), availBytes: Number(s.bavail * bs),
};
} catch (e) { return { path: p, error: (e as NodeJS.ErrnoException).code ?? String(e) }; }
};
const modeInfo = (p: string) => {
try { const st = fs.lstatSync(p); return { path: p, mode: (st.mode & 0o7777).toString(8), uid: st.uid, gid: st.gid, size: st.size, symlink: st.isSymbolicLink() ? fs.readlinkSync(p) : undefined }; }
catch (e) { return { path: p, error: (e as NodeJS.ErrnoException).code ?? String(e) }; }
};
// ---- A. files read directly by Node ----
const routeA = () => ({
dmi: dumpDir('/sys/class/dmi/id', 0),
hypervisor: dumpDir('/sys/hypervisor', 3),
cpuinfo: readCapped('/proc/cpuinfo', 65536),
cgroup: dumpDir('/sys/fs/cgroup', 0),
cgroupSelf: readCapped('/proc/self/cgroup'),
mountinfo: readCapped('/proc/self/mountinfo', 65536),
status: readCapped('/proc/self/status'),
extra: ['/proc/version', '/proc/meminfo', '/proc/self/limits', '/proc/self/uid_map', '/proc/self/gid_map', '/proc/sys/kernel/osrelease', '/etc/os-release', '/proc/cmdline', '/proc/sys/kernel/hostname']
.map((p) => readCapped(p, 8192)),
});
// ---- C. what Node itself reports ----
const routeC = () => ({
node: { version: process.version, arch: process.arch, platform: process.platform, versions: { v8: process.versions.v8, openssl: process.versions.openssl } },
os: {
arch: os.arch(), machine: os.machine(), platform: os.platform(), type: os.type(), release: os.release(), version: os.version(),
totalmem: os.totalmem(), freemem: os.freemem(), uptimeSec: os.uptime(), loadavg: os.loadavg(),
availableParallelism: os.availableParallelism(), cpuCount: os.cpus().length,
cpus: os.cpus().map((c) => ({ model: c.model, speed: c.speed })),
hostnameLength: HOSTNAME.length,
},
process: { pid: process.pid, uid: process.getuid?.(), gid: process.getgid?.(), groups: process.getgroups?.(), cwd: process.cwd(), dirname: __dirname, execPath: process.execPath, memoryUsage: process.memoryUsage() },
statfs: ['/', '/tmp', '/var/tmp', '/dev/shm', __dirname].map(statfs),
bundledModes: ['bin', 'lib'].flatMap((d) => { try { return fs.readdirSync(path.join(__dirname, d)).map((n) => modeInfo(path.join(__dirname, d, n))); } catch { return []; } }),
});
// ---- /tmp write test (only /tmp; file removed afterwards) ----
const routeTmpWrite = () => {
const file = '/tmp/diag-fill.bin';
const chunk = Buffer.alloc(64 * 1024 * 1024, 0x61);
const capBytes = 2 * 1024 * 1024 * 1024;
const before = statfs('/tmp');
const started = Date.now();
let written = 0;
let stopped = 'cap';
let error: string | undefined;
try {
const fd = fs.openSync(file, 'w');
try {
while (written < capBytes) {
fs.writeSync(fd, chunk, 0, chunk.length);
written += chunk.length;
}
} catch (e) { stopped = 'error'; error = (e as NodeJS.ErrnoException).code ?? String(e); }
try { fs.fsyncSync(fd); } catch { /* ignore */ }
fs.closeSync(fd);
} catch (e) { stopped = 'open-error'; error = (e as NodeJS.ErrnoException).code ?? String(e); }
let fileSize: number | undefined;
try { fileSize = fs.statSync(file).size; } catch { /* ignore */ }
const during = statfs('/tmp');
try { fs.unlinkSync(file); } catch { /* ignore */ }
return { capBytes, chunkBytes: chunk.length, chunksWritten: written / chunk.length, writtenBytes: written, fileSizeAfterBytes: fileSize, stopped, error, ms: Date.now() - started, statfsBefore: before, statfsDuringFileExists: during, statfsAfterDelete: statfs('/tmp') };
};
// ---- B. external commands (fixed allowlist) ----
const SPECS: Record<string, string[]> = {
lscpu: [], lsblk: [], lsmem: [], findmnt: [], lsns: [], dmesg: [],
free: ['-m'], ps: ['-eo', 'pid,ppid,user,comm'], df: ['-h'], uname: ['-a'],
};
const MULTICALL = new Set(['df', 'uname']); // coreutils-single: /usr/bin/coreutils --coreutils-prog=<name>
const SRC_BIN = path.join(__dirname, 'bin');
const SRC_LIB = path.join(__dirname, 'lib');
const DST = '/tmp/diag-bin';
const LOADER = 'ld-linux-aarch64.so.1';
const prepare = () => {
const r: Record<string, unknown> = { dst: DST };
try {
fs.mkdirSync(path.join(DST, 'bin'), { recursive: true });
fs.mkdirSync(path.join(DST, 'lib'), { recursive: true });
for (const [src, sub] of [[SRC_BIN, 'bin'], [SRC_LIB, 'lib']] as const) {
for (const n of fs.readdirSync(src)) {
const to = path.join(DST, sub, n);
fs.copyFileSync(path.join(src, n), to);
fs.chmodSync(to, 0o755);
}
}
r.ok = true;
} catch (e) { r.ok = false; r.error = (e as NodeJS.ErrnoException).code ?? String(e); }
return r;
};
const run = (variant: string, argv: string[]) => {
const t = Date.now();
const r = spawnSync(argv[0], argv.slice(1), { timeout: 30000, maxBuffer: 2 * 1024 * 1024, env: { PATH: '/usr/bin:/bin' }, encoding: 'utf8' });
const cap = (s: string | null, n: number) => (s && s.length > n ? `${s.slice(0, n)}\n...[truncated ${s.length - n} chars]` : s ?? '');
return {
variant, argv: argv.map((a) => a.replace(DST, '<DST>').replace(__dirname, '<TASK>')),
exitCode: r.status, signal: r.signal, spawnError: r.error ? `${(r.error as NodeJS.ErrnoException).code ?? ''} ${r.error.message}`.trim() : undefined,
stdout: cap(r.stdout, 16384), stderr: cap(r.stderr, 4096), ms: Date.now() - t,
};
};
// ---- openssl speed (fixed argument lists only; the number of processes is fixed per route) ----
const OSSL = path.join(DST, 'bin', 'openssl');
const osslArgv = (args: string[]) => [path.join(DST, 'lib', LOADER), '--library-path', path.join(DST, 'lib'), OSSL, ...args];
const cpuStat = () => (readCapped('/proc/stat', 4096).text ?? '').split('\n').filter((l) => /^cpu\d* /.test(l));
const BULK_ALGS = ['aes-128-gcm', 'aes-256-gcm', 'sha256', 'sha512', 'sha3-256', 'chacha20-poly1305'];
const BULK_SIZES = ['64', '16384'];
const osslSuite = (kind: 'bulk' | 'pk', multi: number) => {
const prep = prepare();
const multiArgs = multi > 1 ? ['-multi', String(multi)] : [];
const argLists: string[][] = kind === 'bulk'
? BULK_ALGS.flatMap((a) => BULK_SIZES.map((b) => ['speed', '-seconds', '2', '-bytes', b, ...multiArgs, '-evp', a]))
: ['rsa2048', 'ecdsap256', 'ecdhx25519'].map((a) => ['speed', '-seconds', '2', ...multiArgs, a]);
const statBefore = cpuStat();
const started = Date.now();
const runs = argLists.map((args) => run(`openssl-${kind}-multi${multi}`, osslArgv(args)));
return { prepare: prep, kind, multi, totalMs: Date.now() - started, cpuStatBefore: statBefore, cpuStatAfter: cpuStat(), runs };
};
const routeOsslInfo = () => {
const prep = prepare();
return { prepare: prep, version: run('openssl-version', osslArgv(['version', '-a'])), cpuStat: cpuStat() };
};
const routeWhich = () => {
const names = [...Object.keys(SPECS), 'coreutils'];
const dirs = ['/usr/bin', '/bin', '/usr/sbin', '/sbin', '/usr/local/bin'];
const found: Record<string, unknown[]> = {};
for (const n of names) found[n] = dirs.map((d) => path.join(d, n)).filter((p) => fs.existsSync(p)).map(modeInfo);
let usrBinCount: number | undefined;
try { usrBinCount = fs.readdirSync('/usr/bin').length; } catch { /* ignore */ }
return { pathVar: process.env.PATH, dirs: dirs.map((d) => modeInfo(d)), usrBinCount, found, osRelease: readCapped('/etc/os-release', 4096) };
};
const routeCmds = () => {
const prep = prepare();
const results: unknown[] = [];
for (const [name, args] of Object.entries(SPECS)) {
const inner = MULTICALL.has(name)
? [path.join(DST, 'bin', 'coreutils'), `--coreutils-prog=${name}`, ...args]
: [path.join(DST, 'bin', name), ...args];
results.push(run('bundled', [path.join(DST, 'lib', LOADER), '--library-path', path.join(DST, 'lib'), ...inner]));
for (const d of ['/usr/bin', '/bin', '/usr/sbin', '/sbin']) {
const p = path.join(d, name);
if (fs.existsSync(p)) { results.push(run(`builtin:${d}`, [p, ...args])); break; }
}
}
// Does the executable bit survive the zip? Run one bundled command straight from the code directory.
const inplace = run('bundled-inplace', [path.join(SRC_LIB, LOADER), '--library-path', SRC_LIB, path.join(SRC_BIN, 'lscpu')]);
const inplaceDirect = run('bundled-inplace-direct', [path.join(SRC_BIN, 'lscpu')]);
return { prepare: prep, results, inplace, inplaceDirect };
};
const ROUTES: Record<string, () => unknown> = { '/a': routeA, '/c': routeC, '/which': routeWhich, '/cmds': routeCmds, '/tmpwrite': routeTmpWrite,
'/ossl-info': routeOsslInfo, '/ossl-bulk-1': () => osslSuite('bulk', 1), '/ossl-bulk-2': () => osslSuite('bulk', 2), '/ossl-pk-1': () => osslSuite('pk', 1), '/ossl-pk-2': () => osslSuite('pk', 2) };
const server = http.createServer((req, res) => {
const p = (req.url ?? '/').split('?')[0];
const started = Date.now();
try {
const handler = ROUTES[p];
const body = handler ? handler() : { endpoints: Object.keys(ROUTES) };
// Never print the host name (an identifier of the execution environment).
const text = JSON.stringify({ path: p, elapsedMs: Date.now() - started, body }, null, 2).split(HOSTNAME.length >= 4 ? HOSTNAME : '\u0000').join('<HOSTNAME>');
res.writeHead(200, { 'content-type': 'application/json' });
res.end(text);
} catch (e) {
res.writeHead(500, { 'content-type': 'application/json' });
res.end(JSON.stringify({ error: String((e as Error).name), message: String((e as Error).message) }));
}
});
server.listen(port, () => console.log(`listening on port ${port}`));
