/*
 * fapolicyd.c - Main file for the program
 * Copyright (c) 2016-26 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>
 *   Radovan Sroka <rsroka@redhat.com>
 */

#include "config.h"
#include <stdio.h>
#include <poll.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/resource.h>
#include <ctype.h>
#include <cap-ng.h>
#include <sys/file.h>
#include <sys/stat.h>	   /* umask */
#include <sys/signalfd.h>
#include <seccomp.h>
#include <stdbool.h>
#include <stdatomic.h>
#include <limits.h>        /* PATH_MAX */
#include <locale.h>
#include <pthread.h>
#ifdef HAVE_MALLINFO2
#include <malloc.h>
#endif
#include "fanotify-fs-error.h"
#include "notify.h"
#include "attr-lookup-metrics.h"
#include "policy.h"
#include "event.h"
#include "escape.h"
#include "fd-fgets.h"
#include "file.h"
#include "database.h"
#include "message.h"
#include "daemon-config.h"
#include "decision-config.h"
#include "decision-timing.h"
#include "conf.h"
#include "queue.h"
#include "gcc-attributes.h"
#include "avl.h"
#include "paths.h"
#include "string-util.h"
#include "filter.h"
#include "state-report.h"
#include "systemd-notify.h"

// Global program variables
unsigned int debug_mode = 0;
static unsigned int foreground_mode;
const char* mounts = MOUNTS_FILE;

// Signal handler notifications
atomic_bool stop = false, hup = false, run_stats = false;
atomic_uint signal_report_requests;
atomic_uint signal_report_intent;
atomic_uint signal_report_reset_requests;
atomic_int signal_report_reset_request_pid = -1;
atomic_int signal_report_reset_request_uid = -1;

// Global configuration state
conf_t config;
// This holds info about all file systems to watch
struct fs_avl {
	avl_tree_t index;
};
// This is the data about a specific file system to watch
typedef struct fs_data {
	avl_t avl;        // This has to be first
	const char *fs_name;
} fs_data_t;
static struct fs_avl filesystems;
static struct fs_avl ignored_mounts;

// List of mounts being watched
static mlist *m = NULL;
static pthread_mutex_t mlist_lock = PTHREAD_MUTEX_INITIALIZER;
static int pid_file_fd = -1;
static bool pid_file_created;
static bool pid_file_written;

// Reconfiguration
static atomic_bool reconfig_running = false;
// Mount handling
static atomic_bool mounts_running = false;

static __noreturn void usage(void);

/*
 * install_syscall_filter - install daemon seccomp restrictions.
 *
 * Package trust snapshots can be rebuilt after startup. The RPM database
 * uses SQLite locks that are scoped to a process ID. Loading rpmdb in
 * fapolicyd, or in one of its threads, lets a close of an rpmdb descriptor
 * release that process's locks and can corrupt rpmdb during an RPM
 * transaction. The fixed libexec RPM loader therefore runs in a separate
 * process and returns a memfd snapshot.
 *
 * posix_spawn children inherit the daemon's seccomp filter, so builds with
 * a package loader must keep execve available for that fixed loader.
 *
 * Returns nothing.
 */
static void install_syscall_filter(void)
{
	scmp_filter_ctx ctx;
	int rc = -1;

	ctx = seccomp_init(SCMP_ACT_ALLOW);
	if (ctx == NULL)
		goto err_out;
#if !defined(USE_RPM) && !defined(USE_DEB)
	rc = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EACCES),
				SCMP_SYS(execve), 0);
	if (rc < 0)
		goto err_out;

# ifdef HAVE_FEXECVE
#  ifdef __NR_fexecve
	rc = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EACCES),
				SCMP_SYS(fexecve), 0);
	if (rc < 0)
		goto err_out;
#  endif
# endif
#endif
	rc = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EIO),
				SCMP_SYS(sendfile), 0);
	if (rc < 0)
		goto err_out;

	rc = seccomp_load(ctx);
err_out:
	if (rc < 0)
		msg(LOG_ERR, "Failed installing seccomp filter");
	seccomp_release(ctx);
}


static int cmp_fs(void *a, void *b)
{
	return strcmp(((fs_data_t *)a)->fs_name, ((fs_data_t *)b)->fs_name);
}


/*
 * free_filesystem - release storage for a single AVL entry.
 * @s: filesystem data being destroyed.
 * Returns nothing.
 */
static void free_filesystem(fs_data_t *s)
{
	free((void *)s->fs_name);
	free((void *)s);
}

/*
 * destroy_filesystem - remove the first node from an AVL tree.
 * @list: tree holding filesystem style strings.
 * Returns nothing.
 */
static void destroy_filesystem(struct fs_avl *list)
{
	avl_t *cur = list->index.root;

	fs_data_t *tmp =(fs_data_t *)avl_remove(&list->index, cur);
	if ((avl_t *)tmp != cur)
		msg(LOG_DEBUG, "string list: removal of invalid node");
	free_filesystem(tmp);
}

/*
 * destroy_fs_list - free every node from an AVL tree.
 * @list: tree that should be cleared.
 * Returns nothing.
 */
static void destroy_fs_list(struct fs_avl *list)
{
	while (list->index.root)
		destroy_filesystem(list);
}

/*
 * add_filesystem - insert a string into an AVL tree.
 * @list: tree receiving the entry.
 * @f: filesystem data to insert.
 * Returns: 0 on failure, 1 if the item is in the tree.
 */
static int add_filesystem(struct fs_avl *list, fs_data_t *f)
{
	fs_data_t *tmp=(fs_data_t *)avl_insert(&list->index,(avl_t *)(f));
	if (tmp) {
		if (tmp != f) {
			// already in the tree, delete the current item
			msg(LOG_DEBUG, "fs_list: duplicate filesystem found");
			free_filesystem(f);
		}
		return 1;
	}
	return 0;
}

/*
 * new_filesystem - allocate and add a string into an AVL tree.
 * @list: tree receiving the new entry.
 * @fs: string representing the filesystem or mount point.
 * Returns 1 on success and 0 on failure.
 */
static int new_filesystem(struct fs_avl *list, const char *fs)
{
	fs_data_t *tmp = malloc(sizeof(fs_data_t));
	if (tmp) {
		tmp->fs_name = fs ? strdup(fs) : strdup("");
		if (tmp->fs_name == NULL) {
			free(tmp);
			return 0;
		}
		if (add_filesystem(list, tmp) == 0) {
			free((void *)tmp->fs_name);
			free(tmp);
			return 0;
		}
		return 1;
	}
	return 0;
}

/*
 * find_filesystem - search the supplied AVL tree for a string.
 * @list: tree that should be searched.
 * @f: string to locate.
 * Returns a pointer to the stored entry or NULL if not found.
 */
static fs_data_t *find_filesystem(struct fs_avl *list, const char *f)
{
	fs_data_t tmp;

	tmp.fs_name = f;
	return (fs_data_t *)avl_search(&list->index, (avl_t *) &tmp);
}

/*
 * add_ignore_mount_entry - callback that validates and stores ignored mounts.
 * @mount: trimmed ignore_mounts entry.
 * @unused: unused context pointer.
 * Returns 0 to continue iterating entries.
 */
static int add_ignore_mount_entry(const char *mount,
	void *unused __attribute__ ((unused)))
{
	const char *warning;
	int rc;

	rc = check_ignore_mount_warning(mounts, mount, &warning);
	if (warning)
		msg(LOG_ERR, warning, mount, mounts);

	if (rc == 1) {
		if (!new_filesystem(&ignored_mounts, mount))
			msg(LOG_ERR, "Cannot store ignore_mounts entry %s", mount);
	}

	return 0;
}
/*
 * init_ignore_mounts - create the ignored mount AVL tree.
 * @ignore_list: comma separated list of mount points to ignore.
 * Returns nothing.
 */
static void init_ignore_mounts(const char *ignore_list)
{
	avl_init(&ignored_mounts.index, cmp_fs);

	if (ignore_list == NULL)
		return;

	if (iterate_ignore_mounts(ignore_list, add_ignore_mount_entry, NULL)) {
		msg(LOG_ERR, "Cannot duplicate ignore_mounts list");
		return;
	}

	if (ignored_mounts.index.root == NULL) {
		free((void *)config.ignore_mounts);
		config.ignore_mounts = NULL;
	}
}
/*
 * mount_is_ignored - check if a mount point is in the ignore list.
 * @point: mount point path.
 * Returns 1 when the mount point should be skipped and 0 otherwise.
 */
static int mount_is_ignored(const char *point)
{
	return find_filesystem(&ignored_mounts, point) ? 1 : 0;
}

/*
 * init_fs_list - create the filesystem type AVL tree.
 * @watch_fs: comma separated list of filesystem types to watch.
 * Returns 0 on success or 1 when the list cannot be fully initialized.
 */
static int init_fs_list(const char *watch_fs)
{
	char *ptr, *saved, *tmp;

	if (watch_fs == NULL) {
		msg(LOG_ERR, "File systems to watch is empty");
		return 1;
	}
	avl_init(&filesystems.index, cmp_fs);

	// Now parse up list and push into avl
	tmp = strdup(watch_fs);

	if (tmp == NULL) {
		msg(LOG_ERR, "Cannot duplicate watch_fs list");
		return 1;
	}

	ptr = strtok_r(tmp, ",", &saved);
	while (ptr) {
		if (!new_filesystem(&filesystems, ptr)) {
			msg(LOG_ERR, "Cannot store watch_fs entry %s", ptr);
			free(tmp);
			destroy_fs_list(&filesystems);
			return 1;
		}
		ptr = strtok_r(NULL, ",", &saved);
	}
	free(tmp);
	if (filesystems.index.root == NULL) {
		msg(LOG_ERR, "File systems to watch is empty");
		return 1;
	}
	return 0;
}

static void term_handler(int sig __attribute__((unused)))
{
	stop = true;
	/*
	 * Let the dispatcher finish its current nonblocking batch before
	 * workers are woken for shutdown. Otherwise a worker can drain and
	 * exit while a signal-interrupted handle_events() resumes and
	 * enqueues a later record.
	 *
	 * Reconfiguration checks stop between reload phases. This best-effort
	 * guard makes a deliberately sequenced SIGHUP/SIGTERM from a signal-
	 * authorized process abandon later work, but cannot close a
	 * permission-open race after a check. The normal shutdown path therefore
	 * closes fanotify before it waits for helpers.
	 */
}


/*
 * coredump_handler - release fanotify waiters before fatal termination.
 * @sig: fatal signal that selected this handler.
 *
 * Hardware faults are delivered to the thread that caused them, and abort()
 * raises SIGABRT in the calling thread. They cannot safely be redirected to
 * the main thread: the interrupted thread may hold allocator or daemon locks,
 * and blocking synchronous SIGSEGV/SIGBUS/SIGILL/SIGFPE is undefined.
 *
 * Every thread therefore executes this same minimal handler. Closing the last
 * permission-group descriptor removes its marks and allows outstanding
 * permission events, preventing unrelated processes from remaining blocked
 * while the daemon terminates or writes a core.
 *
 * No allocation, logging, locking, formatted output, or shared data-structure
 * traversal is permitted here. After closing the group, restore the default
 * disposition and raise the signal in this thread so normal fatal-signal and
 * core-dump processing occurs. _exit() is only a fallback if that fails.
 *
 * Returns only after successfully queuing @sig for default handling.
 */
static void coredump_handler(int sig)
{
	struct sigaction action = {
		.sa_handler = SIG_DFL,
	};

	fanotify_close_on_fatal_signal();
	sigemptyset(&action.sa_mask);
	if (sigaction(sig, &action, NULL) || raise(sig))
		_exit(128 + sig);
}


static void hup_handler(int sig __attribute__((unused)))
{
	hup = true;
}

/*
 * reload_configuration - refresh runtime configuration settings.
 * @void: no arguments are required.
 * Returns 0 when the configuration was reloaded, non-zero otherwise.
 */
static int reload_configuration(void)
{
	conf_t new_config;
	reset_strategy_t reset_strategy;
	timing_collection_t timing_collection;

	if (load_daemon_config(&new_config)) {
		free_daemon_config(&new_config);
		msg(LOG_ERR, "Failed reloading daemon configuration");
		return 1;
	}

	if (decision_config_publish(&new_config)) {
		free_daemon_config(&new_config);
		msg(LOG_ERR, "Failed publishing decision configuration");
		return 1;
	}

	__atomic_store_n(&config.permissive, new_config.permissive,
			 __ATOMIC_RELAXED);

	if (setpriority(PRIO_PROCESS, 0, -(int)new_config.nice_val) == -1)
		msg(LOG_WARNING, "Couldn't adjust priority (%s)",
		strerror(errno));
	config.nice_val = new_config.nice_val;

	config.do_stat_report = new_config.do_stat_report;
	config.detailed_report = new_config.detailed_report;
	reset_strategy = __atomic_load_n(&config.reset_strategy,
					 __ATOMIC_RELAXED);
	if (new_config.reset_strategy != reset_strategy) {
		__atomic_store_n(&config.reset_strategy,
				 new_config.reset_strategy, __ATOMIC_RELAXED);
		state_report_log_reset_strategy(new_config.reset_strategy);
	}
	timing_collection = __atomic_load_n(&config.timing_collection,
					    __ATOMIC_RELAXED);
	if (new_config.timing_collection != timing_collection) {
		__atomic_store_n(&config.timing_collection,
				 new_config.timing_collection,
				 __ATOMIC_RELAXED);
		decision_timing_apply_config(new_config.timing_collection);
		// Let the decision worker restore timing-owned queue metrics.
		if (new_config.timing_collection == TIMING_COLLECTION_OFF)
			nudge_queue();
	}

	if (new_config.integrity != config.integrity) {
		config.integrity = new_config.integrity;
		msg(LOG_INFO, "fapolicyd integrity is %u", config.integrity);
	}

	if (new_config.syslog_format &&
		(!config.syslog_format ||
		 strcmp(new_config.syslog_format, config.syslog_format) != 0)) {
		char *new_syslog = strdup(new_config.syslog_format);
		if (new_syslog) {
			char *old_syslog;

			lock_rule();
			old_syslog = (char *)config.syslog_format;
			config.syslog_format = new_syslog;
			unlock_rule();

			free(old_syslog);
		} else
			msg(LOG_ERR,
				"Failed replacing syslog_format string");
		}

	config.rpm_sha256_only = new_config.rpm_sha256_only;

	if (new_config.trust && !stop) {
		lock_update_thread();
		if (!config.trust || strcmp(new_config.trust, config.trust) != 0) {
			char *new_trust = strdup(new_config.trust);
			if (new_trust) {
				char *old_trust = (char *)config.trust;
				config.trust = new_trust;
				free(old_trust);
			} else
				msg(LOG_ERR,
				    "Failed replacing trust backend list");
		}
		unlock_update_thread();
	}

	/*
	 * Remaining daemon_config fields require restart-time changes:
	 * q_size, decision_threads, subj_cache_size, and obj_cache_size are
	 * consumed when the event queue and caches are created. uid/gid,
	 * allow_filesystem_mark, watch_fs, and ignore_mounts are applied while
	 * fanotify marks are installed. db_max_size fixes the LMDB map when the
	 * database opens, and report_interval is bound to the decision worker's
	 * timer. None of these components support resizing in-place yet, so their
	 * configuration stays static.
	 */

	free_daemon_config(&new_config);

	return 0;
}

static void reconfigure(void)
{
	if (stop)
		return;

	if (reload_configuration())
		msg(LOG_WARNING,
			"Continuing with previous configuration settings");

	if (stop)
		return;

	filter_destroy();
	if (filter_init())
		msg(LOG_ERR, "Failed initializing filter configuration");
	else if (filter_load_file(NULL))
		msg(LOG_ERR, "Failed reloading filter configuration");

	if (stop)
		return;

	set_reload_rules();
	set_reload_trust_database();
}

/*
 * Reconfiguration reload overview
 * ===============================
 *
 * SIGHUP does not replace all runtime state directly in this detached
 * thread. Lightweight daemon configuration fields are refreshed here, then
 * filter, rule, and trust reloads are requested through their normal owners.
 *
 * Rule reload is transactional. The update thread opens the rule file and
 * builds a complete policy snapshot while the current policy remains active.
 * Readers hold references to the snapshot they started with, so reload can
 * parse slow macro/set based policies without parking the decision worker.
 * The new snapshot is published only after rule and syslog parsing succeed.
 * If any step fails, the previous policy snapshot stays active so decisions
 * and syslog fields do not fall back to empty, partial, or stale candidate
 * state.
 *
 * Trust database reload remains independent of policy reload. A failed trust
 * reload is reported by the database layer and does not imply that policy
 * rules were changed.
 */

/*
 * reconfigure_thread_main - run configuration reload outside event loop.
 * @arg: unused pointer.
 * Returns NULL.
 */
static void *reconfigure_thread_main(void *arg  __attribute__ ((unused)))
{
	sigset_t sigs;

	/* This is a worker thread. Don't handle external signals. */
	sigemptyset(&sigs);
	sigaddset(&sigs, SIGTERM);
	sigaddset(&sigs, SIGHUP);
	sigaddset(&sigs, SIGUSR1);
	sigaddset(&sigs, SIGINT);
	sigaddset(&sigs, SIGQUIT);
	pthread_sigmask(SIG_SETMASK, &sigs, NULL);

	reconfigure();
	atomic_store(&reconfig_running, false);
	return NULL;
}

/*
 * maybe_start_reconfigure_thread - start a reconfigure thread when requested
 * and prevent a second one running until first finishes.
 * @void: no arguments are required.
 * Returns nothing.
 */
static void maybe_start_reconfigure_thread(void)
{
	int rc;
	bool expected = false;

	// Make sure one is not runnning since it is detached
	if (!atomic_compare_exchange_strong(&reconfig_running, &expected, true))
		return;

	// OK to start up the thread
	pthread_t tid;
	pthread_attr_t attr;
	pthread_attr_init(&attr);
	pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

	rc = pthread_create(&tid, &attr,
			reconfigure_thread_main, NULL);
	if (rc) {
		msg(LOG_ERR, "Failed starting reconfigure thread (%s)",
			strerror(rc));
		atomic_store(&reconfig_running, false);
	}
	pthread_attr_destroy(&attr);
}

// This is a workaround for https://bugzilla.redhat.com/show_bug.cgi?id=643031
#define UNUSED(x) (void)(x)
#ifdef USE_RPM
extern int rpmsqEnable (int signum, void *handler);
int rpmsqEnable (int signum, void *handler)
{
	UNUSED(signum);
	UNUSED(handler);
	return 0;
}
#endif


/*
 * remove_pid_file - remove the pid file and release this daemon's lock.
 *
 * The advisory lock must remain held until shutdown has completed so a new
 * daemon cannot create a second pid file while this process still runs.
 *
 * Returns nothing.
 */
static void remove_pid_file(void)
{
	if (pid_file_fd == -1)
		return;

	if ((pid_file_created || pid_file_written) &&
	    unlink(pidfile) && errno != ENOENT)
		msg(LOG_WARNING, "Unable to remove pidfile (%s)",
		    strerror(errno));
	close(pid_file_fd);
	pid_file_fd = -1;
	pid_file_created = false;
	pid_file_written = false;
}

/*
 * write_pid_file - write this process ID to an already locked pid file.
 *
 * Returns 0 on success or 1 after releasing the lock on write failure.
 */
static int write_pid_file(void)
{
	int len;
	char val[16];
	size_t written = 0;

	len = snprintf(val, sizeof(val), "%u\n", getpid());
	if (len <= 0 || (size_t)len >= sizeof(val)) {
		msg(LOG_ERR, "Pid error (%s)", strerror(errno));
		remove_pid_file();
		return 1;
	}
	if (pid_file_fd == -1) {
		msg(LOG_ERR, "Pidfile lock was not acquired");
		return 1;
	}
	if (lseek(pid_file_fd, 0, SEEK_SET) == (off_t)-1 ||
	    ftruncate(pid_file_fd, 0)) {
		msg(LOG_ERR, "Unable to write pidfile (%s)",
			strerror(errno));
		goto err_out;
	}
	while (written < (size_t)len) {
		ssize_t rc = write(pid_file_fd, val + written,
				   (size_t)len - written);

		if (rc > 0) {
			written += (size_t)rc;
			continue;
		}
		if (rc < 0 && errno == EINTR)
			continue;
		if (rc == 0)
			errno = EIO;
		msg(LOG_ERR, "Unable to write pidfile (%s)", strerror(errno));
		goto err_out;
	}
	pid_file_written = true;
	return 0;

err_out:
	remove_pid_file();
	return 1;
}


static int become_daemon(void)
{
	int fd;
	pid_t pid;

	pid = fork();
	switch (pid)
	{
		case 0: // Child
			fd = open("/dev/null", O_RDWR);
			if (fd < 0)
				return -1;
			if (dup2(fd, 0) < 0) {
				close(fd);
				return -1;
			}
			if (dup2(fd, 1) < 0) {
				close(fd);
				close(0);
				return -1;
			}
			if (dup2(fd, 2) < 0) {
				close(fd);
				close(0);
				close(1);
				return -1;
			}
			if (fd > 2)
				close(fd);
			chdir("/");
			if (setsid() < 0)
				return -1;
			break;
		case -1:
			return -1;
			break;
		default:	// Parent
			_exit(0);
			break;
	}
	return 0;
}


// Returns 1 if we care about the entry and 0 if we do not
static int check_mount_entry(const char *point, const char *type)
{
	// Skip entries explicitly ignored by configuration
	if (mount_is_ignored(point))
		return 0;

	// Some we know we don't want
	if (strncmp(point, "/sys/", 5) == 0)
		return 0;

	if (find_filesystem(&filesystems, type))
		return 1;
	else
		return 0;
}

/*
 * parse_mount_entry - extract the mount point and filesystem type.
 * @buf: line from the mounts file to parse in place.
 * @point: where to store the mount point token.
 * @type: where to store the filesystem type token.
 *
 * Returns 0 on success or 1 when the line does not contain the first
 * 3 whitespace-delimited fields expected from /proc/mounts.
 */
static int parse_mount_entry(char *buf, char **point, char **type)
{
	char *s = buf;
	size_t len;
	int field = 0;

	*point = NULL;
	*type = NULL;

	while (*s && field < 3) {
		// Skip over any leading field separators before trying
		// to read the next token.
		s += strspn(s, " \t");
		if (*s == '\0' || *s == '\n')
			break;

		// Scan for anything not a space, tab, or newline
		len = strcspn(s, " \t\n");
		if (len == 0)
			break;

		if (field == 1)
			*point = s;
		else if (field == 2)
			*type = s;

		// If we hit the end, we are done
		if (s[len] == '\0' || s[len] == '\n') {
			s[len] = '\0';
			break;
		}

		// terminate the field and increment
		s[len] = '\0';
		s += len + 1;
		field++;
	}

	return (*point && *type) ? 0 : 1;
}

/*
 * read_mount_list - read a complete replacement list of watched mounts.
 * @fd: file descriptor for the mounts file.
 * @next: empty list that receives matching mount paths.
 *
 * The active list is left untouched until this read and every required
 * allocation succeeds. This avoids applying a partial /proc/mounts snapshot.
 *
 * Returns 0 on success or -1 on a read or allocation failure.
 */
static int read_mount_list(int fd, mlist *next)
{
	char buf[PATH_MAX * 2];
	char *point, *type;
	fd_fgets_state_t *st;
	int rc;

	if (lseek(fd, 0, SEEK_SET) == (off_t)-1) {
		msg(LOG_ERR, "Cannot rewind mounts file (%s)", strerror(errno));
		return -1;
	}
	st = fd_fgets_init();
	if (!st) {
		msg(LOG_ERR, "Cannot allocate mounts reader (%s)", strerror(errno));
		return -1;
	}

	for (;;) {
		rc = fd_fgets_r(st, buf, sizeof(buf), fd);
		// Get a line
		if (rc > 0) {
			// We only need the mount point and filesystem type.
			// Note, the returned pointers point into buf.
			if (parse_mount_entry(buf, &point, &type)) {
				msg(LOG_WARNING,
				    "Skipping malformed mount (%s)", buf);
				continue;
			}
			if (unescape_shell(point, sizeof(buf) -
					   (size_t)(point - buf))) {
				msg(LOG_WARNING,
				    "Skipping unterminated mount point");
				continue;
			}
			// Is this one that we care about?
			if (check_mount_entry(point, type)) {
				if (mlist_append(next, point)) {
					msg(LOG_ERR,
					    "Cannot store mount point %s", point);
					goto err_out;
				}
			}
		} else if (rc < 0) {
			msg(LOG_ERR, "Cannot read mounts file (%s)",
			    strerror(errno));
			goto err_out;
		} else if (!fd_fgets_eof_r(st)) {
			msg(LOG_ERR, "Mounts file read made no progress");
			goto err_out;
		}
		if (fd_fgets_eof_r(st))
			break;
	}

	fd_fgets_destroy(st);
	return 0;

err_out:
	fd_fgets_destroy(st);
	return -1;
}

/*
 * handle_mounts - read mount entries and refresh the watched mount list.
 * @fd: file descriptor for the mounts file.
 *
 * This function serializes access to the global mount list and the
 * subsequent fanotify mark update by holding mlist_lock across the
 * read/parse/update cycle. Callers may invoke it from the main loop or
 * the detached mount thread, but they must pass a descriptor that can be
 * rewound and read while the lock is held.
 *
 * Returns 0 on success or -1 when the snapshot cannot be applied safely.
 */
static int handle_mounts(int fd)
{
	mlist next;
	int rc = -1;

	mlist_create(&next);
	pthread_mutex_lock(&mlist_lock);
	if (read_mount_list(fd, &next))
		goto out;

	if (m == NULL) {
		m = malloc(sizeof(*m));
		if (m == NULL) {
			msg(LOG_ERR, "Cannot allocate mount list (%s)",
			    strerror(errno));
			goto out;
		}
		*m = next;
		mlist_create(&next);
	} else
		mlist_reconcile(m, &next);

	// update marks
	fanotify_update(m);
	rc = 0;

out:
	mlist_clear(&next);
	pthread_mutex_unlock(&mlist_lock);
	return rc;
}

/*
 * handle_mounts_thread_main - run mount processing outside event loop.
 * @arg: pointer to a heap-allocated file descriptor integer.
 * Returns NULL.
 */
static void *handle_mounts_thread_main(void *arg)
{
	int fd;
	sigset_t sigs;

	/* This is a worker thread. Don't handle external signals. */
	sigemptyset(&sigs);
	sigaddset(&sigs, SIGTERM);
	sigaddset(&sigs, SIGHUP);
	sigaddset(&sigs, SIGUSR1);
	sigaddset(&sigs, SIGINT);
	sigaddset(&sigs, SIGQUIT);
	pthread_sigmask(SIG_SETMASK, &sigs, NULL);

	if (arg == NULL) {
		atomic_store(&mounts_running, false);
		return NULL;
	}

	fd = *((int *)arg);
	free(arg);

	if (handle_mounts(fd))
		msg(LOG_ERR, "Mount list update failed; retaining current marks");
	atomic_store(&mounts_running, false);

	return NULL;
}

/*
 * maybe_start_mounts_thread - start a detached mounts thread when needed.
 * @fd: /proc/mounts file descriptor.
 * Returns nothing.
 */
static void maybe_start_mounts_thread(int fd)
{
	int rc;
	bool expected = false;
	int *arg;

	// Make sure one is not runnning since it is detached
	if (!atomic_compare_exchange_strong(&mounts_running, &expected, true))
		return;

	arg = malloc(sizeof(int));
	if (arg == NULL) {
		msg(LOG_ERR, "Failed allocating mount thread arg");
		atomic_store(&mounts_running, false);
		return;
	}
	*arg = fd;

	pthread_t tid;
	pthread_attr_t attr;
	pthread_attr_init(&attr);
	pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

	rc = pthread_create(&tid, &attr,
		handle_mounts_thread_main, arg);
	if (rc) {
		msg(LOG_ERR, "Failed starting mount thread (%s)",
				strerror(rc));
		atomic_store(&mounts_running, false);
		free(arg);
	}
	pthread_attr_destroy(&attr);
}

/*
 * wait_for_mounts_thread - wait for active mount processing to complete.
 * Returns nothing.
 */
static void wait_for_mounts_thread(void)
{
	while (atomic_load(&mounts_running))
		usleep(1000);
}

static void wait_for_reconfigure_thread(void)
{
	while (atomic_load(&reconfig_running))
		usleep(1000);
}


static void usage(void)
{
	fprintf(stderr,
		"Usage: fapolicyd [--debug|--debug-deny|--foreground] [--permissive] "
		"[--no-details] [--version]\n");
	exit(1);
}

#ifdef HAVE_MALLINFO2
static struct mallinfo2 last_mi;
/*
 * memory_use_report - write current glibc allocator utilization.
 * @f: report stream.
 * Returns nothing.
 */
static void memory_use_report(FILE *f)
{
	struct mallinfo2 mi = mallinfo2();

	fprintf(f, "glibc arena (total memory) is: %zu KiB, was: %zu KiB\n",
			(size_t)mi.arena/1024, (size_t)last_mi.arena/1024);
	fprintf(f, "glibc uordblks (in use memory) is: %zu KiB, was: %zu KiB\n",
			(size_t)mi.uordblks/1024,(size_t)last_mi.uordblks/1024);
	fprintf(f,"glibc fordblks (total free space) is: %zu KiB, was: %zu KiB\n",
			(size_t)mi.fordblks/1024,(size_t)last_mi.fordblks/1024);

	memcpy(&last_mi, &mi, sizeof(struct mallinfo2));
}

static void close_memory_report(void)
{
	struct mallinfo2 mi = mallinfo2();

	msg(LOG_DEBUG, "total memory: %zu KiB, was: %zu KiB",
			(size_t)mi.arena/1024, (size_t)last_mi.arena/1024);
	msg(LOG_DEBUG, "in use memory: %zu KiB, was: %zu KiB",
			(size_t)mi.uordblks/1024,(size_t)last_mi.uordblks/1024);
	msg(LOG_DEBUG,"total free memory: %zu KiB, was: %zu KiB",
			(size_t)mi.fordblks/1024,(size_t)last_mi.fordblks/1024);
}
#endif

void do_stat_report(FILE *f, int shutdown)
{
	do_stat_report_reset(f, shutdown, 0);
}

/*
 * do_state_report - write health and configuration state.
 * @f: report stream.
 * @shutdown: non-zero when writing final shutdown report.
 * Returns nothing.
 */
void do_state_report(FILE *f, int shutdown)
{
	const struct decision_config *decision_config;
	const char *ptr;
	reset_strategy_t strategy;
	failure_action_metrics_t failures;
	decision_metrics_t decisions;
	const char *reset_ptr;
	struct state_report_operating_mode mode;

	if (f == NULL)
		return;

	decision_config = decision_config_current();
	ptr = lookup_integrity(decision_config_integrity(decision_config));
	strategy = __atomic_load_n(&config.reset_strategy, __ATOMIC_RELAXED);
	reset_ptr = lookup_reset_strategy(strategy);
	failure_action_snapshot(&failures, 0);
	getDecisionMetrics(&decisions);

	mode.permissive = decision_config_permissive(decision_config);
	mode.integrity = ptr;
	mode.reset_strategy = reset_ptr;
	mode.config_generation = decision_config_generation(decision_config);
	mode.ruleset_generation = decisions.ruleset_generation;
	mode.config_effective_since =
		decision_config_effective_since(decision_config);
	mode.ruleset_effective_since = decisions.ruleset_effective_since;
	database_generation_snapshot(&mode.trust_db);
	mode.config = &config;
	state_report_operating_mode(f, &mode);

	fprintf(f, "\nHeadline activity:\n");
	fprintf(f, "Allowed accesses: %lu\n", getAllowed());
	fprintf(f, "Denied accesses: %lu\n", getDenied());

	fprintf(f, "\nResource configuration:\n");
	fprintf(f, "Daemon PID: %ld\n", (long)getpid());
	fprintf(f, "CPU cores: %ld\n", sysconf(_SC_NPROCESSORS_ONLN));
	fprintf(f, "decision_threads: %u\n", config.decision_threads);
	fprintf(f, "Per worker q_size: %u\n", config.q_size);
	fanotify_defer_config_report(f);
	do_cache_config_report(f);
	database_config_report(f);

	fprintf(f, "\nResource utilization:\n");
	database_utilization_report(f, &config);
	do_cache_utilization_report(f);
#ifdef HAVE_MALLINFO2
	memory_use_report(f);
#endif

	fprintf(f, "\nHealth indicators:\n");
	fprintf(f, "Kernel queue overflow: %lu\n",
		failure_action_metrics_count(&failures,
			FAILURE_REASON_KERNEL_QUEUE_OVERFLOW));
	fprintf(f, "Filesystem errors: %lu\n",
		failure_action_metrics_count(&failures,
			FAILURE_REASON_FANOTIFY_FS_ERROR));
	fanotify_fs_error_report(f);
	fprintf(f, "Reply errors: %lu\n",
		failure_action_metrics_count(&failures,
			FAILURE_REASON_RESPONSE_WRITE_FAILURE));
	fanotify_queue_health_report(f);
	fanotify_defer_fallback_report(f);
	do_cache_health_report(f);
	fanotify_defer_age_report(f);
	decision_failure_action_report(f, &failures);

	fprintf(f, "\nWatched mounts:\n");
	// Report mounts under fanotify watch.
	pthread_mutex_lock(&mlist_lock);
	if (m) {
		const char *path = mlist_first(m);
		while (path) {
			fprintf(f, "watching mount: %s\n", path);
			path = mlist_next(m);
		}
	}
	pthread_mutex_unlock(&mlist_lock);

	// Keep repeated report output separated when generated in a loop.
	fputs("\n", f);
}

/*
 * do_metrics_report_reset - write resettable runtime metrics.
 * @f: report stream.
 * @reset: non-zero resets counters after snapshotting them.
 * Returns nothing.
 */
void do_metrics_report_reset(FILE *f, int reset)
{
	if (f == NULL)
		return;

	decision_report_metrics_reset(f, reset);
	fanotify_metrics_report_reset(f, reset);
	do_cache_metrics_report_reset(f, reset);
	database_metrics_report_reset(f, reset);
	fputs("\n", f);
	fputs("Rule hit counts:\n", f);
	policy_rule_hits_report_reset(f, reset);
	fputs("\n", f);
	attr_lookup_metrics_report(f, reset);
	// Keep repeated report output separated when generated in a loop.
	fputs("\n", f);
}

/*
 * do_stat_report_reset - write state report and optionally reset metrics.
 * @f: output stream.
 * @shutdown: non-zero when writing final shutdown report.
 * @reset: non-zero resets interval counters after copying them.
 *
 * Reset only affects operational counters. Configuration and state identity
 * values such as queue size, integrity mode, database size, and ruleset
 * generation remain unchanged.
 */
void do_stat_report_reset(FILE *f, int shutdown, int reset)
{
	do_state_report(f, shutdown);
	do_metrics_report_reset(f, reset);
}

/*
 * already_running - lock the pid file and reject a live daemon instance.
 *
 * The successful caller retains the advisory lock until final shutdown. That
 * makes the stale-pid check and later write one lifecycle operation instead of
 * a check-then-create race between concurrent starts.
 *
 * Returns 1 when another daemon may be running or the pid file is unsafe to
 * inspect, otherwise returns 0 with pid_file_fd locked for write_pid_file().
 */
int already_running(void)
{
	fd_fgets_state_t *st = NULL;
	char pid_buf[16], exe_buf[80], my_path[80];
	int created = 0;
	int read_len;
	int pid;
	int result = 1;

	if (pid_file_fd != -1)
		return 1;
	pid_file_created = false;
	pid_file_written = false;

	for (;;) {
		pid_file_fd = open(pidfile, O_NOFOLLOW | O_RDWR | O_CLOEXEC);
		if (pid_file_fd >= 0)
			break;
		if (errno != ENOENT) {
			msg(LOG_ERR, "Unable to open pidfile (%s)",
			    strerror(errno));
			return 1;
		}
		/*
		 * flock permits an exclusive lock through a read-only descriptor.
		 * Keep the pid file private so an unprivileged process cannot hold
		 * a stale inode locked after an unclean daemon shutdown.
		 */
		pid_file_fd = open(pidfile, O_CREAT | O_EXCL | O_NOFOLLOW |
				   O_RDWR | O_CLOEXEC, 0600);
		if (pid_file_fd >= 0) {
			created = 1;
			break;
		}
		if (errno != EEXIST) {
			msg(LOG_ERR, "Unable to create pidfile (%s)",
			    strerror(errno));
			return 1;
		}
	}

	pid_file_created = created;
	if (flock(pid_file_fd, LOCK_EX | LOCK_NB)) {
		msg(LOG_ERR, "Unable to lock pidfile (%s)", strerror(errno));
		goto out;
	}
	if (created) {
		result = 0;
		goto out;
	}
	if (lseek(pid_file_fd, 0, SEEK_SET) == (off_t)-1) {
		msg(LOG_ERR, "Unable to read pidfile (%s)", strerror(errno));
		goto out;
	}

	st = fd_fgets_init();
	if (st == NULL) {
		msg(LOG_ERR, "Unable to allocate pidfile reader (%s)",
		    strerror(errno));
		goto out;
	}
	read_len = fd_fgets_r(st, pid_buf, sizeof(pid_buf), pid_file_fd);
	if (read_len < 0) {
		msg(LOG_ERR, "fapolicyd pid file could not be read (%s)",
		    strerror(errno));
		goto out;
	}
	if (read_len == 0) {
		/*
		 * A crash can leave the file empty after creation or truncation.
		 * Holding its exclusive lock proves no live fapolicyd owns it.
		 */
		msg(LOG_WARNING, "Replacing empty stale fapolicyd pid file");
		result = 0;
		goto out;
	}

	// Get our path
	if (get_program_from_pid(getpid(), sizeof(exe_buf), my_path) == NULL)
		goto out; // shouldn't happen, but be safe

	// convert pidfile to integer
	errno = 0;
	pid = strtoul(pid_buf, NULL, 10);
	if (errno)
		goto out; // shouldn't happen, but be safe

	// verify it really is fapolicyd
	if (get_program_from_pid(pid, sizeof(exe_buf), exe_buf) == NULL)
		result = 0; // pid doesn't exist, we're OK
	else if (strstr(exe_buf, "fapolicyd") == NULL)
		result = 0; // a different program owns the stale pid
	else if (strcmp(exe_buf, my_path) != 0 && pid == getpid())
		result = 0; // unexpected path but this process owns the pid

out:
	if (st)
		fd_fgets_destroy(st);
	if (result)
		remove_pid_file();
	return result;
}

int main(int argc, const char *argv[])
{
	struct pollfd pfd[4];
	struct sigaction sa;
	struct rlimit limit;
	sigset_t control_signals;
	nfds_t pfd_count = 3;
	bool ready_pending = false;
	int exit_status = 0;
	int rc;

	setlocale(LC_TIME, "");

	for (int i=1; i < argc; i++) {
		if (strcmp(argv[i], "--help") == 0)
			usage();
		else if (strcmp(argv[i], "--version") == 0) {
			printf("fapolicyd %s\n", VERSION);
			return 0;
		}
	}

	set_message_mode(MSG_STDERR, debug_mode);
	if (load_daemon_config(&config)) {
		free_daemon_config(&config);
		msg(LOG_ERR, "Exiting due to bad configuration");
		return 1;
        }

	// set the debug flags
	for (int i=1; i < argc; i++) {
		if (strcmp(argv[i], "--debug") == 0) {
			debug_mode = 1;
			set_message_mode(MSG_STDERR, DBG_YES);
		} else if (strcmp(argv[i], "--debug-deny") == 0) {
			debug_mode = 2;
			set_message_mode(MSG_STDERR, DBG_YES);
		}
	}
	// process remaining flags
	for (int i=1; i < argc; i++) {
		if (strcmp(argv[i], "--permissive") == 0) {
			__atomic_store_n(&config.permissive, 1,
					 __ATOMIC_RELAXED);
		} else if (strcmp(argv[i], "--foreground") == 0) {
			foreground_mode = 1;
		} else if (strcmp(argv[i], "--no-details") == 0) {
			config.detailed_report = 0;
		} else if (strncmp(argv[i], "--mounts", 8) == 0) {
			if (!debug_mode) {
				msg(LOG_ERR, "the mounts flag can only be"
							 " used in debug mode");
				return 1;
			}
			// require an equals and at least one char long path
			if (strlen(argv[i]) < 10 || argv[i][8] != '=') {
				msg(LOG_ERR, "the mounts flag requires a file"
							 " path: --mounts=/tmp/mounts.txt");
				return 1;
			}
			// ensure we have specified a regular file
			struct stat sb;
			const char *tmp = argv[i] + 9;
			if (stat(tmp, &sb) != 0) {
				msg(LOG_ERR, "cannot stat mounts file %s, %s",
					tmp, strerror(errno));
				return 1;
			}
			if (!S_ISREG(sb.st_mode)) {
				msg(LOG_ERR, "mounts path %s is not a regular file", tmp);
				return 1;
			}
			msg(LOG_INFO, "Overriding mounts file: %s", tmp);
			mounts = tmp;
		} else if (strcmp(argv[i], "--debug") == 0 ||
			   strcmp(argv[i], "--debug-deny") == 0 ||
			   strcmp(argv[i], "--foreground") == 0) {
			// nop; debug flags already set
		} else {
			msg(LOG_ERR, "unknown command option:%s\n", argv[i]);
			free_daemon_config(&config);
			usage();
		}
	}
	/*
	 * Debug runs are manually launched and should not trust inherited
	 * NOTIFY_SOCKET/WATCHDOG_* values. --foreground remains notify-capable
	 * because it is the production systemd service mode.
	 */
	systemd_notify_set_enabled(!debug_mode);

	if (already_running()) {
		msg(LOG_ERR, "fapolicyd is already running");
		exit(1);
	}
	/* Register first so LIFO cleanup releases the lock after later teardown. */
	if (atexit(remove_pid_file)) {
		remove_pid_file();
		msg(LOG_ERR, "Cannot set pidfile exit function");
		exit(1);
	}

	if (decision_config_publish(&config)) {
		remove_pid_file();
		free_daemon_config(&config);
		msg(LOG_ERR, "Exiting due to bad decision configuration");
		return 1;
	}

	// Set a couple signal handlers
	sa.sa_flags = 0;
	sigemptyset(&sa.sa_mask);
	sa.sa_handler = hup_handler;
	sigaction(SIGHUP, &sa, NULL);
	sa.sa_handler = coredump_handler;
	sigaction(SIGSEGV, &sa, NULL);
	sigaction(SIGABRT, &sa, NULL);
	sigaction(SIGBUS, &sa, NULL);
	sigaction(SIGFPE, &sa, NULL);
	sigaction(SIGILL, &sa, NULL);
	sigaction(SIGSYS, &sa, NULL);
	sigaction(SIGTRAP, &sa, NULL);
	sigaction(SIGXCPU, &sa, NULL);
	sigaction(SIGXFSZ, &sa, NULL);
	sigaction(SIGQUIT, &sa, NULL);

	sa.sa_flags = SA_SIGINFO;
	sa.sa_sigaction = usr1_handler;
	sigaction(SIGUSR1, &sa, NULL);
	sa.sa_flags = 0;
	/* These need to be last since they are used later */
	sa.sa_handler = term_handler;
	sigaction(SIGTERM, &sa, NULL);
	sigaction(SIGINT, &sa, NULL);

	// Bump up resources
	limit.rlim_cur = RLIM_INFINITY;
	limit.rlim_max = RLIM_INFINITY;
	setrlimit(RLIMIT_FSIZE, &limit);
	getrlimit(RLIMIT_NOFILE, &limit);
	if (limit.rlim_max >= DAEMON_CONFIG_MIN_NOFILE)
		limit.rlim_cur = limit.rlim_max;
	else
		limit.rlim_max = limit.rlim_cur = DAEMON_CONFIG_MIN_NOFILE;

	if (setrlimit(RLIMIT_NOFILE, &limit))
		msg(LOG_WARNING, "Can't increase file number rlimit - %s",
		    strerror(errno));
	else
		msg(LOG_INFO,"Can handle %lu file descriptors", limit.rlim_cur);

	// get more time slices because everything is waiting on us
	errno = 0;
	nice(-config.nice_val);
	if (errno)
		msg(LOG_WARNING, "Couldn't adjust priority (%s)",
				strerror(errno));

	// Load the rule configuration
	if (load_rules(&config)) {
		remove_pid_file();
		exit(1);
	}
	if (!debug_mode) {
		if (!foreground_mode) {
			if (become_daemon() < 0) {
				remove_pid_file();
				msg(LOG_ERR,
				    "Exiting due to failure daemonizing");
				exit(1);
			}
		}
		set_message_mode(MSG_SYSLOG, DBG_NO);
		openlog("fapolicyd", LOG_PID, LOG_DAEMON);
	}

	/* Keep this before unlink_fifo so shutdown retains its existing order.
	 * message_async_stop() is a safe no-op until the logger starts below.
	 */
	if (atexit(message_async_stop)) {
		remove_pid_file();
		msg(LOG_ERR, "cannot register async log exit handler");
		exit(1);
	}

	state_report_log_reset_strategy(config.reset_strategy);
	decision_timing_apply_config(config.timing_collection);

	// Set the exit function so there is always a fifo cleanup
	if (atexit(unlink_fifo)) {
		remove_pid_file();
		msg(LOG_ERR, "Cannot set exit function");
		exit(1);
	}

	// Setup filesystem to watch list
	init_ignore_mounts(config.ignore_mounts);
	if (init_fs_list(config.watch_fs)) {
		remove_pid_file();
		destroy_rules();
		destroy_fs_list(&filesystems);
		destroy_fs_list(&ignored_mounts);
		decision_config_destroy();
		free_daemon_config(&config);
		exit(1);
	}

	/* The lock acquired above remains held across daemonization. */
	if (write_pid_file()) {
		destroy_rules();
		destroy_fs_list(&filesystems);
		destroy_fs_list(&ignored_mounts);
		decision_config_destroy();
		free_daemon_config(&config);
		exit(1);
	}
	// Set strict umask
	(void) umask( 0117 );

	if (preconstruct_fifo(&config)) {
		remove_pid_file();
		msg(LOG_ERR, "Cannot construct a pipe");
		exit(1);
	}

	// If we are not going to be root, then setup necessary capabilities
	if (config.uid != 0) {
		capng_clear(CAPNG_SELECT_BOTH);
		capng_updatev(CAPNG_ADD, CAPNG_EFFECTIVE|CAPNG_PERMITTED,
			CAP_DAC_OVERRIDE, CAP_SYS_ADMIN, CAP_SYS_PTRACE,
			CAP_SYS_NICE, CAP_SYS_RESOURCE, CAP_AUDIT_WRITE, -1);
		if (capng_change_id(config.uid, config.gid,
							CAPNG_DROP_SUPP_GRP)) {
			msg(LOG_ERR, "Cannot change to uid %d", config.uid);
			exit(1);
		} else
			msg(LOG_DEBUG, "Changed to uid %d", config.uid);
	}

	// Install seccomp filter to prevent escalation
	install_syscall_filter();

	// Start after daemonizing and installing seccomp. This create a new
	// thread. When seccomp is involved, it must be started after the
	// filter is installed so that it inherits the filter.
	rc = message_async_start();
	if (rc) {
		msg(LOG_ERR, "failed starting async logging thread (%s)",
		    strerror(rc));
		exit(1);
	}

	// Setup lru caches
	if (init_event_system(&config)) {
		msg(LOG_ERR, "Cannot initialize event caches");
		destroy_rules();
		destroy_fs_list(&filesystems);
		destroy_fs_list(&ignored_mounts);
		free_daemon_config(&config);
		remove_pid_file();
		exit(1);
	}

	// Init the database
	if (init_database(&config)) {
		destroy_event_system();
		destroy_rules();
		destroy_fs_list(&filesystems);
		destroy_fs_list(&ignored_mounts);
		free_daemon_config(&config);
		remove_pid_file();
		exit(1);
	}

	// Init the file test libraries
	if (file_init()) {
		// file_init cleans up on failure - skip
		destroy_event_system();
		destroy_rules();
		destroy_fs_list(&filesystems);
		destroy_fs_list(&ignored_mounts);
		free_daemon_config(&config);
		remove_pid_file();
		exit(1);
	}

	// Initialize the file watch system
	pfd[0].fd = open(mounts, O_RDONLY);
	if (pfd[0].fd < 0) {
		msg(LOG_ERR, "Cannot open mounts file %s (%s)",
			mounts, strerror(errno));
		file_close();
		close_database();
		destroy_event_system();
		destroy_rules();
		destroy_fs_list(&filesystems);
		destroy_fs_list(&ignored_mounts);
		free_daemon_config(&config);
		remove_pid_file();
		exit(1);
	}
	pfd[0].events = POLLPRI;
	if (handle_mounts(pfd[0].fd)) {
		msg(LOG_ERR, "Cannot initialize watched mount list");
		close(pfd[0].fd);
		file_close();
		close_database();
		destroy_event_system();
		destroy_rules();
		destroy_fs_list(&filesystems);
		destroy_fs_list(&ignored_mounts);
		free_daemon_config(&config);
		remove_pid_file();
		exit(1);
	}
	pfd[1].fd = init_fanotify(&config, m);
	pfd[1].events = POLLIN;
	/*
	 * Without a signal fd, SIGTERM can run its handler after main tests
	 * stop but before the untimed poll begins. No polled descriptor then
	 * becomes ready, so shutdown deadlocks until unrelated filesystem
	 * activity occurs. SIGHUP has the same race and can leave reload
	 * pending. A signalfd makes either request visible to poll regardless
	 * of whether it arrives before or during the system call.
	 */
	sigemptyset(&control_signals);
	sigaddset(&control_signals, SIGTERM);
	sigaddset(&control_signals, SIGINT);
	sigaddset(&control_signals, SIGHUP);
	pfd[2].fd = signalfd(-1, &control_signals,
			SFD_CLOEXEC | SFD_NONBLOCK);
	if (pfd[2].fd >= 0) {
		rc = pthread_sigmask(SIG_BLOCK, &control_signals, NULL);
		if (rc) {
			close(pfd[2].fd);
			pfd[2].fd = -1;
			errno = rc;
		}
	}
	if (pfd[2].fd < 0) {
		msg(LOG_ERR, "Cannot create control signal fd (%s)",
			strerror(errno));
		exit_status = 1;
		goto shutdown;
	}
	pfd[2].events = POLLIN;
#ifdef FAPOLICYD_ENABLE_FANOTIFY_FS_ERROR
	pfd[3].fd = fanotify_fs_error_fd();
	if (pfd[3].fd >= 0) {
		pfd[3].events = POLLIN;
		pfd_count = 4;
	}
#endif

	msg(LOG_INFO, "Starting to listen for events");
	if (systemd_notify_ready()) {
		if (errno == EAGAIN || errno == EWOULDBLOCK)
			ready_pending = true;
		else
			msg(LOG_WARNING,
			    "Cannot notify systemd that startup is complete");
	}
	while (!stop) {
		if (hup) {
			hup = false;
			msg(LOG_DEBUG, "Got SIGHUP");
			maybe_start_reconfigure_thread();
		}
		rc = poll(pfd, pfd_count, ready_pending ? 250 : -1);

#ifdef DEBUG
		msg(LOG_DEBUG, "Main poll interrupted");
#endif
		if (rc < 0) {
			if (errno == EINTR)
				continue;
			else {
				int poll_errno = errno;

				stop = true;
		/*
		 * The main loop is the only reader of the fanotify
		 * permission group. The update thread can open watched
		 * files during a trust reload; those same-process opens are
		 * allowed only after handle_events() reads their permission
		 * events. Once poll fails, joining that thread while the
		 * group is still live can therefore wait forever.
		 *
		 * Close the group before any shutdown work that may wait.
		 * Closing it removes the marks and allows all outstanding
		 * permission events. Unmarking alone is insufficient because
		 * an event already queued still needs a reply or a group
		 * close. nudge_queue() only wakes decision workers, while
		 * close_database() joins the update thread, so neither call
		 * may precede this early close.
		 */
				fanotify_close_for_shutdown();
				nudge_queue();
				close_database();
				msg(LOG_ERR, "Poll error (%s)\n",
					strerror(poll_errno));
				exit(1);
			}
		} else if (rc > 0) {
			int signal_fd_error = 0;

			if (pfd[2].revents &
			    (POLLERR | POLLHUP | POLLNVAL)) {
				signal_fd_error = EIO;
			} else if (pfd[2].revents & POLLIN) {
				struct signalfd_siginfo info;
				ssize_t len;

				do {
					len = read(pfd[2].fd, &info,
						   sizeof(info));
				} while (len < 0 && errno == EINTR);
				if (len != (ssize_t)sizeof(info))
					signal_fd_error = len < 0 ? errno : EIO;
				else if (info.ssi_signo == SIGHUP)
					hup_handler(info.ssi_signo);
				else
					term_handler(info.ssi_signo);
			}
			if (signal_fd_error) {
				msg(LOG_ERR, "Control signal fd failed (%s)",
					strerror(signal_fd_error));
				exit_status = 1;
				stop = true;
			}
			if (stop)
				continue;
			if (pfd[1].revents & POLLIN) {
				handle_events();
			}
			if (pfd_count > 3 && pfd[3].revents & POLLIN) {
				fanotify_fs_error_handle_events();
			}
			if (pfd[0].revents & POLLPRI) {
				msg(LOG_DEBUG, "Mount change detected");
				maybe_start_mounts_thread(pfd[0].fd);
			}

			// This will always need to be here as long as we
			// link against librpm. Turns out that librpm masks
			// signals to prevent corrupted databases during an
			// update. Since we only do read access, we can turn
			// them back on.
#ifdef USE_RPM
			sigaction(SIGTERM, &sa, NULL);
			sigaction(SIGINT, &sa, NULL);
#endif
		}
		/* Retry only after pending permission events have been answered. */
		if (ready_pending) {
			rc = systemd_notify_ready();
			if (rc == 0)
				ready_pending = false;
			else if (errno != EAGAIN && errno != EWOULDBLOCK) {
				ready_pending = false;
				msg(LOG_WARNING,
				    "Cannot notify systemd that startup is complete");
			}
		}
	}
shutdown:
	msg(LOG_INFO, "shutting down...");
	/*
	 * The main loop was the only reader that allowed same-process fanotify
	 * events. Close the group before waiting for any worker or helper that
	 * may already be blocked in a watched open. Moving this close after a
	 * wait can make shutdown deadlock with the close needed to release it.
	 */
	fanotify_close_for_shutdown();
	if (systemd_notify_stopping())
		msg(LOG_WARNING, "Cannot notify systemd that shutdown started");
	wait_for_reconfigure_thread();
	wait_for_mounts_thread();
	shutdown_fanotify();
	close(pfd[0].fd);
	file_close();
	close_database();
#ifdef HAVE_MALLINFO2
	close_memory_report();
#endif
	// Reinstate the strict umask in case rpm messed with it
	(void) umask( 0237 );
	if (config.do_stat_report) {
		FILE *f = fopen(REPORT, "w");
		if (f == NULL)
			msg(LOG_WARNING, "Cannot create usage report");
		else {
			do_stat_report(f, 1);
			run_usage_report(&config, f);
			fclose(f);
		}
	}
	mlist_clear(m);	// removes mounts
	free(m);
	destroy_event_system(); // clears lru caches
	destroy_rules();
	destroy_fs_list(&filesystems);
	destroy_fs_list(&ignored_mounts);
	decision_config_destroy();
	free_daemon_config(&config);
	remove_pid_file();
	if (pfd[2].fd >= 0)
		close(pfd[2].fd);

	return exit_status;
}
