Subject: Re: bug#70477: tail command does not hang on /dev/random

From: Paul Eggert <[email-masked]>
Date: Fri, 19 Apr 2024 21:52:20 -0700

On 2024-04-19 11:40, Ionut Nicula wrote:
> The following command hangs indefinitely, as expected:
> 
>          tail -c 4096 /dev/random
> 
> The following command immediately returns 4097 bytes, unexpectedly:
> 
>          tail -c 4097 /dev/random

Thanks for the bug report. Although the two commands should behave 
similarly, neither should loop: they should both output the requested 
number of random bytes. Similarly for /dev/zero. I installed the 
attached patch to do that.

From fb543b6b82c1f3a20ff88f44cc3ed367bfe811b6 Mon Sep 17 00:00:00 2001
From: Paul Eggert <[email-masked]>
Date: Fri, 19 Apr 2024 21:44:32 -0700
Subject: [PATCH] tail: avoid infloop with -c on /dev/zero

Problem reported by Ionut Nicula in:
https://bugs.gnu.org/70477
* src/tail.c (tail_bytes): Do not loop forever on commands
like 'tail -c 4096 /dev/zero'.
* tests/tail/tail-c.sh: Test this fix.
---
 NEWS                 |  3 +++
 src/tail.c           | 24 +++++++++++++++++++-----
 tests/tail/tail-c.sh | 10 ++++++++++
 3 files changed, 32 insertions(+), 5 deletions(-)

diff --git a/NEWS b/NEWS
index 43ce84d7e..389f72516 100644
--- a/NEWS
+++ b/NEWS
@@ -12,6 +12,9 @@ GNU coreutils NEWS                                    -*- outline -*-
   have exited with a "Function not implemented" error.
   [bug introduced in coreutils-8.28]
 
+  'tail -c 4096 /dev/zero' no longer loops forever.
+  [This bug was present in "the beginning".]
+
 ** Changes in behavior
 
   ls's -f option now simply acts like -aU, instead of also ignoring
diff --git a/src/tail.c b/src/tail.c
index 52c081031..a3b46ca2d 100644
--- a/src/tail.c
+++ b/src/tail.c
@@ -760,7 +760,8 @@ free_lbuffers:
   return ok;
 }
 
-/* Print the last N_BYTES characters from the end of pipe FD.
+/* Print the last N_BYTES characters from the end of FD.
+   Work even if the input is a pipe.
    This is a stripped down version of pipe_lines.
    Return true if successful.  */
 
@@ -1875,15 +1876,28 @@ tail_bytes (char const *pretty_filename, int fd, uintmax_t n_bytes,
     {
       off_t end_pos = -1;
       off_t current_pos = -1;
+      bool copy_from_current_pos = false;
 
       if (! presume_input_pipe && n_bytes <= OFF_T_MAX)
         {
           if (usable_st_size (&stats))
-            end_pos = stats.st_size;
-          else if ((current_pos = lseek (fd, -n_bytes, SEEK_END)) != -1)
-            end_pos = current_pos + n_bytes;
+            {
+              /* Use st_size only if it's so large that this is
+                 probably not a /proc or similar file, where st_size
+                 is notional.  */
+              end_pos = stats.st_size;
+              off_t smallish_size = STP_BLKSIZE (&stats);
+              copy_from_current_pos = smallish_size < end_pos;
+            }
+          else
+            {
+              current_pos = lseek (fd, -n_bytes, SEEK_END);
+              copy_from_current_pos = current_pos != -1;
+              if (copy_from_current_pos)
+                end_pos = current_pos + n_bytes;
+            }
         }
-      if (end_pos <= (off_t) STP_BLKSIZE (&stats))
+      if (! copy_from_current_pos)
         return pipe_bytes (pretty_filename, fd, n_bytes, read_pos);
       if (current_pos == -1)
         current_pos = xlseek (fd, 0, SEEK_CUR, pretty_filename);
diff --git a/tests/tail/tail-c.sh b/tests/tail/tail-c.sh
index f518e5b21..a9f2bc2d1 100755
--- a/tests/tail/tail-c.sh
+++ b/tests/tail/tail-c.sh
@@ -35,4 +35,14 @@ printf '123456' | tail -c3 > out || fail=1
 printf '456' > exp || framework_failure_
 compare exp out || fail=1
 
+# Any part of /dev/zero should be valid for tail -c.
+head -c 4096 /dev/zero >exp || fail=1
+tail -c 4096 /dev/zero >out || fail=1
+compare exp out || fail=1
+
+# Any part of /dev/urandom, if it exists, should be valid for tail -c.
+if test -r /dev/urandom; then
+  timeout --verbose 1 tail -c 4096 /dev/urandom >/dev/null || fail=1
+fi
+
 Exit $fail
-- 
2.40.1