Skip to content

Fix uncaught TypeError in FileHandler::validate_size() on a failed fopen - #602

Open
priskz wants to merge 1 commit into
discourse:mainfrom
priskz:fix/validate-size-unchecked-fopen-handle
Open

Fix uncaught TypeError in FileHandler::validate_size() on a failed fopen#602
priskz wants to merge 1 commit into
discourse:mainfrom
priskz:fix/validate-size-unchecked-fopen-handle

Conversation

@priskz

@priskz priskz commented Aug 11, 2026

Copy link
Copy Markdown

Problem

FileHandler::validate_size() passes fopen()'s return value straight into fstat() without checking it:

$handle = fopen( $this->url, 'r' );
$stat   = fstat( $handle );

When fopen() fails it returns false, and on PHP 8 fstat(false) raises an uncaught TypeError. The request that happened to be writing a log entry dies with a 500, rather than the log write simply failing.

Why it is rarely seen, and where it does bite

The call is guarded by file_exists( $this->url ) in write(), so on local disk the two calls essentially never disagree — which is presumably why this has survived.

On a network-backed filesystem they can. WordPress VIP, for instance, serves wp-content/uploads over HTTP, so file_exists() and the subsequent fopen() are two separate remote operations. They can disagree because of a stale stat cache, a transient service error, or another process rotating the file between them.

Observed in production on WordPress VIP: two requests 500'd inside check_connection_status() — i.e. while rendering the wp-discourse settings screen, which is also where an administrator would go to switch logging back off. Enabling logging can therefore lock you out of the UI that disables it.

The fix

Check the handle, and close it.

On the return value: the failure branch returns true (treat the size as within the limit) rather than false, deliberately. A false return causes write() to ++$this->file_number, set must_rotate, and close(), after which rotate() unlinks old log files. Rotating — and discarding logs — because a read failed would be a poor trade for what is often a transient condition, so skipping the size check once is the safer recovery. Happy to flip it if you'd prefer the opposite bias.

Also closes the handle. The current code opens the file on every write once the log exists and never closes it.

Testing

php -l clean. I have not added a unit test — reproducing it requires an fopen() failure while file_exists() succeeds, which is awkward to simulate against the real filesystem in tests/phpunit/test-file-handler.php without a stream-wrapper fixture. Glad to add one if you can point me at the pattern you'd prefer.

Verified against main at the time of writing; the unchecked handle is still present there.

validate_size() passes fopen()'s return value straight into fstat() without
checking it. When fopen() fails it returns false, and fstat(false) raises an
uncaught TypeError on PHP 8, so the request that was writing a log entry dies
with a 500 instead of the log write simply failing.

The call is guarded by file_exists( $this->url ) in write(), which is why this
is rarely seen on local disk: there, the two calls essentially never disagree.
On a network-backed filesystem they can. WordPress VIP, for example, serves
wp-content/uploads over HTTP, so file_exists() and the following fopen() are two
separate remote operations that can disagree because of a stale stat cache, a
transient service error, or another process rotating the file in between.

Observed in production on WordPress VIP: two requests 500'd inside
check_connection_status(), i.e. while rendering the wp-discourse settings screen,
which is also where an administrator would go to switch logging off again.

The failure branch returns true rather than false deliberately. A false return
causes write() to increment the file number, set must_rotate and close the
handler, and rotate() then unlinks old log files. Rotating because a read failed
would discard logs over what is often a transient condition, so treating the
size as within the limit and skipping the check is the safer recovery.

Also closes the handle. The existing code opens a file on every write once the
log exists and never closes it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant