On 9/8/26 10:32 AM, Sascha Hauer wrote: > Streaming files and files of not-yet-known size carried that fact as > i_size == FILE_SIZE_STREAM, i.e. (loff_t)-1. Overloading i_size this way > is fragile: a value read from media is indistinguishable from the > sentinel, so nothing can tell a genuine special file from a corrupted > inode that happens to hold -1. > > Two unrelated situations were hidden behind that one value: > > - a character device is a genuine sizeless stream, and > - a tftp transfer without a negotiated size has a real, definite size > that is simply not known until the file has been read to its end. > > The latter is not a stream: it has an end of file and it is seekable -- > tftp_lseek() moves forward by reading and discarding and backward by > reopening the transfer. Conflating the two would invite wrong > conclusions such as forbidding seeks on a tftp file. > > Give the inode an i_stream flag for the former and an i_size_unknown flag > for the latter, and leave i_size at a real value in both cases. The two > agree on the only thing that matters to the I/O layer: i_size is not a > valid bound, so reads, writes and seeks must not be clamped to it and the > driver reports the end of the file itself. That shared question is folded > into the i_size_is_bound() helper. FILE_SIZE_STREAM stays only as the > stat sentinel that stat_inode() synthesises from these flags for its > callers. > > No functional change intended; this only moves the "no i_size bound" bit > out of i_size so it can be trusted independently of the on-media value. > > Assisted-by: Claude:claude-fable-5
With S-o-b added: Reviewed-by: Ahmad Fatoum <[email protected]> > --- > fs/devfs.c | 7 ++++++- > fs/fs.c | 22 ++++++++++++++++++---- > fs/tftp.c | 2 +- > include/linux/fs.h | 2 ++ > 4 files changed, 27 insertions(+), 6 deletions(-) > > diff --git a/fs/devfs.c b/fs/devfs.c > index 75df330bcb..cb2980b114 100644 > --- a/fs/devfs.c > +++ b/fs/devfs.c > @@ -110,7 +110,12 @@ static int devfs_open(struct inode *inode, struct file > *f) > return -ENOENT; > } > > - f->f_size = cdev_size(cdev); > + if (cdev_size(cdev) == FILE_SIZE_STREAM) { > + inode->i_stream = true; > + f->f_size = 0; > + } else { > + f->f_size = cdev->size; > + } > f->private_data = cdev; > > return cdev_open(cdev, f->f_flags); > diff --git a/fs/fs.c b/fs/fs.c > index ce41f23f88..85881f6cf1 100644 > --- a/fs/fs.c > +++ b/fs/fs.c > @@ -373,6 +373,18 @@ static int fsdev_truncate(struct file *f, loff_t length) > f->f_inode->i_fop->truncate(f, length) : -EROFS; > } > > +/* > + * Reads, writes and seeks are clamped to i_size, but only when i_size is a > + * meaningful bound. A character device is a sizeless stream, and a file > whose > + * size is only discovered while reading (e.g. a tftp transfer without a > + * negotiated size) starts out with i_size zero; for both, i_size must not > clamp > + * I/O and the driver reports the end of the file itself. > + */ > +static bool i_size_is_bound(const struct inode *inode) > +{ > + return !inode->i_stream && !inode->i_size_unknown; > +} > + > int ftruncate(int fd, loff_t length) > { > struct file *f = fd_to_file(fd, false); > @@ -381,7 +393,7 @@ int ftruncate(int fd, loff_t length) > if (IS_ERR(f)) > return -errno; > > - if (f->f_size == FILE_SIZE_STREAM) > + if (!i_size_is_bound(f->f_inode)) > return 0; > > ret = fsdev_truncate(f, length); > @@ -427,7 +439,7 @@ static ssize_t __read(struct file *f, 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) > + if (i_size_is_bound(f->f_inode) && f->f_pos + count > f->f_size) > count = f->f_size - f->f_pos; > > if (!count) > @@ -487,7 +499,7 @@ 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) { > + if (i_size_is_bound(f->f_inode) && f->f_pos + count > f->f_size) { > ret = fsdev_truncate(f, f->f_pos + count); > if (ret) { > if (ret == -EPERM) > @@ -592,7 +604,7 @@ loff_t lseek(int fd, loff_t offset, int whence) > > pos += offset; > > - if (f->f_size != FILE_SIZE_STREAM && (pos < 0 || pos > f->f_size)) > + if (i_size_is_bound(f->f_inode) && (pos < 0 || pos > f->f_size)) > goto out; > > if (f->f_inode->i_fop->lseek) { > @@ -1105,6 +1117,8 @@ static void stat_inode(struct inode *inode, struct stat > *s) > cdev = cdev_by_name(inode->cdevname); > > s->st_size = cdev ? cdev_size(cdev) : 0; > + } else if (!i_size_is_bound(inode)) { > + s->st_size = FILE_SIZE_STREAM; > } else { > s->st_size = inode->i_size; > } > diff --git a/fs/tftp.c b/fs/tftp.c > index e8a6b62870..dc68f2c77c 100644 > --- a/fs/tftp.c > +++ b/fs/tftp.c > @@ -1088,7 +1088,7 @@ static struct dentry *tftp_lookup(struct inode *dir, > struct dentry *dentry, > if (filesize) > inode->i_size = filesize; > else > - inode->i_size = FILE_SIZE_STREAM; > + inode->i_size_unknown = true; > > d_add(dentry, inode); > > diff --git a/include/linux/fs.h b/include/linux/fs.h > index d3813ad9c5..fc50d207a6 100644 > --- a/include/linux/fs.h > +++ b/include/linux/fs.h > @@ -129,6 +129,8 @@ struct inode { > gid_t i_gid; > u64 i_version; > loff_t i_size; > + bool i_stream; /* sizeless stream, e.g. a > character device */ > + bool i_size_unknown; /* real size, not known until > read, e.g. tftp */ > struct timespec i_atime; > struct timespec i_mtime; > struct timespec i_ctime; > -- Pengutronix e.K. | | Steuerwalder Str. 21 | http://www.pengutronix.de/ | 31137 Hildesheim, Germany | Phone: +49-5121-206917-0 | Amtsgericht Hildesheim, HRA 2686 | Fax: +49-5121-206917-5555 |
