
If the call to execv() is failed (/usr/bin/unl0kr is absent, for example), the child process will exit with EXIT_FAILURE. But since the agent does not check the exit code, it will not notice the problem and will return an empty password to systemd. When the password is used to unlock a PKCS#11 or FIDO2 token, we can waste a limited number of tries or lock the token entirely. The patch adds a check to avoid this sutuation.
686 lines
17 KiB
C
686 lines
17 KiB
C
/*
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
*/
|
|
|
|
#include <sys/epoll.h>
|
|
#include <sys/inotify.h>
|
|
#include <sys/ioctl.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/un.h>
|
|
#include <sys/wait.h>
|
|
#include <assert.h>
|
|
#include <dirent.h>
|
|
#include <errno.h>
|
|
#include <signal.h>
|
|
#include <stdbool.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
|
|
#include <ini.h>
|
|
|
|
#define UNUSED(x) ((void)x)
|
|
|
|
struct Request
|
|
{
|
|
uint64_t not_after;
|
|
char* file;
|
|
char* socket;
|
|
char* message;
|
|
char* icon;
|
|
pid_t pid;
|
|
bool accept_cached;
|
|
bool echo;
|
|
bool silent;
|
|
};
|
|
|
|
void Request_init(struct Request* req)
|
|
{
|
|
req->not_after = 0;
|
|
req->file = NULL;
|
|
req->socket = NULL;
|
|
req->message = NULL;
|
|
req->icon = NULL;
|
|
req->pid = 0;
|
|
req->accept_cached = false;
|
|
req->echo = false;
|
|
req->silent = false;
|
|
}
|
|
|
|
void Request_free(struct Request* req)
|
|
{
|
|
if (req->file)
|
|
free(req->file);
|
|
if (req->socket)
|
|
free(req->socket);
|
|
if (req->message)
|
|
free(req->message);
|
|
if (req->icon)
|
|
free(req->icon);
|
|
}
|
|
|
|
void Request_reset(struct Request* req)
|
|
{
|
|
Request_free(req);
|
|
Request_init(req);
|
|
}
|
|
|
|
struct Request request;
|
|
int fd_epoll, fd_inotify;
|
|
pid_t pid_unl0kr;
|
|
bool unl0kr_exited;
|
|
|
|
timer_t id_timer;
|
|
enum {
|
|
NO_ACTION,
|
|
TERMINATE_UNL0KR,
|
|
KILL_UNL0KR
|
|
} timer_action;
|
|
bool timer_expired;
|
|
|
|
void erase_and_free(char* p)
|
|
{
|
|
const size_t length = strlen(p);
|
|
for (size_t i = 0; i < length; i++)
|
|
p[i] = 0;
|
|
|
|
free(p);
|
|
}
|
|
|
|
int send_password(const char *password)
|
|
{
|
|
int fd_socket = socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC, 0);
|
|
if (fd_socket < 0) {
|
|
int ret = errno;
|
|
perror("socket() is failed");
|
|
return ret;
|
|
}
|
|
|
|
struct sockaddr_un address;
|
|
address.sun_family = AF_UNIX;
|
|
strncpy(address.sun_path, request.socket, sizeof(address.sun_path) - 1);
|
|
address.sun_path[sizeof(address.sun_path) - 1] = 0;
|
|
|
|
ssize_t n = sendto(fd_socket, password, strlen(password), MSG_NOSIGNAL, (const struct sockaddr*) &address, sizeof(address));
|
|
if (n < 0) {
|
|
int ret = errno;
|
|
perror("sendto() is failed");
|
|
close(fd_socket);
|
|
return ret;
|
|
}
|
|
|
|
close(fd_socket);
|
|
return 0;
|
|
}
|
|
|
|
bool to_bool(const char* value)
|
|
{
|
|
if (strcmp(value, "true") == 0) {
|
|
return true;
|
|
} else if (strcmp(value, "false") == 0) {
|
|
return false;
|
|
} else if (strcmp(value, "1") == 0) {
|
|
return true;
|
|
} else if (strcmp(value, "0") == 0) {
|
|
return false;
|
|
} else if (strcmp(value, "yes") == 0) {
|
|
return true;
|
|
} else if (strcmp(value, "no") == 0) {
|
|
return false;
|
|
} else {
|
|
fprintf(stderr, "The value '%s' is not a boolean\n", value);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
int ini_parser(void* user, const char* section, const char* name, const char* value)
|
|
{
|
|
struct Request* d = (struct Request*) user;
|
|
|
|
if (strcmp(section, "Ask") != 0) {
|
|
fprintf(stderr, "The ini file contains unknown section: %s\n", section);
|
|
return 0;
|
|
}
|
|
|
|
if (value[0] == 0x00) {
|
|
fprintf(stderr, "The ini file contains a key without a value: %s\n", name);
|
|
return 0;
|
|
}
|
|
|
|
if (strcmp(name, "NotAfter") == 0) {
|
|
d->not_after = atol(value);
|
|
} else if (strcmp(name, "Socket") == 0) {
|
|
d->socket = strdup(value);
|
|
} else if (strcmp(name, "Message") == 0) {
|
|
d->message = strdup(value);
|
|
} else if (strcmp(name, "Icon") == 0) {
|
|
d->icon = strdup(value);
|
|
} else if (strcmp(name, "PID") == 0) {
|
|
d->pid = atoi(value);
|
|
} else if (strcmp(name, "AcceptCached") == 0) {
|
|
d->accept_cached = to_bool(value);
|
|
} else if (strcmp(name, "Echo") == 0) {
|
|
d->echo = to_bool(value);
|
|
} else if (strcmp(name, "Silent") == 0) {
|
|
d->silent = to_bool(value);
|
|
} else {
|
|
fprintf(stderr, "The ini file contains unknown key: %s = %s\n", name, value);
|
|
return 0;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
int find_request(char** ret_file)
|
|
{
|
|
const char* ask_folder = "/run/systemd/ask-password";
|
|
const size_t ask_folder_length = strlen(ask_folder);
|
|
|
|
DIR* dir = opendir(ask_folder);
|
|
if (!dir) {
|
|
int ret = errno;
|
|
if (errno != ENOENT) {
|
|
fprintf(stderr, "Can't open '%s': %s\n", ask_folder, strerror(errno));
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
struct dirent* entry;
|
|
while ((entry = readdir(dir))) {
|
|
if (entry->d_type != DT_REG && entry->d_type != DT_LNK)
|
|
continue;
|
|
|
|
if (strncmp(entry->d_name, "ask.", 4) != 0)
|
|
continue;
|
|
|
|
break;
|
|
}
|
|
if (!entry) {
|
|
closedir(dir);
|
|
return ENOENT;
|
|
}
|
|
|
|
char* file = malloc(ask_folder_length + 1 + strlen(entry->d_name) + 1);
|
|
if (!file) {
|
|
closedir(dir);
|
|
fprintf(stderr, "Out of memory\n");
|
|
return ENOMEM;
|
|
}
|
|
|
|
strcpy(file, ask_folder);
|
|
strcpy(file + ask_folder_length, "/");
|
|
strcpy(file + ask_folder_length + 1, entry->d_name);
|
|
|
|
closedir(dir);
|
|
|
|
*ret_file = file;
|
|
return 0;
|
|
}
|
|
|
|
int process_inotify_events()
|
|
{
|
|
/* We expect only IN_DELETE_SELF and IN_IGNORED */
|
|
size_t buffer_size = sizeof(struct inotify_event) * 2;
|
|
uint8_t buffer[buffer_size];
|
|
|
|
ssize_t block_size = read(fd_inotify, buffer, buffer_size);
|
|
if (block_size < 0) {
|
|
int ret = errno;
|
|
perror("read() is failed");
|
|
return ret;
|
|
}
|
|
|
|
assert((size_t) block_size == buffer_size);
|
|
|
|
struct inotify_event* ievent1 = (struct inotify_event*) buffer;
|
|
struct inotify_event* ievent2 = ievent1 + 1;
|
|
|
|
assert(ievent1->mask & IN_DELETE_SELF);
|
|
assert(ievent2->mask & IN_IGNORED);
|
|
UNUSED(ievent2);
|
|
|
|
assert(read(fd_inotify, buffer, buffer_size) == -1 && errno == EAGAIN); // no more events
|
|
return 0;
|
|
}
|
|
|
|
void sigalarm(int signo, siginfo_t *info, void *context)
|
|
{
|
|
assert(signo == SIGALRM);
|
|
UNUSED(signo);
|
|
UNUSED(context);
|
|
|
|
if (info->si_code == SI_TIMER) {
|
|
assert(info->si_value.sival_ptr == &id_timer);
|
|
timer_expired = true;
|
|
}
|
|
|
|
switch (timer_action) {
|
|
case TERMINATE_UNL0KR:
|
|
if (kill(pid_unl0kr, SIGTERM) == 0) {
|
|
struct itimerspec spec;
|
|
spec.it_interval.tv_sec = 0;
|
|
spec.it_interval.tv_nsec = 0;
|
|
spec.it_value.tv_sec = 5;
|
|
spec.it_value.tv_nsec = 0;
|
|
timer_settime(id_timer, 0, &spec, NULL);
|
|
timer_action = KILL_UNL0KR;
|
|
}
|
|
break;
|
|
case KILL_UNL0KR:
|
|
kill(pid_unl0kr, SIGKILL);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void sigchild(int signo, siginfo_t *info, void *context)
|
|
{
|
|
assert(signo == SIGCHLD);
|
|
UNUSED(signo);
|
|
UNUSED(context);
|
|
|
|
if (pid_unl0kr == 0)
|
|
return;
|
|
|
|
assert(info->si_pid == pid_unl0kr);
|
|
|
|
if (info->si_code == CLD_EXITED ||
|
|
info->si_code == CLD_KILLED ||
|
|
info->si_code == CLD_DUMPED ) {
|
|
unl0kr_exited = true;
|
|
}
|
|
}
|
|
|
|
int event_loop(pid_t pid)
|
|
{
|
|
int ret = 0;
|
|
int r;
|
|
|
|
if (request.not_after != 0) {
|
|
struct itimerspec spec;
|
|
spec.it_interval.tv_sec = 0;
|
|
spec.it_interval.tv_nsec = 0;
|
|
spec.it_value.tv_sec = request.not_after / 1000000;
|
|
spec.it_value.tv_nsec = (request.not_after % 1000000) * 1000;
|
|
r = timer_settime(id_timer, TIMER_ABSTIME, &spec, NULL);
|
|
if (r == -1)
|
|
perror("timer_settime() is failed");
|
|
}
|
|
|
|
struct epoll_event event;
|
|
pid_unl0kr = pid;
|
|
timer_action = TERMINATE_UNL0KR;
|
|
timer_expired = false;
|
|
unl0kr_exited = false;
|
|
|
|
sigset_t sigmask;
|
|
sigemptyset(&sigmask);
|
|
|
|
for (;;) {
|
|
r = epoll_pwait(fd_epoll, &event, 1, -1, &sigmask);
|
|
if (r == -1) {
|
|
if (errno != EINTR) {
|
|
ret = errno;
|
|
perror("epoll_pwait() is failed");
|
|
break;
|
|
}
|
|
|
|
if (unl0kr_exited)
|
|
break;
|
|
|
|
if (timer_expired && ret != ECANCELED) {
|
|
ret = ETIME;
|
|
fprintf(stderr, "The request has expired\n");
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
r = process_inotify_events();
|
|
if (r != 0) {
|
|
ret = errno;
|
|
break;
|
|
}
|
|
|
|
ret = ECANCELED;
|
|
fprintf(stderr, "The request was cancelled\n");
|
|
|
|
if (timer_expired)
|
|
continue;
|
|
|
|
if (request.not_after != 0) {
|
|
struct itimerspec spec;
|
|
spec.it_interval.tv_sec = 0;
|
|
spec.it_interval.tv_nsec = 0;
|
|
spec.it_value.tv_sec = 0;
|
|
spec.it_value.tv_nsec = 0;
|
|
r = timer_settime(id_timer, 0, &spec, NULL);
|
|
if (r == -1)
|
|
perror("Disarming the timer is failed");
|
|
}
|
|
|
|
r = raise(SIGALRM);
|
|
if (r != 0) {
|
|
ret = errno;
|
|
perror("raise() is failed");
|
|
break;
|
|
}
|
|
}
|
|
|
|
timer_action = NO_ACTION;
|
|
pid_unl0kr = 0;
|
|
|
|
/* Stop the timer unconditionally because it can be armed by sigalarm() */
|
|
struct itimerspec spec;
|
|
spec.it_interval.tv_sec = 0;
|
|
spec.it_interval.tv_nsec = 0;
|
|
spec.it_value.tv_sec = 0;
|
|
spec.it_value.tv_nsec = 0;
|
|
r = timer_settime(id_timer, 0, &spec, NULL);
|
|
if (r == -1)
|
|
perror("Disarming the timer is failed");
|
|
|
|
return ret;
|
|
}
|
|
|
|
int exec_unl0kr(char** ret_password)
|
|
{
|
|
int ret = 0;
|
|
int r;
|
|
|
|
int fd_pipe[2];
|
|
if (pipe(fd_pipe) != 0) {
|
|
ret = errno;
|
|
perror("Can't create a pipe");
|
|
return ret;
|
|
}
|
|
|
|
sigset_t used_signals;
|
|
sigemptyset(&used_signals);
|
|
sigaddset(&used_signals, SIGCHLD);
|
|
sigaddset(&used_signals, SIGALRM);
|
|
|
|
r = sigprocmask(SIG_BLOCK, &used_signals, NULL);
|
|
if (r == -1) {
|
|
ret = errno;
|
|
perror("sigprocmask(SIG_BLOCK) is failed");
|
|
goto exit1;
|
|
}
|
|
|
|
pid_t pid = fork();
|
|
if (pid == -1) {
|
|
ret = errno;
|
|
perror("fork() is failed");
|
|
goto exit2;
|
|
}
|
|
if (pid == 0) {
|
|
/* Child */
|
|
close(fd_pipe[0]);
|
|
|
|
if (dup2(fd_pipe[1], STDOUT_FILENO) == -1) {
|
|
perror("dup2() is failed");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
char* argv[5];
|
|
int argc = 2;
|
|
argv[0] = UNL0KR_BINARY;
|
|
argv[1] = "-n";
|
|
if (request.message) {
|
|
argv[2] = "-m";
|
|
argv[3] = request.message;
|
|
argc += 2;
|
|
}
|
|
argv[argc] = NULL;
|
|
|
|
execv(UNL0KR_BINARY, argv);
|
|
|
|
perror("exec() is failed");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
/* Parent */
|
|
r = event_loop(pid);
|
|
|
|
int status;
|
|
if (waitpid(pid, &status, 0) == -1) {
|
|
ret = errno;
|
|
perror("waitpid() is failed");
|
|
goto exit2;
|
|
}
|
|
|
|
if (r != 0) {
|
|
ret = r;
|
|
goto exit2;
|
|
}
|
|
|
|
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
|
|
ret = ECHILD;
|
|
fprintf(stderr, "unl0kr is failed\n");
|
|
goto exit2;
|
|
}
|
|
|
|
int password_size;
|
|
if (ioctl(fd_pipe[0], FIONREAD, &password_size) == -1) {
|
|
ret = errno;
|
|
perror("ioctl() is failed");
|
|
goto exit2;
|
|
}
|
|
|
|
char* password = malloc(1 + password_size + 1);
|
|
if (!password) {
|
|
ret = ENOMEM;
|
|
fprintf(stderr, "Out of memory\n");
|
|
goto exit2;
|
|
}
|
|
|
|
password[0] = '+';
|
|
if (password_size != 0) {
|
|
password_size = read(fd_pipe[0], password + 1, password_size);
|
|
if (password_size == -1) {
|
|
ret = errno;
|
|
perror("read() is failed");
|
|
free(password);
|
|
goto exit2;
|
|
}
|
|
}
|
|
password[1 + password_size] = 0;
|
|
|
|
*ret_password = password;
|
|
exit2:
|
|
r = sigprocmask(SIG_UNBLOCK, &used_signals, NULL);
|
|
if (r == -1)
|
|
perror("sigprocmask(SIG_UNBLOCK) is failed");
|
|
exit1:
|
|
close(fd_pipe[0]);
|
|
close(fd_pipe[1]);
|
|
return ret;
|
|
}
|
|
|
|
int wait_for_file_removed()
|
|
{
|
|
struct epoll_event event;
|
|
|
|
int r = epoll_wait(fd_epoll, &event, 1, 20000);
|
|
if (r == -1) {
|
|
int ret = errno;
|
|
perror("epoll_wait() is failed");
|
|
return ret;
|
|
} else if (r == 0) {
|
|
fprintf(stderr, "The file '%s' was not removed as expected, exiting.\n", request.file);
|
|
return ETIME;
|
|
}
|
|
|
|
return process_inotify_events();
|
|
}
|
|
|
|
int main()
|
|
{
|
|
int exit_code = EXIT_SUCCESS;
|
|
int r;
|
|
|
|
Request_init(&request);
|
|
|
|
fd_epoll = epoll_create1(EPOLL_CLOEXEC);
|
|
if (fd_epoll == -1) {
|
|
perror("epoll_create1() is failed");
|
|
exit_code = EXIT_FAILURE;
|
|
goto exit1;
|
|
}
|
|
|
|
fd_inotify = inotify_init1(IN_NONBLOCK|IN_CLOEXEC);
|
|
if (fd_inotify == -1) {
|
|
perror("inotify_init1() is failed");
|
|
exit_code = EXIT_FAILURE;
|
|
goto exit2;
|
|
}
|
|
|
|
struct epoll_event epevent_inotify;
|
|
epevent_inotify.events = EPOLLIN|EPOLLET;
|
|
epevent_inotify.data.fd = fd_inotify;
|
|
|
|
r = epoll_ctl(fd_epoll, EPOLL_CTL_ADD, fd_inotify, &epevent_inotify);
|
|
if (r == -1) {
|
|
perror("epoll_ctl() is failed");
|
|
exit_code = EXIT_FAILURE;
|
|
goto exit3;
|
|
}
|
|
|
|
struct sigevent sigevent_timer;
|
|
sigevent_timer.sigev_notify = SIGEV_SIGNAL;
|
|
sigevent_timer.sigev_signo = SIGALRM;
|
|
sigevent_timer.sigev_value.sival_ptr = &id_timer;
|
|
|
|
r = timer_create(CLOCK_MONOTONIC, &sigevent_timer, &id_timer);
|
|
if (r == -1) {
|
|
perror("timer_create() is failed");
|
|
exit_code = EXIT_FAILURE;
|
|
goto exit3;
|
|
}
|
|
|
|
struct sigaction sigaction_alarm;
|
|
sigaction_alarm.sa_sigaction = sigalarm;
|
|
sigaction_alarm.sa_flags = SA_SIGINFO;
|
|
sigemptyset(&sigaction_alarm.sa_mask);
|
|
sigaddset(&sigaction_alarm.sa_mask, SIGCHLD);
|
|
|
|
r = sigaction(SIGALRM, &sigaction_alarm, NULL);
|
|
if (r == -1) {
|
|
perror("sigaction() for SIGALRM is failed");
|
|
exit_code = EXIT_FAILURE;
|
|
goto exit4;
|
|
}
|
|
|
|
struct sigaction sigaction_child;
|
|
sigaction_child.sa_sigaction = sigchild;
|
|
sigaction_child.sa_flags = SA_SIGINFO | SA_NOCLDSTOP;
|
|
sigemptyset(&sigaction_child.sa_mask);
|
|
sigaddset(&sigaction_child.sa_mask, SIGALRM);
|
|
|
|
r = sigaction(SIGCHLD, &sigaction_child, NULL);
|
|
if (r == -1) {
|
|
perror("sigaction() for SIGCHLD is failed");
|
|
exit_code = EXIT_FAILURE;
|
|
goto exit4;
|
|
}
|
|
|
|
for (;;) {
|
|
char* file;
|
|
r = find_request(&file);
|
|
if (r != 0) {
|
|
if (r != ENOENT)
|
|
exit_code = EXIT_FAILURE;
|
|
break;
|
|
}
|
|
|
|
int wd_inotify = inotify_add_watch(fd_inotify, file, IN_DELETE_SELF | IN_DONT_FOLLOW);
|
|
if (wd_inotify == -1) {
|
|
fprintf(stderr, "inotify_add_watch() is failed for '%s': %s\n", file, strerror(errno));
|
|
free(file);
|
|
exit_code = EXIT_FAILURE;
|
|
break;
|
|
}
|
|
|
|
Request_reset(&request);
|
|
request.file = file;
|
|
|
|
r = ini_parse(file, ini_parser, &request);
|
|
if (r < 0) {
|
|
fprintf(stderr, "The file '%s' can't be parsed: %d\n", request.file, r);
|
|
exit_code = EXIT_FAILURE;
|
|
break;
|
|
}
|
|
|
|
if (request.pid != 0) {
|
|
r = kill(request.pid, 0);
|
|
if (r == -1 && errno == ESRCH) {
|
|
fprintf(stderr, "The file '%s' contains invalid PID, removing.\n", request.file);
|
|
remove(request.file);
|
|
goto loop1;
|
|
}
|
|
}
|
|
|
|
if (!request.socket) {
|
|
fprintf(stderr, "The file '%s' doesn't contain a socket, waiting for removal.\n", request.file);
|
|
goto loop1;
|
|
}
|
|
|
|
if (request.not_after != 0) {
|
|
struct timespec ts;
|
|
r = clock_gettime(CLOCK_MONOTONIC, &ts);
|
|
if (r == -1) {
|
|
perror("clock_gettime() is failed");
|
|
exit_code = EXIT_FAILURE;
|
|
break;
|
|
}
|
|
uint64_t now = ts.tv_sec * 1000000 + ts.tv_nsec / 1000;
|
|
if (request.not_after <= now) {
|
|
fprintf(stderr, "The request '%s' expired, waiting for removal.\n", request.file);
|
|
goto loop1;
|
|
}
|
|
}
|
|
|
|
char* password;
|
|
r = exec_unl0kr(&password);
|
|
if (r != 0) {
|
|
if (r == ECANCELED)
|
|
continue;
|
|
else if (r == ETIME) {
|
|
send_password("-");
|
|
goto loop1;
|
|
} else {
|
|
exit_code = EXIT_FAILURE;
|
|
break;
|
|
}
|
|
}
|
|
|
|
r = send_password(password);
|
|
erase_and_free(password);
|
|
if (r != 0) {
|
|
exit_code = EXIT_FAILURE;
|
|
break;
|
|
}
|
|
|
|
loop1:
|
|
r = wait_for_file_removed();
|
|
if (r != 0) {
|
|
exit_code = EXIT_FAILURE;
|
|
break;
|
|
}
|
|
}
|
|
|
|
exit4:
|
|
timer_delete(id_timer);
|
|
exit3:
|
|
close(fd_inotify);
|
|
exit2:
|
|
close(fd_epoll);
|
|
exit1:
|
|
Request_free(&request);
|
|
return exit_code;
|
|
}
|