smartd.cpp: Add ability to run warning script as non-privileged user.

User could be specified with new option '-u, --warn-as-user'.

popen_as_ugid.cpp, popen_as_ugid.h: New files with popen() wrapper
function.  It allows to change uid/gid and also prevents that
unneeded file descriptors are inherited by the child process.

configure.ac, Makefile.am: Enable new functionality for all OS with
POSIX API.

smartd.8.in: Document new functionality.

git-svn-id: http://svn.code.sf.net/p/smartmontools/code/trunk@5268 4ea69e1a-61f1-4043-bf83-b5c94c648137
pull/86/merge
chrfranke 10 months ago
parent cb863a7daa
commit 3d1bc3fcc2
  1. 14
      smartmontools/ChangeLog
  2. 8
      smartmontools/Makefile.am
  3. 11
      smartmontools/configure.ac
  4. 270
      smartmontools/popen_as_ugid.cpp
  5. 39
      smartmontools/popen_as_ugid.h
  6. 20
      smartmontools/smartd.8.in
  7. 88
      smartmontools/smartd.cpp

@ -1,5 +1,19 @@
$Id$
2021-12-13 Christian Franke <franke@computer.org>
smartd.cpp: Add ability to run warning script as non-privileged user.
User could be specified with new option '-u, --warn-as-user'.
popen_as_ugid.cpp, popen_as_ugid.h: New files with popen() wrapper
function. It allows to change uid/gid and also prevents that
unneeded file descriptors are inherited by the child process.
configure.ac, Makefile.am: Enable new functionality for all OS with
POSIX API.
smartd.8.in: Document new functionality.
2021-12-12 Christian Franke <franke@computer.org>
update-smart-drivedb.in: Add usage error messages, add '-h' option.

@ -195,6 +195,14 @@ EXTRA_smartd_SOURCES = \
netbsd_nvme_ioctl.h \
megaraid.h
if OS_POSIX
smartd_SOURCES += \
popen_as_ugid.cpp \
popen_as_ugid.h
endif
if OS_WIN32_MINGW
smartd_SOURCES += \

@ -605,6 +605,7 @@ os_mailer=mail
os_hostname="'hostname' 'uname -n'"
os_dnsdomainname=
os_nisdomainname="'domainname'"
os_posix=yes
os_darwin=no
os_solaris=no
os_win32=no
@ -666,6 +667,7 @@ case "${host}" in
;;
x86_64-*-mingw*)
os_deps='os_win32.o dev_areca.o'
os_posix=no
os_win32=yes
os_win32_mingw=yes
os_win64=yes
@ -674,6 +676,7 @@ case "${host}" in
;;
*-*-mingw*)
os_deps='os_win32.o dev_areca.o'
os_posix=no
os_win32=yes
os_win32_mingw=yes
os_man_filter=Windows
@ -691,6 +694,7 @@ case "${host}" in
;;
*-*-os2-*)
os_deps='os_os2.o'
os_posix=no
;;
*)
os_deps='os_generic.o'
@ -700,6 +704,12 @@ esac
# Replace if '--with-os-deps' was specified
test -z "$with_os_deps" || os_deps="$with_os_deps"
AC_MSG_CHECKING([whether the OS provides a POSIX API])
if test "$os_posix" = "yes"; then
AC_DEFINE(HAVE_POSIX_API, 1, [Define to 1 if the OS provides a POSIX API])
fi
AC_MSG_RESULT([$os_posix])
# Check if we need adapter to old interface (dev_legacy.cpp)
os_src=`echo "${os_deps}"|sed -n 's,^\([[^ .]]*\)\.o.*$,\1.cpp,p'`
AC_MSG_CHECKING([whether ${os_src} uses new interface])
@ -744,6 +754,7 @@ fi
AC_SUBST([DRIVEDB_BRANCH])
# Enable platform-specific makefile sections
AM_CONDITIONAL(OS_POSIX, [test "$os_posix" = "yes"])
AM_CONDITIONAL(OS_DARWIN, [test "$os_darwin" = "yes"])
AM_CONDITIONAL(OS_SOLARIS, [test "$os_solaris" = "yes"])
AM_CONDITIONAL(OS_WIN32, [test "$os_win32" = "yes"])

@ -0,0 +1,270 @@
/*
* popen_as_ugid.cpp
*
* Home page of code is: https://www.smartmontools.org
*
* Copyright (C) 2021 Christian Franke
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "popen_as_ugid.h"
const char * popen_as_ugid_cvsid = "$Id$"
POPEN_AS_UGID_H_CVSID;
#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
static FILE * s_popen_file /* = 0 */;
static pid_t s_popen_pid /* = 0 */;
FILE * popen_as_ugid(const char * cmd, const char * mode, uid_t uid, gid_t gid)
{
// Only "r" supported
if (*mode != 'r') {
errno = EINVAL;
return (FILE *)0;
}
// Only one stream supported
if (s_popen_file) {
errno = EMFILE;
return (FILE *)0;
}
int pd[2] = {-1, -1};
int sd[2] = {-1, -1};
FILE * fp = 0;
pid_t pid;
errno = 0;
if (!(// Create stdout and status pipe ...
!pipe(pd) && !pipe(sd) &&
// ... connect stdout pipe to FILE ...
!!(fp = fdopen(pd[0], "r")) &&
// ... and then fork()
(pid = fork()) != (pid_t)-1) ) {
int err = (errno ? errno : ENOSYS);
if (fp) {
fclose(fp); close(pd[1]);
}
else if (pd[0] >= 0) {
close(pd[0]); close(pd[1]);
}
if (sd[0] >= 0) {
close(sd[0]); close(sd[1]);
}
errno = err;
return (FILE *)0;
}
if (!pid) { // Child
// Do not inherit any unneeded file descriptors
fclose(fp);
for (int i = 0; i < getdtablesize(); i++) {
if (i == pd[1] || i == sd[1])
continue;
close(i);
}
FILE * fc = 0;
int err = errno = 0;
if (!(// Connect stdio to /dev/null ...
open("/dev/null", O_RDWR) == 0 &&
dup(0) == 1 && dup(0) == 2 &&
// ... don't inherit pipes ...
!fcntl(pd[1], F_SETFD, FD_CLOEXEC) &&
!fcntl(sd[1], F_SETFD, FD_CLOEXEC) &&
// ... set group and user (assumes we are root) ...
(!gid || (!setgid(gid) && !setgroups(1, &gid))) &&
(!uid || !setuid(uid)) &&
// ... and then call popen() from std library
!!(fc = popen(cmd, mode)) )) {
err = (errno ? errno : ENOSYS);
}
// Send setup result to parent
if (write(sd[1], &err, sizeof(err)) != (int)sizeof(err))
err = EIO;
close(sd[1]);
if (!fc)
_exit(127);
// Send popen's FILE stream to parent's FILE
int c;
while (!err && (c = getc(fc)) != EOF) {
char cb = (char)c;
if (write(pd[1], &cb, 1) != 1)
err = EIO;
}
// Return status or re-throw signal
int status = pclose(fc);
if (WIFSIGNALED(status))
kill(getpid(), WTERMSIG(status));
_exit(WIFEXITED(status) ? WEXITSTATUS(status) : 127);
}
// Parent
close(pd[1]); close(sd[1]);
// Get setup result from child
int err = 0;
if (read(sd[0], &err, sizeof(err)) != (int)sizeof(err))
err = EIO;
close(sd[0]);
if (err) {
fclose(fp);
errno = err;
return (FILE *)0;
}
// Save for pclose_as_ugid()
s_popen_file = fp;
s_popen_pid = pid;
return fp;
}
int pclose_as_ugid(FILE * f)
{
if (f != s_popen_file) {
errno = EBADF;
return -1;
}
fclose(f);
s_popen_file = 0;
pid_t pid; int status;
do
pid = waitpid(s_popen_pid, &status, 0);
while (pid == (pid_t)-1 && errno == EINTR);
s_popen_pid = 0;
if (pid == (pid_t)-1)
return -1;
return status;
}
const char * parse_ugid(const char * s, uid_t & uid, gid_t & gid,
std::string & uname, std::string & gname )
{
// Split USER:GROUP
int len = strlen(s), n1 = -1, n2 = -1;
char un[64+1] = "", gn[64+1] = "";
if (!( sscanf(s, "%64[^ :]%n:%64[^ :]%n", un, &n1, gn, &n2) >= 1
&& (n1 == len || n2 == len) )) {
return "Syntax error";
}
// Lookup user
const struct passwd * pwd;
unsigned u = 0;
if (sscanf(un, "%u%n", &u, (n1 = -1, &n1)) == 1 && n1 == (int)strlen(un)) {
uid = (uid_t)u;
pwd = getpwuid(uid);
}
else {
pwd = getpwnam(un);
if (!pwd)
return "Unknown user name";
uid = pwd->pw_uid;
}
if (pwd)
uname = pwd->pw_name;
const struct group * grp;
if (gn[0]) {
// Lookup group
unsigned g = 0;
if (sscanf(gn, "%u%n", &g, (n1 = -1, &n1)) == 1 && n1 == (int)strlen(gn)) {
gid = (gid_t)g;
grp = getgrgid(gid);
}
else {
grp = getgrnam(gn);
if (!grp)
return "Unknown group name";
gid = grp->gr_gid;
}
}
else {
// Use default group
if (!pwd)
return "Unknown default group";
gid = pwd->pw_gid;
grp = getgrgid(gid);
}
if (grp)
gname = grp->gr_name;
return (const char *)0;
}
// Test program
#ifdef TEST
int main(int argc, char **argv)
{
const char * user_group, * cmd;
switch (argc) {
case 2: user_group = 0; cmd = argv[1]; break;
case 3: user_group = argv[1]; cmd = argv[2]; break;
default:
printf("Usage: %s [USER[:GROUP]] \"COMMAND ARG...\"\n", argv[0]);
return 1;
}
int leak = open("/dev/null", O_RDONLY);
FILE * f;
if (user_group) {
uid_t uid; gid_t gid;
std::string uname = "unknown", gname = "unknown";
const char * err = parse_ugid(user_group, uid, gid, uname, gname);
if (err) {
fprintf(stderr, "Error: %s\n", err);
return 1;
}
printf("popen_as_ugid(\"%s\", \"r\", %u(%s), %u(%s)):\n", cmd,
(unsigned)uid, uname.c_str(), (unsigned)gid, gname.c_str());
f = popen_as_ugid(cmd, "r", uid, gid);
}
else {
printf("popen(\"%s\", \"r\"):\n", cmd);
f = popen(cmd, "r");
}
fflush(stdout);
close(leak);
if (!f) {
perror("popen");
return 1;
}
int cnt, c;
for (cnt = 0; (c = getc(f)) != EOF; cnt++)
putchar(c);
printf("[EOF]\nread %d bytes\n", cnt);
int status;
if (user_group)
status = pclose_as_ugid(f);
else
status = pclose(f);
if (status == -1) {
perror("pclose");
return 1;
}
printf("pclose() = 0x%04x (exit = %d, sig = %d)\n",
status, WEXITSTATUS(status), WTERMSIG(status));
return 0;
}
#endif

@ -0,0 +1,39 @@
/*
* popen_as_ugid.h
*
* Home page of code is: https://www.smartmontools.org
*
* Copyright (C) 2021 Christian Franke
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#ifndef POPEN_AS_UGID_H_CVSID
#define POPEN_AS_UGID_H_CVSID "$Id$"
#include <grp.h>
#include <pwd.h>
#include <stdio.h>
#include <string>
// Wrapper for popen(3) which prevents that unneeded file descriptors
// are inherited to the command run by popen() and optionally drops
// privileges of root user:
// If uid != 0, popen() is run as this user.
// If gid != 0, popen() is run as this group and no supplemental groups.
// Only mode "r" is supported. Only one open stream at a time is supported.
FILE * popen_as_ugid(const char * cmd, const char * mode, uid_t uid, gid_t gid);
// Call corresponding pclose(3) and return its result.
int pclose_as_ugid(FILE * f);
// Parse "USER[:GROUP]" string and set uid, gid, uname and gname accordingly.
// USER and GROUP may be specified as numeric ids or names.
// If a numeric id is used and the corresponding user (or group) does not
// exist, the function succeeds but leaves uname (or gname) unchanged.
// If no GROUP is specified, the default group of USER is used instead.
// Returns nullptr on success or a message string on error.
const char * parse_ugid(const char * s, uid_t & uid, gid_t & gid,
std::string & uname, std::string & gname );
#endif // POPEN_AS_UGID_H_CVSID

@ -489,6 +489,26 @@ The default script is
.\" %IF OS Windows
.\"! \fBEXEDIR/smartd_warning.cmd\fP.
.\" %ENDIF OS Windows
.\" %IF OS Darwin FreeBSD Linux NetBSD OpenBSD Solaris Cygwin
.TP
.B \-u USER[:GROUP], \-\-warn\-as\-user=USER[:GROUP]
[NEW EXPERIMENTAL SMARTD FEATURE]
Run the warning script as a non-privileged user instead of root.
The USER and optional GROUP may be specified as numeric ids or names.
If no GROUP is specified, the default group of USER is used instead.
.Sp
If a warning occurs, a child process is created with \fBfork\fP(2).
This process closes all inherited file descriptors, connects stdio to
/dev/null, changes the user and group ids, removes any supplementary
group ids and then calls the \fBpopen\fP(3) function from the standard
library.
.Sp
If \*(Aq0:0\*(Aq is specified, user and group are not changed, but the
remaining actions still apply.
.Sp
If \*(Aq-\*(Aq is specified, \fBpopen\fP(3) is called directly.
This is the default.
.\" %ENDIF OS Darwin FreeBSD Linux NetBSD OpenBSD Solaris Cygwin
.\" %IF OS Windows
.TP
.B \-\-service

@ -73,6 +73,10 @@ typedef int pid_t;
#include "nvmecmds.h"
#include "utility.h"
#ifdef HAVE_POSIX_API
#include "popen_as_ugid.h"
#endif
#ifdef _WIN32
// fork()/signal()/initd simulation for native Windows
#include "os_win32/daemon_win32.h" // daemon_main/detach/signal()
@ -179,6 +183,14 @@ static std::string configfile_alt;
// warning script file
static std::string warning_script;
#ifdef HAVE_POSIX_API
// run warning script as non-privileged user
static bool warn_as_user;
static uid_t warn_uid;
static gid_t warn_gid;
static std::string warn_uname, warn_gname;
#endif
// command-line: when should we exit?
enum quit_t {
QUIT_NODEV, QUIT_NODEVSTARTUP, QUIT_NEVER, QUIT_ONECHECK,
@ -1115,19 +1127,39 @@ static void MailWarning(const dev_config & cfg, dev_state & state, int which, co
#endif
// tell SYSLOG what we are about to do...
PrintOut(LOG_INFO,"%s %s to %s ...\n",
which?"Sending warning via":"Executing test of", executable, newadd);
PrintOut(LOG_INFO,"%s %s to %s%s ...\n",
(which ? "Sending warning via" : "Executing test of"), executable, newadd,
(
#ifdef HAVE_POSIX_API
warn_as_user ?
strprintf(" (uid=%u(%s) gid=%u(%s))",
(unsigned)warn_uid, warn_uname.c_str(),
(unsigned)warn_gid, warn_gname.c_str() ).c_str() :
#endif
""
)
);
// issue the command to send mail or to run the user's executable
errno=0;
FILE * pfp;
if (!(pfp=popen(command, "r")))
#ifdef HAVE_POSIX_API
if (warn_as_user) {
pfp = popen_as_ugid(command, "r", warn_uid, warn_gid);
} else
#endif
{
pfp = popen(command, "r");
}
if (!pfp)
// failed to popen() mail process
PrintOut(LOG_CRIT,"%s %s to %s: failed (fork or pipe failed, or no memory) %s\n",
newwarn, executable, newadd, errno?strerror(errno):"");
else {
// pipe succeeded!
int len, status;
int len;
char buffer[EBUFLEN];
// if unexpected output on stdout/stderr, null terminate, print, and flush
@ -1153,7 +1185,18 @@ static void MailWarning(const dev_config & cfg, dev_state & state, int which, co
// if something went wrong with mail process, print warning
errno=0;
if (-1==(status=pclose(pfp)))
int status;
#ifdef HAVE_POSIX_API
if (warn_as_user) {
status = pclose_as_ugid(pfp);
} else
#endif
{
status = pclose(pfp);
}
if (status == -1)
PrintOut(LOG_CRIT,"%s %s to %s: pclose(3) failed %s\n", newwarn, executable, newadd,
errno?strerror(errno):"");
else {
@ -1501,6 +1544,10 @@ static const char *GetValidArgList(char opt)
case 'p':
case 'w':
return "<FILE_NAME>";
#ifdef HAVE_POSIX_API
case 'u':
return "<USER>[:<GROUP>]";
#endif
case 'i':
return "<INTEGER_SECONDS>";
default:
@ -1583,6 +1630,10 @@ static void Usage()
#else
PrintOut(LOG_INFO," [default is %s/smartd_warning.cmd]\n\n", get_exe_dir().c_str());
#endif
#ifdef HAVE_POSIX_API
PrintOut(LOG_INFO," -u USER[:GROUP], --warn-as-user=USER[:GROUP]\n");
PrintOut(LOG_INFO," Run warning script as non-privileged USER\n\n");
#endif
#ifdef _WIN32
PrintOut(LOG_INFO," --service\n");
PrintOut(LOG_INFO," Running as windows service (see man page), install with:\n");
@ -4912,6 +4963,9 @@ static int parse_options(int argc, char **argv)
// Please update GetValidArgList() if you edit shortopts
static const char shortopts[] = "c:l:q:dDni:p:r:s:A:B:w:Vh?"
#ifdef HAVE_POSIX_API
"u:"
#endif
#ifdef HAVE_LIBCAP_NG
"C"
#endif
@ -4940,6 +4994,9 @@ static int parse_options(int argc, char **argv)
{ "copyright", no_argument, 0, 'V' },
{ "help", no_argument, 0, 'h' },
{ "usage", no_argument, 0, 'h' },
#ifdef HAVE_POSIX_API
{ "warn-as-user", required_argument, 0, 'u' },
#endif
#ifdef HAVE_LIBCAP_NG
{ "capabilities", no_argument, 0, 'C' },
#endif
@ -4948,6 +5005,7 @@ static int parse_options(int argc, char **argv)
opterr=optopt=0;
bool badarg = false;
const char * badarg_msg = nullptr;
bool use_default_db = true; // set false on '-B FILE'
// Parse input options.
@ -5088,6 +5146,19 @@ static int parse_options(int argc, char **argv)
case 'w':
warning_script = optarg;
break;
#ifdef HAVE_POSIX_API
case 'u':
warn_as_user = false;
if (strcmp(optarg, "-")) {
warn_uname = warn_gname = "unknown";
badarg_msg = parse_ugid(optarg, warn_uid, warn_gid,
warn_uname, warn_gname );
if (badarg_msg)
break;
warn_as_user = true;
}
break;
#endif
case 'V':
// print version and CVS info
debugmode = 1;
@ -5140,14 +5211,17 @@ static int parse_options(int argc, char **argv)
}
// Check to see if option had an unrecognized or incorrect argument.
if (badarg) {
if (badarg || badarg_msg) {
debugmode=1;
PrintHead();
// It would be nice to print the actual option name given by the user
// here, but we just print the short form. Please fix this if you know
// a clean way to do it.
PrintOut(LOG_CRIT, "=======> INVALID ARGUMENT TO -%c: %s <======= \n", optchar, optarg);
PrintValidArgs(optchar);
if (badarg_msg)
PrintOut(LOG_CRIT, "%s\n", badarg_msg);
else
PrintValidArgs(optchar);
PrintOut(LOG_CRIT, "\nUse smartd -h to get a usage summary\n\n");
return EXIT_BADCMD;
}

Loading…
Cancel
Save