Fix uncaught TypeError in FileHandler::validate_size() on a failed fopen - #602
Open
priskz wants to merge 1 commit into
Open
Fix uncaught TypeError in FileHandler::validate_size() on a failed fopen#602priskz wants to merge 1 commit into
priskz wants to merge 1 commit into
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
FileHandler::validate_size()passesfopen()'s return value straight intofstat()without checking it:When
fopen()fails it returnsfalse, and on PHP 8fstat(false)raises an uncaughtTypeError. 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 )inwrite(), 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/uploadsover HTTP, sofile_exists()and the subsequentfopen()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 thanfalse, deliberately. Afalsereturn causeswrite()to++$this->file_number, setmust_rotate, andclose(), after whichrotate()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 -lclean. I have not added a unit test — reproducing it requires anfopen()failure whilefile_exists()succeeds, which is awkward to simulate against the real filesystem intests/phpunit/test-file-handler.phpwithout a stream-wrapper fixture. Glad to add one if you can point me at the pattern you'd prefer.Verified against
mainat the time of writing; the unchecked handle is still present there.