git: 927f8d8bbbed - main - Handle NULL return from localtime(3) in ls(1) and find(1)

From: Kirk McKusick <mckusick_at_FreeBSD.org>
Date: Fri, 09 Sep 2022 21:30:59 UTC
The branch main has been updated by mckusick:

URL: https://cgit.FreeBSD.org/src/commit/?id=927f8d8bbbed70f6c88d05c19b5b366f8e7532c9

commit 927f8d8bbbed70f6c88d05c19b5b366f8e7532c9
Author:     Kirk McKusick <mckusick@FreeBSD.org>
AuthorDate: 2022-09-09 21:29:53 +0000
Commit:     Kirk McKusick <mckusick@FreeBSD.org>
CommitDate: 2022-09-09 21:30:42 +0000

    Handle NULL return from localtime(3) in ls(1) and find(1)
    
    The ls(1) (with -l option) and find(1) (with -ls option) utilties
    segment fault when operating on files with very large modification
    times. A recent disk corruption set a spurious bit in the mtime
    field of one of my files to 0x8000000630b0167 (576460753965089127)
    which is in year 18,266,940,962. I discovered the problem when
    running fsck_ffs(8) which uses ctime(3) to convert it to a readable
    format. Ctime cannot fit the year into its four character field, so
    returns ??? ??? ?? ??:??:?? ???? (typically Thu Nov 24 18:22:48 2021).
    
    With the filesystem mounted, I used `ls -l' to see how it would
    report the modification time and it segment faulted. The find(1)
    program also segment faulted (see script below). Both these utilities
    call the localtime(3) function to decode the modification time.
    Localtime(3) returns a pointer to a struct tm (which breaks things
    out into its component pieces: year, month, day, hour, minute,
    second). The ls(1) and find(1) utilities then print out the date
    based on the appropriate fields in the returned tm structure.
    
    Although not documented in the localtime(3) manual page, localtime(3)
    returns a NULL pointer if the passed in time translates to a year
    that will not fit in an "int" (which if "int" is 32-bits cannot
    hold the year 18,266,940,962). Since ls(1) and find(1) do not check
    for a NULL struct tm * return from localtime(3), they segment fault
    when they try to dereference it.
    
    When localtime(3) returns NULL, the attached patches produce a date
    string of "bad date val". This string is chosen because it has the
    same number of characters (12) and white spaces (2) as the usual
    date string, for example "Sep 3 22:06" or "May 15 2017".
    
    The most recent ANSI standard for localtime(3) does say that localtime(3)
    can return NULL (see https://pubs.opengroup.org/onlinepubs/9699919799/
    and enter localtime in the search box). Our localtime(3) man page should
    be updated to indicate that NULL is a possible return. More importantly,
    there are over 100 uses of localtime(3) in the FreeBSD source tree (see
    Differential Revision D36474 for the list). Most do not check for a NULL
    return from localtime(3).
    
    Reported by:  Peter Holm
    Reviewed by:  kib, Chuck Silvers, Warner Losh
    MFC after:    2 weeks
    Sponsored by: The FreeBSD Foundation
    Differential Revision: https://reviews.freebsd.org/D36474
---
 bin/ls/print.c    | 11 ++++++-----
 usr.bin/find/ls.c |  6 +++++-
 2 files changed, 11 insertions(+), 6 deletions(-)

diff --git a/bin/ls/print.c b/bin/ls/print.c
index bbe5c6f8a6f6..5e8a54ca0620 100644
--- a/bin/ls/print.c
+++ b/bin/ls/print.c
@@ -432,18 +432,17 @@ printdev(size_t width, dev_t dev)
 	(void)printf("%#*jx ", (u_int)width, (uintmax_t)dev);
 }
 
-static size_t
+static void
 ls_strftime(char *str, size_t len, const char *fmt, const struct tm *tm)
 {
 	char *posb, nfmt[BUFSIZ];
 	const char *format = fmt;
-	size_t ret;
 
 	if ((posb = strstr(fmt, "%b")) != NULL) {
 		if (month_max_size == 0) {
 			compute_abbreviated_month_size();
 		}
-		if (month_max_size > 0) {
+		if (month_max_size > 0 && tm != NULL) {
 			snprintf(nfmt, sizeof(nfmt),  "%.*s%s%*s%s",
 			    (int)(posb - fmt), fmt,
 			    get_abmon(tm->tm_mon),
@@ -453,8 +452,10 @@ ls_strftime(char *str, size_t len, const char *fmt, const struct tm *tm)
 			format = nfmt;
 		}
 	}
-	ret = strftime(str, len, format, tm);
-	return (ret);
+	if (tm != NULL)
+		strftime(str, len, format, tm);
+	else
+		strlcpy(str, "bad date val", len);
 }
 
 static void
diff --git a/usr.bin/find/ls.c b/usr.bin/find/ls.c
index 8c4c16ed3461..8d7406216256 100644
--- a/usr.bin/find/ls.c
+++ b/usr.bin/find/ls.c
@@ -88,6 +88,7 @@ printtime(time_t ftime)
 	static time_t lnow;
 	const char *format;
 	static int d_first = -1;
+	struct tm *tm;
 
 #ifdef D_MD_ORDER
 	if (d_first < 0)
@@ -103,7 +104,10 @@ printtime(time_t ftime)
 	else
 		/* mmm dd  yyyy || dd mmm  yyyy */
 		format = d_first ? "%e %b  %Y " : "%b %e  %Y ";
-	strftime(longstring, sizeof(longstring), format, localtime(&ftime));
+	if ((tm = localtime(&ftime)) != NULL)
+		strftime(longstring, sizeof(longstring), format, tm);
+	else
+		strlcpy(longstring, "bad date val ", sizeof(longstring));
 	fputs(longstring, stdout);
 }