diff --git a/NEWS b/NEWS index cacd907b0..76a809c7f 100644 --- a/NEWS +++ b/NEWS @@ -52,6 +52,12 @@ GNU coreutils NEWS -*- outline -*- it would have failed if the control flag encoding was different. [This bug was present in "the beginning".] + 'tail --follow' no longer ignores an overflow of the inotify event queue, + and now reverts to polling in that case. Previously tail could fail + to notice new data, or that a file followed by name was replaced, + if events arrived faster than tail could process them. + [bug introduced in coreutils-7.5] + 'unexpand' no longer overflows a heap buffer on non-GLIBC platforms, given whitespace characters whose display width is negative. E.g., this edge case can occur with \u0085 (next line) characters on musl. diff --git a/src/tail.c b/src/tail.c index 0778c9985..fd42fc34c 100644 --- a/src/tail.c +++ b/src/tail.c @@ -1766,6 +1766,15 @@ tail_forever_inotify (int wd, struct File_spec *f, int n_files, ev = void_ev; evbuf_off += sizeof (*ev) + ev->len; + /* If the kernel discarded events for any of the watched files + or directories, so we can no longer depend on our view of them. + Revert to polling now. */ + if (ev->mask & IN_Q_OVERFLOW) + { + error (0, 0, _("inotify event queue overflowed")); + return; + } + /* If a directory is deleted, IN_DELETE_SELF is emitted with ev->name of length 0. We need to catch it, otherwise it would wait forever, diff --git a/tests/local.mk b/tests/local.mk index 7d75b4b5a..bb6d90144 100644 --- a/tests/local.mk +++ b/tests/local.mk @@ -207,6 +207,7 @@ all_tests = \ tests/tail/inotify-rotate-resources.sh \ tests/tail/inotify-dir-recreate.sh \ tests/tail/inotify-only-regular.sh \ + tests/tail/inotify-overflow.sh \ tests/tail/quote-headers.sh \ tests/chmod/no-x.sh \ tests/chgrp/basic.sh \ diff --git a/tests/tail/inotify-overflow.sh b/tests/tail/inotify-overflow.sh new file mode 100755 index 000000000..c74bfa5e6 --- /dev/null +++ b/tests/tail/inotify-overflow.sh @@ -0,0 +1,75 @@ +#!/bin/sh +# Ensure tail -F copes with the kernel discarding inotify events + +# Copyright (C) 2026 Free Software Foundation, Inc. + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +. "${srcdir=.}/tests/init.sh"; path_prepend_ ./src +print_ver_ tail +require_inotify_supported_ + +max_events=$(cat /proc/sys/fs/inotify/max_queued_events) && +test "$max_events" -le 100000 || +skip_ 'inotify queue limit unknown or too large' + +# Terminate any background tail process +cleanup_() +{ + kill -CONT "$pid" 2>/dev/null + kill "$pid" 2>/dev/null + wait "$pid" 2>/dev/null + pid= +} + +# $check_re - string to be found +check_tail_output_ () +{ + local delay="$1" + grep "$check_re" out > /dev/null || + { sleep $delay ; return 1; } +} + +grep_timeout_ () +{ + check_re="$1" + retry_delay_ check_tail_output_ .1 7 +} + +# Speed up the fallback-to-polling phase. +fastpoll='-s.1 --max-unchanged-stats=1' + +mkdir dir && echo x > dir/file || framework_failure_ + +tail --debug --pid=$$ -F $fastpoll dir/file >out 2>&1 & pid=$! +grep_timeout_ 'using notification mode' || + { cat out; skip_ "inotify can't be used"; } + +# Fill the event queue while tail is not reading it, +# so that the events for the following rotation are discarded. +kill -STOP $pid || framework_failure_ +(cd dir && seq $max_events | xargs touch) || framework_failure_ +mv dir/file dir/file.1 && +echo y > dir/file && +kill -CONT $pid || framework_failure_ + +# From coreutils-7.5 to 9.12 inclusive, the new file never noticed. +grep_timeout_ '^y$' || { cat out; fail=1; } +grep 'inotify event queue overflowed' out || { + cat out + test "$fail" = 1 || framework_failure_ +} + + +Exit $fail