/
githubmirror
/
audit-userspace
Обзор
Документация
Войти
/
githubmirror
/
audit-userspace
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/auditd-event.c
1 559 строк
41 KB
Steve Grubb
auditd: reap plugin children in dispatcher
15 июл 2026, 06:52
15 июл 2026, 06:52
1f550db
Код
Авторство
О чём код?
/* auditd-event.c -- * Copyright 2004-08,2011,2013,2015-16,2018,2021 Red Hat Inc. * All Rights Reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, 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; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * Authors: * Steve Grubb <sgrubb@redhat.com> * */ #include "config.h" #include <stdlib.h> #include <unistd.h> #include <pthread.h> #include <signal.h> #include <fcntl.h> /* O_NOFOLLOW needs gnu defined */ #include <sys/stat.h> #include <errno.h> #include <string.h> #include <sys/vfs.h> #include <ctype.h> /* toupper */ #include <libgen.h> /* dirname */ #include "auditd-event.h" #include "auditd-children.h" #include "auditd-reconfigure.h" #include "libaudit.h" #include "private.h" #include "auparse.h" #include "auparse-idata.h" #include "common.h" /* This is defined in auditd.c */ #ifdef HAVE_ATOMIC extern ATOMIC_INT stop; #else extern volatile ATOMIC_INT stop; #endif /* * This function is provided by auditd.c and marked weak so test utilities * that don't link auditd.c can still link this file. */ extern int event_is_prealloc(struct auditd_event *e) __attribute__((weak)); /* Local function prototypes */ static void send_ack(const struct auditd_event *e, int ack_type, const char *msg); static void write_to_log(const struct auditd_event *e); static void check_log_file_size(void); static void check_space_left(void); static void do_space_left_action(int admin); static void do_disk_full_action(void); static void do_disk_error_action(const char *func, int err); static void fix_disk_permissions(void); static void check_excess_logs(void); static void rotate_logs_now(void); static void rotate_logs(unsigned int num_logs, unsigned int keep_logs); static void shift_logs(void); static int open_audit_log(void); static pid_t safe_exec(const char *exe, int resume_on_exit); static void reconfigure(struct auditd_event *e); static int get_log_fd(void); static void init_flush_thread(void); /* Local Data */ static struct daemon_conf *config; #ifdef HAVE_ATOMIC static ATOMIC_INT log_fd; #else static volatile ATOMIC_INT log_fd; #endif static FILE *log_file = NULL; static unsigned int disk_err_warning = 0; static int fs_space_warning = 0; static int fs_admin_space_warning = 0; static int fs_space_left = 1; static int logging_suspended = 0; static unsigned int known_logs = 0; static char *format_buf = NULL; static off_t log_size = 0; static pthread_t flush_thread; static pthread_mutex_t flush_lock; static pthread_cond_t do_flush; #ifdef HAVE_ATOMIC static ATOMIC_INT flush; #else static volatile ATOMIC_INT flush; #endif static auparse_state_t *au = NULL; /* Local definitions */ // The reason 24 was chosen is lost in time. Enriched events start life with // format_raw which reserves 32 bytes. After that a newline, space a separator // get added. That leaves roughly 28 bytes. It is suspected that 24 was chosen // to at least allow one syscall or a uid to fully fit. In practice // MIN_SPACE_LEFT should never be hit unless the system uses very long paths. #define MIN_SPACE_LEFT 24 static inline int from_network(const struct auditd_event *e) { if (e && e->ack_func) return 1; return 0; } int dispatch_network_events(void) { return config->distribute_network_events; } void write_logging_state(FILE *f) { fprintf(f, "writing to logs = %s\n", config->write_logs ? "yes" : "no"); if (config->daemonize == D_BACKGROUND && config->write_logs) { int rc; struct statfs buf; fprintf(f, "current log size = %llu KiB\n", (long long unsigned)log_size/1024); fprintf(f, "max log size = %lu KiB\n", config->max_log_size * (MEGABYTE/1024)); fprintf(f,"logs detected last rotate/shift = %u\n", known_logs); fprintf(f, "space left on partition = %s\n", fs_space_left ? "yes" : "no"); rc = fstatfs(AUDIT_ATOMIC_LOAD(log_fd), &buf); if (rc == 0) { fprintf(f, "Logging partition free space = %llu MiB\n", (long long unsigned) (buf.f_bavail * buf.f_bsize)/MEGABYTE); fprintf(f, "space_left setting = %lu MiB\n", config->space_left); fprintf(f, "admin_space_left setting = %lu MiB\n", config->admin_space_left); } fprintf(f, "logging suspended = %s\n", logging_suspended ? "yes" : "no"); fprintf(f, "file system space action performed = %s\n", fs_space_warning ? "yes" : "no"); fprintf(f, "admin space action performed = %s\n", fs_admin_space_warning ? "yes" : "no"); fprintf(f, "disk error detected = %s\n", disk_err_warning ? "yes" : "no"); } } void shutdown_events(void) { // We are no longer processing events, sync the disk and close up. pthread_cancel(flush_thread); free((void *)format_buf); auparse_destroy_ext(au, AUPARSE_DESTROY_ALL); if (AUDIT_ATOMIC_LOAD(log_fd) >= 0) fsync(AUDIT_ATOMIC_LOAD(log_fd)); if (log_file) fclose(log_file); } int init_event(struct daemon_conf *conf) { /* Store the netlink descriptor and config info away */ config = conf; AUDIT_ATOMIC_STORE(log_fd, -1); /* Now open the log */ if (config->daemonize == D_BACKGROUND) { fix_disk_permissions(); if (open_audit_log()) return 1; setup_percentages(config, AUDIT_ATOMIC_LOAD(log_fd)); } else { AUDIT_ATOMIC_STORE(log_fd, 1); // stdout log_file = fdopen(AUDIT_ATOMIC_LOAD(log_fd), "a"); if (log_file == NULL) { audit_msg(LOG_ERR, "Error setting up stdout descriptor (%s)", strerror(errno)); return 1; } /* Set it to line buffering */ setlinebuf(log_file); } if (config->daemonize == D_BACKGROUND) { check_log_file_size(); check_excess_logs(); /* At this stage, auditd is not fully initialized and operational. This means we can't notify the parent process that initialization is complete. However, if space_left_action is set to SINGLE, we must avoid switching to that runlevel. Before entering the SINGLE runlevel requires auditd to finish initialization. But auditd will not start properly or signal the init system that it has started, as it is blocked by the attempt to switch to single-user mode, resulting in a deadlock. */ // check_space_left(); } format_buf = (char *)malloc(FORMAT_BUF_LEN); if (format_buf == NULL) { audit_msg(LOG_ERR, "No memory for formatting, exiting"); if (log_file) fclose(log_file); log_file = NULL; return 1; } init_flush_thread(); return 0; } /* This tells the OS that pending writes need to get going. * Its only used when flush == incremental_async. */ static void *flush_thread_main(void *arg) { sigset_t sigs; /* This is a worker thread. Don't handle signals. */ sigemptyset(&sigs); sigaddset(&sigs, SIGTERM); sigaddset(&sigs, SIGHUP); sigaddset(&sigs, SIGUSR1); sigaddset(&sigs, SIGUSR2); sigaddset(&sigs, SIGCHLD); sigaddset(&sigs, SIGCONT); pthread_sigmask(SIG_SETMASK, &sigs, NULL); while (!AUDIT_ATOMIC_LOAD(stop)) { pthread_mutex_lock(&flush_lock); // In the event that the logging thread requests another // flush before the first completes, this simply turns // into a loop of fsyncs. while (AUDIT_ATOMIC_LOAD(flush) == 0) { pthread_cond_wait(&do_flush, &flush_lock); if (AUDIT_ATOMIC_LOAD(stop)) { pthread_mutex_unlock(&flush_lock); return NULL; } } AUDIT_ATOMIC_STORE(flush, 0); pthread_mutex_unlock(&flush_lock); if (AUDIT_ATOMIC_LOAD(log_fd) >= 0) fsync(AUDIT_ATOMIC_LOAD(log_fd)); } return NULL; } /* We setup the flush thread no matter what. This is incase a reconfig * changes from non incremental to incremental or vice versa. */ static void init_flush_thread(void) { pthread_mutex_init(&flush_lock, NULL); pthread_cond_init(&do_flush, NULL); AUDIT_ATOMIC_STORE(flush, 0); pthread_create(&flush_thread, NULL, flush_thread_main, NULL); pthread_detach(flush_thread); } static void replace_event_msg(struct auditd_event *e, const char *buf) { if (buf) { size_t len = strlen(buf); if (e->reply.message != e->reply.msg.data) free((void *)e->reply.message); if (len < MAX_AUDIT_MESSAGE_LENGTH - 1) e->reply.message = strdup(buf); else { // If too big, we must truncate the event due to API e->reply.message = strndup(buf, MAX_AUDIT_MESSAGE_LENGTH-1); len = MAX_AUDIT_MESSAGE_LENGTH; } // For network originating events, len should be used if (!from_network(e)) // V1 protocol msg size e->reply.msg.nlh.nlmsg_len = e->reply.len; e->reply.len = len; // V2 protocol msg size } } /* * Cap snprintf's return value to the bytes actually stored in format_buf. * nlen is the number of bytes that would have been written, limit is the * destination buffer size passed to snprintf, and the return value is the * number of bytes available in format_buf excluding the trailing NUL. */ static int clamp_format_len(int nlen, size_t limit) { if (nlen < 1) return 0; if ((size_t)nlen >= limit) return limit - 1; return nlen; } /* * This function will take an audit structure and return a * text buffer that's formatted for writing to disk. If there is * an error the return value is 0 and the format_buf is truncated. * format_buf will have any '\n' removed on return. */ static int format_raw(const struct audit_reply *rep) { char *ptr; int nlen; size_t limit; format_buf[0] = 0; if (rep == NULL) { if (config->node_name_format != N_NONE) { limit = FORMAT_BUF_LEN - 32; nlen = snprintf(format_buf, limit, "node=%s type=DAEMON_ERR op=format-raw msg=NULL res=failed", config->node_name); } else { limit = MAX_AUDIT_MESSAGE_LENGTH; nlen = snprintf(format_buf, limit, "type=DAEMON_ERR op=format-raw msg=NULL res=failed"); } nlen = clamp_format_len(nlen, limit); if (nlen == 0) return 0; } else { int len; const char *type, *message; char unknown[32]; type = audit_msg_type_to_name(rep->type); if (type == NULL) { snprintf(unknown, sizeof(unknown), "UNKNOWN[%d]", rep->type); type = unknown; } if (rep->message == NULL) { message = "lost"; len = 4; } else { message = rep->message; len = rep->len; } // Note: This can truncate messages if // MAX_AUDIT_MESSAGE_LENGTH is too small if (config->node_name_format != N_NONE) { limit = FORMAT_BUF_LEN - 32; nlen = snprintf(format_buf, limit, "node=%s type=%s msg=%.*s", config->node_name, type, len, message); } else { limit = MAX_AUDIT_MESSAGE_LENGTH - 32; nlen = snprintf(format_buf, limit, "type=%s msg=%.*s", type, len, message); } nlen = clamp_format_len(nlen, limit); if (nlen == 0) return 0; /* Replace \n with space so it looks nicer. */ ptr = format_buf; while (*ptr) { if (unlikely(*ptr == '\n')) *ptr = ' '; ptr++; } /* Trim trailing space off since it wastes space */ if (format_buf[nlen-1] == ' ') { format_buf[nlen-1] = 0; nlen--; } } return nlen; } static int sep_done = 0; static int add_separator(unsigned int len_left) { if (sep_done == 0) { format_buf[FORMAT_BUF_LEN - len_left] = AUDIT_INTERP_SEPARATOR; sep_done++; return 1; } sep_done++; return 0; } // returns length used, 0 on error #define NAME_SIZE 64 static int add_simple_field(auparse_state_t *au, size_t len_left, int encode) { const char *value, *nptr; char *enc = NULL; char *ptr, field_name[NAME_SIZE]; size_t nlen, vlen, tlen; unsigned int i; int num; // prepare field name i = 0; nptr = auparse_get_field_name(au); while (*nptr && i < (NAME_SIZE - 1)) { field_name[i] = toupper(*nptr); i++; nptr++; } field_name[i] = 0; nlen = i; // get the translated value value = auparse_interpret_field(au); if (value == NULL) value = "?"; vlen = strlen(value); if (encode) { enc = audit_encode_nv_string(field_name, value, vlen); if (enc == NULL) return 0; vlen = strlen(enc); tlen = 1 + vlen + 1; } else // calculate length to use tlen = 1 + nlen + 1 + vlen + 1; // If no room, do not truncate - just do nothing if (tlen >= len_left) { free(enc); return 0; } // Setup pointer ptr = &format_buf[FORMAT_BUF_LEN - len_left]; if (sep_done > 1) { *ptr = ' '; ptr++; num = 1; } else num = 0; // Add the field if (encode) { // encoded: "%s" memcpy(ptr, enc, vlen); ptr[vlen] = 0; num += vlen; free(enc); } else { // plain: "%s=%s" memcpy(ptr, field_name, nlen); ptr += nlen; *ptr++ = '='; memcpy(ptr, value, vlen); ptr[vlen] = 0; num += nlen + 1 + vlen; } return num; } /* * This function will take an audit structure and return a text * buffer that's formatted and enriched. If there is an error the * return value is the raw formatted buffer (which may be truncated if it * had an error)or an error message in the format_buffer. The return * value is never NULL. */ static const char *format_enrich(const struct audit_reply *rep) { if (rep == NULL) { if (config->node_name_format != N_NONE) snprintf(format_buf, FORMAT_BUF_LEN - 32, "node=%s type=DAEMON_ERR op=format-enriched msg=NULL res=failed", config->node_name); else snprintf(format_buf, MAX_AUDIT_MESSAGE_LENGTH, "type=DAEMON_ERR op=format-enriched msg=NULL res=failed"); } else { int rc, rtype; size_t mlen, len; // Do raw format to get event started mlen = format_raw(rep); // How much room is left? if (mlen >= FORMAT_BUF_LEN) return format_buf; len = FORMAT_BUF_LEN - mlen; if (len <= MIN_SPACE_LEFT) return format_buf; // Add carriage return so auparse sees it correctly format_buf[mlen] = 0x0A; format_buf[mlen+1] = 0; mlen++; // Increase the length so auparse copies the '\n' // init auparse if (au == NULL) { au = auparse_init(AUSOURCE_BUFFER, format_buf); if (au == NULL) { format_buf[mlen-1] = 0; //remove newline return format_buf; } auparse_set_escape_mode(au, AUPARSE_ESC_RAW); auparse_set_eoe_timeout(config->end_of_event_timeout); } else auparse_new_buffer(au, format_buf, mlen); sep_done = 0; // Loop over all fields while possible to add field rc = auparse_first_record(au); if (rc != 1) format_buf[mlen-1] = 0; //remove newline on failure rtype = auparse_get_type(au); switch (rtype) { // Flush before adding to pickup new associations case AUDIT_ADD_USER: case AUDIT_ADD_GROUP: _auparse_flush_caches(au); break; default: break; } while (rc > 0 && len > MIN_SPACE_LEFT) { // See what kind of field we have size_t vlen; int type = auparse_get_field_type(au); switch (type) { case AUPARSE_TYPE_UID: case AUPARSE_TYPE_GID: if (add_separator(len)) len--; vlen = add_simple_field(au, len, 1); len -= vlen; break; case AUPARSE_TYPE_SYSCALL: case AUPARSE_TYPE_ARCH: case AUPARSE_TYPE_SOCKADDR: if (add_separator(len)) len--; vlen = add_simple_field(au, len, 0); len -= vlen; break; default: break; } rc = auparse_next_field(au); //remove newline when nothing added if (rc < 1 && sep_done == 0) format_buf[mlen-1] = 0; } switch(rtype) { // Flush after modification to remove stale entries case AUDIT_USER_MGMT: case AUDIT_DEL_USER: case AUDIT_DEL_GROUP: case AUDIT_GRP_MGMT: _auparse_flush_caches(au); break; default: break; } } return format_buf; } void format_event(struct auditd_event *e) { const char *buf; switch (config->log_format) { case LF_RAW: format_raw(&e->reply); buf = format_buf; break; case LF_ENRICHED: buf = format_enrich(&e->reply); break; default: buf = NULL; break; } replace_event_msg(e, buf); } /* This function free's all memory associated with events */ void cleanup_event(struct auditd_event *e) { // Over in send_audit_event we sometimes have message pointing // into the middle of the reply allocation. Check for it. if (e->reply.message != e->reply.msg.data) free((void *)e->reply.message); if (!event_is_prealloc) free(e); else if (!event_is_prealloc(e)) free(e); } /* This function takes a reconfig event and sends it to the handler */ void enqueue_event(struct auditd_event *e) { e->ack_func = NULL; e->ack_data = NULL; e->sequence_id = 0; handle_event(e); cleanup_event(e); } /* This function allocates memory and fills the event fields with passed arguments. Caller must free memory. */ struct auditd_event *create_event(const char *msg, ack_func_type ack_func, void *ack_data, uint32_t sequence_id) { struct auditd_event *e; e = (struct auditd_event *)calloc(1, sizeof (*e)); if (e == NULL) { audit_msg(LOG_ERR, "Cannot allocate audit reply"); return NULL; } e->ack_func = ack_func; e->ack_data = ack_data; e->sequence_id = sequence_id; /* Network originating events need things adjusted to mimic netlink. */ if (from_network(e)) replace_event_msg(e, msg); return e; } /* This function takes the event and handles it. */ static unsigned int count = 0L; void handle_event(struct auditd_event *e) { if (e->reply.type == AUDIT_DAEMON_RECONFIG && e->ack_func == NULL) { reconfigure(e); if (config->write_logs == 0 && config->daemonize == D_BACKGROUND) return; format_event(e); } else if (e->reply.type == AUDIT_DAEMON_ROTATE) { rotate_logs_now(); if (config->write_logs == 0 && config->daemonize == D_BACKGROUND) return; } if (!logging_suspended && (config->write_logs || config->daemonize == D_FOREGROUND)) { write_to_log(e); /* See if we need to flush to disk manually */ if (config->flush == FT_INCREMENTAL || config->flush == FT_INCREMENTAL_ASYNC) { count++; if ((count % config->freq) == 0) { int rc; errno = 0; do { rc = fflush_unlocked(log_file); } while (rc < 0 && errno == EINTR); if (errno) { if (errno == ENOSPC && fs_space_left == 1) { fs_space_left = 0; do_disk_full_action(); } else //EIO is only likely failure mode do_disk_error_action("flush", errno); } if (config->daemonize == D_BACKGROUND) { if (config->flush == FT_INCREMENTAL) { /* EIO is only likely failure */ if (AUDIT_ATOMIC_LOAD(log_fd) >= 0 && fsync(AUDIT_ATOMIC_LOAD(log_fd)) != 0) { do_disk_error_action( "fsync", errno); } } else { pthread_mutex_lock(&flush_lock); AUDIT_ATOMIC_STORE(flush, 1); pthread_cond_signal(&do_flush); pthread_mutex_unlock( &flush_lock); } } } } } else if (!config->write_logs && config->daemonize == D_BACKGROUND) send_ack(e, AUDIT_RMW_TYPE_ACK, ""); else if (logging_suspended) send_ack(e,AUDIT_RMW_TYPE_DISKERROR,"remote logging suspended"); } static void send_ack(const struct auditd_event *e, int ack_type, const char *msg) { if (from_network(e)) { unsigned char header[AUDIT_RMW_HEADER_SIZE]; AUDIT_RMW_PACK_HEADER(header, 0, ack_type, strlen(msg), e->sequence_id); e->ack_func(e->ack_data, header, msg); } } void resume_logging(void) { audit_msg(LOG_NOTICE, "Audit daemon is attempting to resume logging."); logging_suspended = 0; fs_space_left = 1; // User space action scripts cause fd to close // Need to reopen here to recreate the file if the // script deleted or moved it. if (log_file == NULL) { fix_disk_permissions(); if (open_audit_log()) { int saved_errno = errno; audit_msg(LOG_WARNING, "Could not reopen a log after resume logging"); logging_suspended = 1; do_disk_error_action("resume", saved_errno); } else check_log_file_size(); audit_msg(LOG_NOTICE, "Audit daemon resumed logging."); } disk_err_warning = 0; fs_space_warning = 0; fs_admin_space_warning = 0; } /* This function writes the given buf to the current log file */ static void write_to_log(const struct auditd_event *e) { int rc; int ack_type = AUDIT_RMW_TYPE_ACK; const char *msg = ""; /* write it to disk */ rc = fprintf(log_file, "%s\n", e->reply.message); /* error? Handle it */ if (rc < 0) { if (errno == ENOSPC) { ack_type = AUDIT_RMW_TYPE_DISKFULL; msg = "disk full"; send_ack(e, ack_type, msg); if (fs_space_left == 1) { fs_space_left = 0; do_disk_full_action(); } } else { int saved_errno = errno; ack_type = AUDIT_RMW_TYPE_DISKERROR; msg = "disk write error"; send_ack(e, ack_type, msg); do_disk_error_action("write", saved_errno); } } else { /* check log file size & space left on partition */ if (config->daemonize == D_BACKGROUND) { // If either of these fail, I consider it an // inconvenience as opposed to something that is // actionable. There may be some temporary condition // that the system recovers from. The real error // occurs on write. log_size += rc; check_log_file_size(); // Keep loose tabs on the free space if ((log_size % 8) < 3) check_space_left(); } if (fs_space_warning) ack_type = AUDIT_RMW_TYPE_DISKLOW; send_ack(e, ack_type, msg); disk_err_warning = 0; } } static void check_log_file_size(void) { /* did we cross the size limit? */ off_t sz = log_size / MEGABYTE; if (config->write_logs == 0) return; if (sz >= config->max_log_size && (config->daemonize == D_BACKGROUND)) { switch (config->max_log_size_action) { case SZ_IGNORE: break; case SZ_SYSLOG: audit_msg(LOG_ERR, "Audit daemon log file is larger than max size"); break; case SZ_EXEC: if (log_file) fclose(log_file); log_file = NULL; AUDIT_ATOMIC_STORE(log_fd, -1); logging_suspended = 1; if (safe_exec(config->max_log_file_exe, 1) < 1) audit_msg(LOG_ALERT, "Audit daemon failed to exec max_log_file_action helper - logging suspended"); break; case SZ_SUSPEND: audit_msg(LOG_ERR, "Audit daemon is suspending logging due to logfile size."); // We need to close the file so that manual // intervention can move or delete the file. // We don't want to keep logging to a deleted // file. if (log_file) fclose(log_file); log_file = NULL; AUDIT_ATOMIC_STORE(log_fd, -1); logging_suspended = 1; break; case SZ_ROTATE: if (config->num_logs > 1) { audit_msg(LOG_INFO, "Audit daemon rotating log files"); rotate_logs(0, 0); } break; case SZ_KEEP_LOGS: audit_msg(LOG_INFO, "Audit daemon rotating log files with keep option"); shift_logs(); break; default: audit_msg(LOG_ALERT, "Audit daemon log file is larger than max size and unknown action requested"); break; } } } static void check_space_left(void) { int rc; struct statfs buf; if (AUDIT_ATOMIC_LOAD(log_fd) < 0) return; rc = fstatfs(AUDIT_ATOMIC_LOAD(log_fd), &buf); if (rc == 0) { if (buf.f_bavail < 5) { /* we won't consume the last 5 blocks */ fs_space_left = 0; do_disk_full_action(); } else { unsigned long blocks; unsigned long block_size = buf.f_bsize; blocks = config->space_left * (MEGABYTE/block_size); if (buf.f_bavail < blocks) { if (fs_space_warning == 0) { do_space_left_action(0); // Allow unlimited rotation if (config->space_left_action != FA_ROTATE) fs_space_warning = 1; } } else if (fs_space_warning && config->space_left_action == FA_SYSLOG){ // Auto reset only if failure action is syslog fs_space_warning = 0; } blocks=config->admin_space_left * (MEGABYTE/block_size); if (buf.f_bavail < blocks) { if (fs_admin_space_warning == 0) { do_space_left_action(1); // Allow unlimited rotation if (config->admin_space_left_action != FA_ROTATE) fs_admin_space_warning = 1; } } else if (fs_admin_space_warning && config->admin_space_left_action == FA_SYSLOG) { // Auto reset only if failure action is syslog fs_admin_space_warning = 0; } } } else audit_msg(LOG_DEBUG, "fstatfs returned:%d, %s", rc, strerror(errno)); } extern int sendmail(const char *subject, const char *content, const char *mail_acct); static void do_space_left_action(int admin) { int action; char buffer[256]; const char *next_actions; // Select the appropriate action and generate a meaningful message // explaining what happens if disk space reaches a threshold or // becomes completely full. if (admin) { action = config->admin_space_left_action; snprintf(buffer, sizeof(buffer), "If the disk becomes full, audit will %s.", failure_action_to_str(config->disk_full_action)); } else { action = config->space_left_action; snprintf(buffer, sizeof(buffer), "If the admin space left threshold is reached, audit will %s. " "If the disk becomes full, audit will %s.", failure_action_to_str(config->admin_space_left_action), failure_action_to_str(config->disk_full_action)); } next_actions = buffer; // If space_left is reached and FA_HALT is set in any of these fields // we need to inform logged in users. if (config->admin_space_left_action == FA_HALT || config->disk_full_action == FA_HALT) { wall_message("The audit system is low on disk space and is now halting the system for admin corrective action."); } switch (action) { case FA_IGNORE: break; case FA_SYSLOG: audit_msg(LOG_ALERT, "Audit daemon is low on disk space for logging. %s", next_actions); break; case FA_ROTATE: if (config->num_logs > 1) { audit_msg(LOG_INFO, "Audit daemon rotating log files"); rotate_logs(0, 0); } break; case FA_EMAIL: { char content[512]; const char *subject; if (admin == 0) { subject = "Audit Disk Space Alert"; snprintf(content, sizeof(content), "The audit daemon is low on disk space for logging! Please take action\n" "to ensure no loss of service.\n" "%s", next_actions); } else { subject = "Audit Admin Space Alert"; snprintf(content, sizeof(content), "The audit daemon is very low on disk space for logging! Immediate action\n" "is required to ensure no loss of service.\n" "%s", next_actions); } sendmail(subject, content, config->action_mail_acct); audit_msg(LOG_ALERT, "%s", content); break; } case FA_EXEC: // Close the logging file in case the script zips or // moves the file. We'll reopen in sigusr2 handler if (log_file) fclose(log_file); log_file = NULL; AUDIT_ATOMIC_STORE(log_fd, -1); logging_suspended = 1; if (admin) safe_exec(config->admin_space_left_exe, 0); else safe_exec(config->space_left_exe, 0); break; case FA_SUSPEND: audit_msg(LOG_ALERT, "Audit daemon is suspending logging due to low disk space."); // We need to close the file so that manual // intervention can move or delete the file. We // don't want to keep logging to a deleted file. if (log_file) fclose(log_file); log_file = NULL; AUDIT_ATOMIC_STORE(log_fd, -1); logging_suspended = 1; break; case FA_SINGLE: audit_msg(LOG_ALERT, "The audit daemon is now changing the system to single user mode and exiting due to low disk space"); change_runlevel(SINGLE); AUDIT_ATOMIC_STORE(stop, 1); break; case FA_HALT: // Only available for admin audit_msg(LOG_ALERT, "The audit daemon is now halting the system and exiting due to low disk space"); change_runlevel(HALT); AUDIT_ATOMIC_STORE(stop, 1); break; default: audit_msg(LOG_ALERT, "Audit daemon is low on disk space for logging and unknown action requested"); break; } } static void do_disk_full_action(void) { audit_msg(LOG_ALERT, "Audit daemon has no space left on logging partition"); switch (config->disk_full_action) { case FA_IGNORE: case FA_SYSLOG: /* Message is syslogged above */ break; case FA_ROTATE: if (config->num_logs > 1) { audit_msg(LOG_INFO, "Audit daemon rotating log files"); rotate_logs(0, 0); } break; case FA_EXEC: // Close the logging file in case the script zips or // moves the file. We'll reopen in sigusr2 handler if (log_file) fclose(log_file); log_file = NULL; AUDIT_ATOMIC_STORE(log_fd, -1); logging_suspended = 1; safe_exec(config->disk_full_exe, 0); break; case FA_SUSPEND: audit_msg(LOG_ALERT, "Audit daemon is suspending logging due to no space left on logging partition."); // We need to close the file so that manual // intervention can move or delete the file. We // don't want to keep logging to a deleted file. if (log_file) fclose(log_file); log_file = NULL; AUDIT_ATOMIC_STORE(log_fd, -1); logging_suspended = 1; break; case FA_SINGLE: audit_msg(LOG_ALERT, "The audit daemon is now changing the system to single user mode and exiting due to no space left on logging partition"); change_runlevel(SINGLE); AUDIT_ATOMIC_STORE(stop, 1); break; case FA_HALT: audit_msg(LOG_ALERT, "The audit daemon is now halting the system and exiting due to no space left on logging partition"); change_runlevel(HALT); AUDIT_ATOMIC_STORE(stop, 1); break; default: audit_msg(LOG_ALERT, "Unknown disk full action requested"); break; } } static void do_disk_error_action(const char *func, int err) { char text[128]; switch (config->disk_error_action) { case FA_IGNORE: break; case FA_SYSLOG: if (disk_err_warning < 5) { snprintf(text, sizeof(text), "%s: Audit daemon detected an error writing an event to disk (%s)", func, strerror(err)); audit_msg(LOG_ALERT, "%s", text); disk_err_warning++; } break; case FA_EXEC: // Close the logging file in case the script zips or // moves the file. We'll reopen in sigusr2 handler if (log_file) fclose(log_file); log_file = NULL; AUDIT_ATOMIC_STORE(log_fd, -1); logging_suspended = 1; safe_exec(config->disk_error_exe, 0); break; case FA_SUSPEND: audit_msg(LOG_ALERT, "Audit daemon is suspending logging due to previously mentioned write error"); // We need to close the file so that manual // intervention can move or delete the file. We // don't want to keep logging to a deleted file. if (log_file) fclose(log_file); log_file = NULL; AUDIT_ATOMIC_STORE(log_fd, -1); logging_suspended = 1; break; case FA_SINGLE: audit_msg(LOG_ALERT, "The audit daemon is now changing the system to single user mode and exiting due to previously mentioned write error"); change_runlevel(SINGLE); AUDIT_ATOMIC_STORE(stop, 1); break; case FA_HALT: audit_msg(LOG_ALERT, "The audit daemon is now halting the system and exiting due to previously mentioned write error."); change_runlevel(HALT); AUDIT_ATOMIC_STORE(stop, 1); break; default: audit_msg(LOG_ALERT, "Unknown disk error action requested"); break; } } static void rotate_logs_now(void) { /* Don't rotate in debug mode */ if (config->daemonize == D_FOREGROUND) return; if (config->max_log_size_action == SZ_KEEP_LOGS) shift_logs(); else rotate_logs(0, 0); } /* Check for and remove excess logs so that we don't run out of room */ static void check_excess_logs(void) { int rc; unsigned int i, len; char *name; // Only do this if rotate is the log size action // and we actually have a limit if (config->max_log_size_action != SZ_ROTATE || config->num_logs < 2) return; len = strlen(config->log_file) + 16; name = (char *)malloc(len); if (name == NULL) { /* Not fatal - just messy */ audit_msg(LOG_ERR, "No memory checking excess logs"); return; } // We want 1 beyond the normal logs i = config->num_logs; rc = 0; while (rc == 0) { snprintf(name, len, "%s.%u", config->log_file, i++); rc=unlink(name); if (rc == 0) audit_msg(LOG_NOTICE, "Log %s removed as it exceeds num_logs parameter", name); } free(name); } static void fix_disk_permissions(void) { char *path, *dir; unsigned int i, len; if (config == NULL || config->log_file == NULL) return; len = strlen(config->log_file) + 16; path = malloc(len); if (path == NULL) return; // Start with the directory strcpy(path, config->log_file); dir = dirname(path); if (chmod(dir,config->log_group ? S_IRWXU|S_IRGRP|S_IXGRP: S_IRWXU) < 0) audit_msg(LOG_WARNING, "Couldn't change access mode of " "%s (%s)", dir, strerror(errno)); if (chown(dir, 0, config->log_group ? config->log_group : 0) < 0) audit_msg(LOG_WARNING, "Couldn't change ownership of " "%s (%s)", dir, strerror(errno)); // Now, for each file... for (i = 1; i < config->num_logs; i++) { int rc; snprintf(path, len, "%s.%u", config->log_file, i); rc = chmod(path, config->log_group ? S_IRUSR|S_IRGRP : S_IRUSR); if (rc && errno == ENOENT) break; } // Now the current file chmod(config->log_file, config->log_group ? S_IWUSR|S_IRUSR|S_IRGRP : S_IWUSR|S_IRUSR); free(path); } static void rotate_logs(unsigned int num_logs, unsigned int keep_logs) { int rc, i; unsigned int len; char *oldname, *newname; /* Check that log rotation is enabled in the configuration file. There * is no need to check for max_log_size_action == SZ_ROTATE because * this could be invoked externally by receiving a USR1 signal, * independently on the action parameter. */ if (config->num_logs < 2 && !keep_logs){ audit_msg(LOG_NOTICE, "Log rotation disabled (num_logs < 2), skipping"); return; } /* Close audit file. fchmod and fchown errors are not fatal because we * already adjusted log file permissions and ownership when opening the * log file. */ if (AUDIT_ATOMIC_LOAD(log_fd) >= 0) { if (fchmod(AUDIT_ATOMIC_LOAD(log_fd), config->log_group ? S_IRUSR|S_IRGRP : S_IRUSR) < 0){ audit_msg(LOG_WARNING, "Couldn't change permissions while " "rotating log file (%s)", strerror(errno)); } if (fchown(AUDIT_ATOMIC_LOAD(log_fd), 0, config->log_group) < 0) { audit_msg(LOG_WARNING, "Couldn't change ownership while " "rotating log file (%s)", strerror(errno)); } } if (log_file) { AUDIT_ATOMIC_STORE(log_fd, -1); fclose(log_file); log_file = NULL; } /* Rotate */ len = strlen(config->log_file) + 16; oldname = (char *)malloc(len); if (oldname == NULL) { /* Not fatal - just messy */ audit_msg(LOG_ERR, "No memory rotating logs"); logging_suspended = 1; return; } newname = (char *)malloc(len); if (newname == NULL) { /* Not fatal - just messy */ audit_msg(LOG_ERR, "No memory rotating logs"); free(oldname); logging_suspended = 1; return; } /* If we are rotating, get number from config */ if (num_logs == 0) num_logs = config->num_logs; /* Handle this case first since it will not enter the for loop */ if (num_logs == 2) snprintf(oldname, len, "%s.1", config->log_file); known_logs = 0; for (i=(int)num_logs - 1; i>1; i--) { snprintf(oldname, len, "%s.%d", config->log_file, i-1); snprintf(newname, len, "%s.%d", config->log_file, i); /* if the old file exists */ rc = rename(oldname, newname); if (rc == -1 && errno != ENOENT) { // Likely errors: ENOSPC, ENOMEM, EBUSY int saved_errno = errno; audit_msg(LOG_ERR, "Error rotating logs from %s to %s (%s)", oldname, newname, strerror(errno)); if (saved_errno == ENOSPC && fs_space_left == 1) { fs_space_left = 0; do_disk_full_action(); } else do_disk_error_action("rotate", saved_errno); } else if (rc == 0 && known_logs == 0) known_logs = i + 1; } free(newname); /* At this point, oldname should point to lowest number - use it */ newname = oldname; rc = rename(config->log_file, newname); if (rc == -1 && errno != ENOENT) { // Likely errors: ENOSPC, ENOMEM, EBUSY int saved_errno = errno; audit_msg(LOG_ERR, "Error rotating logs from %s to %s (%s)", config->log_file, newname, strerror(errno)); if (saved_errno == ENOSPC && fs_space_left == 1) { fs_space_left = 0; do_disk_full_action(); } else do_disk_error_action("rotate2", saved_errno); /* At this point, we've failed to rotate the original log. * So, let's make the old log writable and try again next * time */ chmod(config->log_file, config->log_group ? S_IWUSR|S_IRUSR|S_IRGRP : S_IWUSR|S_IRUSR); } free(newname); /* open new audit file */ if (open_audit_log()) { int saved_errno = errno; audit_msg(LOG_CRIT, "Could not reopen a log after rotating."); logging_suspended = 1; do_disk_error_action("reopen", saved_errno); } } static unsigned int last_log = 1; static void shift_logs(void) { // The way this has to work is to start scanning from .1 up until // no file is found. Then do the rotate algorithm using that number // instead of log_max. unsigned int num_logs, len; char *name; len = strlen(config->log_file) + 16; name = (char *)malloc(len); if (name == NULL) { /* Not fatal - just messy */ audit_msg(LOG_ERR, "No memory shifting logs"); return; } // Find last log num_logs = last_log; while (num_logs) { snprintf(name, len, "%s.%u", config->log_file, num_logs); if (access(name, R_OK) != 0) break; num_logs++; } known_logs = num_logs; /* Our last known file disappeared, start over... */ if (num_logs <= last_log && last_log > 1) { audit_msg(LOG_WARNING, "Last known log disappeared (%s)", name); num_logs = last_log = 1; while (num_logs) { snprintf(name, len, "%s.%u", config->log_file, num_logs); if (access(name, R_OK) != 0) break; num_logs++; } audit_msg(LOG_INFO, "Next log to use will be %s", name); } last_log = num_logs; rotate_logs(num_logs+1, 1); free(name); } /* * This function handles opening a descriptor for the audit log * file and ensuring the correct options are applied to the descriptor. * It returns 0 on success and 1 on failure. */ static int open_audit_log(void) { int flags, lfd; if (config->write_logs == 0) return 0; flags = O_WRONLY|O_APPEND|O_NOFOLLOW|O_CLOEXEC; if (config->flush == FT_DATA) flags |= O_DSYNC; else if (config->flush == FT_SYNC) flags |= O_SYNC; // Likely errors for open: Almost anything // Likely errors on rotate: ENFILE, ENOMEM, ENOSPC retry: lfd = open(config->log_file, flags); if (lfd < 0) { if (errno == ENOENT) { lfd = create_log_file(config->log_file); if (lfd < 0) { audit_msg(LOG_CRIT, "Couldn't create log file %s (%s)", config->log_file, strerror(errno)); return 1; } close(lfd); lfd = open(config->log_file, flags); log_size = 0; } else if (errno == ENFILE) { // All system descriptors used, try again... goto retry; } if (lfd < 0) { audit_msg(LOG_CRIT, "Couldn't open log file %s (%s)", config->log_file, strerror(errno)); return 1; } } else { // Get initial size struct stat st; int rc = fstat(lfd, &st); if (rc == 0) log_size = st.st_size; else { close(lfd); return 1; } } if (fchmod(lfd, config->log_group ? S_IRUSR|S_IWUSR|S_IRGRP : S_IRUSR|S_IWUSR) < 0) { audit_msg(LOG_ERR, "Couldn't change permissions of log file (%s)", strerror(errno)); close(lfd); return 1; } if (fchown(lfd, 0, config->log_group) < 0) { audit_msg(LOG_ERR, "Couldn't change ownership of log file (%s)", strerror(errno)); close(lfd); return 1; } AUDIT_ATOMIC_STORE(log_fd, lfd); log_file = fdopen(lfd, "a"); if (log_file == NULL) { audit_msg(LOG_CRIT, "Error setting up log descriptor (%s)", strerror(errno)); close(lfd); return 1; } /* Set it to line buffering */ setlinebuf(log_file); return 0; } /* * Execute an audit action helper and remember it for the SIGCHLD callback. * @exe: path to the helper executable * @resume_on_exit: nonzero when logging must resume after this helper exits * * Returns -1 on allocation or fork failure and the child PID on success. */ static pid_t safe_exec(const char *exe, int resume_on_exit) { char *argv[2]; pid_t pid; struct sigaction sa; if (exe == NULL) { audit_msg(LOG_ALERT, "Safe_exec passed NULL for program to execute"); return -1; } pid = auditd_fork_child(resume_on_exit ? resume_logging : NULL); if (pid < 0) { audit_msg(LOG_ALERT, "Audit daemon failed to fork doing safe_exec"); return -1; } if (pid) /* Parent */ return pid; /* Child */ sigfillset(&sa.sa_mask); sigprocmask(SIG_UNBLOCK, &sa.sa_mask, 0); #ifdef HAVE_CLOSE_RANGE close_range(3, ~0U, 0); /* close all past stderr */ #else for (int i=3; i<24; i++) /* Arbitrary number */ close(i); #endif argv[0] = (char *)exe; argv[1] = NULL; execve(exe, argv, NULL); audit_msg(LOG_ALERT, "Audit daemon failed to exec %s", exe); _exit(EXIT_FAILURE); // Avoid running the atexit handlers } /* * get_log_fd - Return the current audit log file descriptor. * * Returns: The current audit log file descriptor. */ static int get_log_fd(void) { return AUDIT_ATOMIC_LOAD(log_fd); } /* * reconfigure - Build the context and apply a reconfigure event. * @e: Audit daemon reconfigure event carrying the new configuration. * * Returns: None. */ static void reconfigure(struct auditd_event *e) { struct auditd_reconfigure_context ctx = { .event = e, .config = config, .state = { .log_file = &log_file, .disk_err_warning = &disk_err_warning, .fs_space_warning = &fs_space_warning, .fs_admin_space_warning = &fs_admin_space_warning, .fs_space_left = &fs_space_left, .logging_suspended = &logging_suspended, }, .ops = { .check_log_file_size = check_log_file_size, .check_space_left = check_space_left, .fix_disk_permissions = fix_disk_permissions, .check_excess_logs = check_excess_logs, .do_disk_error_action = do_disk_error_action, .open_audit_log = open_audit_log, .get_log_fd = get_log_fd, }, }; auditd_reconfigure(&ctx); }