| 1 | #include <unistd.h> |
| 2 | #include <errno.h> |
| 3 | #include <sys/types.h> |
| 4 | #include <grp.h> |
| 5 | #include <pwd.h> |
| 6 | #include <stdio.h> |
| 7 | #include <stdlib.h> |
| 8 | #include <string.h> |
| 9 | |
| 10 | #include "config.h" |
| 11 | |
| 12 | #ifdef HAVE_CAPABILITY |
| 13 | #include <sys/capability.h> |
| 14 | #endif |
| 15 | |
| 16 | #define FALLBACK_USER "nobody" |
| 17 | |
| 18 | void show_help() { |
| 19 | fprintf(stdout, "\n"); |
| 20 | fprintf(stdout, "nd-run\n"); |
| 21 | fprintf(stdout, "\n"); |
| 22 | fprintf(stdout, "Copyright 2025 Netdata Inc.\n"); |
| 23 | fprintf(stdout, "\n"); |
| 24 | fprintf(stdout, "A helper to run a command as an unprivileged user without any extra privileges\n"); |
| 25 | fprintf(stdout, "\n"); |
| 26 | fprintf(stdout, "Defaults to running the command as '%s', but will fall back to '%s' if '%s' is not found on the system.\n", NETDATA_USER, FALLBACK_USER, NETDATA_USER); |
| 27 | fprintf(stdout, "\n"); |
| 28 | fprintf(stdout, "If it's not possible to switch users, the command will run as the current user instead.\n"); |
| 29 | #ifdef HAVE_CAPABILITY |
| 30 | fprintf(stdout, "\n"); |
| 31 | fprintf(stdout, "Regardless of whether it switched users, all capabilities will be dropped.\n"); |
| 32 | #endif |
| 33 | } |
| 34 | |
| 35 | static void fatal(const char *msg) { |
| 36 | perror(msg); |
| 37 | exit(EXIT_FAILURE); |
| 38 | } |
| 39 | |
| 40 | #ifdef HAVE_CAPABILITY |
| 41 | static void clear_caps() { |
| 42 | // Clear out all capabilities |
| 43 | // |
| 44 | // This does not require any special privileges since it is reducing |
| 45 | // the process’s privileges. |
| 46 | cap_t caps = cap_init(); |
| 47 | |
| 48 | if (caps == NULL) fatal("cap_init"); |
| 49 | |
| 50 | if (cap_clear(caps) == -1) { |
| 51 | cap_free(caps); |
| 52 | fatal("cap_clear"); |
| 53 | } |
| 54 | |
| 55 | if (cap_set_proc(caps) == -1) { |
| 56 | cap_free(caps); |
| 57 | fatal("cap_set_proc"); |
| 58 | } |
| 59 | |
| 60 | cap_free(caps); |
| 61 | } |
| 62 | #endif |
| 63 | |
| 64 | static void set_env_var(const char *name, const char *value) { |
| 65 | // Set an environment variable if the specified value is not a NULL pointer. |
| 66 | char buf[64]; |
| 67 | |
| 68 | if (value == NULL) { |
| 69 | return; |
| 70 | } |
| 71 | |
| 72 | if (setenv(name, value, 1) != 0) { |
| 73 | snprintf(buf, 64, "setenv %s", name); |
| 74 | perror(buf); |
| 75 | } |
| 76 | } |
| 77 | |
| 78 | static void clean_environment(struct passwd *pw) { |
| 79 | // Explicitly scrub the environment, only passing on a few things |
| 80 | // we know are needed to make things work correctly. |
| 81 | |
| 82 | // First, save copies of the environment variables we want to keep. |
| 83 | // We must copy them before clearing the environment, as getenv() |
| 84 | // returns pointers into the environment block which will be invalidated. |
| 85 | char *saved_path = NULL; |
| 86 | char *saved_tz = NULL; |
| 87 | char *saved_tzdir = NULL; |
| 88 | char *saved_tmpdir = NULL; |
| 89 | char *saved_pwd = NULL; |
| 90 | |
| 91 | const char *tmp; |
| 92 | if ((tmp = getenv("PATH")) != NULL) { |
| 93 | saved_path = strdup(tmp); |
| 94 | if (!saved_path) fatal("strdup PATH"); |
| 95 | } |
| 96 | if ((tmp = getenv("TZ")) != NULL) { |
| 97 | saved_tz = strdup(tmp); |
| 98 | if (!saved_tz) fatal("strdup TZ"); |
| 99 | } |
| 100 | if ((tmp = getenv("TZDIR")) != NULL) { |
| 101 | saved_tzdir = strdup(tmp); |
| 102 | if (!saved_tzdir) fatal("strdup TZDIR"); |
| 103 | } |
| 104 | if ((tmp = getenv("TMPDIR")) != NULL) { |
| 105 | saved_tmpdir = strdup(tmp); |
| 106 | if (!saved_tmpdir) fatal("strdup TMPDIR"); |
| 107 | } |
| 108 | if ((tmp = getenv("PWD")) != NULL) { |
| 109 | saved_pwd = strdup(tmp); |
| 110 | if (!saved_pwd) fatal("strdup PWD"); |
| 111 | } |
| 112 | |
| 113 | // Now clear the environment |
| 114 | #ifdef HAVE_CLEARENV |
| 115 | clearenv(); |
| 116 | #else |
| 117 | extern char **environ; |
| 118 | environ = NULL; |
| 119 | #endif |
| 120 | |
| 121 | // Set the new environment with our saved values |
| 122 | set_env_var("USER", pw->pw_name); |
| 123 | set_env_var("LOGNAME", pw->pw_name); |
| 124 | set_env_var("HOME", pw->pw_dir); |
| 125 | set_env_var("SHELL", "/bin/sh"); // Ignore user default shell |
| 126 | set_env_var("LC_ALL", "C"); // Force C locale |
| 127 | set_env_var("PATH", saved_path); |
| 128 | set_env_var("PWD", saved_pwd); |
| 129 | set_env_var("TZ", saved_tz); |
| 130 | set_env_var("TZDIR", saved_tzdir); |
| 131 | set_env_var("TMPDIR", (saved_tmpdir == NULL) ? "/tmp" : saved_tmpdir); // Use a sane default for TMPDIR if it wasn't set. |
| 132 | |
| 133 | // Free the saved copies |
| 134 | free(saved_path); |
| 135 | free(saved_tz); |
| 136 | free(saved_tzdir); |
| 137 | free(saved_tmpdir); |
| 138 | free(saved_pwd); |
| 139 | } |
| 140 | |
| 141 | int main(int argc, char *argv[]) { |
| 142 | if (argc < 2) { |
| 143 | show_help(); |
| 144 | return EXIT_FAILURE; |
| 145 | } |
| 146 | |
| 147 | uid_t euid = geteuid(); |
| 148 | |
| 149 | struct passwd *pw = getpwnam(NETDATA_USER); |
| 150 | if (!pw) { |
| 151 | pw = getpwnam(FALLBACK_USER); |
| 152 | if (!pw) { |
| 153 | fprintf(stderr, "Fallback user '%s' not found either\n", FALLBACK_USER); |
| 154 | return EXIT_FAILURE; |
| 155 | } |
| 156 | } |
| 157 | |
| 158 | if (euid != pw->pw_uid) { |
| 159 | // Set supplementary groups for this user (must be done before dropping privs) |
| 160 | if (initgroups(pw->pw_name, pw->pw_gid) != 0) { |
| 161 | if (euid == 0) { |
| 162 | if (setgroups(0, NULL) != 0) { |
| 163 | fatal("setgroups"); |
| 164 | } |
| 165 | } else if (errno != EPERM) { |
| 166 | fatal("initgroups"); |
| 167 | } |
| 168 | } |
| 169 | |
| 170 | // Drop GID then UID. Prefer setres* when available to also drop saved IDs. |
| 171 | // Linux/BSD generally provide setresgid/setresuid; macOS does not. |
| 172 | #ifdef HAVE_SETRESGID |
| 173 | if (setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) != 0) { |
| 174 | if (euid == 0 || errno != EPERM) { |
| 175 | fatal("setresgid"); |
| 176 | } |
| 177 | } |
| 178 | #else |
| 179 | if (setgid(pw->pw_gid) != 0) { |
| 180 | if (euid == 0 || errno != EPERM) { |
| 181 | fatal("setgid"); |
| 182 | } |
| 183 | } |
| 184 | #endif |
| 185 | |
| 186 | #ifdef HAVE_SETRESUID |
| 187 | if (setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid) != 0) { |
| 188 | if (euid == 0 || errno != EPERM) { |
| 189 | fatal("setresuid"); |
| 190 | } |
| 191 | } |
| 192 | #else |
| 193 | if (setuid(pw->pw_uid) != 0) { |
| 194 | if (euid == 0 || errno != EPERM) { |
| 195 | fatal("setuid"); |
| 196 | } |
| 197 | } |
| 198 | #endif |
| 199 | } |
| 200 | |
| 201 | #ifdef HAVE_CAPABILITY |
| 202 | clear_caps(); |
| 203 | #endif |
| 204 | |
| 205 | clean_environment(pw); |
| 206 | |
| 207 | // Exec the requested command (replaces the current process on success) |
| 208 | execvp(argv[1], &argv[1]); |
| 209 | fatal("execvp"); // Only reached on error |
| 210 | } |