__write() grows the file when the write end would pass EOF, but based the decision on the same target dependent mixed sign comparison that __read() had: f->f_pos + count > f->f_size compares the signed 64-bit position and size (loff_t) against count (size_t), which is 32-bit on 32-bit arches and 64-bit on 64-bit arches. For negative file sizes except for the FILE_SIZE_STREAM sentinel this is broken:
- On 32-bit arches count is converted to the signed 64-bit type of f->f_pos, so for e.g. f->f_size = -512 the comparison evaluated true. fsdev_truncate() was then called for the corrupted file size, attempting to grow the file to f->f_pos + count. - On 64-bit arches size_t cannot be represented by signed 64-bit, so the usual arithmetic conversions turned the whole comparison unsigned: f->f_size = -512 was reinterpreted as a value near 2^64, the comparison stayed false, the file was never grown and the write proceeded unclamped against the bogus size. Additionally, when writing at a position past the end of the file (reachable via pwrite() with a large offset) and fsdev_truncate() failed with -ENOSPC, the fallback count f->f_size - f->f_pos was negative and wrapped to a huge value in the unsigned count. __write() now rejects negative file sizes with -EINVAL, like __read() does. The write end f->f_pos + count is computed in u64, making the growth check target independent, and extending the file beyond MAX_LFS_FILESIZE is rejected with -EFBIG. On -ENOSPC the write is now limited to the bytes remaining until EOF, or aborted when f->f_pos is at or past the end of the file, instead of wrapping the negative remainder. Signed-off-by: Stefan Kerkmann <[email protected]> --- fs/fs.c | 42 +++++++++++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/fs/fs.c b/fs/fs.c index a8f2b78294..3803decc2a 100644 --- a/fs/fs.c +++ b/fs/fs.c @@ -482,7 +482,10 @@ EXPORT_SYMBOL(read); static ssize_t __write(struct file *f, const void *buf, size_t count) { + u64 size = (u64)f->f_size; + u64 pos = (u64)f->f_pos; struct fs_driver *fsdrv; + u64 end; int ret; fsdrv = f->fsdev->driver; @@ -495,18 +498,35 @@ static ssize_t __write(struct file *f, const void *buf, size_t count) if (fsdrv != ramfs_driver) assert_command_context(); - if (f->f_size != FILE_SIZE_STREAM && f->f_pos + count > f->f_size) { - ret = fsdev_truncate(f, f->f_pos + count); - if (ret) { - if (ret == -EPERM) - ret = -ENOSPC; - if (ret != -ENOSPC) + if (f->f_size != FILE_SIZE_STREAM) { + if (f->f_size < 0) { + ret = -EINVAL; + goto out; + } + + /* Writing past the end of the file requires growing it first */ + end = pos + count; + if (end > size) { + /* New file size must be representable as loff_t */ + if (end > (u64)MAX_LFS_FILESIZE || + (f->f_pos >= 0 && end < pos)) { + ret = -EFBIG; goto out; - count = f->f_size - f->f_pos; - if (!count) - goto out; - } else { - f->f_size = f->f_pos + count; + } + + ret = fsdev_truncate(f, end); + if (ret) { + if (ret == -EPERM) + ret = -ENOSPC; + if (ret != -ENOSPC) + goto out; + /* Truncate failed; write what fits into the file */ + count = pos < size ? size - pos : 0; + if (!count) + goto out; + } else { + f->f_size = end; + } } } -- 2.47.3
