Repository navigation
Failed seek on php://memory and SQLite blob streams sets the stream position to -1 #23905
Description
Activity
There more inconsistencies of the fseek exceeding behavior.
Here is a script to compare different streams: https://github.com/proxy/gist.github.com/marc-mabe/add8a121960023e8334324e7d76f1fbfThis is showing even more inconsistencies:
1. Whether a failed seek keeps the position depends on whence
- wrong position after
SEEK_END -12for ...- php://temp in memory, SplTempFileObject
- php://memory
- SQLite3 / Pdo\Sqlite blob
- wrong position after
SEEK_CUR -7for ...- php://memory
- SQLite3 / Pdo\Sqlite blob
2. The reported position and the real one drift apart
On failure, the memory handler moves its own position to 0. The SQLite blob handlers move theirs to 0 or to the end. Meanwhile PHP reports false.
- After a failed seek, fread() silently returns data from 0 or from the end.
- The next fseek(…, SEEK_CUR) aborts the debug build at ZEND_ASSERT(stream->position >= 0)
3. php://temp changes behaviour depending on its size
Before spilling to disk, a failed SEEK_END loses the position. After spilling, the same seek keeps it. The temp stream forwards seeks to an inner stream, and the inner memory and file streams fail differently.
4. Phar refuses seeks past the end, and the following write or read then corrupts silently
Phar's seek handler rejects any target past the entry's size and gives no warning. The position stays at 6, so:
- On a writable entry,
fwrite("!")lands at 6: the data becomes "hello !orld". Code that doesn't check the fseek() return value overwrites data where POSIX would append after a gap. - On a read-only entry, fread(5) returns "world" instead of "".
5. Phar sets EOF too early
After phar's fread(5) returns "world", reading exactly up to the end, EOF is true. gz in the same situation reports false, and earlier I checked that a plain file and php://memory also report false. Those only set EOF once a read hits the end.
6. SQLite blobs: refusing is fine, the side effects aren't
- Refusing past-end seeks and failing writes ("It is not possible to increase the size of a BLOB") fits a fixed-size object. It's the fmemopen() model rather than the file model.
- But the position is lost and can't be restored, because ftell() is false. In this run the read and write therefore happen at 11, only because printing the data left the blob there.
7. gz writes on seek
In write mode a forward seek past the end writes the zeros straight away. The data right after the seek already shows "hello world\000". With POSIX, a seek that is never followed by a write leaves the file unchanged; here the gzip file changes anyway.
Output:
Compares how seeking outside of a stream behaves across stream types. For every stream type (writable and read-only) and every seek below, the stream is freshly created with the data "hello world" and positioned at the middle (offset 6) via SEEK_SET. Then a single seek before the beginning or after the end is done using SEEK_SET, SEEK_CUR or SEEK_END, printing the seek result, the resulting position and any warnings. For seeks after the end the result of feof() and the full stream data are printed as well. Lines starting with "+" show what happens when reading (read-only streams) or writing (writable streams) on the same stream afterwards. Printing the data moves the position, so it is restored to where the seek left it first. PHP 8.6.0-dev (Linux 64-bit) data: "hello world" (11 bytes) === file (writable) === -- seek before the beginning -- fseek(-1, SEEK_SET) => -1 ftell=6 fseek(-7, SEEK_CUR) => -1 ftell=6 fseek(-12, SEEK_END) => -1 ftell=6 -- seek after the end -- fseek(12, SEEK_SET) => 0 ftell=12 feof=false data="hello world" + fwrite("!") => 1 ftell=13 feof=false data="hello world\000!" fseek(6, SEEK_CUR) => 0 ftell=12 feof=false data="hello world" + fwrite("!") => 1 ftell=13 feof=false data="hello world\000!" fseek(1, SEEK_END) => 0 ftell=12 feof=false data="hello world" + fwrite("!") => 1 ftell=13 feof=false data="hello world\000!" === file (read-only) === -- seek before the beginning -- fseek(-1, SEEK_SET) => -1 ftell=6 fseek(-7, SEEK_CUR) => -1 ftell=6 fseek(-12, SEEK_END) => -1 ftell=6 -- seek after the end -- fseek(12, SEEK_SET) => 0 ftell=12 feof=false data="hello world" + fread(5) => "" ftell=12 feof=true data="hello world" fseek(6, SEEK_CUR) => 0 ftell=12 feof=false data="hello world" + fread(5) => "" ftell=12 feof=true data="hello world" fseek(1, SEEK_END) => 0 ftell=12 feof=false data="hello world" + fread(5) => "" ftell=12 feof=true data="hello world" === SplFileObject (writable) === -- seek before the beginning -- SplFileObject->fseek(-1, SEEK_SET) => -1 ftell=6 SplFileObject->fseek(-7, SEEK_CUR) => -1 ftell=6 SplFileObject->fseek(-12, SEEK_END) => -1 ftell=6 -- seek after the end -- SplFileObject->fseek(12, SEEK_SET) => 0 ftell=12 eof=false data="hello world" + SplFileObject->fwrite("!") => 1 ftell=13 eof=false data="hello world\000!" SplFileObject->fseek(6, SEEK_CUR) => 0 ftell=12 eof=false data="hello world" + SplFileObject->fwrite("!") => 1 ftell=13 eof=false data="hello world\000!" SplFileObject->fseek(1, SEEK_END) => 0 ftell=12 eof=false data="hello world" + SplFileObject->fwrite("!") => 1 ftell=13 eof=false data="hello world\000!" === SplFileObject (read-only) === -- seek before the beginning -- SplFileObject->fseek(-1, SEEK_SET) => -1 ftell=6 SplFileObject->fseek(-7, SEEK_CUR) => -1 ftell=6 SplFileObject->fseek(-12, SEEK_END) => -1 ftell=6 -- seek after the end -- SplFileObject->fseek(12, SEEK_SET) => 0 ftell=12 eof=false data="hello world" + SplFileObject->fread(5) => "" ftell=12 eof=true data="hello world" SplFileObject->fseek(6, SEEK_CUR) => 0 ftell=12 eof=false data="hello world" + SplFileObject->fread(5) => "" ftell=12 eof=true data="hello world" SplFileObject->fseek(1, SEEK_END) => 0 ftell=12 eof=false data="hello world" + SplFileObject->fread(5) => "" ftell=12 eof=true data="hello world" === php://temp (writable) === -- seek before the beginning -- fseek(-1, SEEK_SET) => -1 ftell=6 fseek(-7, SEEK_CUR) => -1 ftell=6 fseek(-12, SEEK_END) => -1 ftell=false -- seek after the end -- fseek(12, SEEK_SET) => 0 ftell=12 feof=false data="hello world" + fwrite("!") => 1 ftell=13 feof=false data="hello world\000!" fseek(6, SEEK_CUR) => 0 ftell=12 feof=false data="hello world" + fwrite("!") => 1 ftell=13 feof=false data="hello world\000!" fseek(1, SEEK_END) => 0 ftell=12 feof=false data="hello world" + fwrite("!") => 1 ftell=13 feof=false data="hello world\000!" === php://temp (read-only) === -- seek before the beginning -- note: read-only temp stream cannot be pre-filled -> empty fseek(-1, SEEK_SET) => -1 ftell=6 fseek(-7, SEEK_CUR) => -1 ftell=6 fseek(-12, SEEK_END) => -1 ftell=false -- seek after the end -- fseek(12, SEEK_SET) => 0 ftell=12 feof=false data="" + fread(5) => "" ftell=12 feof=true data="" fseek(6, SEEK_CUR) => 0 ftell=12 feof=false data="" + fread(5) => "" ftell=12 feof=true data="" fseek(1, SEEK_END) => 0 ftell=1 feof=false data="" + fread(5) => "" ftell=1 feof=true data="" === php://temp/maxmemory:0 (spilled to disk) (writable) === -- seek before the beginning -- fseek(-1, SEEK_SET) => -1 ftell=6 fseek(-7, SEEK_CUR) => -1 ftell=6 fseek(-12, SEEK_END) => -1 ftell=6 -- seek after the end -- fseek(12, SEEK_SET) => 0 ftell=12 feof=false data="hello world" + fwrite("!") => 1 ftell=13 feof=false data="hello world\000!" fseek(6, SEEK_CUR) => 0 ftell=12 feof=false data="hello world" + fwrite("!") => 1 ftell=13 feof=false data="hello world\000!" fseek(1, SEEK_END) => 0 ftell=12 feof=false data="hello world" + fwrite("!") => 1 ftell=13 feof=false data="hello world\000!" === php://temp/maxmemory:0 (spilled to disk) (read-only) === -- seek before the beginning -- note: read-only temp stream cannot be pre-filled -> empty fseek(-1, SEEK_SET) => -1 ftell=6 fseek(-7, SEEK_CUR) => -1 ftell=6 fseek(-12, SEEK_END) => -1 ftell=false -- seek after the end -- fseek(12, SEEK_SET) => 0 ftell=12 feof=false data="" + fread(5) => "" ftell=12 feof=true data="" fseek(6, SEEK_CUR) => 0 ftell=12 feof=false data="" + fread(5) => "" ftell=12 feof=true data="" fseek(1, SEEK_END) => 0 ftell=1 feof=false data="" + fread(5) => "" ftell=1 feof=true data="" === SplTempFileObject (writable) === -- seek before the beginning -- SplTempFileObject->fseek(-1, SEEK_SET) => -1 ftell=6 SplTempFileObject->fseek(-7, SEEK_CUR) => -1 ftell=6 SplTempFileObject->fseek(-12, SEEK_END) => -1 ftell=false -- seek after the end -- SplTempFileObject->fseek(12, SEEK_SET) => 0 ftell=12 eof=false data="hello world" + SplTempFileObject->fwrite("!") => 1 ftell=13 eof=false data="hello world\000!" SplTempFileObject->fseek(6, SEEK_CUR) => 0 ftell=12 eof=false data="hello world" + SplTempFileObject->fwrite("!") => 1 ftell=13 eof=false data="hello world\000!" SplTempFileObject->fseek(1, SEEK_END) => 0 ftell=12 eof=false data="hello world" + SplTempFileObject->fwrite("!") => 1 ftell=13 eof=false data="hello world\000!" === file + string.rot13 filter (writable) === -- seek before the beginning -- note: read+write filter: stored rot13 encoded, read back decoded fseek(-1, SEEK_SET) => -1 ftell=6 fseek(-7, SEEK_CUR) => -1 ftell=6 fseek(-12, SEEK_END) => -1 ftell=6 -- seek after the end -- fseek(12, SEEK_SET) => 0 ftell=12 feof=false data="hello world" + fwrite("!") => 1 ftell=13 feof=false data="hello world\000!" fseek(6, SEEK_CUR) => 0 ftell=12 feof=false data="hello world" + fwrite("!") => 1 ftell=13 feof=false data="hello world\000!" fseek(1, SEEK_END) => 0 ftell=12 feof=false data="hello world" + fwrite("!") => 1 ftell=13 feof=false data="hello world\000!" === file + string.rot13 filter (read-only) === -- seek before the beginning -- note: read filter: stored rot13 encoded, read back decoded fseek(-1, SEEK_SET) => -1 ftell=6 fseek(-7, SEEK_CUR) => -1 ftell=6 fseek(-12, SEEK_END) => -1 ftell=6 -- seek after the end -- fseek(12, SEEK_SET) => 0 ftell=12 feof=false data="hello world" + fread(5) => "" ftell=12 feof=true data="hello world" fseek(6, SEEK_CUR) => 0 ftell=12 feof=false data="hello world" + fread(5) => "" ftell=12 feof=true data="hello world" fseek(1, SEEK_END) => 0 ftell=12 feof=false data="hello world" + fread(5) => "" ftell=12 feof=true data="hello world" === phar entry (writable) === -- seek before the beginning -- note: phar.readonly='0' (writing phar archives requires running with -d phar.readonly=0) fseek(-1, SEEK_SET) => -1 ftell=6 fseek(-7, SEEK_CUR) => -1 ftell=6 fseek(-12, SEEK_END) => -1 ftell=6 -- seek after the end -- fseek(12, SEEK_SET) => -1 ftell=6 feof=false data="hello world" + fwrite("!") => 1 ftell=7 feof=false data="hello !orld" fseek(6, SEEK_CUR) => -1 ftell=6 feof=false data="hello world" + fwrite("!") => 1 ftell=7 feof=false data="hello !orld" fseek(1, SEEK_END) => -1 ftell=6 feof=false data="hello world" + fwrite("!") => 1 ftell=7 feof=false data="hello !orld" === phar entry (read-only) === -- seek before the beginning -- note: phar.readonly='0' (writing phar archives requires running with -d phar.readonly=0) fseek(-1, SEEK_SET) => -1 ftell=6 fseek(-7, SEEK_CUR) => -1 ftell=6 fseek(-12, SEEK_END) => -1 ftell=6 -- seek after the end -- fseek(12, SEEK_SET) => -1 ftell=6 feof=false data="hello world" + fread(5) => "world" ftell=11 feof=true data="hello world" fseek(6, SEEK_CUR) => -1 ftell=6 feof=false data="hello world" + fread(5) => "world" ftell=11 feof=true data="hello world" fseek(1, SEEK_END) => -1 ftell=6 feof=false data="hello world" + fread(5) => "world" ftell=11 feof=true data="hello world" === php://memory (writable) === -- seek before the beginning -- fseek(-1, SEEK_SET) => -1 ftell=6 fseek(-7, SEEK_CUR) => -1 ftell=false fseek(-12, SEEK_END) => -1 ftell=false -- seek after the end -- fseek(12, SEEK_SET) => 0 ftell=12 feof=false data="hello world" + fwrite("!") => 1 ftell=13 feof=false data="hello world\000!" fseek(6, SEEK_CUR) => 0 ftell=12 feof=false data="hello world" + fwrite("!") => 1 ftell=13 feof=false data="hello world\000!" fseek(1, SEEK_END) => 0 ftell=12 feof=false data="hello world" + fwrite("!") => 1 ftell=13 feof=false data="hello world\000!" === php://memory (read-only) === -- seek before the beginning -- note: read-only memory stream cannot be pre-filled -> empty fseek(-1, SEEK_SET) => -1 ftell=6 fseek(-7, SEEK_CUR) => -1 ftell=false fseek(-12, SEEK_END) => -1 ftell=false -- seek after the end -- fseek(12, SEEK_SET) => 0 ftell=12 feof=false data="" + fread(5) => "" ftell=12 feof=true data="" fseek(6, SEEK_CUR) => 0 ftell=12 feof=false data="" + fread(5) => "" ftell=12 feof=true data="" fseek(1, SEEK_END) => 0 ftell=1 feof=false data="" + fread(5) => "" ftell=1 feof=true data="" === SQLite3::openBlob() (writable) === -- seek before the beginning -- fseek(-1, SEEK_SET) => -1 ftell=6 fseek(-7, SEEK_CUR) => -1 ftell=false fseek(-12, SEEK_END) => -1 ftell=false -- seek after the end -- fseek(12, SEEK_SET) => -1 ftell=false feof=false data="hello world" (position false cannot be restored) + fwrite("!") => false ftell=11 feof=true [fwrite(): It is not possible to increase the size of a BLOB] data="hello world" fseek(6, SEEK_CUR) => -1 ftell=false feof=false data="hello world" (position false cannot be restored) + fwrite("!") => false ftell=11 feof=true [fwrite(): It is not possible to increase the size of a BLOB] data="hello world" fseek(1, SEEK_END) => -1 ftell=false feof=false data="hello world" (position false cannot be restored) + fwrite("!") => false ftell=11 feof=true [fwrite(): It is not possible to increase the size of a BLOB] data="hello world" === SQLite3::openBlob() (read-only) === -- seek before the beginning -- fseek(-1, SEEK_SET) => -1 ftell=6 fseek(-7, SEEK_CUR) => -1 ftell=false fseek(-12, SEEK_END) => -1 ftell=false -- seek after the end -- fseek(12, SEEK_SET) => -1 ftell=false feof=false data="hello world" (position false cannot be restored) + fread(5) => "" ftell=11 feof=true data="hello world" fseek(6, SEEK_CUR) => -1 ftell=false feof=false data="hello world" (position false cannot be restored) + fread(5) => "" ftell=11 feof=true data="hello world" fseek(1, SEEK_END) => -1 ftell=false feof=false data="hello world" (position false cannot be restored) + fread(5) => "" ftell=11 feof=true data="hello world" === Pdo\Sqlite::openBlob() (writable) === -- seek before the beginning -- fseek(-1, SEEK_SET) => -1 ftell=6 fseek(-7, SEEK_CUR) => -1 ftell=false fseek(-12, SEEK_END) => -1 ftell=false -- seek after the end -- fseek(12, SEEK_SET) => -1 ftell=false feof=false data="hello world" (position false cannot be restored) + fwrite("!") => false ftell=11 feof=true [fwrite(): It is not possible to increase the size of a BLOB] data="hello world" fseek(6, SEEK_CUR) => -1 ftell=false feof=false data="hello world" (position false cannot be restored) + fwrite("!") => false ftell=11 feof=true [fwrite(): It is not possible to increase the size of a BLOB] data="hello world" fseek(1, SEEK_END) => -1 ftell=false feof=false data="hello world" (position false cannot be restored) + fwrite("!") => false ftell=11 feof=true [fwrite(): It is not possible to increase the size of a BLOB] data="hello world" === Pdo\Sqlite::openBlob() (read-only) === -- seek before the beginning -- fseek(-1, SEEK_SET) => -1 ftell=6 fseek(-7, SEEK_CUR) => -1 ftell=false fseek(-12, SEEK_END) => -1 ftell=false -- seek after the end -- fseek(12, SEEK_SET) => -1 ftell=false feof=false data="hello world" (position false cannot be restored) + fread(5) => "" ftell=11 feof=true data="hello world" fseek(6, SEEK_CUR) => -1 ftell=false feof=false data="hello world" (position false cannot be restored) + fread(5) => "" ftell=11 feof=true data="hello world" fseek(1, SEEK_END) => -1 ftell=false feof=false data="hello world" (position false cannot be restored) + fread(5) => "" ftell=11 feof=true data="hello world" === zlib gzseek() (writable) === -- seek before the beginning -- note: write mode: gzseek() only supports forward seeks, so the stream stays at 11 instead of 6; data is read by flushing the stream and inflating the file gzseek(-1, SEEK_SET) => -1 gztell=11 gzseek(-7, SEEK_CUR) => -1 gztell=11 gzseek(-12, SEEK_END) => -1 gztell=11 [gzseek(): SEEK_END is not supported] -- seek after the end -- gzseek(12, SEEK_SET) => 0 gztell=12 gzeof=false data="hello world\000" + gzwrite("!") => 1 gztell=13 gzeof=false data="hello world\000!" gzseek(6, SEEK_CUR) => 0 gztell=17 gzeof=false data="hello world\000\000\000\000\000\000" + gzwrite("!") => 1 gztell=18 gzeof=false data="hello world\000\000\000\000\000\000!" gzseek(1, SEEK_END) => -1 gztell=11 gzeof=false [gzseek(): SEEK_END is not supported] data="hello world" + gzwrite("!") => 1 gztell=12 gzeof=false data="hello world!" === zlib gzseek() (read-only) === -- seek before the beginning -- gzseek(-1, SEEK_SET) => -1 gztell=6 gzseek(-7, SEEK_CUR) => -1 gztell=6 gzseek(-12, SEEK_END) => -1 gztell=6 [gzseek(): SEEK_END is not supported] -- seek after the end -- gzseek(12, SEEK_SET) => 0 gztell=12 gzeof=false data="hello world" + gzread(5) => "" gztell=12 gzeof=true data="hello world" gzseek(6, SEEK_CUR) => 0 gztell=12 gzeof=false data="hello world" + gzread(5) => "" gztell=12 gzeof=true data="hello world" gzseek(1, SEEK_END) => -1 gztell=6 gzeof=false [gzseek(): SEEK_END is not supported] data="hello world" + gzread(5) => "world" gztell=11 gzeof=false data="hello world"- wrong position after
Description
When a seek fails on a php://memory stream, or on a blob stream from SQLite3::openBlob() or Pdo\Sqlite::openBlob(), the stream's position is set to -1. Meanwhile the stream's own read/write offset is moved: php://memory resets it to 0, and the blob streams clamp it to 0 or to the blob size.
After that:
Plain files and php://temp behave correctly: a failed seek returns -1 and leaves the position unchanged.
Reproducer 1: php://memory
Actual output:
Actual output (debug build):
Expected output:
Reproducer 2: SQLite3 blob (Pdo\Sqlite::openBlob() is affected the same way)
Actual output:
(Debug builds abort with the same assertion as reproducer 1.)
Expected output:
Related: #21433 #20964 #20927
PHP Version
Operating System
Ubuntu 24.04