Subject: Re: [devuan-dev] bug#860: util-linux: more when waiting for keyboard response has 100 percent cpu utilisation

From: Mark Hindley <[email-masked]>
Date: Sat, 28 Jun 2025 10:44:47 +0100

Control: tags -1 = upstream

Arthur,

I have now managed to reproduce this with util-linux 2.41-4devuan1, but not
2.38.1-5+deb12u3devuan1.

The more.c diff does show some poll() changes. This probably needs to go
upstream.

Mark
diff --git a/text-utils/more.c b/text-utils/more.c
index e3e2a4336..a035591af 100644
--- a/text-utils/more.c
+++ b/text-utils/more.c
@@ -57,7 +57,6 @@
 #include <sys/ioctl.h>
 #include <sys/stat.h>
 #include <sys/file.h>
-#include <sys/ttydefaults.h>
 #include <sys/wait.h>
 #include <regex.h>
 #include <assert.h>
@@ -66,6 +65,10 @@
 #include <paths.h>
 #include <getopt.h>
 
+#ifdef HAVE_SYS_TTYDEFAULTS_H
+# include <sys/ttydefaults.h>
+#endif
+
 #if defined(HAVE_NCURSESW_TERM_H)
 # include <ncursesw/term.h>
 #elif defined(HAVE_NCURSES_TERM_H)
@@ -83,7 +86,6 @@
 #include "xalloc.h"
 #include "widechar.h"
 #include "closestream.h"
-#include "rpmatch.h"
 #include "env.h"
 
 #ifdef TEST_PROGRAM
@@ -199,37 +201,38 @@ struct more_control {
 	magic_t magic;			/* libmagic database entries */
 #endif
 	unsigned int
-		bad_stdout:1,		/* true if overwriting does not turn off standout */
-		catch_suspend:1,	/* we should catch the SIGTSTP signal */
-		clear_line_ends:1,	/* do not scroll, paint each screen from the top */
-		clear_first:1,		/* is first character in file \f */
-		dumb_tty:1,		/* is terminal type known */
-		eat_newline:1,		/* is newline ignored after 80 cols */
-		erase_input_ok:1,	/* is erase input supported */
-		erase_previous_ok:1,	/* is erase previous supported */
-		exit_on_eof:1,		/* exit on EOF */
-		first_file:1,		/* is the input file the first in list */
-		fold_long_lines:1,	/* fold long lines */
-		hard_tabs:1,		/* print spaces instead of '\t' */
-		hard_tty:1,		/* is this hard copy terminal (a printer or such) */
-		leading_colon:1,	/* key command has leading ':' character */
-		is_eof:1,               /* EOF detected */
-		is_paused:1,		/* is output paused */
-		no_quit_dialog:1,	/* suppress quit dialog */
-		no_scroll:1,		/* do not scroll, clear the screen and then display text */
-		no_tty_in:1,		/* is input in interactive mode */
-		no_tty_out:1,		/* is output in interactive mode */
-		no_tty_err:1,           /* is stderr terminal */
-		print_banner:1,		/* print file name banner */
-		reading_num:1,		/* are we reading leading_number */
-		report_errors:1,	/* is an error reported */
-		search_at_start:1,	/* search pattern defined at start up */
-		search_called:1,	/* previous more command was a search */
-		squeeze_spaces:1,	/* suppress white space */
-		stdout_glitch:1,	/* terminal has standout mode glitch */
-		stop_after_formfeed:1,	/* stop after form feeds */
-		suppress_bell:1,	/* suppress bell */
-		wrap_margin:1;		/* set if automargins */
+		ignore_stdin,  		/* POLLHUP; peer closed pipe */
+		bad_stdout,  		/* true if overwriting does not turn off standout */
+		catch_suspend,  	/* we should catch the SIGTSTP signal */
+		clear_line_ends,  	/* do not scroll, paint each screen from the top */
+		clear_first,  		/* is first character in file \f */
+		dumb_tty,  		/* is terminal type known */
+		eat_newline,  		/* is newline ignored after 80 cols */
+		erase_input_ok,  	/* is erase input supported */
+		erase_previous_ok,  	/* is erase previous supported */
+		exit_on_eof,  		/* exit on EOF */
+		first_file,  		/* is the input file the first in list */
+		fold_long_lines,  	/* fold long lines */
+		hard_tabs,  		/* print spaces instead of '\t' */
+		hard_tty,  		/* is this hard copy terminal (a printer or such) */
+		leading_colon,  	/* key command has leading ':' character */
+		is_eof,                 /* EOF detected */
+		is_paused,  		/* is output paused */
+		no_quit_dialog,  	/* suppress quit dialog */
+		no_scroll,  		/* do not scroll, clear the screen and then display text */
+		no_tty_in,  		/* is input in interactive mode */
+		no_tty_out,  		/* is output in interactive mode */
+		no_tty_err,             /* is stderr terminal */
+		print_banner,  		/* print file name banner */
+		reading_num,  		/* are we reading leading_number */
+		report_errors,  	/* is an error reported */
+		search_at_start,  	/* search pattern defined at start up */
+		search_called,  	/* previous more command was a search */
+		squeeze_spaces,  	/* suppress white space */
+		stdout_glitch,  	/* terminal has standout mode glitch */
+		stop_after_formfeed,  	/* stop after form feeds */
+		suppress_bell,  	/* suppress bell */
+		wrap_margin;		/* set if automargins */
 };
 
 static void __attribute__((__noreturn__)) usage(void)
@@ -238,7 +241,7 @@ static void __attribute__((__noreturn__)) usage(void)
 	printf(_(" %s [options] <file>...\n"), program_invocation_short_name);
 
 	printf("%s", USAGE_SEPARATOR);
-	printf("%s\n", _("A file perusal filter for CRT viewing."));
+	printf("%s\n", _("Display the contents of a file in a terminal."));
 
 	printf("%s", USAGE_OPTIONS);
 	printf("%s\n", _(" -d, --silent          display help instead of ringing bell"));
@@ -299,7 +302,7 @@ static void argscan(struct more_control *ctl, int as_argc, char **as_argv)
 			}
 		}
 		if (move) {
-			as_argc = remote_entry(as_argv, opt, as_argc);
+			as_argc = ul_remove_entry(as_argv, opt, as_argc);
 			opt--;
 		}
 	}
@@ -354,14 +357,14 @@ static void env_argscan(struct more_control *ctl, const char *s)
 	char *str = xstrdup(s);
 	char *key = NULL, *tok;
 
-	env_argv = xmalloc(sizeof(char *) * size);
+	env_argv = xreallocarray(NULL, size, sizeof(char *));
 	env_argv[0] = _("MORE environment variable");	/* program name */
 	for (tok = strtok_r(str, delim, &key); tok; tok = strtok_r(NULL, delim, &key)) {
-		env_argv[env_argc++] = tok;
-		if (size < env_argc) {
+		if (size == env_argc) {
 			size *= 2;
-			env_argv = xrealloc(env_argv, sizeof(char *) * size);
+			env_argv = xreallocarray(env_argv, size, sizeof(char *));
 		}
+		env_argv[env_argc++] = tok;
 	}
 
 	argscan(ctl, env_argc, env_argv);
@@ -379,7 +382,7 @@ static void more_fseek(struct more_control *ctl, off_t pos)
 
 static int more_getc(struct more_control *ctl)
 {
-	int ret = getc(ctl->current_file);
+	int ret = fgetc(ctl->current_file);
 	ctl->file_position = ftello(ctl->current_file);
 	return ret;
 }
@@ -626,9 +629,6 @@ static int get_line(struct more_control *ctl, int *length)
 			*p++ = 'L';
 			column += 2;
 			ctl->is_paused = 1;
-		} else if (c == EOF) {
-			*length = p - ctl->line_buf;
-			return column;
 		} else {
 #ifdef HAVE_WIDECHAR
 			if (ctl->fold_long_lines && MB_CUR_MAX > 1) {
@@ -1254,8 +1254,7 @@ static void __attribute__((__format__ (__printf__, 3, 4)))
 		}
 		va_end(argp);
 
-		args = alloca(sizeof(char *) * (argcount + 1));
-		args[argcount] = NULL;
+		args = xcalloc(argcount + 1, sizeof(char *));
 
 		va_start(argp, cmd);
 		arg = va_arg(argp, char *);
@@ -1353,50 +1352,98 @@ static void read_line(struct more_control *ctl)
 	*p = '\0';
 }
 
-static int more_poll(struct more_control *ctl, int timeout)
+/* returns: 0 timeout or nothing; <0 error, >0 success */
+static int more_poll(struct more_control *ctl, int timeout, int *stderr_active)
 {
-	struct pollfd pfd[2];
+	enum {
+		POLLFD_SIGNAL = 0,
+		POLLFD_STDIN,
+		POLLFD_STDERR,
+	};
+	struct pollfd pfd[] = {
+		[POLLFD_SIGNAL] = { .fd = ctl->sigfd,    .events = POLLIN | POLLERR | POLLHUP },
+		[POLLFD_STDIN]  = { .fd = STDIN_FILENO,  .events = POLLIN | POLLERR | POLLHUP },
+		[POLLFD_STDERR] = { .fd = STDERR_FILENO, .events = POLLIN | POLLERR | POLLHUP }
+	};
+	int has_data = 0;
 
-	pfd[0].fd = ctl->sigfd;
-	pfd[0].events = POLLIN | POLLERR | POLLHUP;
-	pfd[1].fd = STDIN_FILENO;
-	pfd[1].events = POLLIN;
+	if (stderr_active)
+		*stderr_active = 0;
 
-	if (poll(pfd, 2, timeout) < 0) {
-		if (errno == EAGAIN)
-			return 1;
-		more_error(ctl, _("poll failed"));
-		return 1;
-	}
-	if (pfd[0].revents != 0) {
-		struct signalfd_siginfo info;
-		ssize_t sz;
-
-		sz = read(pfd[0].fd, &info, sizeof(info));
-		assert(sz == sizeof(info));
-		switch (info.ssi_signo) {
-		case SIGINT:
-			more_exit(ctl);
-			break;
-		case SIGQUIT:
-			sigquit_handler(ctl);
-			break;
-		case SIGTSTP:
-			sigtstp_handler(ctl);
-			break;
-		case SIGCONT:
-			sigcont_handler(ctl);
-			break;
-		case SIGWINCH:
-			sigwinch_handler(ctl);
-			break;
-		default:
-			abort();
+	while (!has_data) {
+		int rc;
+
+		if (ctl->ignore_stdin)
+			pfd[POLLFD_STDIN].fd = -1;	/* probably closed, ignore */
+
+		rc = poll(pfd, ARRAY_SIZE(pfd), timeout);
+
+		/* error */
+		if (rc < 0) {
+			if (errno == EAGAIN)
+				continue;
+
+			more_error(ctl, _("poll failed"));
+			return rc;
+		}
+
+		/* timeout */
+		if (rc == 0)
+			return 0;
+
+		/* event on signal FD */
+		if (pfd[POLLFD_SIGNAL].revents) {
+			struct signalfd_siginfo info;
+			ssize_t sz;
+
+			sz = read(pfd[POLLFD_SIGNAL].fd, &info, sizeof(info));
+			assert(sz == sizeof(info));
+			switch (info.ssi_signo) {
+			case SIGINT:
+				more_exit(ctl);
+				break;
+			case SIGQUIT:
+				sigquit_handler(ctl);
+				break;
+			case SIGTSTP:
+				sigtstp_handler(ctl);
+				break;
+			case SIGCONT:
+				sigcont_handler(ctl);
+				break;
+			case SIGWINCH:
+				sigwinch_handler(ctl);
+				break;
+			default:
+				abort();
+			}
+		}
+
+		/* event on stdin */
+		if (pfd[POLLFD_STDIN].revents) {
+			/* Check for POLLERR and POLLHUP in stdin revents */
+			if ((pfd[POLLFD_STDIN].revents & POLLERR) &&
+			    (pfd[POLLFD_STDIN].revents & POLLHUP))
+				more_exit(ctl);
+
+			/* poll() return POLLHUP event after pipe close() and POLLNVAL
+			 * means that fd is already closed. */
+			if ((pfd[POLLFD_STDIN].revents & POLLHUP) ||
+			    (pfd[POLLFD_STDIN].revents & POLLNVAL))
+				ctl->ignore_stdin = 1;
+			else
+				has_data++;
+		}
+
+		/* event on stderr (we reads user commands from stderr!) */
+		if (pfd[POLLFD_STDERR].revents) {
+			has_data++;
+			if (stderr_active)
+				*stderr_active = 1;
 		}
 	}
-	if (pfd[1].revents == 0)
-		return 1;
-	return 0;
+
+	return has_data;
 }
 
 /* Search for nth occurrence of regular expression contained in buf in
@@ -1464,7 +1511,7 @@ static void search(struct more_control *ctl, char buf[], int n)
 			}
 			break;
 		}
-		more_poll(ctl, 1);
+		more_poll(ctl, 0, NULL);
 	}
 	/* Move ctrl+c signal handling back to more_key_command(). */
 	signal(SIGINT, SIG_DFL);
@@ -1618,7 +1665,7 @@ static int skip_forwards(struct more_control *ctl, int nlines, cc_t comchar)
 static int more_key_command(struct more_control *ctl, char *filename)
 {
 	int retval = 0;
-	int done = 0, search_again = 0;
+	int done = 0, search_again = 0, stderr_active = 0;
 	char cmdbuf[INIT_BUF];
 	struct number_command cmd;
 
@@ -1628,13 +1675,18 @@ static int more_key_command(struct more_control *ctl, char *filename)
 		ctl->report_errors = 0;
 	ctl->search_called = 0;
 	for (;;) {
-		if (more_poll(ctl, -1) != 0)
+		if (more_poll(ctl, -1, &stderr_active) <= 0)
+			continue;
+		if (stderr_active == 0)
 			continue;
 		cmd = read_command(ctl);
 		if (cmd.key == more_kc_unknown_command)
 			continue;
 		if (cmd.key == more_kc_repeat_previous)
 			cmd = ctl->previous_command;
+		else
+			ctl->previous_command = cmd;
+
 		switch (cmd.key) {
 		case more_kc_backwards:
 			if (ctl->no_tty_in) {
@@ -1800,7 +1852,6 @@ static int more_key_command(struct more_control *ctl, char *filename)
 			fflush(NULL);
 			break;
 		}
-		ctl->previous_command = cmd;
 		if (done) {
 			cmd.key = more_kc_unknown_command;
 			break;
@@ -2046,14 +2097,16 @@ int main(int argc, char **argv)
 	bindtextdomain(PACKAGE, LOCALEDIR);
 	textdomain(PACKAGE);
 	close_stdout_atexit();
-	setlocale(LC_ALL, "");
 
 	/* Auto set no scroll on when binary is called page */
 	if (!(strcmp(program_invocation_short_name, "page")))
 		ctl.no_scroll++;
 
+	ctl.exit_on_eof = getenv("POSIXLY_CORRECT") ? 0 : 1;
+
 	if ((s = getenv("MORE")) != NULL)
 		env_argscan(&ctl, s);
+
 	argscan(&ctl, argc, argv);
 
 	/* clear any inherited settings */