* [PATCH 0/2] fs: reject inodes whose size the superblock cannot address
@ 2026-09-08 8:32 Sascha Hauer
2026-09-08 8:32 ` [PATCH 1/2] fs: track streaming and unknown-size files in dedicated inode fields Sascha Hauer
` (2 more replies)
0 siblings, 3 replies; 6+ messages in thread
From: Sascha Hauer @ 2026-09-08 8:32 UTC (permalink / raw)
To: BAREBOX
This is a continuation of the patch previously sent as a single patch.
Additionally I added a patch cleaning up the FILE_SIZE_STREAM disambiguity.
The problem is that we overloaded the inode size with a flag indicating
that the inode belongs to a stream with undefined file size. The new
patch solves this by adding extra flags to struct inode.
Signed-off-by: Sascha Hauer <s.hauer@pengutronix.de>
---
Sascha Hauer (2):
fs: track streaming and unknown-size files in dedicated inode fields
fs: reject inodes whose size the superblock cannot address
fs/devfs.c | 7 ++++++-
fs/fs.c | 37 +++++++++++++++++++++++++++++++++----
fs/tftp.c | 2 +-
include/linux/fs.h | 2 ++
4 files changed, 42 insertions(+), 6 deletions(-)
---
base-commit: 42e510a258d7c276c35f06c103e685c80d376eab
change-id: 20260908-fs-filesize-fixes-2f932f665027
Best regards,
--
Sascha Hauer <s.hauer@pengutronix.de>
^ permalink raw reply [flat|nested] 6+ messages in thread* [PATCH 1/2] fs: track streaming and unknown-size files in dedicated inode fields 2026-09-08 8:32 [PATCH 0/2] fs: reject inodes whose size the superblock cannot address Sascha Hauer @ 2026-09-08 8:32 ` Sascha Hauer 2026-09-08 8:50 ` Ahmad Fatoum 2026-09-08 8:32 ` [PATCH 2/2] fs: reject inodes whose size the superblock cannot address Sascha Hauer 2026-09-11 14:04 ` [PATCH 0/2] " Sascha Hauer 2 siblings, 1 reply; 6+ messages in thread From: Sascha Hauer @ 2026-09-08 8:32 UTC (permalink / raw) To: BAREBOX 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 --- 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; -- 2.47.3 ^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [PATCH 1/2] fs: track streaming and unknown-size files in dedicated inode fields 2026-09-08 8:32 ` [PATCH 1/2] fs: track streaming and unknown-size files in dedicated inode fields Sascha Hauer @ 2026-09-08 8:50 ` Ahmad Fatoum 0 siblings, 0 replies; 6+ messages in thread From: Ahmad Fatoum @ 2026-09-08 8:50 UTC (permalink / raw) To: Sascha Hauer, BAREBOX 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 <a.fatoum@pengutronix.de> > --- > 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 | ^ permalink raw reply [flat|nested] 6+ messages in thread
* [PATCH 2/2] fs: reject inodes whose size the superblock cannot address 2026-09-08 8:32 [PATCH 0/2] fs: reject inodes whose size the superblock cannot address Sascha Hauer 2026-09-08 8:32 ` [PATCH 1/2] fs: track streaming and unknown-size files in dedicated inode fields Sascha Hauer @ 2026-09-08 8:32 ` Sascha Hauer 2026-09-08 8:55 ` Ahmad Fatoum 2026-09-11 14:04 ` [PATCH 0/2] " Sascha Hauer 2 siblings, 1 reply; 6+ messages in thread From: Sascha Hauer @ 2026-09-08 8:32 UTC (permalink / raw) To: BAREBOX f_size is an alias for the inode's i_size, and filesystems read that size straight from untrusted media. ext4's ext4_isize() builds it as ((loff_t)size_high << 32) | size, so bit 31 of size_high lands in the sign bit; squashfs takes an unchecked le64 for LREG inodes. A negative i_size then feeds the offset arithmetic in __read(), __write() and friends, where it either wraps a size_t count to something huge or -- depending on whether size_t is 32 or 64 bit -- turns the comparison unsigned and skips the EOF clamp altogether. Catch this the way Linux does: give the superblock a ceiling and refuse sizes beyond it, instead of hardening every arithmetic site. Default s_maxbytes to MAX_LFS_FILESIZE in init_super(), which runs before the driver probe, so the filesystems that already lower it (jffs2, ubifs, squashfs, 9p) keep doing so and everyone else stops sitting at zero. The check goes into do_dentry_open() rather than into iget: ten drivers only learn the size in their ->open() callback and write it to the inode through file->f_size, so a lookup time check would miss them. Casting to u64 makes a negative size exceed any sane s_maxbytes, so the sign is covered too; streaming files carry no size of their own and are exempted on the i_stream flag rather than on an i_size value. Assisted-by: Claude:claude-opus-5 Signed-off-by: Sascha Hauer <s.hauer@pengutronix.de> --- fs/fs.c | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/fs/fs.c b/fs/fs.c index 85881f6cf1..29478cb419 100644 --- a/fs/fs.c +++ b/fs/fs.c @@ -998,6 +998,7 @@ int fsdev_open_cdev(struct fs_device *fsdev) static void init_super(struct super_block *sb) { INIT_LIST_HEAD(&sb->s_inodes); + sb->s_maxbytes = MAX_LFS_FILESIZE; } static int fsdev_umount(struct fs_device *fsdev) @@ -2627,6 +2628,14 @@ static int rmdirat(int dirfd, const char *pathname) return errno_set(error); } +static bool i_size_valid(struct inode *inode) +{ + if (inode->i_stream) + return true; + + return (u64)inode->i_size <= inode->i_sb->s_maxbytes; +} + static int do_dentry_open(struct file *f) { int error; @@ -2642,6 +2651,12 @@ static int do_dentry_open(struct file *f) return error; } + if (!i_size_valid(f->f_inode)) { + dev_warn(&f->fsdev->dev, "%s: bad i_size value: %lld\n", + f->path, f->f_size); + return -EUCLEAN; + } + if (f->f_flags & O_TRUNC) { error = fsdev_truncate(f, 0); f->f_size = 0; -- 2.47.3 ^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [PATCH 2/2] fs: reject inodes whose size the superblock cannot address 2026-09-08 8:32 ` [PATCH 2/2] fs: reject inodes whose size the superblock cannot address Sascha Hauer @ 2026-09-08 8:55 ` Ahmad Fatoum 0 siblings, 0 replies; 6+ messages in thread From: Ahmad Fatoum @ 2026-09-08 8:55 UTC (permalink / raw) To: Sascha Hauer, BAREBOX On 9/8/26 10:32 AM, Sascha Hauer wrote: > f_size is an alias for the inode's i_size, and filesystems read that > size straight from untrusted media. ext4's ext4_isize() builds it as > ((loff_t)size_high << 32) | size, so bit 31 of size_high lands in the > sign bit; squashfs takes an unchecked le64 for LREG inodes. A negative > i_size then feeds the offset arithmetic in __read(), __write() and > friends, where it either wraps a size_t count to something huge or -- > depending on whether size_t is 32 or 64 bit -- turns the comparison > unsigned and skips the EOF clamp altogether. > > Catch this the way Linux does: give the superblock a ceiling and refuse > sizes beyond it, instead of hardening every arithmetic site. Default > s_maxbytes to MAX_LFS_FILESIZE in init_super(), which runs before the > driver probe, so the filesystems that already lower it (jffs2, ubifs, > squashfs, 9p) keep doing so and everyone else stops sitting at zero. > > The check goes into do_dentry_open() rather than into iget: ten drivers > only learn the size in their ->open() callback and write it to the > inode through file->f_size, so a lookup time check would miss them. > Casting to u64 makes a negative size exceed any sane s_maxbytes, so the > sign is covered too; streaming files carry no size of their own and are > exempted on the i_stream flag rather than on an i_size value. > > Assisted-by: Claude:claude-opus-5 > Signed-off-by: Sascha Hauer <s.hauer@pengutronix.de> Acked-by: Ahmad Fatoum <a.fatoum@pengutronix.de> > --- > fs/fs.c | 15 +++++++++++++++ > 1 file changed, 15 insertions(+) > > diff --git a/fs/fs.c b/fs/fs.c > index 85881f6cf1..29478cb419 100644 > --- a/fs/fs.c > +++ b/fs/fs.c > @@ -998,6 +998,7 @@ int fsdev_open_cdev(struct fs_device *fsdev) > static void init_super(struct super_block *sb) > { > INIT_LIST_HEAD(&sb->s_inodes); > + sb->s_maxbytes = MAX_LFS_FILESIZE; > } > > static int fsdev_umount(struct fs_device *fsdev) > @@ -2627,6 +2628,14 @@ static int rmdirat(int dirfd, const char *pathname) > return errno_set(error); > } > > +static bool i_size_valid(struct inode *inode) > +{ > + if (inode->i_stream) > + return true; > + > + return (u64)inode->i_size <= inode->i_sb->s_maxbytes; > +} > + > static int do_dentry_open(struct file *f) > { > int error; > @@ -2642,6 +2651,12 @@ static int do_dentry_open(struct file *f) > return error; > } > > + if (!i_size_valid(f->f_inode)) { > + dev_warn(&f->fsdev->dev, "%s: bad i_size value: %lld\n", > + f->path, f->f_size); > + return -EUCLEAN; > + } > + > if (f->f_flags & O_TRUNC) { > error = fsdev_truncate(f, 0); > f->f_size = 0; > -- 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 | ^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [PATCH 0/2] fs: reject inodes whose size the superblock cannot address 2026-09-08 8:32 [PATCH 0/2] fs: reject inodes whose size the superblock cannot address Sascha Hauer 2026-09-08 8:32 ` [PATCH 1/2] fs: track streaming and unknown-size files in dedicated inode fields Sascha Hauer 2026-09-08 8:32 ` [PATCH 2/2] fs: reject inodes whose size the superblock cannot address Sascha Hauer @ 2026-09-11 14:04 ` Sascha Hauer 2 siblings, 0 replies; 6+ messages in thread From: Sascha Hauer @ 2026-09-11 14:04 UTC (permalink / raw) To: BAREBOX, Sascha Hauer On Tue, 08 Sep 2026 10:32:08 +0200, Sascha Hauer wrote: > This is a continuation of the patch previously sent as a single patch. > > Additionally I added a patch cleaning up the FILE_SIZE_STREAM disambiguity. > The problem is that we overloaded the inode size with a flag indicating > that the inode belongs to a stream with undefined file size. The new > patch solves this by adding extra flags to struct inode. > > [...] Applied, thanks! [1/2] fs: track streaming and unknown-size files in dedicated inode fields https://git.pengutronix.de/cgit/barebox/commit/?id=260a4c6af4e3 (link may not be stable) [2/2] fs: reject inodes whose size the superblock cannot address https://git.pengutronix.de/cgit/barebox/commit/?id=b37ad309e6ca (link may not be stable) Best regards, -- Sascha Hauer <s.hauer@pengutronix.de> ^ permalink raw reply [flat|nested] 6+ messages in thread
end of thread, other threads:[~2026-09-11 14:05 UTC | newest] Thread overview: 6+ messages (download: mbox.gz / follow: Atom feed) -- links below jump to the message on this page -- 2026-09-08 8:32 [PATCH 0/2] fs: reject inodes whose size the superblock cannot address Sascha Hauer 2026-09-08 8:32 ` [PATCH 1/2] fs: track streaming and unknown-size files in dedicated inode fields Sascha Hauer 2026-09-08 8:50 ` Ahmad Fatoum 2026-09-08 8:32 ` [PATCH 2/2] fs: reject inodes whose size the superblock cannot address Sascha Hauer 2026-09-08 8:55 ` Ahmad Fatoum 2026-09-11 14:04 ` [PATCH 0/2] " Sascha Hauer
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox