├── LICENSE.txt ├── Makefile ├── README.md ├── checkrestart.1 └── checkrestart.c /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Thomas Hurst 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROG= checkrestart 2 | MAN= checkrestart.1 3 | LDADD= -ljail -lprocstat -lxo 4 | WARNS= 6 5 | 6 | .include 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NAME 2 | 3 | **checkrestart** - check for processes that may need restarting 4 | 5 | # SYNOPSIS 6 | 7 | **checkrestart** \[**--libxo**] \[**-bHw**] \[**-j** *jail*] \[**-u** *user*] \[*proc ...*] 8 | 9 | # DESCRIPTION 10 | 11 | The **checkrestart** command attempts to find processes that need restarting following a software upgrade, as indicated by their underlying executable or shared libraries no longer appearing on disk. 12 | 13 | **checkrestart** does not perform any system changes itself — it is strictly informational and best-effort (See the *BUGS* section). It is the responsibility of the system administrator to interpret the results and take any necessary action. 14 | 15 | For full system-wide checks, **checkrestart** should be executed as the superuser to allow it access to global virtual memory mappings. 16 | 17 | The following options are available: 18 | 19 | **--libxo** 20 | 21 | > Generate formatted output via libxo(3) in a selection of human and machine-readable formats. 22 | > See xo\_parse\_args(3) for details on available arguments. 23 | 24 | **-b** 25 | 26 | > Check only for missing binaries, skipping the far more expensive check for stale libraries. 27 | 28 | **-H** 29 | 30 | > Suppress the header. 31 | 32 | **-w** 33 | 34 | > Print the full width of the ARGUMENTS column even if it will wrap in the terminal. 35 | 36 | **-j** *jail* 37 | 38 | > Filter output by specified jail name or ID. 39 | 40 | **-u** *user* 41 | 42 | > Filter output by specified user name or ID. 43 | 44 | If any *proc* operands are specified, they are treated as process names, IDs, and group IDs to limit checks to. 45 | 46 | # EXAMPLES 47 | 48 | Check all processes visible by the user: 49 | 50 | # checkrestart 51 | PID JID USER COMMAND WHY ARGUMENTS 52 | 44960 0 freaky weechat .so /usr/local/bin/weechat 53 | 81345 0 freaky tmux bin tmux: server (/tmp/tmux-1001/default) 54 | 80307 0 freaky tmux bin tmux: client (/tmp/tmux-1001/default) 55 | 18115 1 nobody memcached bin /usr/local/bin/memcached 56 | 57 | This output indicates **weechat** is using an out of date library, a **tmux** client/server pair is using an out-of-date executable, having replaced its arguments list obscuring its location, and **memcached**, running in jail 1, is also out of date having left its arguments list as the full path to its original executable. 58 | 59 | Check only processes named weechat and tmux: 60 | 61 | # checkrestart weechat tmux 62 | 63 | Check only processes with PID 142 and 157: 64 | 65 | # checkrestart 142 157 66 | 67 | Check only processes in PGID 117: 68 | 69 | # checkrestart -- -117 70 | 71 | # SEE ALSO 72 | 73 | procstat(1), libxo(3), xo\_parse\_args(3), jail(8), service(8) 74 | 75 | # HISTORY 76 | 77 | A **checkrestart** command first appeared in the debian-extras package in Debian Linux. 78 | 79 | This implementation follows a similar idea, and is based on a prior version in the author's **pkg-cruft** Ruby script. 80 | 81 | An unrelated but similar **checkrestart** command is also available as an OpenBSD port. 82 | 83 | # AUTHORS 84 | 85 | Thomas Hurst <tom@hur.st> 86 | 87 | # BUGS 88 | 89 | **checkrestart** may report both false positives and false negatives, depending on program and kernel behaviour, and should be considered strictly "best-effort". 90 | 91 | In particular, retrieval of pathnames is implemented using the kernel's name cache — if an executable or library path is not in the name cache due to an eviction, or use of a file system which does not use the name cache, **checkrestart** will consider this the same as if a file is missing. 92 | 93 | The use of the name cache also means it is not yet possible to report which files are considered missing. 94 | -------------------------------------------------------------------------------- /checkrestart.1: -------------------------------------------------------------------------------- 1 | .Dd February 21, 2020 2 | .Dt CHECKRESTART 1 3 | .Os 4 | .Sh NAME 5 | .Nm checkrestart 6 | .Nd check for processes that may need restarting 7 | .Sh SYNOPSIS 8 | .Nm 9 | .Op Fl -libxo 10 | .Op Fl bHw 11 | .Op Fl j Ar jail 12 | .Op Fl u Ar user 13 | .Op Ar proc ... 14 | .Sh DESCRIPTION 15 | The 16 | .Nm 17 | command attempts to find processes that need restarting following a software 18 | upgrade, as indicated by their underlying executable or shared libraries no 19 | longer appearing on disk. 20 | .Pp 21 | .Nm 22 | does not perform any system changes itself \(em it is strictly informational and 23 | best-effort (See the 24 | .Sx BUGS 25 | section). 26 | It is the responsibility of the system administrator to interpret the results 27 | and take any necessary action. 28 | .Pp 29 | For full system-wide checks, 30 | .Nm 31 | should be executed as the superuser to allow it access to global virtual memory 32 | mappings. 33 | .Pp 34 | The following options are available: 35 | .Bl -tag -width indent 36 | .It Fl -libxo 37 | Generate formatted output via 38 | .Xr libxo 3 39 | in a selection of human and machine-readable formats. 40 | See 41 | .Xr xo_parse_args 3 42 | for details on available arguments. 43 | .It Fl b 44 | Check only for missing binaries, skipping the far more expensive check for stale 45 | libraries. 46 | .It Fl H 47 | Suppress the header. 48 | .It Fl w 49 | Print the full width of the ARGUMENTS column even if it will wrap in the terminal. 50 | .It Fl j Ar jail 51 | Filter output by specified jail name or ID. 52 | .It Fl u Ar user 53 | Filter output by specified user name or ID. 54 | .El 55 | .Pp 56 | If any 57 | .Ar proc 58 | operands are specified, they are treated as process names, IDs, and group IDs to 59 | limit checks to. 60 | .Sh EXAMPLES 61 | Check all processes visible by the user: 62 | .Bd -literal -offset indent 63 | # checkrestart 64 | PID JID USER COMMAND WHY ARGUMENTS 65 | 44960 0 freaky weechat .so /usr/local/bin/weechat 66 | 81345 0 freaky tmux bin tmux: server (/tmp/tmux-1001/default) 67 | 80307 0 freaky tmux bin tmux: client (/tmp/tmux-1001/default) 68 | 18115 1 nobody memcached bin /usr/local/bin/memcached 69 | .Ed 70 | .Pp 71 | This output indicates 72 | .Nm weechat 73 | is using an out of date library, a 74 | .Nm tmux 75 | client/server pair is using an out-of-date executable, having replaced its 76 | arguments list obscuring its location, and 77 | .Nm memcached , 78 | running in jail 1, is also out of date having left its arguments list as the 79 | full path to its original executable. 80 | .Pp 81 | Check only processes named weechat and tmux: 82 | .Bd -literal -offset indent 83 | # checkrestart weechat tmux 84 | .Ed 85 | .Pp 86 | Check only processes with PID 142 and 157: 87 | .Bd -literal -offset indent 88 | # checkrestart 142 157 89 | .Ed 90 | .Pp 91 | Check only processes in PGID 117: 92 | .Bd -literal -offset indent 93 | # checkrestart -- -117 94 | .Ed 95 | .Sh SEE ALSO 96 | .Xr procstat 1 , 97 | .Xr libxo 3 , 98 | .Xr xo_parse_args 3 , 99 | .Xr jail 8 , 100 | .Xr service 8 101 | .Sh HISTORY 102 | A 103 | .Nm 104 | command first appeared in the debian-extras package in Debian Linux. 105 | .Pp 106 | This implementation follows a similar idea, and is based on a prior version 107 | in the author's 108 | .Nm pkg-cruft 109 | Ruby script. 110 | .Pp 111 | A similar tool is also available in 112 | .Fx 113 | ports as sysutils/lsop. 114 | .Pp 115 | An unrelated but similar 116 | .Nm 117 | command is also available as an 118 | .Ox 119 | port. 120 | .Sh AUTHORS 121 | .An Thomas Hurst Aq tom@hur.st 122 | .Sh BUGS 123 | .Nm 124 | may report both false positives and false negatives, depending on program and 125 | kernel behaviour, and should be considered strictly "best-effort". 126 | .Pp 127 | In particular, retrieval of pathnames is implemented using the kernel's name 128 | cache \(em if an executable or library path is not in the name cache due to 129 | an eviction, or use of a file system which does not use the name cache, 130 | .Nm 131 | will consider this the same as if a file is missing. 132 | .Pp 133 | The use of the name cache also means it is not yet possible to report which 134 | files are considered missing. 135 | -------------------------------------------------------------------------------- /checkrestart.c: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | #define CHECKRESTART_XO_VERSION "2" 25 | #define CHECKRESTART_XO_CONTAINER "checkrestart" 26 | #define CHECKRESTART_XO_PROCESS "process" 27 | 28 | enum Reason { MissingExe, MissingLib }; 29 | 30 | static int filter_jid = -1; 31 | static int termwidth = 0; 32 | static uid_t filter_uid = 0; 33 | static bool binonly = false; 34 | static bool jflag = false; 35 | static bool needheader = true; 36 | static bool uflag = false; 37 | 38 | static void 39 | usage(void) 40 | { 41 | xo_error("usage: %s [--libxo] [-bHw] [-j jail] [-u [user]] [proc ...]\n", getprogname()); 42 | xo_finish(); 43 | exit(EX_USAGE); 44 | } 45 | 46 | static int 47 | gettermwidth(void) 48 | { 49 | struct winsize ws = { .ws_row = 0 }; 50 | const char *err; 51 | char *colenv; 52 | int cols; 53 | 54 | colenv = getenv("COLUMNS"); 55 | if (colenv != NULL) { 56 | cols = strtonum(colenv, 1, 512, &err); 57 | if (err == NULL) { 58 | return (cols); 59 | } 60 | } 61 | 62 | if (ioctl(STDOUT_FILENO, TIOCGWINSZ, (char *)&ws) != -1 || 63 | ioctl(STDERR_FILENO, TIOCGWINSZ, (char *)&ws) != -1 || 64 | ioctl(STDIN_FILENO, TIOCGWINSZ, (char *)&ws) != -1) { 65 | return (ws.ws_col); 66 | } 67 | 68 | return (0); 69 | } 70 | 71 | static int 72 | getprocstr(pid_t pid, int node, char *str, size_t maxlen) 73 | { 74 | int name[4] = { CTL_KERN, KERN_PROC, node, pid }; 75 | size_t len = maxlen; 76 | int error; 77 | 78 | str[0] = '\0'; 79 | error = sysctl(name, nitems(name), str, &len, NULL, 0); 80 | if (error != 0) { 81 | if (errno == ENOMEM) { 82 | str[len] = '\0'; 83 | } else { 84 | return (errno); 85 | } 86 | } 87 | return (0); 88 | } 89 | 90 | static int 91 | getpathname(pid_t pid, char *pathname, size_t maxlen) 92 | { 93 | return (getprocstr(pid, KERN_PROC_PATHNAME, pathname, maxlen)); 94 | } 95 | 96 | static int 97 | getargs(pid_t pid, char *args, size_t maxlen) 98 | { 99 | return (getprocstr(pid, KERN_PROC_ARGS, args, maxlen)); 100 | } 101 | 102 | static char * 103 | user_getname(uid_t uid) { 104 | static char uidstr[11] = ""; 105 | struct passwd *pw = getpwuid(uid); 106 | 107 | if (pw != NULL) { 108 | return (pw->pw_name); 109 | } else { 110 | snprintf(uidstr, sizeof(uidstr), "%d", (unsigned int)uid); 111 | return (uidstr); 112 | } 113 | } 114 | 115 | static bool 116 | user_getuid(const char *username, uid_t *uid) { 117 | struct passwd *pw = getpwnam(username); 118 | 119 | if (pw != NULL) { 120 | *uid = pw->pw_uid; 121 | return (true); 122 | } else { 123 | return (false); 124 | } 125 | } 126 | 127 | static void 128 | needsrestart(const struct kinfo_proc *proc, const enum Reason reason, const char *args) 129 | { 130 | char fmtbuf[sizeof("{:arguments/%.4294967295s}\n")]; 131 | int col, width; 132 | 133 | if (needheader) { 134 | needheader = false; 135 | xo_emit( 136 | "{Tw:/%5s}{Tw:/%5s}{Tw:/%-12.12s}{Tw:/%-12.12s}{Tw:/%-3s}{T:/%s}\n", 137 | "PID", "JID", "USER", "COMMAND", "WHY", "ARGUMENTS" 138 | ); 139 | } 140 | 141 | xo_open_instance(CHECKRESTART_XO_PROCESS); 142 | col = xo_emit("{kw:pid/%5d/%d}", proc->ki_pid); 143 | col += xo_emit("{w:jid/%5d/%d}", proc->ki_jid); 144 | col += xo_emit("{e:uid/%d/%d}", proc->ki_uid); 145 | col += xo_emit("{w:user/%-12.12s/%s}", user_getname(proc->ki_uid)); 146 | col += xo_emit("{w:command/%-12.12s/%s}", proc->ki_comm); 147 | col += xo_emit("{w:why/%-3s/%s}", reason == MissingExe ? "bin" : ".so"); 148 | 149 | if (termwidth && xo_get_style(NULL) == XO_STYLE_TEXT) { 150 | width = MAX(termwidth - col, (int)sizeof("ARGUMENTS") - 1); 151 | snprintf(fmtbuf, sizeof(fmtbuf), "{:arguments/%%.%ds}\n", width); 152 | xo_emit(fmtbuf, args); 153 | } else { 154 | xo_emit("{:arguments/%s}\n", args); 155 | } 156 | xo_close_instance(CHECKRESTART_XO_PROCESS); 157 | } 158 | 159 | static void 160 | checkrestart(struct procstat *prstat, struct kinfo_proc *proc) 161 | { 162 | char args[PATH_MAX], pathname[PATH_MAX]; 163 | struct kinfo_vmentry *kve, *vmaps; 164 | unsigned int cnt, error, i; 165 | 166 | // Skip kernel processes 167 | if (proc->ki_ppid == 0) { 168 | return; 169 | } 170 | 171 | if (jflag && proc->ki_jid != filter_jid) { 172 | return; 173 | } 174 | 175 | if (uflag && proc->ki_uid != filter_uid) { 176 | return; 177 | } 178 | 179 | error = getpathname(proc->ki_pid, pathname, sizeof(pathname)); 180 | if (error != 0 && error != ENOENT) { 181 | return; 182 | } 183 | 184 | if (error == ENOENT) { 185 | // Verify ENOENT isn't down to the process going away 186 | if (kill(proc->ki_pid, 0) == -1 && errno == ESRCH) { 187 | return; 188 | } 189 | 190 | // Binary path is just empty. Get its argv instead 191 | (void)getargs(proc->ki_pid, args, sizeof(args)); 192 | needsrestart(proc, MissingExe, args); 193 | } else if (!binonly) { 194 | vmaps = procstat_getvmmap(prstat, proc, &cnt); 195 | if (vmaps == NULL) { 196 | return; 197 | } 198 | 199 | for (i = 0; i < cnt; i++) { 200 | kve = &vmaps[i]; 201 | 202 | if (kve->kve_protection & KVME_PROT_EXEC && // executable mapping 203 | kve->kve_type == KVME_TYPE_VNODE && // backed by a vnode 204 | kve->kve_path[0] == '\0') { // with no associated path 205 | needsrestart(proc, MissingLib, pathname); 206 | break; 207 | } 208 | } 209 | 210 | procstat_freevmmap(prstat, vmaps); 211 | } 212 | } 213 | 214 | int 215 | main(int argc, char *argv[]) 216 | { 217 | struct kinfo_proc *p; 218 | struct procstat *prstat; 219 | const char *err; 220 | unsigned int cnt, i; 221 | int ch, rc, filterc; 222 | pid_t pid; 223 | 224 | rc = EX_TEMPFAIL; // most likely we just didn't find anything 225 | termwidth = gettermwidth(); 226 | 227 | xo_set_flags(NULL, XOF_WARN | XOF_COLUMNS); 228 | argc = xo_parse_args(argc, argv); 229 | if (argc < 0) { 230 | return (EX_USAGE); 231 | } 232 | 233 | while ((ch = getopt(argc, argv, "bHj:u:w")) != -1) { 234 | switch (ch) { 235 | case 'b': 236 | binonly = true; 237 | break; 238 | case 'H': 239 | needheader = false; 240 | break; 241 | case 'j': 242 | jflag = true; 243 | filter_jid = strtonum(optarg, 0, INT_MAX, &err); 244 | if (err != NULL) { 245 | filter_jid = jail_getid(optarg); 246 | if (filter_jid == -1) { 247 | xo_errx(EX_NOHOST, "jail \"%s\" not found", optarg); 248 | } 249 | } 250 | break; 251 | case 'u': 252 | uflag = true; 253 | filter_uid = strtonum(optarg, 0, UID_MAX, &err); 254 | if (err != NULL && !user_getuid(optarg, &filter_uid)) { 255 | xo_errx(EX_NOUSER, "user \"%s\" not found", optarg); 256 | } 257 | break; 258 | case 'w': 259 | termwidth = 0; 260 | break; 261 | case '?': 262 | default: 263 | usage(); 264 | } 265 | } 266 | argc -= optind; 267 | argv += optind; 268 | 269 | prstat = procstat_open_sysctl(); 270 | if (prstat == NULL) { 271 | xo_errx(EX_OSERR, "procstat_open()"); 272 | } 273 | 274 | p = procstat_getprocs(prstat, KERN_PROC_PROC, 0, &cnt); 275 | if (p == NULL) { 276 | xo_warn("procstat_getprocs()"); 277 | } else { 278 | xo_set_version(CHECKRESTART_XO_VERSION); 279 | xo_open_container(CHECKRESTART_XO_CONTAINER); 280 | xo_open_list(CHECKRESTART_XO_PROCESS); 281 | 282 | for (i = 0; i < cnt; i++) { 283 | if (argc) { 284 | for (filterc = 0; filterc < argc; filterc++) { 285 | pid = strtonum(argv[filterc], INT_MIN, INT_MAX, &err); 286 | if (err != NULL) { 287 | pid = 0; 288 | } else if (pid == 0) { 289 | usage(); 290 | } 291 | 292 | if ( 293 | (pid > 0 && p[i].ki_pid == pid) || 294 | (pid < 0 && p[i].ki_pgid == abs(pid)) || 295 | (pid == 0 && strcmp(argv[filterc], p[i].ki_comm) == 0) 296 | ) { 297 | rc = EX_OK; 298 | checkrestart(prstat, &p[i]); 299 | break; 300 | } 301 | } 302 | } else { 303 | rc = EX_OK; 304 | checkrestart(prstat, &p[i]); 305 | } 306 | } 307 | 308 | xo_close_list(CHECKRESTART_XO_PROCESS); 309 | xo_close_container(CHECKRESTART_XO_CONTAINER); 310 | 311 | procstat_freeprocs(prstat, p); 312 | } 313 | xo_finish(); 314 | 315 | procstat_close(prstat); 316 | return (rc); 317 | } 318 | --------------------------------------------------------------------------------