[PATCH] drivers/char/mem.c: fix null and zero dev lseek

David Timber posted 1 patch 2 weeks, 1 day ago
There is a newer version of this series
drivers/char/mem.c | 12 ++++++++++++
1 file changed, 12 insertions(+)
[PATCH] drivers/char/mem.c: fix null and zero dev lseek
Posted by David Timber 2 weeks, 1 day ago
lseek() on /dev/null and /dev/zero always returns 0. This is
problematic for userland programs that detect holes and advancing
the offset by the delta (SEEK_HOLE - SEEK_DATA), which will always be
calculated as zero.

Link: https://github.com/util-linux/util-linux/pull/4132

Signed-off-by: David Timber <dxdt@dev.snart.me>
---
 drivers/char/mem.c | 12 ++++++++++++
 1 file changed, 12 insertions(+)

diff --git a/drivers/char/mem.c b/drivers/char/mem.c
index cca4529431f8..e2bde4ad5677 100644
--- a/drivers/char/mem.c
+++ b/drivers/char/mem.c
@@ -573,9 +573,21 @@ static ssize_t write_full(struct file *file, const char __user *buf,
  * Special lseek() function for /dev/null and /dev/zero.  Most notably, you
  * can fopen() both devices with "a" now.  This was previously impossible.
  * -- SRB.
+ *
+ * For SEEK_DATA and SEEK_HOLE, return an error. Otherwise, userland conforming
+ * to the POSIX spec could end up in an infinite loop.
  */
 static loff_t null_lseek(struct file *file, loff_t offset, int orig)
 {
+	switch (orig) {
+	case SEEK_CUR:
+	case SEEK_SET:
+	case SEEK_END:
+		break;
+	default:
+		return -EINVAL;
+	}
+
 	return file->f_pos = 0;
 }
 
-- 
2.53.0.1.ga224b40d3f.dirty
Re: [PATCH] drivers/char/mem.c: fix null and zero dev lseek
Posted by Greg KH 2 weeks, 1 day ago
On Fri, Mar 20, 2026 at 01:17:22PM +0900, David Timber wrote:
> lseek() on /dev/null and /dev/zero always returns 0. This is
> problematic for userland programs that detect holes and advancing
> the offset by the delta (SEEK_HOLE - SEEK_DATA), which will always be
> calculated as zero.

But it's always worked this way, what changed to require this to now
change?

What userspace tools are you going to break by doing this?

> Link: https://github.com/util-linux/util-linux/pull/4132
> 
> Signed-off-by: David Timber <dxdt@dev.snart.me>
> ---
>  drivers/char/mem.c | 12 ++++++++++++
>  1 file changed, 12 insertions(+)
> 
> diff --git a/drivers/char/mem.c b/drivers/char/mem.c
> index cca4529431f8..e2bde4ad5677 100644
> --- a/drivers/char/mem.c
> +++ b/drivers/char/mem.c
> @@ -573,9 +573,21 @@ static ssize_t write_full(struct file *file, const char __user *buf,
>   * Special lseek() function for /dev/null and /dev/zero.  Most notably, you
>   * can fopen() both devices with "a" now.  This was previously impossible.
>   * -- SRB.
> + *
> + * For SEEK_DATA and SEEK_HOLE, return an error. Otherwise, userland conforming
> + * to the POSIX spec could end up in an infinite loop.

Where does POSIX require this to happen for these device nodes?


>   */
>  static loff_t null_lseek(struct file *file, loff_t offset, int orig)
>  {
> +	switch (orig) {
> +	case SEEK_CUR:
> +	case SEEK_SET:
> +	case SEEK_END:
> +		break;
> +	default:
> +		return -EINVAL;

You just changed how userspace reacts to this, are you SURE that is
going to be ok?  And if you really want to do this, shouldn't you
explicitly list what options you are going to return an error for, not
the other way around in this switch statement?

thanks,

greg k-h
Re: [PATCH] drivers/char/mem.c: fix null and zero dev lseek
Posted by David Timber 1 week, 5 days ago
(resending due to mail format error)

On 3/20/26 15:02, Greg KH wrote:
> On Fri, Mar 20, 2026 at 01:17:22PM +0900, David Timber wrote:
>> lseek() on /dev/null and /dev/zero always returns 0. This is
>> problematic for userland programs that detect holes and advancing
>> the offset by the delta (SEEK_HOLE - SEEK_DATA), which will always be
>> calculated as zero.
> But it's always worked this way, what changed to require this to now
> change?
No, it has not. This time, it's Linux who's at fault. For any other
whence the fs does not understand, it should return EINVAL. We can all
agree that this is bad because prooly written userspace code could go
unnoticed just because it's tested against /dev/null.
> What userspace tools are you going to break by doing this?
It will break poorly written userspace tools that call lseek() with a
wrong value or corrupted stack. I cannot give you the exact data, but in
my opinion, there's none. Yes, we don't break userspace, but in this
case, there wouldn't be any userspace to break.
> Where does POSIX require this to happen for these device nodes?
It is inferred from the fact that the offsets to SEEK_DATA and SEEK_HOLE
cannot be the same. Ever. This is why whence needs to be checked,
regardless of how the particular special device node or VFS behaves.

I don't know about the other device node types, but /dev/null is a
special case because its common use case is symlinking to mask regular
files. Say there's a unsuspecting script or userspace program that
applies the logic of dig_holes() without doing lstat() or [ -f "$FILE"
]. The userspace open()'s the path, the dereferenced path happens to be
/dev/null. The poor userspace steps into the loop and loops indefinitely.
> You just changed how userspace reacts to this, are you SURE that is
> going to be ok?
As far as I'm concerned, yes /I/ am sure. As outlined above, the start
of a hole and data cannot be the same.

I've check the behaviour of /dev/null on BSD's(the descendants of the
386BSD codebase) and Unices. All of the ones I've checked checks the
validity of whence(i.e. EINVAL is returned). It is in my opinion that
Linux fix this incorrect behaviour before SEEK_DATA and SEEK_HOLE(made
POSIX in 2024 spec) become widespread and the userspace implementations
depending on this broken behaviour start to appear.

At the end of the day, even if Linux decides to remain as that black
sheep, userspace can always cater to the peculiar of Linux as always.
Your call.
> And if you really want to do this, shouldn't you
> explicitly list what options you are going to return an error for, not
> the other way around in this switch statement?
Sure. See rerolled.

Davo
Re: [PATCH] drivers/char/mem.c: fix null and zero dev lseek
Posted by David Timber 1 week, 5 days ago
On 3/22/26 20:52, David Timber wrote:
>> Where does POSIX require this to happen for these device nodes?
> It is inferred from the fact that the offsets to SEEK_DATA and SEEK_HOLE
> cannot be the same. Ever.
>> You just changed how userspace reacts to this, are you SURE that is
>> going to be ok?
> As far as I'm concerned, yes /I/ am sure. As outlined above, the start
> of a hole and data cannot be the same.
On the second thought, one possible scenario where the offsets to hole
and data being the same would be a TOCTOU condition where a hole has
been dug in between lseek() calls. Either way, that means the file is
being manipulated by multiple processes so in fallocate's case, it might
as well just terminate prematurely to be on the safe side.

btw, I don't maintain util-linux, so I'd just like to mention that it's
up to the maintainers to decide. In addition to that, other sparse-aware
userspace tools like compression filter, tar, and rsync always lstat()
before doing anything "sparse". The kernel returning EINVAL is more for
extereme edge cases like fallocate(to prevent such programs from
shooting themselves in the foot).
>> And if you really want to do this, shouldn't you
>> explicitly list what options you are going to return an error for, not
>> the other way around in this switch statement?
> Sure. See rerolled.
I just removed the confusing comments and made the function use one
return statement. We could do other errnos as per POSIX spec, but to be
honest, EINVAL should suffice. I get the gist of where you were getting
at - /dev/null isn't really the scope of POSIX so technically Linux
could do whatever.

I wouldn't complicate things. It's a simple dev node. So keep the code
simple?

Davo
[PATCH v1] drivers/char/mem.c: fix null and zero dev lseek
Posted by David Timber 1 week, 5 days ago
Return -EINVAL if whence is not understood by /dev/null or /dev/zero.

lseek() on /dev/null and /dev/zero always returns 0. This is
problematic for userland programs that detect holes and advancing the
offset by the calculated delta (SEEK_HOLE - SEEK_DATA), which will
always be calculated as zero.

Link: https://github.com/util-linux/util-linux/pull/4132

Signed-off-by: David Timber <dxdt@dev.snart.me>
---
 drivers/char/mem.c | 14 +++++++++++++-
 1 file changed, 13 insertions(+), 1 deletion(-)

diff --git a/drivers/char/mem.c b/drivers/char/mem.c
index cca4529431f8..f0318e924db9 100644
--- a/drivers/char/mem.c
+++ b/drivers/char/mem.c
@@ -576,7 +576,19 @@ static ssize_t write_full(struct file *file, const char __user *buf,
  */
 static loff_t null_lseek(struct file *file, loff_t offset, int orig)
 {
-	return file->f_pos = 0;
+	loff_t ret;
+
+	switch (orig) {
+	case SEEK_SET:
+	case SEEK_CUR:
+	case SEEK_END:
+		ret = file->f_pos = 0;
+		break;
+	default:
+		ret = -EINVAL;
+	}
+
+	return ret;
 }
 
 /*
-- 
2.53.0.1.ga224b40d3f.dirty