-
Notifications
You must be signed in to change notification settings - Fork 7.9k
fgets() does not return false on error from a zlib.inflate filtered stream with large content and invalid checksum #13264
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
Can reproduce, and changing to 1000 gets the expected behaviour. Thanks for the report. |
Another strange thing that may be related is that adding |
Yes, it's supposed to set the stream to eof on |
The problem is that diff --git a/main/streams/streams.c b/main/streams/streams.c
index d45a9bfab8..aa4e949d7f 100644
--- a/main/streams/streams.c
+++ b/main/streams/streams.c
@@ -981,7 +981,12 @@ PHPAPI char *_php_stream_get_line(php_stream *stream, char *buf, size_t maxlen,
}
}
- php_stream_fill_read_buffer(stream, toread);
+ if (UNEXPECTED(php_stream_fill_read_buffer(stream, toread) != SUCCESS)) {
+ if (grow_mode) {
+ efree(bufstart);
+ }
+ return NULL;
+ }
if (stream->writepos - stream->readpos == 0) {
break;
EDIT: running a test with the leak fix too on my fork nielsdos#86 |
Test PR (contains leak and behaviour fix) seems to have passed. I'm starting to doubt the expected behaviour here though. The memory leak is no doubt a real issue. The behaviour issue might be intended but unexpected behaviour... What happens is that the read buffer fills up, then errors out and marks the stream as borked. But there is still data in the read buffer at that point, so we can still get lines until the read buffer is depleted and fgets returns false. So it might be intended. cc @bukka any thoughts on this? |
The thing is, the data is considered to be corrupted since the checksum failed (it is not in my example since I altered the checksum and not the data, but it will probably be the other way around in real life). Returning garbage data in fgets is not useful. Also, even if fgets returns false at the end of the buffer, feof does return true. So if the fgets error is silenced, it can not be detected with feof later (this is the issue that made me discover this behaviour in the first place). |
I don't think It might make more sense to introduce some flag to mark the error terminating and in such situation fail. In this case diff --git a/main/php_streams.h b/main/php_streams.h
index 31b80de986..8996ee8bcb 100644
--- a/main/php_streams.h
+++ b/main/php_streams.h
@@ -223,6 +223,9 @@ struct _php_stream {
/* whether stdio cast flushing is in progress */
uint16_t fclose_stdiocast_flush_in_progress:1;
+ /* whether fatal error happened and all operations should terminates as soon as possible */
+ uint16_t fatal_error:1;
+
char mode[16]; /* "rwb" etc. ala stdio */
uint32_t flags; /* PHP_STREAM_FLAG_XXX */
diff --git a/main/streams/streams.c b/main/streams/streams.c
index 9f79821f0c..e2a55e62a8 100644
--- a/main/streams/streams.c
+++ b/main/streams/streams.c
@@ -636,6 +636,7 @@ PHPAPI zend_result _php_stream_fill_read_buffer(php_stream *stream, size_t size)
/* some fatal error. Theoretically, the stream is borked, so all
* further reads should fail. */
stream->eof = 1;
+ stream->fatal_error = 1;
efree(chunk_buf);
retval = FAILURE;
goto out_is_eof;
@@ -998,7 +999,12 @@ PHPAPI char *_php_stream_get_line(php_stream *stream, char *buf, size_t maxlen,
}
}
- php_stream_fill_read_buffer(stream, toread);
+ if (php_stream_fill_read_buffer(stream, toread) == FAILURE && stream->fatal_error) {
+ if (grow_mode) {
+ efree(bufstart);
+ }
+ return NULL;
+ }
if (stream->writepos - stream->readpos == 0) {
break; This would be just for PHP-8.3 and master. PHP-8.2 is using uint8_t but there should be some space due to alignment and still possibly one flag free but not sure if it's not too risky for 8.2 anyway. Ideally we could let user decide which error is terminating but then it comes to an error propagation in general. That's a bigger problem and I plan to be looking into it soon. The idea is to potentially introduce some sort of stream error handler that would be called on each error and the decision whether to continue could be done there. |
Yes I agree. At first I didn't think of this but when I started working more and more on the issue I started realizing that it might be a problem. I think your proposed patch makes sense. |
I have just done some investigation of the filtering and it looks to me that most filters only clears the currently processed input bucket when failing with Output brigade can leak data when filter creates more buckets like zlib.inflate filter can do if Input brigade lead is a bit more complex because it requires more than one filters to be involved when the first filter has to produced extra buckets. The switch is there to pass output brigade of the first filter (that would contain more than one bucket in such scenario) and pass it as an input brigade to the second filter. If the second filter fails when processing first bucket, it will just free that bucket but leaves the subsequent buckets in the input brigade. It means that we should also free the input brigade. So we basically have two options. Handle it in each filter like it's done in user filter or do it in fill buffer which is what you have done. As this is kind of common issue for all filters I think it makes sense to do it for all filter and clear both brigades there. We could then remove the out brigade clearing from user filter and keep there produced warning when something is left in input brigade. Although I'm not sure that warning makes always sense because why would user filter need to read all input before returning |
Nice work finding that out. |
Yeah agreed. Do you want me to handle it or you want to do it? I'm cool to sort it out if you prefer to concentrate on other things. |
If you could do it, that would be great, as I'd need time to get a bit more familiar with the filters. I kinda forgot about this and am working currently more focussing on DOM stuff. |
sure, no probs, will handle it hopefully next week. |
just a quick update that I have been really busy last and this week but should hopefully find some time on this later next week. |
Part one of the fix in #13790 - memory leak fix |
Re-opening as only part of this is fixed now. |
… filter fatal error This happens because there are no checks in php_stream_fill_read_buffer calls. This should not fail always but only on fatal error so special flag is needed for that. Closes phpGH-18778
The second part took me a bit longer but PR finally created: #18778 |
Uh oh!
There was an error while loading. Please reload this page.
Description
The following code:
Resulted in this output:
But I expected this output instead:
As fgets manual says:
Also note that by replacing
10000
with a smaller number, say1000
I get the expected output.PHP Version
PHP 8.2.12
Operating System
Debian testing
The text was updated successfully, but these errors were encountered: