Skip to content

Cover concurrent reads from one shared file handle (#280) - #281

Merged
EdmondDantes merged 5 commits into
mainfrom
280-guzzle-streamfread-reads-with-a-264-2-length-and-fails-with-async-io-error-during-s3-upload-of-large-local-file
Sep 8, 2026
Merged

Cover concurrent reads from one shared file handle (#280)#281
EdmondDantes merged 5 commits into
mainfrom
280-guzzle-streamfread-reads-with-a-264-2-length-and-fails-with-async-io-error-during-s3-upload-of-large-local-file

Conversation

@EdmondDantes

@EdmondDantes EdmondDantes commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Regression tests for #280.

tests/io/085-shared_handle_concurrent_reads.phpt — two coroutines read one
fopen() handle of a file holding 2000 distinct 16-byte records. Every byte must
reach exactly one reader; today 1024 records of 2000 survive, while the byte
count still adds up — which is why the S3 upload in the issue produced a wrong
body instead of an error.

- records: 2000, unique: 2000
+ records: 2000, unique: 1024

tests/io/086-close_during_io.phpt — a second coroutine closes the handle
while the first is parked inside the read or the write. The stream is freed
there, and the buffered layer then reached it again: through the lock on the read
path, and through the eof check of php_stream_write_buffer() on the write
path. On the previous php-src code this segfaults under a sanitizer build (and
under a poisoned free); with the fix both loops run to the end.

Both are green with true-async/php-src#32, which serialises the
derive-read-credit sequence in the buffered stream API and keeps the lock alive
across a close.

Two coroutines reading the same handle lose and duplicate data: both derive
the same buffer address from a state neither has finished updating, so 1024
of 2000 distinct records survive. The test states the contract rather than
the counts: every byte reaches exactly one reader.

Needs the buffer lock from true-async/php-src#32.
@codecov

codecov Bot commented Sep 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Closing a handle from a second coroutine frees the stream inside the first
one's ops->read or ops->write, and the buffered layer then reached the
freed stream through its lock and through the write path's eof check. The
test reads and writes under such a close; on the previous php-src code it
segfaults under a sanitizer build.

Needs true-async/php-src#32.
Six coroutine tests over one shared handle: a read filter appended and removed
while another coroutine is inside fread(), a cast to a file descriptor during a
read, and a close landing while a copy, a stream_get_contents() or a write
filter flush is parked. Each one loses data, hangs or crashes without the lock
those paths now take.
@EdmondDantes
EdmondDantes merged commit 60dc095 into main Sep 8, 2026
9 checks passed
@EdmondDantes
EdmondDantes deleted the 280-guzzle-streamfread-reads-with-a-264-2-length-and-fails-with-async-io-error-during-s3-upload-of-large-local-file branch September 8, 2026 16:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Guzzle Stream::fread() reads with a 2^64-2 length and fails with async IO error during S3 upload of large local file

1 participant