From 849655be491b2fe3023bc87f6a650c2917e0c198 Mon Sep 17 00:00:00 2001
From: Jiajie Chen <c@jia.je>
Date: Wed, 18 Jan 2023 21:36:12 +0800
Subject: [PATCH] [linux] Fix handling of empty command name, closing #246

Add test from #246, where lsof returns stale command name when the
command name is empty.

If getting command name failed, return NULL instead of empty string.
---
 dialects/linux/dproc.c                | 88 +++++++++++++++------------
 print.c                               | 11 ++--
 tests/case-22-empty-process-name.bash | 29 +++++++++
 3 files changed, 84 insertions(+), 44 deletions(-)
 create mode 100755 tests/case-22-empty-process-name.bash

diff --git a/dialects/linux/dproc.c b/dialects/linux/dproc.c
index e2dce74..922db6e 100644
--- a/dialects/linux/dproc.c
+++ b/dialects/linux/dproc.c
@@ -339,12 +339,12 @@ gather_proc_info()
 		continue;
 	    uid = (UID_ARG)sb.st_uid;
 	    ht = pidts = 0;
-	/*
-	 * Get the PID's command name.
-	 */
+	    /*
+	     * Get the PID's command name.
+	     */
 	    (void) make_proc_path(pidpath, n, &path, &pathl, "stat");
 	    if ((prv = read_id_stat(path, pid, &cmd, &ppid, &pgid)) < 0)
-		cmd = "(unknown)";
+		cmd = NULL; /* NULL means failure to get command name */
 
 #if	defined(HASTASKS)
 	/*
@@ -356,11 +356,16 @@ gather_proc_info()
 	 * options work properly.
 	 */
 	    else if (!IgnTasks && (Selflags & SELTASK)) {
+		/*
+		 * Copy cmd before next call to read_id_stat due to static
+		 * variables
+		 */
 		if (cmd) {
 		    strncpy(cmdbuf, cmd, sizeof(cmdbuf) - 1);
+		    cmdbuf[sizeof(cmdbuf) - 1] = '\0';
+		    cmd = cmdbuf;
 		}
-		cmdbuf[sizeof(cmdbuf) - 1] = '\0';
-		cmd = cmdbuf;
+
 		(void) make_proc_path(pidpath, n, &taskpath, &taskpathl,
 				      "task");
 		tx = n + 4;
@@ -1436,10 +1441,9 @@ compare_mntns(pid)
  */
 
 static void
-process_proc_map(p, s, ss)
-	char *p;			/* path to process maps file */
-	struct stat *s;			/* executing text file state buffer */
-	int ss;				/* *s status -- i.e., SB_* values */
+process_proc_map(char *p,			/* path to process maps file */
+	struct stat *s,			/* executing text file state buffer */
+	int ss)				/* *s status -- i.e., SB_* values */
 {
 	char buf[MAXPATHLEN + 1], *ep, fmtbuf[32], **fp, nmabuf[MAXPATHLEN + 1];
 	dev_t dev;
@@ -1705,14 +1709,12 @@ process_proc_map(p, s, ss)
  *          1 == ID is a zombie
  *	    2 == ID is a thread
  */
-
 static int
-read_id_stat(p, id, cmd, ppid, pgid)
-	char *p;			/* path to status file */
-	int id;				/* ID: PID or LWP */
-	char **cmd;			/* malloc'd command name */
-	int *ppid;			/* returned parent PID for PID type */
-	int *pgid;			/* returned process group ID for PID
+read_id_stat(char *p,			/* path to status file */
+	int id,				/* ID: PID or LWP */
+	char **cmd,			/* malloc'd command name */
+	int *ppid,			/* returned parent PID for PID type */
+	int *pgid)			/* returned process group ID for PID
 					 * type */
 {
 	char buf[MAXPATHLEN], *cp, *cp1, **fp;
@@ -1757,14 +1759,22 @@ read_id_stat(p, id, cmd, ppid, pgid)
 	    goto read_id_stat_exit;
 	cp++;
 	pc = 1;			/* start the parenthesis balance count at 1 */
-/*
- * Enter the command characters safely.  Supply them from the initial read
- * of the stat file line, a '\n' if the initial read didn't yield a ')'
- * command closure, or by reading the rest of the command a character at
- * a time from the stat file.  Count embedded '(' characters and balance
- * them with embedded ')' characters.  The opening '(' starts the balance
- * count at one.
- */
+
+	/* empty process name to avoid leaking previous process name,
+	 * see issue #246
+	 */
+	if (cbf) {
+	    cbf[0] = '\0';
+	}
+
+	/*
+	* Enter the command characters safely.  Supply them from the initial read
+	* of the stat file line, a '\n' if the initial read didn't yield a ')'
+	* command closure, or by reading the rest of the command a character at
+	* a time from the stat file.  Count embedded '(' characters and balance
+	* them with embedded ')' characters.  The opening '(' starts the balance
+	* count at one.
+	*/
 	for (cx = es = 0;;) {
 	    if (!es)
 		ch = *cp++;
@@ -1776,10 +1786,10 @@ read_id_stat(p, id, cmd, ppid, pgid)
 		pc++;
 	    if (ch == ')') {
 
-	    /*
-	     * Balance parentheses when a closure is encountered.  When
-	     * they are balanced, this is the end of the command.
-	     */
+		/*
+		* Balance parentheses when a closure is encountered.  When
+		* they are balanced, this is the end of the command.
+		*/
 		pc--;
 		if (!pc)
 		    break;
@@ -1793,12 +1803,12 @@ read_id_stat(p, id, cmd, ppid, pgid)
 		es = 1;		/* Switch to fgetc() when a '\0' appears. */
 	}
 	*cmd = cbf;
-/*
- * Read the remainder of the stat line if it was necessary to read command
- * characters individually from the stat file.
- *
- * Separate the reminder into fields.
- */
+	/*
+	* Read the remainder of the stat line if it was necessary to read command
+	* characters individually from the stat file.
+	*
+	* Separate the reminder into fields.
+	*/
 	if (es)
 	    cp = fgets(buf, sizeof(buf), fs);
 	(void) fclose(fs);
@@ -1841,13 +1851,11 @@ read_id_stat(p, id, cmd, ppid, pgid)
  *	 This function should be used only when links have been successfully
  *	 resolved in the /proc path by getlinksrc().
  */
-
 static int
-statEx(p, s, ss)
-	char *p;			/* file path */
-	struct stat *s;			/* stat() result -- NULL if none
+statEx(char *p,			/* file path */
+	struct stat *s,			/* stat() result -- NULL if none
 					 * wanted */
-	int *ss;			/* stat() status --  SB_* values */
+	int *ss)			/* stat() status --  SB_* values */
 {
 	static size_t ca = 0;
 	static char *cb = NULL;
diff --git a/print.c b/print.c
index 3671a69..4b9f14c 100644
--- a/print.c
+++ b/print.c
@@ -742,10 +742,13 @@ print_file()
 	    (void) printf(" %*s %s\n", NodeColW, NODETTL, NMTTL);
 	    Hdr++;
 	}
-/*
- * Size or print the command.
- */
-	cp = (Lp->cmd && *Lp->cmd != '\0') ? Lp->cmd : "(unknown)";
+	/*
+	* Size or print the command.
+	*
+	* CAUTION: command can be empty, see issue #246,
+	* use NULL to represent failure instead of empty string
+	*/
+	cp = Lp->cmd ? Lp->cmd : "(unknown)";
 	if (!PrPass) {
 	    len = safestrlen(cp, 2);
 	    if (CmdLim && (len > CmdLim))
diff --git a/tests/case-22-empty-process-name.bash b/tests/case-22-empty-process-name.bash
new file mode 100755
index 0000000..ce4039c
--- /dev/null
+++ b/tests/case-22-empty-process-name.bash
@@ -0,0 +1,29 @@
+#!/bin/bash
+# See https://github.com/lsof-org/lsof/issues/246
+source tests/common.bash
+
+{
+    perl -e '$0 = ""; sleep 999' &
+    pid=$!
+    sleep 1
+
+    output=$($lsof -w -ad cwd -F c -p "$pid")
+    echo "lsof output:" >> $report
+    echo "$output" >> $report
+    kill $pid
+    for entry in $output
+    do
+	if [[ $entry =~ ^p[0-9]+$ ]]; then
+	    if [[ $entry != p$pid ]]; then
+	        echo "Incorrect pid, expect p$pid, got $entry" >> $report
+		exit 1
+	    fi
+	elif [[ $entry =~ c* ]]; then
+	    if [[ $entry != c ]]; then
+	        echo "Process name should be empty, expect c, got $entry" >> $report
+		exit 1
+	    fi
+	fi
+    done
+    exit 0
+} > $report 2>&1
-- 
2.27.0

