microsoft / microsoft/msphpsql
Handled signal during a query kills the connection (08S01, TCP Provider 0x2714): EINTR treated as fatal; SQL_ATTR_QUERY_TIMEOUT makes it unavoidable — deterministic repro
Nobody has claimed this yet.
- Dominant language
- PHP
- Stars
- 1.9k
- Forks
- 385
- Avg merge
- 3d 22h
- Merged PRs (30d)
- 1
Description
Summary
A handled POSIX signal delivered while the driver is waiting for results fatally kills the connection with SQLSTATE[08S01] ... TCP Provider: Error code 0x2714 (0x2714 = 10004 = WSAEINTR). The driver treats EINTR on its socket wait as a fatal connection error instead of retrying the wait.
With SQL_ATTR_QUERY_TIMEOUT set (PDO::SQLSRV_ATTR_QUERY_TIMEOUT), this is unavoidable: the result wait becomes ppoll(fd, deadline, sigmask=NULL), and ppoll is never restarted by SA_RESTART (POSIX), so even well-behaved signal handlers kill the connection. Without the timeout attribute, the wait is a blocking recvfrom() that SA_RESTART restarts — but a handler registered without SA_RESTART kills that path too, so the underlying EINTR handling is the defect in both cases.
Deterministic: 12/12 across the test matrix below, plus a cross-check against a second SQL Server major version.
Environment
- PHP 8.5.9 (NTS), pdo_sqlsrv 5.13.3
- msodbcsql18 18.6.2.1-1, unixODBC 2.3.12
- Debian-based container, linux/arm64 (mechanism is syscall-level; older reports of the same error on x86_64: #365, #835, #885)
- SQL Server 2022 (16.0.4250) and SQL Server 2019 (
mcr.microsoft.com/mssql/serverimages) — identical results, as expected for a client-side defect - TLS on (
Encrypt=1;TrustServerCertificate=1); reproduces with unixODBC pooling on and off
Reproduction
docker network create repro
docker run -d --name mssql --network repro --network-alias mssql \
-e ACCEPT_EULA=Y -e MSSQL_SA_PASSWORD='Repro0x2714!Pw' \
mcr.microsoft.com/mssql/server:2022-latest
matrix.php (needs pdo_sqlsrv + pcntl):
<?php
// argv: <armed|control> <restart|norestart>
[$mode, $restart] = [$argv[1], $argv[2] === 'restart'];
$opts = $mode === 'armed' ? [PDO::SQLSRV_ATTR_QUERY_TIMEOUT => 30] : [];
$pdo = new PDO("sqlsrv:Server=mssql,1433;Database=master;Encrypt=1;TrustServerCertificate=1",
'sa', getenv('SA_PASSWORD'), $opts);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
pcntl_async_signals(true);
pcntl_signal(SIGUSR1, function () {}, $restart); // restart = SA_RESTART on/off
try {
$pdo->query("WAITFOR DELAY '00:00:04'");
$q = 'COMPLETED';
} catch (PDOException $e) {
$q = str_contains($e->getMessage(), '0x2714') ? '0x2714' : substr($e->getMessage(), 0, 40);
}
try { $pdo->query('SELECT 1'); $c = 'usable'; } catch (PDOException $e) { $c = 'DEAD'; }
printf("%-8s restart=%-5s query=%-9s conn=%s\n", $mode, var_export($restart, true), $q, $c);
Driver script — run the query, deliver one signal mid-wait:
php matrix.php armed restart & PID=$!; sleep 2; kill -USR1 $PID; wait $PID
Results (3 runs per cell, fully deterministic)
| handler with SA_RESTART | handler without SA_RESTART | |
|---|---|---|
SQLSRV_ATTR_QUERY_TIMEOUT set |
0x2714, connection DEAD |
0x2714, connection DEAD |
| no timeout attribute | COMPLETED, connection usable | 0x2714, connection DEAD |
Same 2×2 against SQL Server 2019: identical.
The signal does not need to come from pcntl: an external kill -PROF <pid> against the SIGPROF handler PHP itself installs (Zend execution-timeout machinery, present in every PHP process) produces the same 0x2714 — which is how real php-fpm deployments hit this without any signal-related code of their own.
strace
Same query (WAITFOR DELAY '00:00:04'), strace -f -T:
# no timeout attribute — restartable blocking read:
recvfrom(5, "\27\3\3\0a...", 20776, 0, NULL, NULL) = 102 <4.002522>
# SQL_ATTR_QUERY_TIMEOUT set — non-restartable deadline poll:
ppoll([{fd=5, events=POLLIN}], 1, {tv_sec=2, tv_nsec=0}, NULL, 0) = 0 (Timeout) <2.002987>
strace also shows the timeout implementation creates no timers and no signals of its own (no timer_create/setitimer/alarm) — the deadline is purely the ppoll timeout, so the defect is confined to EINTR handling on the socket waits.
Expected behavior
EINTR from ppoll/recvfrom is not a connection failure. The wait should be retried with the remaining deadline (or the ppoll sigmask argument used to defer signals for the wait's duration). As it stands, setting a query timeout converts any routinely-signaled process — php-fpm being the canonical case, with its built-in SIGPROF/SIGUSR handlers — into one that sporadically kills healthy connections mid-query with a fatal, unretried 08S01.
Related (all closed, none with a deterministic repro)
#365 (random 0x2714 + the SMux 08S02 companion), #835 (0x2714 near the timeout boundary), #885 (SIGALRM during query), #1242 (0x2714 after Apache reload — the reload's signals, in hindsight).
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the provided matrix.php reproduction and run the 2×2 signal/timeout matrix against SQL Server, then compare the ppoll and recvfrom behavior with strace -f -T. Trace the pdo_sqlsrv socket-wait path for EINTR handling. Done means interrupted waits are retried without killing the connection and the matrix completes with a usable connection in every cell.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- linux, php, sql
- Domain
- backend, database
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 58/100