Skip to content

--web + PDO sqlite prepare()/execute(): non-deterministic heap corruption, workers double-free #511

Description

@mirchaemanuel

Summary

Under elephc --web, using PDO sqlite from a request handler causes
non-deterministic heap corruption in the worker process: requests return
empty responses (curl exit 52), and macOS crash reports show a double-free
during HTTP response writing. In CLI mode (no --web) the identical PDO
code compiles and runs stably. This blocked shipping PDO-backed storage for
a pastebin app and forced a move to filesystem-backed storage instead.

Environment

  • elephc v0.26.0 (release, commit 892ff162), macOS ARM64
  • PHP 8.4 (php -S): the identical code is stable

Crash signature

EXC_BREAKPOINT / SIGTRAP (or SIGABRT "pointer being freed was not allocated")
mfm_free -> bytes::bytes::shared_drop -> hyper poll_write -> elephc_web::worker::serve

Minimal reproduction

<?php
if (isset($_SERVER['REQUEST_METHOD'])) {
    $pdo = new PDO('sqlite:/tmp/x.db');
    $pdo->exec('CREATE TABLE IF NOT EXISTS n (a INTEGER PRIMARY KEY, b INTEGER)');
    $st = $pdo->prepare('INSERT OR REPLACE INTO n (a, b) VALUES (?, ?)');
    $st->execute([1, 2]);          // <-- trigger
    header('Content-Type: text/plain');
    echo "ok\n";
}
./app --listen 127.0.0.1:8080 --workers 1
curl http://127.0.0.1:8080/

Measurements (30 requests, single worker, clean port each run)

variant crash rate
handler with no PDO (echo, $_POST, header) 0/30
new PDO + exec() (INSERT via exec() too) 0/30
prepare() without execute() 0/30
prepare() + execute([int, int]) INSERT up to 30/30
prepare() + execute() + fetch() SELECT intermittent (1/30 to 25/25)
query() + fetch() intermittent (3/25 to 25/25)

The crash rate is layout-dependent: the exact same logic with one line more
or fewer moves from ~4% to 100% crash, and varies between a worker and its
respawned replacement. This is heap corruption (something frees the response
buffer or allocator metadata out of turn), not a deterministic logic error.

Things tried without success

  • --heap-size 32M / 64M / 128M / 256M (at 256M workers die on startup
    instead)
  • --ir-opt=off
  • --regalloc=stack
  • --null-repr=tagged
  • Rewriting with exec()/query()/quote() instead of prepared
    statements+execute()
  • A warm-up request before the measured ones
  • --ast-backend --web doesn't even link:
    Undefined symbols: _fn_header referenced from _fn_setcookie

Impact

No app-level workaround was found to be reliable: e2e and byte-parity checks
against php -S fail non-deterministically (typically 5–6 ok / 10–11 fail
out of 16 runs). The only workaround that worked was abandoning PDO for
filesystem-backed storage under --web entirely.

Expected

PDO sqlite operations that go through prepare()/execute() (or query())
under --web should not corrupt the heap or crash workers; behavior should
match CLI mode.

Notes

Found building a pastebin single-binary app (--web) for the Discord
contest, where PDO sqlite was the original storage design.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions