git: 621f45532c58 - main - tail: Fix heap overflow in -F case.

From: Dag-Erling Smørgrav <des_at_FreeBSD.org>
Date: Wed, 29 Nov 2023 21:49:55 UTC
The branch main has been updated by des:

URL: https://cgit.FreeBSD.org/src/commit/?id=621f45532c5887c96b708ce232c52878d0053325

commit 621f45532c5887c96b708ce232c52878d0053325
Author:     Dag-Erling Smørgrav <des@FreeBSD.org>
AuthorDate: 2023-11-29 21:48:50 +0000
Commit:     Dag-Erling Smørgrav <des@FreeBSD.org>
CommitDate: 2023-11-29 21:49:38 +0000

    tail: Fix heap overflow in -F case.
    
    The number of events we track can vary over time, but we only allocate
    enough space for the exact number of events we are tracking when we
    first begin, resulting in a trivially reproducable heap overflow.  Fix
    this by allocating enough space for the greatest possible number of
    events (two per file) and clean up the code a bit.
    
    Also add a test case which triggers the aforementioned heap overflow,
    although we don't currently have a way to detect it.
    
    MFC after:      1 week
    Sponsored by:   Klara, Inc.
    Reviewed by:    allanjude, markj
    Differential Revision:  https://reviews.freebsd.org/D42839
---
 usr.bin/tail/forward.c          | 39 ++++++++++++++++++---------------------
 usr.bin/tail/tests/tail_test.sh | 21 ++++++++++++++++++++-
 2 files changed, 38 insertions(+), 22 deletions(-)

diff --git a/usr.bin/tail/forward.c b/usr.bin/tail/forward.c
index e8e2cb89ab01..a8f52ed376c4 100644
--- a/usr.bin/tail/forward.c
+++ b/usr.bin/tail/forward.c
@@ -272,7 +272,7 @@ set_events(file_info_t *files)
 
 	action = USE_KQUEUE;
 	for (i = 0, file = files; i < no_files; i++, file++) {
-		if (! file->fp)
+		if (!file->fp)
 			continue;
 
 		if (fstatfs(fileno(file->fp), &sf) == 0 &&
@@ -304,27 +304,21 @@ set_events(file_info_t *files)
 void
 follow(file_info_t *files, enum STYLE style, off_t off)
 {
-	int active, ev_change, i, n = -1;
+	int active, ev_change, i, n;
 	struct stat sb2;
 	file_info_t *file;
 	FILE *ftmp;
 	struct timespec ts;
 
 	/* Position each of the files */
-
-	file = files;
 	active = 0;
-	n = 0;
-	for (i = 0; i < no_files; i++, file++) {
-		if (file->fp) {
-			active = 1;
-			n++;
-			if (vflag || (qflag == 0 && no_files > 1))
-				printfn(file->file_name, 1);
-			forward(file->fp, file->file_name, style, off, &file->st);
-			if (Fflag && fileno(file->fp) != STDIN_FILENO)
-				n++;
-		}
+	for (i = 0, file = files; i < no_files; i++, file++) {
+		if (!file->fp)
+			continue;
+		active = 1;
+		if (vflag || (qflag == 0 && no_files > 1))
+			printfn(file->file_name, 1);
+		forward(file->fp, file->file_name, style, off, &file->st);
 	}
 	if (!Fflag && !active)
 		return;
@@ -334,9 +328,14 @@ follow(file_info_t *files, enum STYLE style, off_t off)
 	kq = kqueue();
 	if (kq < 0)
 		err(1, "kqueue");
-	ev = malloc(n * sizeof(struct kevent));
-	if (! ev)
-	    err(1, "Couldn't allocate memory for kevents.");
+	/*
+	 * The number of kqueue events we track may vary over time and may
+	 * even grow past its initial value in the -F case, but it will
+	 * never exceed two per file, so just preallocate that.
+	 */
+	ev = malloc(no_files * 2 * sizeof(struct kevent));
+	if (ev == NULL)
+		err(1, "Couldn't allocate memory for kevents.");
 	set_events(files);
 
 	for (;;) {
@@ -410,9 +409,7 @@ follow(file_info_t *files, enum STYLE style, off_t off)
 			 */
 			do {
 				n = kevent(kq, NULL, 0, ev, 1, Fflag ? &ts : NULL);
-				if (n < 0 && errno == EINTR)
-					continue;
-				if (n < 0)
+				if (n < 0 && errno != EINTR)
 					err(1, "kevent");
 			} while (n < 0);
 			if (n == 0) {
diff --git a/usr.bin/tail/tests/tail_test.sh b/usr.bin/tail/tests/tail_test.sh
index 8123a310fe67..9c941f8a2c2f 100755
--- a/usr.bin/tail/tests/tail_test.sh
+++ b/usr.bin/tail/tests/tail_test.sh
@@ -329,10 +329,28 @@ follow_stdin_body()
 	atf_check kill $pid
 }
 
+atf_test_case follow_create
+follow_create_head()
+{
+	atf_set "descr" "Verify that -F works when a file is created"
+}
+follow_create_body()
+{
+	local pid
+
+	rm -f infile
+	tail -F infile > outfile &
+	pid=$!
+	seq 1 5 >infile
+	sleep 2
+	atf_check cmp infile outfile
+	atf_check kill $pid
+}
+
 atf_test_case follow_rename
 follow_rename_head()
 {
-	atf_set "descr" "Verify that -F works"
+	atf_set "descr" "Verify that -F works when a file is replaced"
 }
 follow_rename_body()
 {
@@ -424,6 +442,7 @@ atf_init_test_cases()
 	atf_add_test_case stdin
 	atf_add_test_case follow
 	atf_add_test_case follow_stdin
+	atf_add_test_case follow_create
 	atf_add_test_case follow_rename
 	atf_add_test_case silent_header
 	atf_add_test_case verbose_header