git: 92e8a76b8786 - main - pw: use _PWDASH pseudo-fd for "-" in pw_checkfd

From: Baptiste Daroussin <bapt_at_FreeBSD.org>
Date: Fri, 04 Sep 2026 10:43:38 UTC
The branch main has been updated by bapt:

URL: https://cgit.FreeBSD.org/src/commit/?id=92e8a76b87862aa2ee7e6c3f637034e60ffbc2e6

commit 92e8a76b87862aa2ee7e6c3f637034e60ffbc2e6
Author:     Baptiste Daroussin <bapt@FreeBSD.org>
AuthorDate: 2026-09-04 10:17:02 +0000
Commit:     Baptiste Daroussin <bapt@FreeBSD.org>
CommitDate: 2026-09-04 10:42:29 +0000

    pw: use _PWDASH pseudo-fd for "-" in pw_checkfd
    
    pw_checkfd() returned the character "-" (45) for the "-" argument,
    which was ambiguous with a real file descriptor.
    
    MFC After:      1 week
---
 usr.sbin/pw/pw.h                     |   1 +
 usr.sbin/pw/pw_group.c               |   6 +-
 usr.sbin/pw/pw_user.c                |   6 +-
 usr.sbin/pw/pw_utils.c               |   2 +-
 usr.sbin/pw/tests/Makefile           |  10 +++-
 usr.sbin/pw/tests/pw_unit_test.c     | 107 +++++++++++++++++++++++++++++++++++
 usr.sbin/pw/tests/pw_useradd_test.sh |  35 ++++++++++++
 7 files changed, 159 insertions(+), 8 deletions(-)

diff --git a/usr.sbin/pw/pw.h b/usr.sbin/pw/pw.h
index 6be667f166fb..fcb5214a10e5 100644
--- a/usr.sbin/pw/pw.h
+++ b/usr.sbin/pw/pw.h
@@ -63,6 +63,7 @@ enum _which
 
 #define _DEF_DIRMODE	(S_IRWXU | S_IRWXG | S_IRWXO)
 #define _PW_CONF	"pw.conf"
+#define _PWDASH		(-2) /* pseudo-fd returned by pw_checkfd for '-' */
 #define _UC_MAXLINE	1024
 #define _UC_MAXSHELLS	32
 
diff --git a/usr.sbin/pw/pw_group.c b/usr.sbin/pw/pw_group.c
index 2840e972af59..2df22e014b67 100644
--- a/usr.sbin/pw/pw_group.c
+++ b/usr.sbin/pw/pw_group.c
@@ -56,7 +56,7 @@ grp_set_passwd(struct group *grp, bool update, int fd, bool precrypted)
 	if (fd == -1)
 		return;
 
-	if (fd == '-') {
+	if (fd == _PWDASH) {
 		grp->gr_passwd = "*";	/* No access */
 		return;
 	}
@@ -538,7 +538,7 @@ pw_group_add(int argc, char **argv, char *arg1)
 				    "exclusive options");
 			fd = pw_checkfd(optarg);
 			precrypted = true;
-			if (fd == '-')
+			if (fd == _PWDASH)
 				errx(EX_USAGE, "-H expects a file descriptor");
 			break;
 		case 'h':
@@ -636,7 +636,7 @@ pw_group_mod(int argc, char **argv, char *arg1)
 				    "exclusive options");
 			fd = pw_checkfd(optarg);
 			precrypted = true;
-			if (fd == '-')
+			if (fd == _PWDASH)
 				errx(EX_USAGE, "-H expects a file descriptor");
 			break;
 		case 'h':
diff --git a/usr.sbin/pw/pw_user.c b/usr.sbin/pw/pw_user.c
index abb8a09ce468..e2b84b2a83d3 100644
--- a/usr.sbin/pw/pw_user.c
+++ b/usr.sbin/pw/pw_user.c
@@ -171,7 +171,7 @@ pw_set_passwd(struct passwd *pwd, int fd, bool precrypted, bool update)
 	char		line[_PASSWORD_LEN+1];
 	char		*p;
 
-	if (fd == '-') {
+	if (fd == _PWDASH) {
 		if (!pwd->pw_passwd || *pwd->pw_passwd != '*') {
 			pwd->pw_passwd = "*";	/* No access */
 			return (1);
@@ -1306,7 +1306,7 @@ pw_user_add(int argc, char **argv, char *arg1)
 				    "exclusive options");
 			fd = pw_checkfd(optarg);
 			precrypted = true;
-			if (fd == '-')
+			if (fd == _PWDASH)
 				errx(EX_USAGE, "-H expects a file descriptor");
 			break;
 		case 'h':
@@ -1629,7 +1629,7 @@ pw_user_mod(int argc, char **argv, char *arg1)
 				    "exclusive options");
 			fd = pw_checkfd(optarg);
 			precrypted = true;
-			if (fd == '-')
+			if (fd == _PWDASH)
 				errx(EX_USAGE, "-H expects a file descriptor");
 			break;
 		case 'h':
diff --git a/usr.sbin/pw/pw_utils.c b/usr.sbin/pw/pw_utils.c
index 87dd421ca8a3..363cceafec39 100644
--- a/usr.sbin/pw/pw_utils.c
+++ b/usr.sbin/pw/pw_utils.c
@@ -42,7 +42,7 @@ pw_checkfd(char *nptr)
 	int fd = -1;
 
 	if (strcmp(nptr, "-") == 0)
-		return '-';
+		return (_PWDASH);
 	fd = strtonum(nptr, 0, INT_MAX, &errstr);
 	if (errstr != NULL)
 		errx(EX_USAGE, "Bad file descriptor '%s': %s",
diff --git a/usr.sbin/pw/tests/Makefile b/usr.sbin/pw/tests/Makefile
index 910a563d680d..b92897ab9208 100644
--- a/usr.sbin/pw/tests/Makefile
+++ b/usr.sbin/pw/tests/Makefile
@@ -2,8 +2,16 @@ PACKAGE=	tests
 
 BINDIR=		${TESTSDIR}
 
+.PATH:		${SRCTOP}/usr.sbin/pw
+
 PROGS+=		crypt
-LIBADD+=	crypt
+LIBADD.crypt+=	crypt
+
+ATF_TESTS_C=	pw_unit_test
+SRCS.pw_unit_test=	pw_unit_test.c \
+			pw_utils.c strtounum.c
+LIBADD.pw_unit_test=	util
+CFLAGS.pw_unit_test=	-I${SRCTOP}/usr.sbin/pw
 
 ATF_TESTS_SH=	pw_etcdir_test \
 		pw_lock_test \
diff --git a/usr.sbin/pw/tests/pw_unit_test.c b/usr.sbin/pw/tests/pw_unit_test.c
new file mode 100644
index 000000000000..f335f8e100a5
--- /dev/null
+++ b/usr.sbin/pw/tests/pw_unit_test.c
@@ -0,0 +1,107 @@
+/*
+ * Copyright (c) 2026 Baptiste Daroussin <bapt@FreeBSD.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <sys/types.h>
+
+#include <atf-c.h>
+#include <limits.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sysexits.h>
+
+#include "pw.h"
+#include "pwupd.h"
+
+struct pwconf conf;
+
+/* stub */
+struct userconf *
+read_userconfig(char const *file __unused)
+{
+
+	return (NULL);
+}
+
+ATF_TC_WITHOUT_HEAD(checkfd_dash);
+ATF_TC_BODY(checkfd_dash, tc)
+{
+	char dash[] = "-";
+
+	ATF_CHECK_EQ(pw_checkfd(dash), _PWDASH);
+}
+
+ATF_TC_WITHOUT_HEAD(checkfd_zero);
+ATF_TC_BODY(checkfd_zero, tc)
+{
+	char zero[] = "0";
+
+	ATF_CHECK_EQ(pw_checkfd(zero), 0);
+}
+
+ATF_TC_WITHOUT_HEAD(checkfd_valid);
+ATF_TC_BODY(checkfd_valid, tc)
+{
+	char five[] = "5";
+
+	ATF_CHECK_EQ(pw_checkfd(five), 5);
+}
+
+ATF_TC_WITHOUT_HEAD(checkfd_max);
+ATF_TC_BODY(checkfd_max, tc)
+{
+	char max[] = "2147483647";
+
+	ATF_CHECK_EQ(pw_checkfd(max), INT_MAX);
+}
+
+static void
+checkfd_invalid(const char *value, const char *errstr)
+{
+	pid_t pid;
+	char buf[32];
+	char experr[128];
+
+	strlcpy(buf, value, sizeof(buf));
+	snprintf(experr, sizeof(experr),
+	    "pw_unit_test: Bad file descriptor '%s': %s\n", value, errstr);
+	pid = atf_utils_fork();
+	if (pid == 0) {
+		pw_checkfd(buf);
+		exit(1);
+	}
+	atf_utils_wait(pid, EX_USAGE, "", experr);
+}
+
+ATF_TC_WITHOUT_HEAD(checkfd_invalid);
+ATF_TC_BODY(checkfd_invalid, tc)
+{
+	checkfd_invalid("abc", "invalid");
+}
+
+ATF_TC_WITHOUT_HEAD(checkfd_negative);
+ATF_TC_BODY(checkfd_negative, tc)
+{
+	checkfd_invalid("-1", "too small");
+}
+
+ATF_TC_WITHOUT_HEAD(checkfd_overflow);
+ATF_TC_BODY(checkfd_overflow, tc)
+{
+	checkfd_invalid("999999999999", "too large");
+}
+
+ATF_TP_ADD_TCS(tp)
+{
+	ATF_TP_ADD_TC(tp, checkfd_dash);
+	ATF_TP_ADD_TC(tp, checkfd_zero);
+	ATF_TP_ADD_TC(tp, checkfd_valid);
+	ATF_TP_ADD_TC(tp, checkfd_max);
+	ATF_TP_ADD_TC(tp, checkfd_invalid);
+	ATF_TP_ADD_TC(tp, checkfd_negative);
+	ATF_TP_ADD_TC(tp, checkfd_overflow);
+
+	return (atf_no_error());
+}
diff --git a/usr.sbin/pw/tests/pw_useradd_test.sh b/usr.sbin/pw/tests/pw_useradd_test.sh
index 4b0778759203..82c8d8b8524f 100755
--- a/usr.sbin/pw/tests/pw_useradd_test.sh
+++ b/usr.sbin/pw/tests/pw_useradd_test.sh
@@ -528,6 +528,38 @@ user_add_already_in_group_body()
 		grep testuser ${HOME}/group
 }
 
+atf_test_case user_add_fd_dash
+user_add_fd_dash_body()
+{
+	populate_etc_skel
+
+	atf_check -s exit:0 ${PW} useradd fdtest -h -
+	atf_check -s exit:0 -o match:"^fdtest:\*:" \
+		grep "^fdtest:" ${HOME}/master.passwd
+}
+
+atf_test_case user_add_fd_numeric
+user_add_fd_numeric_body()
+{
+	populate_etc_skel
+
+	echo "testpass123" > ${HOME}/pwfile
+	fd=$(echo ${HOME}/pwfile)
+	echo "stdinpass" | atf_check -s exit:0 \
+		${PW} useradd fdstdin -h 0
+	atf_check -s exit:0 -o not-match:"^fdstdin:\*:" \
+		grep "^fdstdin:" ${HOME}/master.passwd
+}
+
+atf_test_case user_add_H_dash_rejected
+user_add_H_dash_rejected_body()
+{
+	populate_etc_skel
+
+	atf_check -s exit:64 -e inline:"pw: -H expects a file descriptor\n" \
+		${PW} useradd htest -H -
+}
+
 atf_init_test_cases() {
 	atf_add_test_case user_add
 	atf_add_test_case user_add_noupdate
@@ -572,4 +604,7 @@ atf_init_test_cases() {
 	atf_add_test_case user_add_conf_defaultpasswd
 	atf_add_test_case user_add_existing_login_group
 	atf_add_test_case user_add_already_in_group
+	atf_add_test_case user_add_fd_dash
+	atf_add_test_case user_add_fd_numeric
+	atf_add_test_case user_add_H_dash_rejected
 }