[php-src] Issue #24544: ext/session: ftruncate failure in files save handler silently leaves stale session data

From: Date: Thu, 22 Oct 2026 15:06:11 +0000
Subject: [php-src] Issue #24544: ext/session: ftruncate failure in files save handler silently leaves stale session data
Groups: php.bugs 
Request: Send a blank email to php-bugs+get-253096@lists.php.net to get a copy of this message
Issue: https://github.com/php/php-src/issues/24544 Author: AsahiSoftWareEngineer ## Summary ps_files_write() in ext/session/mod_files.c silently ignores a failed ftruncate() call (line 265), leaving stale bytes from a previous, longer session at the end of the file. On the next read, PS_READ_FUNC allocates a buffer sized by fstat() (the actual on-disk size, not the intended size) and reads back the stale tail, injecting old session variables into the new request. ## Code ```c // ext/session/mod_files.c:264-268 if (ZSTR_LEN(val) < data->st_size) { php_ignore_value(ftruncate(data->fd, ZSTR_LEN(val))); // failure silently discarded } data->st_size = ZSTR_LEN(val); // updated to intended size regardless of ftruncate result return SUCCESS; ``` On the next request: ```c // PS_READ_FUNC: reads actual file size, not the intended size if (zend_fstat(data->fd, &sbuf)) { return FAILURE; } data->st_size = sbuf.st_size; // e.g. 100 bytes (old size), not 50 bytes (intended) *val = zend_string_alloc(sbuf.st_size, false); n = pread(data->fd, ZSTR_VAL(*val), ZSTR_LEN(*val), 0); // reads all 100 bytes ``` The default (php) session serializer consumes all bytes up to endptr (val + vallen), so any tail bytes that form a valid key|value entry are deserialized and added to $_SESSION. ## Conditions ftruncate() can fail in common deployments: - NFS mounts (ESTALE, EPERM, EIO) — frequently used for shared session storage behind load balancers - Disk quota exhaustion (EDQUOT) - Read-only filesystem after a mount error (EROFS) ## Concrete impact A session write that shrinks the data (e.g. session_unset() removing variables followed by session_write_close()) may silently fail to remove old variables. An example that illustrates the data flow: ``` Request A writes (100 bytes): role|s:5:"admin";token|s:32:"..."; Request B writes (17 bytes): token|s:32:"..."; → pwrite(17 bytes at offset 0) succeeds → ftruncate(17) fails silently → file on disk: [17 bytes new] + [83 bytes old tail] Request C reads (100 bytes): → deserializes both token|... AND role|s:5:"admin"; → $_SESSION['role'] == "admin" unexpectedly restored ``` Applications that clear session variables to invalidate auth state without calling session_destroy() (a common pattern) are affected. ## Relation to GH-23653 GH-23653 (c2b02f24dff) fixed the inverse problem: truncating to zero *before* a write that then fails, causing data loss. The fix moved the truncation to *after* a successful write and added the data->st_size update. The silent discard of the ftruncate return value is a separate, remaining issue. ## Expected behavior When ftruncate() fails, ps_files_write() should either: 1. Return FAILURE (safest — lets the caller surface the problem), or 2. Emit E_WARNING and return FAILURE. ## Suggested fix ```c if (ZSTR_LEN(val) < data->st_size) { if (ftruncate(data->fd, ZSTR_LEN(val)) == -1) { php_error_docref(NULL, E_WARNING, "ftruncate failed: %s (%d)", strerror(errno), errno); return FAILURE; } } data->st_size = ZSTR_LEN(val); return SUCCESS; ``` A longer-term improvement would be an atomic write via a temporary file and rename(), eliminating both the ftruncate race and partial-write exposure entirely. ## PHP versions Reproducible on master (after c2b02f24dff) and all maintained branches (8.3, 8.4, 8.5, 8.6).

« previous php.bugs (#253096) next »