diff --git a/Makefile.am b/Makefile.am index 52134df64..e175dc8d7 100644 --- a/Makefile.am +++ b/Makefile.am @@ -74,6 +74,7 @@ myhtopsources = \ Process.c \ ProcessLocksScreen.c \ ProcessTable.c \ + ProgramLauncher.c \ Row.c \ RichString.c \ Scheduling.c \ @@ -144,6 +145,7 @@ myhtopheaders = \ Process.h \ ProcessLocksScreen.h \ ProcessTable.h \ + ProgramLauncher.h \ ProvideCurses.h \ ProvideTerm.h \ RichString.h \ diff --git a/OpenFilesScreen.c b/OpenFilesScreen.c index f8e802b5f..cba1003b5 100644 --- a/OpenFilesScreen.c +++ b/OpenFilesScreen.c @@ -22,6 +22,7 @@ in the source distribution for its full text. #include "Macros.h" #include "Panel.h" +#include "ProgramLauncher.h" #include "ProvideCurses.h" #include "Vector.h" #include "XUtils.h" @@ -46,6 +47,8 @@ typedef struct OpenFiles_FileData_ { struct OpenFiles_FileData_* next; } OpenFiles_FileData; +static ProgramLauncher OpenFiles_ProgramLauncher; + static size_t getIndexForType(char type) { switch (type) { case 'f': @@ -100,6 +103,12 @@ static OpenFiles_ProcessData* OpenFilesScreen_getProcessData(pid_t pid) { pdata->cols[getIndexForType('o')] = 8; pdata->cols[getIndexForType('i')] = 8; + ProgramLauncher_setPath(&OpenFiles_ProgramLauncher, "lsof"); + if (OpenFiles_ProgramLauncher.lastErrno != 0) { + pdata->error = 1; + return pdata; + } + int fdpair[2] = {-1, -1}; if (pipe(fdpair) < 0) { pdata->error = 1; @@ -127,9 +136,18 @@ static OpenFiles_ProcessData* OpenFilesScreen_getProcessData(pid_t pid) { close(fdnull); char buffer[32] = {0}; xSnprintf(buffer, sizeof(buffer), "%d", pid); - // Use of NULL in variadic functions must have a pointer cast. - // The NULL constant is not required by standard to have a pointer type. - execlp("lsof", "lsof", "-P", "-o", "-p", buffer, "-F", (char*)NULL); + + const char* argv[] = { + "lsof", + "-P", + "-o", + "-p", + buffer, + "-F", + NULL + }; + ProgramLauncher_execv_const(&OpenFiles_ProgramLauncher, argv); + exit(127); } close(fdpair[1]); diff --git a/ProgramLauncher.c b/ProgramLauncher.c new file mode 100644 index 000000000..b9c6d997d --- /dev/null +++ b/ProgramLauncher.c @@ -0,0 +1,405 @@ +/* +htop - ProgramLauncher.c +(C) 2026 htop dev team +Released under the GNU GPLv2+, see the COPYING file +in the source distribution for its full text. +*/ + +#include "config.h" // IWYU pragma: keep + +#include "ProgramLauncher.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "XUtils.h" + + +typedef struct ProgramFileRef_ { + dev_t dev; + ino_t ino; + char* path; +} ProgramFileRef; + +extern char** environ; + +static gid_t ProgramLauncher_savedSetGid = (gid_t)-1; +static uid_t ProgramLauncher_savedSetUid = (uid_t)-1; + +void ProgramLauncher_done(ProgramLauncher* this) { + if (this->fileRef) { + free(((const ProgramFileRef*)this->fileRef)->path); + } + free(this->fileRef); + this->fileRef = NULL; + this->lastErrno = 0; +} + +static void ProgramLauncher_dropSetUid(void) { + gid_t egid = getegid(); + if (ProgramLauncher_savedSetGid == (gid_t)-1) + ProgramLauncher_savedSetGid = egid; + + assert(ProgramLauncher_savedSetGid == egid); + + uid_t euid = geteuid(); + if (ProgramLauncher_savedSetUid == (uid_t)-1) + ProgramLauncher_savedSetUid = euid; + + assert(ProgramLauncher_savedSetUid == euid); + + int res = 0; + + gid_t rgid = getgid(); +#ifdef _POSIX_SAVED_IDS + if (egid != rgid) + res = setegid(rgid); +#else + // See Glibc manual about the real UID and effective UID swap: + // https://sourceware.org/glibc/manual/latest/html_node/Enable_002fDisable-Setuid.html + if (ProgramLauncher_savedSetGid != rgid) + res = setregid(egid, rgid); +#endif + if (res != 0) + fail(); + + uid_t ruid = getuid(); +#ifdef _POSIX_SAVED_IDS + if (euid != ruid) + res = seteuid(ruid); +#else + if (ProgramLauncher_savedSetUid != ruid) + res = setreuid(euid, ruid); +#endif + if (res != 0) + fail(); +} + +static void ProgramLauncher_restoreSetUid(void) { + assert(ProgramLauncher_savedSetGid != (gid_t)-1); + assert(ProgramLauncher_savedSetUid != (uid_t)-1); + if (ProgramLauncher_savedSetGid == (gid_t)-1 || ProgramLauncher_savedSetUid == (uid_t)-1) { + fail(); + } + + gid_t egid = getegid(); + if (egid != ProgramLauncher_savedSetGid) { +#ifdef _POSIX_SAVED_IDS + int res = setegid(ProgramLauncher_savedSetGid); +#else + int res = setregid(egid, ProgramLauncher_savedSetGid); +#endif + if (res != 0) + fail(); + } + + uid_t euid = geteuid(); + if (euid != ProgramLauncher_savedSetUid) { +#ifdef _POSIX_SAVED_IDS + int res = seteuid(ProgramLauncher_savedSetUid); +#else + int res = setreuid(euid, ProgramLauncher_savedSetUid); +#endif + if (res != 0) + fail(); + } +} + +#ifndef HAVE_GROUP_MEMBER +static int group_member(gid_t gid) { + int n = getgroups(0, NULL); + if (n <= 0) + return 0; + + gid_t* groups = xMallocArray((size_t)n, sizeof(*groups)); + + int found = 0; + if (getgroups(n, groups) >= 0) { + for (size_t i = 0; i < (size_t)n; i++) { + if (groups[i] == gid) { + found = 1; + break; + } + } + } + free(groups); + return found; +} +#endif + +static bool ProgramLauncher_canTrustExecStat(const struct stat* sb) { + if (sb->st_uid == geteuid()) + return S_ISREG(sb->st_mode) && (sb->st_mode & S_IXUSR); + if (sb->st_gid == getegid() || group_member(sb->st_gid)) + return S_ISREG(sb->st_mode) && (sb->st_mode & S_IXGRP); + + // To prevent users from executing programs they might not trust, + // ignore S_IXOTH except for programs owned by the root user. + // This is stricter than what OS would permit executing. + if (sb->st_uid == 0) + return S_ISREG(sb->st_mode) && (sb->st_mode & S_IXOTH); + + return false; +} + +static int ProgramLauncher_openAndCheckStat(const ProgramLauncher* this, ProgramFileRef* newFileRef) { + const char* path; + if (newFileRef) { + assert(!this->fileRef); + path = newFileRef->path; + } else { + assert(this->fileRef); + path = ((const ProgramFileRef*)this->fileRef)->path; + } + assert(path); + + int fd = -1; + int savedErrno = errno; + if (!(this->options & PROGRAM_LAUNCH_NO_SCRIPT)) { + int openFlags = O_RDONLY | O_NOCTTY | O_NONBLOCK; +#ifdef O_REGULAR + // O_REGULAR is supported in NetBSD. + openFlags |= O_REGULAR; +#endif + do { + fd = open(path, openFlags); + } while (fd < 0 && errno == EINTR); + + if (fd < 0 && errno != EACCES) { + return fd; + } + } + + if (fd < 0) { + int openFlags = O_NOCTTY | O_NONBLOCK; +#ifdef O_EXEC + openFlags |= O_EXEC; +#else + // O_EXEC is not supported in Linux. + openFlags |= O_RDONLY; +#endif +#ifdef O_PATH + // O_PATH is specific to Linux and FreeBSD. + openFlags |= O_PATH; +#endif +#ifdef O_CLOEXEC + openFlags |= O_CLOEXEC; +#endif +#ifdef O_REGULAR + // O_REGULAR is supported in NetBSD. + openFlags |= O_REGULAR; +#endif + do { + fd = open(path, openFlags); + } while (fd < 0 && errno == EINTR); + } + + if (fd < 0) + return fd; + + errno = savedErrno; + + struct stat sb; + int res = fstat(fd, &sb); + if (res != 0) { + savedErrno = errno; + goto fail; + } + + if (!newFileRef) { + const ProgramFileRef* fileRef = (const ProgramFileRef*)this->fileRef; + if (fileRef->dev != sb.st_dev || fileRef->ino != sb.st_ino) { + // The original file is gone and another file takes its place + // with the same name. Deny execution for safety. + savedErrno = ENOENT; + goto fail; + } + } + + if (!ProgramLauncher_canTrustExecStat(&sb)) { + savedErrno = EACCES; + goto fail; + } + + if (newFileRef) { + newFileRef->dev = sb.st_dev; + newFileRef->ino = sb.st_ino; + } + return fd; + +fail: + close(fd); + errno = savedErrno; + return -1; +} + +static bool ProgramLauncher_isScriptFile(int fd) { + assert(fd >= 0); + + char buf[2]; // This is intentionally not null terminated + ssize_t len = 0; + off_t start = 0; + do { + do { + len = pread(fd, buf + start, sizeof(buf) - (size_t)start, start); + } while (len < 0 && errno == EINTR); + + if (len <= 0) { + // End of file (len == 0) or a read error other than EINTR + return false; + } + start += len; + } while (start < (off_t)sizeof(buf)); + + assert(start == (off_t)sizeof(buf)); + return (buf[0] == '#' && buf[1] == '!'); +} + +void ProgramLauncher_setPath(ProgramLauncher* this, const char* path) { + if (this->lastErrno != 0) + return; + + ProgramFileRef* newFileRef = NULL; + const char* envPath = NULL; + char* csPathBuf = NULL; + if (this->fileRef) { + path = ((const ProgramFileRef*)this->fileRef)->path; + } else { + newFileRef = xMalloc(sizeof(*newFileRef)); + + if (!strchr(path, '/')) { + envPath = getenv("PATH"); + + if (!(envPath && envPath[0])) { + size_t csPathSize = confstr(_CS_PATH, NULL, 0); + if (csPathSize > sizeof("")) { + csPathBuf = xMalloc(csPathSize); + if (confstr(_CS_PATH, csPathBuf, csPathSize) == csPathSize) { + assert(csPathBuf[0] != '\0'); + envPath = csPathBuf; + } + } + } + + if (!(envPath && envPath[0])) { + this->lastErrno = ENOENT; + goto end; + } + } + } + + if (!(this->options & PROGRAM_LAUNCH_KEEP_SETUID)) + ProgramLauncher_dropSetUid(); + + const char* pathPrefix = envPath; + while (true) { + char* newPath = NULL; + const char* pathPrefixEnd = NULL; + if (newFileRef) { + if (!envPath) { + newPath = xStrdup(path); + } else { + pathPrefixEnd = String_strchrnul(pathPrefix, ':'); + assert(pathPrefixEnd >= pathPrefix); + + if (pathPrefixEnd > pathPrefix) { + int pathPrefixLen = (int)(pathPrefixEnd - pathPrefix) - (*(pathPrefixEnd - 1) == '/'); + assert(pathPrefixLen >= 0); + xAsprintf(&newPath, "%.*s/%s", pathPrefixLen, pathPrefix, path); + } else { + // POSIX allows zero-length prefix as "a legacy feature". + newPath = String_cat("./", path); + } + } + newFileRef->path = newPath; + } + + int fd = ProgramLauncher_openAndCheckStat(this, newFileRef); + // execlp() and execvp() from libc stop searching when there is a + // file with execute permission is found. They can stop searching + // even when the file has no read permission and the script + // interpreter would definitely fail on reading the script. + + if (fd >= 0) { + this->lastErrno = 0; + if (newFileRef) + this->fileRef = newFileRef; + + if (!(this->options & PROGRAM_LAUNCH_NO_SCRIPT) && !ProgramLauncher_isScriptFile(fd)) + this->options |= PROGRAM_LAUNCH_NO_SCRIPT; + + close(fd); + break; + } + + // Keep "Permission denied" error if there is one during the + // search. + if (this->lastErrno != EACCES) + this->lastErrno = errno; + + free(newPath); + + if (!(pathPrefixEnd && pathPrefixEnd[0])) + break; + + assert(pathPrefixEnd[0] == ':'); + pathPrefix = pathPrefixEnd + strlen(":"); + } + + if (!(this->options & PROGRAM_LAUNCH_KEEP_SETUID)) + ProgramLauncher_restoreSetUid(); + +end: + free(csPathBuf); + + assert(this->lastErrno != 0 || this->fileRef); + if (this->lastErrno != 0) { + free(newFileRef); + errno = this->lastErrno; + } +} + +void ProgramLauncher_execve(ProgramLauncher* this, char* const* argv, char* const* envp) { + if (!envp) + envp = environ; + + assert(this->fileRef); + assert(((const ProgramFileRef*)this->fileRef)->path); + if (!this->fileRef || !((const ProgramFileRef*)this->fileRef)->path) { + errno = EFAULT; + return; + } + + if (!(this->options & PROGRAM_LAUNCH_KEEP_SETUID)) { + // Permanently drop privileges + if (setgid(getgid()) != 0) { + return; + } + if (setuid(getuid()) != 0) { + return; + } + } + + int fd = ProgramLauncher_openAndCheckStat(this, NULL); + if (fd < 0) + return; + +#ifdef HAVE_FEXECVE + fexecve(fd, argv, envp); + + // Clean up if fexecve() fails + int savedErrno = errno; + close(fd); + errno = savedErrno; +#else + close(fd); + execve(((const ProgramFileRef*)this->fileRef)->path, argv, envp); +#endif +} diff --git a/ProgramLauncher.h b/ProgramLauncher.h new file mode 100644 index 000000000..dbac87bb5 --- /dev/null +++ b/ProgramLauncher.h @@ -0,0 +1,77 @@ +#ifndef HEADER_ProgramLauncher +#define HEADER_ProgramLauncher +/* +htop - ProgramLauncher.h +(C) 2026 htop dev team +Released under the GNU GPLv2+, see the COPYING file +in the source distribution for its full text. +*/ + +#include "Macros.h" + + +enum ProgramLauncherOptions_ { + /* Option to preserve the "setuid"/"setgid" privilege when launching + an external program (as opposed to dropping the privilege). + Be careful! It's often a security risk for leaking the privilege + to *any* third-party program. + This "setuid" privilege is different from "sudo" privilege! To + launch external programs with elevated privileges, it's very + likely you want to run "sudo htop" instead. */ + PROGRAM_LAUNCH_KEEP_SETUID = 1 << 0, + /* Option to disallow launching scripts from this module. Files that + begin with "#!" (shebang) and require an interpreter are scripts + in this context. */ + PROGRAM_LAUNCH_NO_SCRIPT = 1 << 2, +}; + +typedef unsigned int ProgramLauncherOptions; + +typedef struct ProgramLauncher_ { + void* fileRef; + int lastErrno; + ProgramLauncherOptions options; +} ProgramLauncher; + +// POSIX declares the type of "argv" and "envp" arguments in execve() +// as "char *const[]" for compability reason (see the "Ratinale" of +// ). +// Unfortunately that prevents the function from accepting the +// "const char *const[]" type, which is arguably more const-correct. +// An explicit cast from "char **" to "const char *const *" will +// generate a "-Wcast-qual" warning. Use this union as a workaround. +typedef union ExecStrPtrPtr_ { + const char* const* cpp; + char* const* pp; +} ExecStrPtrPtr; + +ATTR_NONNULL +void ProgramLauncher_done(ProgramLauncher* this); + +ATTR_NONNULL +void ProgramLauncher_setPath(ProgramLauncher* this, const char* path); + +ATTR_NONNULL_N(1, 2) +void ProgramLauncher_execve(ProgramLauncher* this, char* const* argv, char* const* envp); + +ATTR_NONNULL_N(1, 2) +static inline void ProgramLauncher_execve_const(ProgramLauncher* this, const char* const* argv, const char* const* envp) { + ExecStrPtrPtr argv_cast, envp_cast; + argv_cast.cpp = argv; + envp_cast.cpp = envp; + ProgramLauncher_execve(this, argv_cast.pp, envp_cast.pp); +} + +ATTR_NONNULL +static inline void ProgramLauncher_execv(ProgramLauncher* this, char* const* argv) { + ProgramLauncher_execve(this, argv, NULL); +} + +ATTR_NONNULL +static inline void ProgramLauncher_execv_const(ProgramLauncher* this, const char* const* argv) { + ExecStrPtrPtr argv_cast; + argv_cast.cpp = argv; + ProgramLauncher_execv(this, argv_cast.pp); +} + +#endif diff --git a/TraceScreen.c b/TraceScreen.c index f87700f2b..6b74f0ad2 100644 --- a/TraceScreen.c +++ b/TraceScreen.c @@ -24,6 +24,7 @@ in the source distribution for its full text. #include "CRT.h" #include "FunctionBar.h" #include "Panel.h" +#include "ProgramLauncher.h" #include "ProvideCurses.h" #include "XUtils.h" @@ -34,6 +35,8 @@ static const char* const TraceScreenKeys[] = {"F3", "F4", "F8", "F9", "Esc"}; static const int TraceScreenEvents[] = {KEY_F(3), KEY_F(4), KEY_F(8), KEY_F(9), 27}; +static ProgramLauncher TraceScreen_programLauncher; + TraceScreen* TraceScreen_new(const Process* process) { // This initializes all TraceScreen variables to "false" so only default = true ones need to be set below TraceScreen* this = xCalloc(1, sizeof(TraceScreen)); @@ -67,6 +70,14 @@ static void TraceScreen_draw(InfoScreen* this) { } bool TraceScreen_forkTracer(TraceScreen* this) { +#if defined(HTOP_FREEBSD) || defined(HTOP_OPENBSD) || defined(HTOP_NETBSD) || defined(HTOP_DRAGONFLYBSD) || defined(HTOP_SOLARIS) + ProgramLauncher_setPath(&TraceScreen_programLauncher, "truss"); +#elif defined(HTOP_LINUX) + ProgramLauncher_setPath(&TraceScreen_programLauncher, "strace"); +#else + TraceScreen_programLauncher.lastErrno = ENOSYS; +#endif + int fdpair[2] = {-1, -1}; if (pipe(fdpair) < 0) @@ -93,17 +104,43 @@ bool TraceScreen_forkTracer(TraceScreen* this) { xSnprintf(buffer, sizeof(buffer), "%d", Process_getPid(this->super.process)); #if defined(HTOP_FREEBSD) || defined(HTOP_OPENBSD) || defined(HTOP_NETBSD) || defined(HTOP_DRAGONFLYBSD) || defined(HTOP_SOLARIS) - // Use of NULL in variadic functions must have a pointer cast. - // The NULL constant is not required by standard to have a pointer type. - execlp("truss", "truss", "-s", "512", "-p", buffer, (void*)NULL); + const char* argv[] = { + "truss", + "-s", + "512", + "-p", + buffer, + NULL + }; + if (TraceScreen_programLauncher.lastErrno == 0) { + ProgramLauncher_execv_const(&TraceScreen_programLauncher, argv); + } else { + errno = TraceScreen_programLauncher.lastErrno; + } // Should never reach here, unless execlp fails ... + fprintf(stderr, "Could not execute 'truss': %s\n", strerror(errno)); const char* message = "Could not execute 'truss'. Please make sure it is available in your $PATH."; (void)! write(STDERR_FILENO, message, strlen(message)); #elif defined(HTOP_LINUX) - execlp("strace", "strace", "-T", "-tt", "-s", "512", "-p", buffer, (void*)NULL); + const char* argv[] = { + "strace", + "-T", + "-tt", + "-s", + "512", + "-p", + buffer, + NULL + }; + if (TraceScreen_programLauncher.lastErrno == 0) { + ProgramLauncher_execv_const(&TraceScreen_programLauncher, argv); + } else { + errno = TraceScreen_programLauncher.lastErrno; + } // Should never reach here, unless execlp fails ... + fprintf(stderr, "Could not execute 'strace': %s\n", strerror(errno)); const char* message = "Could not execute 'strace'. Please make sure it is available in your $PATH."; (void)! write(STDERR_FILENO, message, strlen(message)); #else // HTOP_DARWIN, HTOP_PCP == HTOP_UNSUPPORTED diff --git a/configure.ac b/configure.ac index 4f2b359eb..e4c087015 100644 --- a/configure.ac +++ b/configure.ac @@ -412,7 +412,9 @@ AC_SEARCH_LIBS([clock_gettime], [rt]) AC_CHECK_FUNCS([ \ dladdr \ faccessat \ + fexecve \ fstatat \ + group_member \ host_get_clock_service \ memfd_create \ openat \ diff --git a/linux/OpenRCMeter.c b/linux/OpenRCMeter.c index 03f891eaf..f16ddba8c 100644 --- a/linux/OpenRCMeter.c +++ b/linux/OpenRCMeter.c @@ -20,6 +20,7 @@ in the source distribution for its full text. #include "CRT.h" #include "Macros.h" #include "Object.h" +#include "ProgramLauncher.h" #include "RichString.h" #include "Settings.h" #include "XUtils.h" @@ -35,6 +36,8 @@ typedef struct OpenRCMeterContext { static OpenRCMeterContext_t ctx_system; static OpenRCMeterContext_t ctx_user; +static ProgramLauncher OpenRCMeter_programLauncher; + static void OpenRCMeter_done(ATTR_UNUSED Meter* this) { OpenRCMeterContext_t* ctx = String_eq(Meter_name(this), "OpenRCUser") ? &ctx_user : &ctx_system; @@ -48,6 +51,10 @@ static void updateViaExec(bool user) { if (Settings_isReadonly()) return; + ProgramLauncher_setPath(&OpenRCMeter_programLauncher, "rc-status"); + if (OpenRCMeter_programLauncher.lastErrno != 0) + return; + int fdpair[2] = {-1, -1}; if (pipe(fdpair) < 0) return; @@ -68,11 +75,15 @@ static void updateViaExec(bool user) { exit(1); dup2(fdnull, STDERR_FILENO); close(fdnull); - if (user) { - execlp("rc-status", "rc-status", "--user", "-a", (char*)NULL); - } else { - execlp("rc-status", "rc-status", "-a", (char*)NULL); - } + + const char* argv[] = { + "rc-status", + "-a", + (user ? "--user" : NULL), + NULL + }; + ProgramLauncher_execv_const(&OpenRCMeter_programLauncher, argv); + exit(127); } close(fdpair[1]); diff --git a/linux/SystemdMeter.c b/linux/SystemdMeter.c index d594bf364..ae766f29d 100644 --- a/linux/SystemdMeter.c +++ b/linux/SystemdMeter.c @@ -21,6 +21,7 @@ in the source distribution for its full text. #include "CRT.h" #include "Macros.h" #include "Object.h" +#include "ProgramLauncher.h" #include "RichString.h" #include "Settings.h" #include "XUtils.h" @@ -68,6 +69,8 @@ typedef struct SystemdMeterContext { static SystemdMeterContext_t ctx_system; static SystemdMeterContext_t ctx_user; +static ProgramLauncher SystemdMeter_programLauncher; + static void SystemdMeter_done(ATTR_UNUSED Meter* this) { SystemdMeterContext_t* ctx = String_eq(Meter_name(this), "SystemdUser") ? &ctx_user : &ctx_system; @@ -217,6 +220,10 @@ static void updateViaExec(bool user) { if (Settings_isReadonly()) return; + ProgramLauncher_setPath(&SystemdMeter_programLauncher, "systemctl"); + if (SystemdMeter_programLauncher.lastErrno != 0) + return; + int fdpair[2] = {-1, -1}; if (pipe(fdpair) < 0) return; @@ -237,19 +244,20 @@ static void updateViaExec(bool user) { exit(1); dup2(fdnull, STDERR_FILENO); close(fdnull); - // Use of NULL in variadic functions must have a pointer cast. - // The NULL constant is not required by standard to have a pointer type. - execlp( - "systemctl", + + const char *argv[] = { "systemctl", "show", - user ? "--user" : "--system", + (user ? "--user" : "--system"), "--property=SystemState", "--property=NFailedUnits", "--property=NNames", "--property=NJobs", "--property=NInstalledJobs", - (char*)NULL); + NULL + }; + ProgramLauncher_execv_const(&SystemdMeter_programLauncher, argv); + exit(127); } close(fdpair[1]);