blob: a70a19a051597f0ec4aa19b4b7228a74359e76a9 [file] [log] [blame]
/* Key watching facility.
*
* Copyright (C) 2019 Red Hat, Inc. All Rights Reserved.
* Written by David Howells (dhowells@redhat.com)
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public Licence
* as published by the Free Software Foundation; either version
* 2 of the Licence, or (at your option) any later version.
*/
#define _GNU_SOURCE
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdarg.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>
#include <ctype.h>
#include <errno.h>
#include <poll.h>
#include <getopt.h>
#include <sys/wait.h>
#include "keyutils.h"
#include <limits.h>
#include "keyctl.h"
#include "watch_queue.h"
#define MAX_MESSAGE_COUNT 256
static int consumer_stop;
static pid_t pid_con = -1, pid_cmd = -1;
static key_serial_t session;
static int watch_fd;
static int debug;
static struct watch_notification_filter filter = {
.nr_filters = 0,
.filters = {
/* Reserve a slot */
[0] = {
.type = WATCH_TYPE_KEY_NOTIFY,
},
},
};
static inline bool after_eq(unsigned int a, unsigned int b)
{
return (signed int)(a - b) >= 0;
}
static void consumer_term(int sig)
{
consumer_stop = 1;
}
static void saw_key_change(FILE *log, struct watch_notification *n,
unsigned int len)
{
struct key_notification *k = (struct key_notification *)n;
if (len != sizeof(struct key_notification))
return;
switch (n->subtype) {
case NOTIFY_KEY_INSTANTIATED:
fprintf(log, "%u inst\n", k->key_id);
break;
case NOTIFY_KEY_UPDATED:
fprintf(log, "%u upd\n", k->key_id);
break;
case NOTIFY_KEY_LINKED:
fprintf(log, "%u link %u\n", k->key_id, k->aux);
break;
case NOTIFY_KEY_UNLINKED:
fprintf(log, "%u unlk %u\n", k->key_id, k->aux);
break;
case NOTIFY_KEY_CLEARED:
fprintf(log, "%u clr\n", k->key_id);
break;
case NOTIFY_KEY_REVOKED:
fprintf(log, "%u rev\n", k->key_id);
break;
case NOTIFY_KEY_INVALIDATED:
fprintf(log, "%u inv\n", k->key_id);
break;
case NOTIFY_KEY_SETATTR:
fprintf(log, "%u attr\n", k->key_id);
break;
}
}
/*
* Handle removal notification.
*/
static void saw_removal_notification(FILE *gc, struct watch_notification *n,
unsigned int len)
{
key_serial_t key = 0;
unsigned int wp;
wp = (n->info & WATCH_INFO_ID) >> WATCH_INFO_ID__SHIFT;
if (len >= sizeof(struct watch_notification_removal)) {
struct watch_notification_removal *r = (void *)n;
key = r->id;
}
fprintf(gc, "%u gc\n", key);
if (wp == 1)
exit(0);
}
/*
* Consume and display events.
*/
static __attribute__((noreturn))
int consumer(FILE *log, FILE *gc, int fd)
{
unsigned char buffer[433], *p, *end;
union {
struct watch_notification n;
unsigned char buf1[128];
} n;
ssize_t buf_len;
setlinebuf(log);
setlinebuf(gc);
signal(SIGTERM, consumer_term);
do {
if (!consumer_stop) {
struct pollfd pf[1];
pf[0].fd = fd;
pf[0].events = POLLIN;
pf[0].revents = 0;
if (poll(pf, 1, -1) == -1) {
if (errno == EINTR)
continue;
error("poll");
}
}
buf_len = read(fd, buffer, sizeof(buffer));
if (buf_len == -1) {
perror("read");
exit(1);
}
if (buf_len == 0) {
printf("-- END --\n");
exit(0);
}
if (buf_len > sizeof(buffer)) {
fprintf(stderr, "Read buffer overrun: %zd\n", buf_len);
exit(4);
}
if (debug)
fprintf(stderr, "read() = %zd\n", buf_len);
p = buffer;
end = buffer + buf_len;
while (p < end) {
size_t largest, len;
largest = end - p;
if (largest > 128)
largest = 128;
if (largest < sizeof(struct watch_notification)) {
fprintf(stderr, "Short message header: %zu\n", largest);
exit(4);
}
memcpy(&n, p, largest);
if (debug)
fprintf(stderr, "NOTIFY[%03zx]: ty=%06x sy=%02x i=%08x\n",
p - buffer, n.n.type, n.n.subtype, n.n.info);
len = n.n.info & WATCH_INFO_LENGTH;
if (len < sizeof(n.n) || len > largest) {
fprintf(stderr, "Bad message length: %zu/%zu\n", len, largest);
exit(1);
}
switch (n.n.type) {
case WATCH_TYPE_META:
switch (n.n.subtype) {
case WATCH_META_REMOVAL_NOTIFICATION:
saw_removal_notification(gc, &n.n, len);
break;
case WATCH_META_LOSS_NOTIFICATION:
fprintf(log, "-- LOSS --\n");
break;
default:
if (debug)
fprintf(stderr, "other meta record\n");
break;
}
break;
case WATCH_TYPE_KEY_NOTIFY:
saw_key_change(log, &n.n, len);
break;
default:
if (debug)
fprintf(stderr, "other type\n");
break;
}
p += len;
}
} while (!consumer_stop);
fprintf(log, "Monitoring terminated\n");
if (gc != log)
fprintf(gc, "Monitoring terminated\n");
exit(0);
}
/*
* Open the watch device and allocate a buffer.
*/
static int open_watch(void)
{
int pipefd[2], fd;
if (pipe2(pipefd, O_NOTIFICATION_PIPE | O_NONBLOCK) == -1)
error("pipe2");
fd = pipefd[0];
if (ioctl(fd, IOC_WATCH_QUEUE_SET_SIZE, MAX_MESSAGE_COUNT) == -1)
error("/dev/watch_queue(size)");
if (filter.nr_filters &&
ioctl(fd, IOC_WATCH_QUEUE_SET_FILTER, &filter) == -1)
error("/dev/watch_queue(filter)");
return fd;
}
/*
* Parse a filter character representation into a subtype number.
*/
static bool parse_subtype(struct watch_notification_type_filter *t, char filter)
{
static const char filter_mapping[] =
"i" /* 0 NOTIFY_KEY_INSTANTIATED */
"p" /* 1 NOTIFY_KEY_UPDATED */
"l" /* 2 NOTIFY_KEY_LINKED */
"n" /* 3 NOTIFY_KEY_UNLINKED */
"c" /* 4 NOTIFY_KEY_CLEARED */
"r" /* 5 NOTIFY_KEY_REVOKED */
"v" /* 6 NOTIFY_KEY_INVALIDATED */
"s" /* 7 NOTIFY_KEY_SETATTR */
;
const char *p;
unsigned int st_bits;
unsigned int st_index;
unsigned int st_bit;
int subtype;
p = strchr(filter_mapping, filter);
if (!p)
return false;
subtype = p - filter_mapping;
st_bits = sizeof(t->subtype_filter[0]) * 8;
st_index = subtype / st_bits;
st_bit = 1U << (subtype % st_bits);
t->subtype_filter[st_index] |= st_bit;
return true;
}
/*
* Parse filters.
*/
static void parse_watch_filter(char *str)
{
struct watch_notification_filter *f = &filter;
struct watch_notification_type_filter *t0 = &f->filters[0];
f->nr_filters = 1;
t0->type = WATCH_TYPE_KEY_NOTIFY;
for (; *str; str++) {
if (parse_subtype(t0, *str))
continue;
fprintf(stderr, "Unknown filter character '%c'\n", *str);
exit(2);
}
}
/*
* Watch a key or keyring for changes.
*/
void act_keyctl_watch(int argc, char *argv[])
{
key_serial_t key;
int wfd, opt;
while (opt = getopt(argc, argv, "f:"),
opt != -1) {
switch (opt) {
case 'f':
parse_watch_filter(optarg);
break;
default:
fprintf(stderr, "Unknown option\n");
exit(2);
}
}
argv += optind;
argc -= optind;
if (argc != 1)
format();
key = get_key_id(argv[0]);
wfd = open_watch();
if (keyctl_watch_key(key, wfd, 0x01) == -1)
error("keyctl_watch_key");
consumer(stdout, stdout, wfd);
}
/*
* Add a watch on a key to the monitor created by watch_session.
*/
void act_keyctl_watch_add(int argc, char *argv[])
{
key_serial_t key;
int fd;
if (argc != 3)
format();
fd = atoi(argv[1]);
key = get_key_id(argv[2]);
if (keyctl_watch_key(key, fd, 0x02) == -1)
error("keyctl_watch_key");
exit(0);
}
/*
* Remove a watch on a key from the monitor created by watch_session.
*/
void act_keyctl_watch_rm(int argc, char *argv[])
{
key_serial_t key;
int fd;
if (argc != 3)
format();
fd = atoi(argv[1]);
key = get_key_id(argv[2]);
if (keyctl_watch_key(key, fd, -1) == -1)
error("keyctl_watch_key");
exit(0);
}
static void exit_cleanup(void)
{
pid_t me = getpid();
int w;
if (me != pid_cmd && me != pid_con) {
keyctl_watch_key(session, watch_fd, -1);
if (pid_cmd != -1) {
kill(pid_cmd, SIGTERM);
waitpid(pid_cmd, &w, 0);
}
if (pid_con != -1) {
kill(pid_con, SIGTERM);
waitpid(pid_con, &w, 0);
}
}
}
static void run_command(int argc, char *argv[], int wfd)
{
char buf[16];
pid_cmd = fork();
if (pid_cmd == -1)
error("fork");
if (pid_cmd != 0)
return;
pid_cmd = -1;
pid_con = -1;
sprintf(buf, "%u", wfd);
setenv("KEYCTL_WATCH_FD", buf, true);
/* run the standard shell if no arguments */
if (argc == 0) {
const char *q = getenv("SHELL");
if (!q)
q = "/bin/sh";
execl(q, q, NULL);
error(q);
}
/* run the command specified */
execvp(argv[0], argv);
error(argv[0]);
}
/*
* Open a logfiles.
*/
static FILE *open_logfile(const char *logfile)
{
unsigned int flags;
FILE *log;
int lfd;
log = fopen(logfile, "a");
if (!log)
error(logfile);
lfd = fileno(log);
flags = fcntl(lfd, F_GETFD);
if (flags == -1)
error("F_GETFD");
if (fcntl(lfd, F_SETFD, flags | FD_CLOEXEC) == -1)
error("F_SETFD");
return log;
}
/*
* Set up a new session keyring with a monitor that is exposed on an explicit
* file descriptor in the program that it starts.
*/
void act_keyctl_watch_session(int argc, char *argv[])
{
const char *session_name = NULL;
const char *logfile, *gcfile, *target_fd;
unsigned int flags;
pid_t pid;
FILE *log, *gc;
int wfd, tfd, opt, w, e = 0, e2 = 0;
while (opt = getopt(argc, argv, "+df:n:"),
opt != -1) {
switch (opt) {
case 'd':
debug = 1;
break;
case 'f':
parse_watch_filter(optarg);
break;
case 'n':
session_name = optarg;
break;
default:
fprintf(stderr, "Unknown option\n");
exit(2);
}
}
argv += optind;
argc -= optind;
if (argc < 4)
format();
logfile = argv[0];
gcfile = argv[1];
target_fd = argv[2];
tfd = atoi(target_fd);
if (tfd < 3 || tfd > 9) {
fprintf(stderr, "The target fd must be between 3 and 9\n");
exit(2);
}
wfd = open_watch();
if (wfd != tfd) {
if (dup2(wfd, tfd) == -1)
error("dup2");
close(wfd);
wfd = tfd;
}
watch_fd = wfd;
atexit(exit_cleanup);
/* We want the fd to be inherited across a fork. */
flags = fcntl(wfd, F_GETFD);
if (flags == -1)
error("F_GETFD");
if (fcntl(wfd, F_SETFD, flags & ~FD_CLOEXEC) == -1)
error("F_SETFD");
log = open_logfile(logfile);
gc = open_logfile(gcfile);
pid_con = fork();
if (pid_con == -1)
error("fork");
if (pid_con == 0) {
pid_cmd = -1;
pid_con = -1;
consumer(log, gc, wfd);
}
/* Create a new session keyring and watch it. */
session = keyctl_join_session_keyring(session_name);
if (session == -1)
error("keyctl_join_session_keyring");
if (keyctl_watch_key(session, wfd, 0x01) == -1)
error("keyctl_watch_key/session");
fprintf(stderr, "Joined session keyring: %d\n", session);
/* Start the command and then wait for it to finish and the
* notification consumer to clean up.
*/
run_command(argc - 3, argv + 3, wfd);
close(wfd);
wfd = -1;
while (pid = wait(&w),
pid != -1) {
if (pid == pid_cmd) {
if (pid_con != -1)
kill(pid_con, SIGTERM);
if (WIFEXITED(w)) {
e2 = WEXITSTATUS(w);
pid_cmd = -1;
} else if (WIFSIGNALED(w)) {
e2 = WTERMSIG(w) + 128;
pid_cmd = -1;
} else if (WIFSTOPPED(w)) {
raise(WSTOPSIG(w));
}
} else if (pid == pid_con) {
if (pid_cmd != -1)
kill(pid_cmd, SIGTERM);
if (WIFEXITED(w)) {
e = WEXITSTATUS(w);
pid_con = -1;
} else if (WIFSIGNALED(w)) {
e = WTERMSIG(w) + 128;
pid_con = -1;
}
}
}
if (e == 0)
e = e2;
exit(e);
}
/*
* Wait for monitoring to synchronise.
*/
void act_keyctl_watch_sync(int argc, char *argv[])
{
long ret;
int wfd, count;
if (argc != 2)
format();
wfd = atoi(argv[1]);
ret = ioctl(wfd, PIPE_IOC_SYNC);
if (ret == 0)
exit(0);
if (ret == -1 && errno != ENOTTY)
error("ioctl(PIPE_IOC_SYNC)");
for (;;) {
ret = ioctl(wfd, FIONREAD, &count);
if (ret == -1)
error("ioctl(FIONREAD)");
if (count == 0)
break;
usleep(200 * 1000);
}
exit(0);
}