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

Open
#1,682 3 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

area: external/odbc bug odbc
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/server images) — 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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.