
Lambda Web Functions の実行環境を調べてみた
はじめに
2026年10月に GA となった AWS Lambda Web Functions は、1 つの実行環境に 2 GB のメモリと 2 vCPU を割り当てます。
この記事では、実行環境から確認できる範囲で、CPU の世代と OpenSSL のベンチマーク性能を調べました。あわせて、従来の Lambda(Event Functions)、Lambda MicroVMs、EC2 の c6g、c7g、c8g インスタンスと比較しました。
検証内容
検証には、Node.js 24(arm64)で動作する診断アプリを使用しました。固定のエンドポイントから /proc と /sys を読み取り、許可リストにあるコマンドを実行します。また、/tmp への書き込みをテストし、結果を JSON で返します。環境変数は取得しません。診断データは、東京リージョンの 1 つの実行環境から取得しました。コードは記事末尾に掲載しています。
実行環境のスペック
Lambda Web Functions の実行環境から確認できた情報は、以下のとおりです。
CPU とメモリ
| 項目 | 値 |
|---|---|
| Node.js | v24.21.0(arm64) |
| カーネル | 6.18.51-120.163.amzn2023.aarch64.microvm |
| OS | Amazon Linux 2023.12.20260930 |
| vCPU | 2(os.availableParallelism()、/proc/cpuinfo の件数、Cpus_allowed_list のいずれも 2 CPU) |
| CPU | CPU part 0xd40、CPU variant 0x1、BogoMIPS 2100.00 |
| メモリ(公式ドキュメント) | 2 GB |
| メモリ(/proc/meminfo の MemTotal) | 2460024 kB(約 2.35 GiB)。os.totalmem() は 2519064576 バイト、スワップは 0 |
| os.cpus() の model | unknown |
公式のメモリ割り当て量は 2 GB ですが、実行環境の /proc/meminfo では、MemTotal が約 2.35 GiB と表示されました。
権限とサンドボックス
/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
アプリは root ではない一般ユーザーで動作しており、dmesg の実行は拒否されました(後述)。ps -eo pid,ppid,user,comm では、PID 1 は root の init、PID 2 は sbx_user… の node でした。lsns では、time、user、net、mnt、uts、ipc、pid、cgroup の 8 種類の名前空間を確認しました。
ファイルシステムと /tmp
実行環境の df -h の出力です。
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
/ は ext4 の ro,nosuid,nodev、/var/task は squashfs の ro,nosuid,nodev でマウントされていました。/tmp は ext4 の rw,relatime で、noexec は付いていませんでした。/tmp は読み書き可能で、アプリからの書き込みにも成功しました。/dev/shm は存在せず、アクセスすると ENOENT になりました。
/tmp に 64 MiB ずつ書き込むと、ファイルが 526,385,152 バイト(約 502 MiB)になった時点で ENOSPC になりました。書き込み前の空き容量は 527,192,064 バイトで、ファイルの削除後には同じ値まで回復しました。statfs が返すファイルシステムの容量は 525 MiB でしたが、一時ファイルに使える量の目安は約 502 MiB です。
/tmp などローカルのファイルは、実行環境が再利用される間は残ることがあります。ただし、実行環境の寿命は保証されません。
/proc と /sys
| パス | 結果 |
|---|---|
| /sys/class/dmi/id | ENOENT |
| /sys/hypervisor | ENOENT |
| /sys/fs/cgroup | ENOENT |
| /proc/self/cgroup | 読める |
| /proc/self/mountinfo | 読める(7 マウント、sysfs なし) |
| /proc/cpuinfo、/proc/meminfo、/proc/self/status、/proc/self/limits | 読める |
| /proc/cmdline | EACCES |
cgroup の CPU とメモリの上限、DMI、ハイパーバイザーの情報は、これらのパスから取れませんでした。CPU 数と実行環境から見えるメモリ情報は、/proc/cpuinfo、/proc/meminfo、Node.js の os モジュールから取得できました。
外部コマンド
バイナリの回収と同梱
診断に使用したコマンドのうち、実行環境に最初から存在したのは df、uname、coreutils だけでした。それ以外のコマンドは、AL2023 の arm64 コンテナ(public.ecr.aws/amazonlinux/amazonlinux:2023、linux/arm64)からバイナリと共有ライブラリを回収し、デプロイパッケージの bin/ と lib/ に同梱しました。同梱版は util-linux 2.37.4、procps-ng 3.3.17、coreutils 8.32 でした。
collect-binaries.sh として保存しました。
#!/bin/bash
# AL2023 (linux/arm64) コンテナ内で util-linux / procps-ng / coreutils のバイナリと依存ライブラリを bin/ lib/ に回収する。
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 は coreutils-single の multi-call バイナリ(/usr/bin/coreutils)への shebang スクリプトなので、本体を回収する
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
# ローダー本体は 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
回収は次のコマンドで実行しました。diag-bin に bin/ と lib/ が出力されました。
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 と uname は shebang スクリプトで、実体は multi-call バイナリの coreutils でした。coreutils を同梱し、--coreutils-prog=<name> を付けて呼びました。それ以外のコマンドは、同梱したローダーにライブラリパスを渡す次の形で実行しました。
<DST>/lib/ld-linux-aarch64.so.1 --library-path <DST>/lib <DST>/bin/<cmd>
診断アプリでは <DST> を /tmp/diag-bin にしています。
zip にした bin/ と lib/ の 24 ファイルは、すべてモード 755 のまま /var/task に展開されました。/var/task は noexec ではなく、/var/task/bin/lscpu は直接実行できました。lscpu は、直接実行、ローダー経由、/tmp にコピーしてからの実行のいずれでも、同じエラーで終了コード 1 になりました。次の表は、/tmp にコピーしてローダー経由で実行した結果です。
実行結果
| コマンド | 同梱版の結果 |
|---|---|
| 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(実行環境の /usr/bin/df と同一の出力) |
| uname -a | exit 0(実行環境の /usr/bin/uname と同一の出力) |
失敗したのは、/sys を読む lscpu、lsblk、lsmem と、権限が要る dmesg でした。/proc で足りる findmnt、lsns、free、ps は、同梱すれば動きました。実行環境の /usr/bin は 175 エントリで、df、uname、coreutils 以外の対象コマンドは /usr/bin、/bin、/usr/sbin、/sbin、/usr/local/bin のどこにもありませんでした。
CPU の世代と性能
Graviton3 世代の CPU
Linux カーネルの CPU part 定義では、0xd40 は Neoverse V1 に対応します。AWS Graviton の技術ガイドでは、Graviton3(E) のコンパイルオプションが -mcpu=neoverse-v1 で、Graviton2 は neoverse-n1(0xd0c)、Graviton4 は neoverse-v2(0xd4f)です。
Web Function と、c6g.large、c7g.large、c8g.large の EC2 を比べた CPU の識別情報です。
| 項目 | 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 | —(/sys が無く lscpu が失敗) |
| Features の SVE 系 | なし | sve、svebf16、svei8mm(sve2 なし) | sve2、sveaes、svesha3 など | sve、svebf16、svei8mm(sve2 なし) |
| OPENSSL_armcap | 0xbd | 0x13ffd | 0x179fd | 0x13ffd |
Web Function の CPU part、CPU variant、BogoMIPS、Features、OPENSSL_armcap は、いずれも c7g.large と一致しました。Graviton2 にない SVE 系の機能を備える一方、Graviton4 が備える SVE2 はありません。以上から、Web Function には Graviton3 世代の CPU が使われていると推測できます。ただし、Graviton3 と Graviton3E は CPU part だけでは区別できません。また、Web Function では機種名の文字列を取得できなかったため、この判断は各値の一致に基づきます。
OpenSSL speed の比較
同梱した openssl 3.5.8 で、Web Function(1 つの実行環境)と 3 機種の EC2(各 1 台)に、同じ引数の openssl speed を実行しました。EC2 には dnf install で同じ版を入れ、ap-northeast-1a の AL2023 で SSM Run Command から実行しました。測定コマンドの形は次のとおりです。
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}
1 プロセスで測定した 17 項目のうち、代表的な 4 項目を示します。各値は 3 ラウンドの中央値です。
| 項目 | 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 は多くの項目で c7g.large に近い結果でした。
次に、2 プロセス(-multi 2)の処理性能を、1 プロセスに対する倍率で示します。17 項目 × 3 ラウンドの 51 件で集計しています。
| 環境 | 中央値 | 最小〜最大 |
|---|---|---|
| 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 |
EC2 の 3 機種は 2.00 倍前後で、Web Function は 2 vCPU でも 1.84 倍でした。
Lambda MicroVMs・従来の Lambda との比較
Web Functions、Lambda MicroVMs、従来の Lambda で、同じ診断を実行しました。MicroVMs はベースライン 4 GB / 2 vCPU、従来の Lambda は arm64 / 3,538 MB に設定しています。
| 項目 | Web Functions | Lambda MicroVMs | 従来の Lambda |
|---|---|---|---|
| 見える CPU 数 | 2 | 8(ピーク) | 3 |
| MemTotal | 2460024 kB | 16437604 kB(ピーク) | 4510668 kB |
| 実行ユーザーと権限 | uid 993、CapEff 0、Seccomp フィルター 8 個 | uid 0、CapEff 14 個、Seccomp 0 | uid 993、CapEff 0、Seccomp フィルター 6 個 |
| /sys | なし | sysfs(読み取り専用)。DMI と hypervisor はなし | なし |
| lscpu、lsblk、lsmem | exit 1 | exit 0 | exit 1 |
| dmesg | 拒否 | exit 0 | 拒否 |
| /tmp | 約 502 MiB で ENOSPC | 1 GiB まで書けた(テストの上限) | 約 495 MiB で ENOSPC(既定の 512 MB) |
| CPU part | 0xd40 | 0xd40(sve 系なし、1 台) | 0xd0c と 0xd40 |
| openssl speed の 2 プロセスの伸び | 1.84 | 2.00 | 1.69 |
1 つの実行環境で測定した MicroVMs では、root で動作し、/sys も参照できたため、lscpu、lsblk、lsmem、dmesg はすべて成功しました。openssl speed は 8 プロセスで 7.996 倍(51 件の中央値)になりました。
従来の Lambda は、uid、ケーパビリティ、/sys の可視性、および対象コマンドの成否が Web Functions と同じでした。見える CPU は 3 でしたが、openssl speed の合計は 2 プロセスで 1.686 倍、3 プロセスで 1.698 倍、4 プロセスで 1.696 倍と、約 1.7 倍で頭打ちになりました。同じ設定の従来の Lambda でも、実行環境によって CPU は Graviton2 と Graviton3 に分かれました。OpenSSL の性能測定には、Graviton3 の実行環境を使用しています。
1 時間あたりの費用の試算
実測した請求ではなく、料金表の単価から計算した試算値です。前提は次のとおりです。
- 単価: 東京リージョン、Arm、USD で、2026-10-01 発効の Price List API の値を使いました。コンピュート料金だけを対象にしています。
- CPU 使用率: 使った CPU 時間 ÷(2 vCPU × 3,600 秒)です。2 vCPU の構成を 1 時間動かし続けるものとします。
- Web Functions: vCPU の使用時間とメモリ使用量に課金されるものとして計算しました。メモリ使用量は測定していないため、1 GB と仮定しています。
- MicroVMs: ベースライン 4 GB / 2 vCPU の範囲内として計算しました。
| CPU 使用率 | EC2 c7g.large | Web Functions(消費メモリ 1 GB) | 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 |
使用率が約 33% を下回ると、Web Functions の試算額が EC2 を下回ります。
計算式
- Web Functions
vCPU 7,200 vCPU-秒 × $0.0000322421 = $0.2321
メモリ 3,600 GB-秒 × $0.0000042688 = $0.0154
合計(使用率 100%)= $0.2475 / 時間
EC2 と同額になる使用率
($0.0910 − $0.0154) ÷ $0.2321 = 0.326(32.6%)
- 従来の Lambda(メモリ 3,538 MB)を 1 時間実行した場合($0.1658)
3,538 MB ÷ 1,024 = 3.455 GB
3.455 GB × 3,600 秒 = 12,438 GB-秒
12,438 GB-秒 × $0.0000133334 = $0.1658
まとめ
今回の検証は、Lambda Web Functions の 1 つの実行環境を対象としたものです。確認した環境は Amazon Linux 2023 上で動作する 2 vCPU 構成で、CPU の識別情報とベンチマーク結果から、Graviton3 世代の CPU が使われていると推測できます。
メモリなどの条件を揃えた比較ではありませんが、DB や外部 API の応答待ちが多い Web サーバーでは、CPU 使用率に応じた課金が有利になる可能性があります。
従来の Lambda と比べると、現時点では利用できるランタイムや構成に制約があります。一方、CPU 使用率が低い Web ワークロードでは有力な選択肢になり得ますので、ぜひお試しください。
診断アプリのコード(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}`));










