blob: cdc809937b9b03c20d9a27c7c65ea4a38c5f875a [file] [edit]
/*
* pscap.c - A program that lists running processes with capabilities
* Copyright (c) 2009,2012,2020 Red Hat Inc.
* All Rights Reserved.
*
* This software may be freely redistributed and/or modified under the
* terms of the GNU General Public License as published by the Free
* Software Foundation; either version 2, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to the
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor
* Boston, MA 02110-1335, USA.
*
* Authors:
* Steve Grubb <sgrubb@redhat.com>
*/
#include "config.h"
#include <stdio.h>
#include <stdio_ext.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <dirent.h>
#include <fcntl.h>
#include <pwd.h>
#include <stdbool.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include "cap-ng.h"
#include "proc-sanitize.h"
#define CMD_LEN 16
#define ACCOUNT_LEN 32
#define USERNS_MARK_LEN 3 // two characters plus '\0'.
static void usage(void)
{
fprintf(stderr, "usage: pscap [-a] [-p pid] [--tree]\n");
exit(1);
}
struct proc_info {
pid_t pid;
pid_t ppid;
char *cmd;
char account[ACCOUNT_LEN];
char *caps_text;
};
static int get_euid(int pid)
{
char path[32], buf[128];
FILE *f;
int euid = -1;
snprintf(path, sizeof(path), "/proc/%d/status", pid);
f = fopen(path, "rte");
if (f == NULL)
return 0;
__fsetlocking(f, FSETLOCKING_BYCALLER);
while (fgets(buf, sizeof(buf), f)) {
if (memcmp(buf, "Uid:", 4) == 0) {
int uid;
sscanf(buf, "Uid: %d %d", &uid, &euid);
break;
}
}
fclose(f);
if (euid < 0)
return 0;
return euid;
}
static void get_account_name(int pid, char *account, size_t account_len)
{
struct passwd *p;
int euid;
euid = get_euid(pid);
if (euid == 0) {
strncpy(account, "root", account_len - 1);
account[account_len - 1] = '\0';
return;
}
p = getpwuid(euid);
if (p && p->pw_name) {
strncpy(account, p->pw_name, account_len - 1);
account[account_len - 1] = '\0';
return;
}
snprintf(account, account_len, "%d", euid);
}
static int get_width(void)
{
struct winsize ws;
char *e;
long c;
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0)
return ws.ws_col;
e = getenv("COLUMNS");
if (e) {
char *endptr;
errno = 0;
c = strtol(e, &endptr, 10);
if (errno == 0 && endptr != e && *endptr == '\0' && c > 0 &&
c < 400)
return (int)c;
}
return 80;
}
static size_t wrap_to(const char *text, size_t max)
{
size_t len = strlen(text);
size_t i;
if (len <= max)
return len;
for (i = max; i > 0; i--) {
if (text[i - 1] == ',') {
if (i < len && text[i] == ' ')
return i + 1;
return i;
}
if (text[i - 1] == ' ')
return i;
}
return max;
}
/*
* compare_pid - order processes by pid for sorting/bsearch
* @a: pointer to left struct proc_info
* @b: pointer to right struct proc_info
*
* Returns -1, 0, or 1 for ordering.
*/
static int compare_pid(const void *a, const void *b)
{
const struct proc_info *left = a;
const struct proc_info *right = b;
if (left->pid < right->pid)
return -1;
if (left->pid > right->pid)
return 1;
return 0;
}
/*
* find_proc - locate a process record by pid
* @procs: process array sorted by pid
* @count: number of entries in @procs
* @pid: process id to locate
*
* Returns pointer to the matching entry or NULL if not found.
*/
static void *find_proc(struct proc_info *procs, size_t count, pid_t pid)
{
struct proc_info key;
key.pid = pid;
return bsearch(&key, procs, count, sizeof(*procs), compare_pid);
}
/*
* append_marker - append a marker string to the capability text
* @text: capability text buffer pointer to append to
* @marker: marker string to append (e.g. " @" or " +")
*
* Returns 0 on success, -1 on allocation failure.
*/
static int append_marker(char **text, const char *marker)
{
size_t len = strlen(*text);
size_t marker_len = strlen(marker);
char *tmp = realloc(*text, len + marker_len + 1);
if (!tmp)
return -1;
memcpy(tmp + len, marker, marker_len + 1);
*text = tmp;
return 0;
}
/*
* format_caps - format capability text with optional markers
* @caps: capability summary state from capng_have_capabilities()
* @ambient: true if ambient capabilities are present
* @bounds: true if bounding set differs from full
*
* Returns allocated capability string or NULL on allocation failure.
*/
static char *format_caps(int caps, bool ambient, bool bounds)
{
char *text;
if (caps == CAPNG_PARTIAL)
text = capng_print_caps_text(CAPNG_PRINT_BUFFER,
CAPNG_PERMITTED);
else if (caps == CAPNG_FULL)
text = strdup("full");
else
text = strdup("none");
if (!text)
return NULL;
if (ambient)
append_marker(&text, " @");
if (bounds)
append_marker(&text, " +");
return text;
}
/*
* print_tree_node - render a node and its children in tree mode
* @procs: process array
* @count: number of entries in @procs
* @index: index of current node in @procs
* @prefix: current line prefix
* @is_last: true if this node is the last child of its parent
* @is_root: true if this node is a tree root
*
* Returns nothing. Recurses through children to emit full subtree.
*/
static void print_tree_node(struct proc_info *procs, size_t count,
size_t index, const char *prefix, bool is_last,
bool is_root, int width)
{
struct proc_info *proc = &procs[index];
size_t child_total = 0;
size_t child_seen = 0;
size_t i;
size_t prefix_len;
size_t cont_len;
size_t avail;
size_t n;
const char *caps;
char head[64];
const char *branch = "";
char *line_prefix;
char *cont_prefix;
char *child_prefix;
if (!is_root)
branch = is_last ? " " : "│ ";
line_prefix = malloc(strlen(prefix) + strlen(is_root ? "" :
(is_last ? "└─ " : "├─ ")) + 1);
if (!line_prefix)
return;
strcpy(line_prefix, prefix);
if (!is_root)
strcat(line_prefix, is_last ? "└─ " : "├─ ");
cont_prefix = malloc(strlen(prefix) + strlen(branch) + 1);
if (!cont_prefix) {
free(line_prefix);
return;
}
strcpy(cont_prefix, prefix);
strcat(cont_prefix, branch);
snprintf(head, sizeof(head), "%s(%d:%s) [", proc->cmd,
proc->pid, proc->account);
prefix_len = strlen(line_prefix);
cont_len = strlen(cont_prefix);
caps = proc->caps_text;
if ((int)(prefix_len + strlen(head) + strlen(caps) + 1) <= width) {
printf("%s%s%s]\n", line_prefix, head, caps);
goto children;
}
avail = width > (int)(prefix_len + strlen(head)) ?
(size_t)(width - (int)(prefix_len + strlen(head))) : 10;
if (avail < 10)
avail = 10;
n = wrap_to(caps, avail);
printf("%s%s%.*s\n", line_prefix, head, (int)n, caps);
caps += n;
while (*caps) {
avail = width > (int)cont_len ? (size_t)(width - (int)cont_len) : 10;
if (avail < 10)
avail = 10;
if (strlen(caps) + 1 <= avail) {
printf("%s%s]\n", cont_prefix, caps);
break;
}
n = wrap_to(caps, avail);
printf("%s%.*s\n", cont_prefix, (int)n, caps);
caps += n;
}
children:
free(line_prefix);
free(cont_prefix);
for (i = 0; i < count; i++) {
if (procs[i].ppid == proc->pid)
child_total++;
}
if (child_total == 0)
return;
child_prefix = malloc(strlen(prefix) + strlen(branch) + 1);
if (!child_prefix)
return;
strcpy(child_prefix, prefix);
strcat(child_prefix, branch);
for (i = 0; i < count; i++) {
if (procs[i].ppid != proc->pid)
continue;
child_seen++;
print_tree_node(procs, count, i, child_prefix,
child_seen == child_total, false, width);
}
free(child_prefix);
}
/*
* print_tree - render all process trees in pid order
* @procs: process array
* @count: number of entries in @procs
*
* Returns nothing. Each tree starts at a pid whose parent isn't present.
*/
static void print_tree(struct proc_info *procs, size_t count)
{
size_t i;
int width = get_width();
if (count > 1)
qsort(procs, count, sizeof(*procs), compare_pid);
for (i = 0; i < count; i++) {
if (!find_proc(procs, count, procs[i].ppid))
print_tree_node(procs, count, i, "", true, true, width);
}
}
/*
* Precise recursive checks for parent-child relation between namespaces
* using ioctl() were avoided, because there didn't seem to be any case when
* we may dereference the namespace symlink in /proc/PID/ns for processes in
* user namespaces other than the current or child ones. Thus, the check just
* tries to dereference the link and checks that it does not point to the
* current NS.
*/
static bool in_child_userns(int pid)
{
char ns_file_path[32];
struct stat statbuf;
ino_t own_ns_inode;
dev_t own_ns_dev;
if (stat("/proc/self/ns/user", &statbuf) < 0)
return false;
own_ns_inode = statbuf.st_ino;
own_ns_dev = statbuf.st_dev;
snprintf(ns_file_path, sizeof(ns_file_path), "/proc/%d/ns/user", pid);
if (stat(ns_file_path, &statbuf) < 0)
return false;
return statbuf.st_ino != own_ns_inode || statbuf.st_dev != own_ns_dev;
}
int main(int argc, char *argv[])
{
char *endptr = NULL;
DIR *d;
struct dirent *ent;
int header = 0, show_all = 0, caps;
pid_t our_pid = getpid();
pid_t target_pid = 0;
int uid = -1;
char *name = NULL;
int tree_mode = 0;
struct proc_info *procs = NULL;
size_t proc_count = 0;
size_t proc_capacity = 0;
size_t i;
for (i = 1; i < (size_t)argc; i++) {
if (strcmp(argv[i], "-a") == 0) {
show_all = 1;
continue;
}
if (strcmp(argv[i], "--tree") == 0) {
tree_mode = 1;
continue;
}
if (strcmp(argv[i], "-p") == 0) {
if (i + 1 >= (size_t)argc)
usage();
errno = 0;
target_pid = strtol(argv[++i], &endptr, 10);
if (errno) {
fprintf(stderr, "Can't read pid: %s\n",
argv[i]);
return 1;
}
if ((endptr == argv[i]) || (*endptr != '\0')
|| !target_pid) {
fprintf(stderr, "Invalid pid argument: %s\n",
argv[i]);
return 1;
}
if (target_pid == 1)
show_all = 1;
continue;
}
usage();
}
d = opendir("/proc");
if (d == NULL) {
fprintf(stderr, "Can't open /proc: %s\n", strerror(errno));
return 1;
}
while (( ent = readdir(d) )) {
int pid, ppid;
char buf[100];
char *safe_cmd = NULL;
char *tmp, cmd[CMD_LEN + USERNS_MARK_LEN], state;
int fd, len;
struct passwd *p;
// Skip non-process dir entries
if(*ent->d_name<'0' || *ent->d_name>'9')
continue;
errno = 0;
pid = strtol(ent->d_name, NULL, 10);
if (errno)
continue;
if (target_pid && (pid != target_pid))
continue;
/* Skip our pid so we aren't listed */
if (pid == our_pid)
continue;
// Parse up the stat file for the proc
snprintf(buf, sizeof(buf), "/proc/%d/stat", pid);
fd = open(buf, O_RDONLY|O_CLOEXEC, 0);
if (fd < 0)
continue;
len = read(fd, buf, sizeof(buf) - 1);
close(fd);
if (len < 40)
continue;
buf[len] = 0;
tmp = strrchr(buf, ')');
if (tmp)
*tmp = 0;
else
continue;
memset(cmd, 0, sizeof(cmd));
sscanf(buf, "%d (%15c", &ppid, cmd); // ppid is throwaway
sscanf(tmp+2, "%c %d", &state, &ppid);
// Skip kthreads
if (pid == 2 || ppid == 2)
continue;
// now get the capabilities
capng_clear(CAPNG_SELECT_ALL);
capng_setpid(pid);
if (capng_get_caps_process())
continue;
// And print out anything with capabilities
caps = capng_have_capabilities(CAPNG_SELECT_CAPS);
safe_cmd = sanitize_untrusted_field(cmd);
if (!safe_cmd)
continue;
if (in_child_userns(pid)) {
char *marked = malloc(strlen(safe_cmd) + 3);
if (!marked) {
free(safe_cmd);
continue;
}
snprintf(marked, strlen(safe_cmd) + 3, "%s *", safe_cmd);
free(safe_cmd);
safe_cmd = marked;
}
if (tree_mode) {
char *caps_text;
bool has_ambient;
bool has_bounds;
if (!show_all && caps <= CAPNG_NONE) {
free(safe_cmd);
continue;
}
has_ambient = capng_have_capabilities(
CAPNG_SELECT_AMBIENT) > CAPNG_NONE;
has_bounds = capng_have_capabilities(
CAPNG_SELECT_BOUNDS) > CAPNG_NONE;
caps_text = format_caps(caps, has_ambient, has_bounds);
if (!caps_text) {
free(safe_cmd);
continue;
}
if (proc_count == proc_capacity) {
size_t new_capacity = proc_capacity ?
proc_capacity * 2 : 256;
struct proc_info *pi_tmp;
pi_tmp = realloc(procs, new_capacity *
sizeof(*procs));
if (!pi_tmp) {
free(caps_text);
free(safe_cmd);
continue;
}
procs = pi_tmp;
proc_capacity = new_capacity;
}
procs[proc_count].pid = pid;
procs[proc_count].ppid = ppid;
procs[proc_count].cmd = safe_cmd;
get_account_name(pid, procs[proc_count].account,
sizeof(procs[proc_count].account));
procs[proc_count].caps_text = caps_text;
proc_count++;
safe_cmd = NULL;
} else if (caps > CAPNG_NONE) {
int euid = get_euid(pid);
if (header == 0) {
printf("%-7s %-7s %-16s %-15s %s\n",
"ppid", "pid", "uid", "command",
"capabilities");
header = 1;
}
if (euid == 0) {
// Take short cut for this one
name = "root";
uid = 0;
} else if (euid != uid) {
// Only look up if name changed
p = getpwuid(euid);
uid = euid;
if (p)
name = p->pw_name;
else
name = NULL;
}
if (name) {
printf("%-7d %-7d %-16s %-15s ", ppid, pid,
name, safe_cmd);
} else
printf("%-7d %-7d %-16d %-15s ", ppid, pid,
uid, safe_cmd);
if (caps == CAPNG_PARTIAL) {
capng_print_caps_text(CAPNG_PRINT_STDOUT,
CAPNG_PERMITTED);
if (capng_have_capabilities(
CAPNG_SELECT_AMBIENT) > CAPNG_NONE)
printf(" @");
if (capng_have_capabilities(CAPNG_SELECT_BOUNDS)
> CAPNG_NONE)
printf(" +");
printf("\n");
} else {
printf("full");
if (capng_have_capabilities(
CAPNG_SELECT_AMBIENT) > CAPNG_NONE)
printf(" @");
if (capng_have_capabilities(CAPNG_SELECT_BOUNDS)
> CAPNG_NONE)
printf(" +");
printf("\n");
}
free(safe_cmd);
safe_cmd = NULL;
}
if (safe_cmd)
free(safe_cmd);
}
closedir(d);
if (tree_mode) {
print_tree(procs, proc_count);
for (i = 0; i < proc_count; i++) {
free(procs[i].cmd);
free(procs[i].caps_text);
}
free(procs);
}
return 0;
}