Skip to content

Commit

Permalink
Provides recurring metric reports at a specified interval.
Browse files Browse the repository at this point in the history
Uses a nonblocking timerfd alongside pthread_cond_timedwait to create timed reporting intervals. The interval is configured through the report_interval key in fapolicyd.conf. This value represents the length of interval in seconds between reports. If set to 0 reporting intervals are disabled, this is the default.

Reports are written to the fapolicyd.state file. The output format and contents of this file are unchanged by this PR. This is the same file that was written at shutdown and upon receiving SIGUSR1. The signal handling function remains unchanged.

Error handling is based around disabling interval reporting to preserve the decision thread. When an unrecoverable reporting error occurs the interval reports are disabled and the decision thread will use the non-interval report handler for the remainder of it's execution.

The structuring of the reporting loop gives priority to the fan handling loop. When a reporting interval expires, the fan handler runs prior to writing the report to prioritize the fan handling and also to ensure the cache stats are as recent as possible. Also the reporting loop is only entered when no fan events have been recorded.

This PR also restructure the initialization of the decision thread and reporting loop to ensure a fresh fapolicyd.state file when fapolicyd starts. The previous behavior was that the file from the last shutdown was still present until either a SIGUSR1 or shutdown.
  • Loading branch information
jw3 committed Apr 23, 2024
1 parent 929eb69 commit c102878
Show file tree
Hide file tree
Showing 7 changed files with 121 additions and 11 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,11 @@ had a common denominator. Some primes you might consider for cache size are:
1021, 1549, 2039, 4099, 6143, 8191, 10243, 12281, 16381, 20483, 24571,
28669, 32687, 40961, 49157, 57347, 65353, etc.

This report can be scheduled to be written periodically by setting the
configuration option `report_interval`. This option is set to `0` by default
which disables the reporting interval. A positive value for this option
specifies the number of seconds to wait between reports.

Also, it should be mentioned that the more rules in the policy, the more
rules it will have to iterate over to make a decision. As for the system
performance impact, this is very workload dependent. For a typical desktop
Expand Down
2 changes: 1 addition & 1 deletion doc/fapolicyd-cli.8
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Opens fapolicyd.conf and parses it to see if there are any syntax errors in the
Check the PATH environmental variable against the trustdb to look for file not in the trustdb which could cause problems at run time.
.TP
.B \-\-check-status
Dump the daemon's internal performance statistics.
Dump the daemon's internal performance statistics. See also the fapolicyd.conf option \fBreport_interval\fP.
.TP
.B \-\-check-trustdb
Check the trustdb against the files on disk to look for mismatches that will cause problems at run time.
Expand Down
4 changes: 4 additions & 0 deletions doc/fapolicyd.conf.5
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ The option set to 1 forces the daemon to work only with SHA256 hashes. This is u
.B allow_filesystem_mark
When this option is set to 1, it allows fapolicyd to monitor file access events on the underlying file system when they are bind mounted or are overlayed (e.g. the overlayfs). Normally they block fapolicyd from seeing events on the underlying file systems. This may or may not be desirable. For example, you might start seeing containers accessing things outside of the container but there is no source of trust for the container. In that case you probably do not want to see access from the container. Or maybe you do not use containers but want to control anything run by systemd-run when dynamic users are allowed. In that case you probably want to turn it on. Not all kernel's support this option. Therefore the default value is 0.

.TP
.B report_interval
This option specifies a reporting interval, measured in seconds, which fapolicyd uses to schedule a recurring dump of internal performance statistics to the \fBfapolicyd.state\fP file. The default value of 0 disables interval reporting.

.SH "SEE ALSO"
.BR fapolicyd (8),
.BR fapolicyd-cli (8)
Expand Down
1 change: 1 addition & 0 deletions init/fapolicyd.conf
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ integrity = none
syslog_format = rule,dec,perm,auid,pid,exe,:,path,ftype,trust
rpm_sha256_only = 0
allow_filesystem_mark = 0
report_interval = 0
109 changes: 99 additions & 10 deletions src/daemon/notify.c
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
#include <pthread.h>
#include <signal.h>
#include <sys/syscall.h>
#include <sys/timerfd.h>
#include <stdatomic.h>
#include "policy.h"
#include "event.h"
Expand All @@ -56,11 +57,14 @@ static pthread_t deadmans_switch_thread;
static pthread_mutexattr_t decision_lock_attr;
static pthread_mutex_t decision_lock;
static pthread_cond_t do_decision;
static pthread_condattr_t rpt_timer_attr;
static volatile atomic_bool events_ready;
static volatile atomic_int alive = 1;
static int fd = -1;
static int rpt_timer_fd = -1;
static uint64_t mask;
static unsigned int mark_flag;
static unsigned int rpt_interval;

// External functions
void do_stat_report(FILE *f, int shutdown);
Expand Down Expand Up @@ -113,8 +117,11 @@ int init_fanotify(const conf_t *conf, mlist *m)
pthread_mutexattr_settype(&decision_lock_attr,
PTHREAD_MUTEX_ERRORCHECK);
pthread_mutex_init(&decision_lock, &decision_lock_attr);
pthread_cond_init(&do_decision, NULL);
pthread_condattr_init(&rpt_timer_attr);
pthread_condattr_setclock(&rpt_timer_attr, CLOCK_MONOTONIC);
pthread_cond_init(&do_decision, &rpt_timer_attr);
events_ready = 0;
rpt_interval = conf->report_interval;
pthread_create(&decision_thread, NULL, decision_thread_main, NULL);
pthread_create(&deadmans_switch_thread, NULL,
deadmans_switch_thread_main, NULL);
Expand Down Expand Up @@ -238,6 +245,7 @@ void shutdown_fanotify(mlist *m)

// Clean up
q_close(q);
close(rpt_timer_fd);

// Report results
msg(LOG_DEBUG, "Allowed accesses: %lu", getAllowed());
Expand Down Expand Up @@ -297,6 +305,42 @@ static void *deadmans_switch_thread_main(void *arg)
return NULL;
}

// disable interval reports, used on unrecoverable errors
static void rpt_disable(const char *why)
{
rpt_interval = 0;
close(rpt_timer_fd);
msg(LOG_WARNING, "interval reports disabled; %s", why);
}

// initialize interval reporting
static void rpt_init(struct timespec *t)
{
rpt_timer_fd = timerfd_create(CLOCK_REALTIME, TFD_NONBLOCK);
if (rpt_timer_fd == -1) {
rpt_disable("timer create failure");
} else {
t->tv_nsec = t->tv_sec = 0;
struct itimerspec rpt_deadline = { {rpt_interval, 0}, {rpt_interval, 0} };
if (timerfd_settime(rpt_timer_fd, TFD_TIMER_ABSTIME, &rpt_deadline, NULL) == -1) {
// settime errors are unrecoverable
rpt_disable(strerror(errno));
} else {
msg(LOG_INFO, "interval reports configured; %us", rpt_interval);
}
}
}

// write a stat report to file at the standard location
static void rpt_write(void)
{
FILE *f = fopen(STAT_REPORT, "w");
if (f) {
do_stat_report(f, 0);
fclose(f);
}
}

static void *decision_thread_main(void *arg)
{
sigset_t sigs;
Expand All @@ -310,27 +354,72 @@ static void *decision_thread_main(void *arg)
sigaddset(&sigs, SIGQUIT);
pthread_sigmask(SIG_SETMASK, &sigs, NULL);

// interval reporting state
int rpt_is_stale = 0;
struct timespec rpt_timeout;

// if an interval was configured, reports are enabled
if (rpt_interval) {
rpt_init(&rpt_timeout);
}

// start with a fresh report
run_stats = 1;

while (!stop) {
int len;
struct fanotify_event_metadata metadata[MAX_EVENTS];

pthread_mutex_lock(&decision_lock);
while (get_ready() == 0) {
pthread_cond_wait(&do_decision, &decision_lock);
// if an interval has been configured
if (rpt_interval) {
// check for timer expirations
uint64_t expired = 0;
if (read(rpt_timer_fd, &expired, sizeof(uint64_t)) == -1) {
// EAGAIN expected with nonblocking timer
// any other error is unrecoverable
if (errno != EAGAIN) {
rpt_disable(strerror(errno));
continue;
}
}
// if the timer expired or stats were explicitly requested
if (expired || run_stats) {
// write a new report only when one of
// 1. new events observed since last report
// 2. explicitly requested with run_stats
if (rpt_is_stale || run_stats) {
rpt_write();
run_stats = 0;
rpt_is_stale = 0;
}
// adjust the pthread timeout to a full interval from now
if (clock_gettime(CLOCK_MONOTONIC, &rpt_timeout)) {
// gettime errors are unrecoverable
rpt_disable("clock failure");
continue;
}
rpt_timeout.tv_sec += rpt_interval;
}
// await a fan event, timing out at the next report interval
pthread_cond_timedwait(&do_decision, &decision_lock, &rpt_timeout);
} else {
if (run_stats) {
rpt_write();
run_stats = 0;
}
// no interval reports, so await a fan event indefinitely
pthread_cond_wait(&do_decision, &decision_lock);
}

if (stop) {
pthread_mutex_unlock(&decision_lock);
return NULL;
}
if (run_stats) {
FILE *f = fopen(STAT_REPORT, "w");
if (f) {
do_stat_report(f, 0);
fclose(f);
}
run_stats = 0;
}
}
alive = 1;
rpt_is_stale = 1;

// Grab up to MAX_EVENTS events while locked
unsigned i = 0;
Expand Down
1 change: 1 addition & 0 deletions src/library/conf.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ typedef struct conf
const char *syslog_format;
unsigned int rpm_sha256_only;
unsigned int allow_filesystem_mark;
unsigned int report_interval;
} conf_t;

#endif
10 changes: 10 additions & 0 deletions src/library/daemon-config.c
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ static int rpm_sha256_only_parser(const struct nv_pair *nv, int line,
conf_t *config);
static int fs_mark_parser(const struct nv_pair *nv, int line,
conf_t *config);
static int report_interval_parser(const struct nv_pair *nv, int line,
conf_t *config);

static const struct kw_pair keywords[] =
{
Expand All @@ -113,6 +115,7 @@ static const struct kw_pair keywords[] =
{"syslog_format", syslog_format_parser },
{"rpm_sha256_only", rpm_sha256_only_parser},
{"allow_filesystem_mark", fs_mark_parser },
{"report_interval", report_interval_parser },
{ NULL, NULL }
};

Expand Down Expand Up @@ -142,6 +145,7 @@ static void clear_daemon_config(conf_t *config)
strdup("rule,dec,perm,auid,pid,exe,:,path,ftype");
config->rpm_sha256_only = 0;
config->allow_filesystem_mark = 0;
config->report_interval = 0;
}

int load_daemon_config(conf_t *config)
Expand Down Expand Up @@ -529,6 +533,12 @@ static int watch_fs_parser(const struct nv_pair *nv, int line,
return 1;
}

static int report_interval_parser(const struct nv_pair *nv, int line,
conf_t *config)
{
return unsigned_int_parser(&(config->report_interval), nv->value, line);
}


static int trust_parser(const struct nv_pair *nv, int line,
conf_t *config)
Expand Down

0 comments on commit c102878

Please sign in to comment.