/* auditd-event.c --
* Copyright 2004-08,2011,2013,2015-16,2018 Red Hat Inc.,Durham, North Carolina.
* 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 <time.h>
#include <sys/time.h>
#include <sys/vfs.h>
#include <limits.h> /* POSIX_HOST_NAME_MAX */
#include <ctype.h> /* toupper */
#include <libgen.h> /* dirname */
#include "auditd-event.h"
#include "auditd-dispatch.h"
#include "auditd-listen.h"
#include "libaudit.h"
#include "private.h"
#include "auparse.h"
/* This is defined in auditd.c */
extern volatile int stop;
/* 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 void change_runlevel(const char *level);
static void safe_exec(const char *exe);
static void reconfigure(struct auditd_event *e);
static void init_flush_thread(void);
/* Local Data */
static struct daemon_conf *config;
static volatile int log_fd;
static FILE *log_file;
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 const char *SINGLE = "1";
static const char *HALT = "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;
static volatile int flush;
/* Local definitions */
#define FORMAT_BUF_LEN (MAX_AUDIT_MESSAGE_LENGTH + _POSIX_HOST_NAME_MAX)
#define MIN_SPACE_LEFT 24
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 = %lu KB\n", log_size/1024);
fprintf(f, "max log size = %lu KB\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(log_fd, &buf);
if (rc == 0) {
fprintf(f, "Logging partition free space %lu MB\n",
(buf.f_bavail * buf.f_bsize)/MEGABYTE);
fprintf(f, "space_left setting %lu MB\n",
config->space_left);
fprintf(f, "admin_space_left setting %lu MB\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)
{
/* Give it 5 seconds to clear the queue */
alarm(5);
// Nudge the flush thread
pthread_cond_signal(&do_flush);
pthread_join(flush_thread, NULL);
free((void *)format_buf);
if (log_file)
fclose(log_file);
auparse_destroy_ext(NULL, AUPARSE_DESTROY_ALL);
}
int init_event(struct daemon_conf *conf)
{
/* Store the netlink descriptor and config info away */
config = conf;
log_fd = -1;
/* Now open the log */
if (config->daemonize == D_BACKGROUND) {
fix_disk_permissions();
if (open_audit_log())
return 1;
setup_percentages(config, log_fd);
} else {
log_fd = 1; // stdout
log_file = fdopen(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();
check_space_left();
}
format_buf = (char *)malloc(FORMAT_BUF_LEN);
if (format_buf == NULL) {
audit_msg(LOG_ERR, "No memory for formatting, exiting");
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, SIGALRM);
sigaddset(&sigs, SIGTERM);
sigaddset(&sigs, SIGHUP);
sigaddset(&sigs, SIGUSR1);
sigaddset(&sigs, SIGUSR2);
pthread_sigmask(SIG_SETMASK, &sigs, NULL);
while (!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 (flush == 0) {
pthread_cond_wait(&do_flush, &flush_lock);
if (stop)
return NULL;
}
flush = 0;
pthread_mutex_unlock(&flush_lock);
fsync(log_fd);
}
return NULL;
}
/* We setup the flush thread no matter what. This is incase a reconfig
* changes from non incremental to incremental or vise versa. */
static void init_flush_thread(void)
{
pthread_mutex_init(&flush_lock, NULL);
pthread_cond_init(&do_flush, NULL);
flush = 0;
pthread_create(&flush_thread, NULL, flush_thread_main, NULL);
}
static void replace_event_msg(struct auditd_event *e, const char *buf)
{
if (buf) {
size_t len = strlen(buf);
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
}
}
/*
* 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 NULL.
*/
static const char *format_raw(const struct audit_reply *rep)
{
char *ptr;
if (rep == NULL) {
if (config->node_name_format != N_NONE)
snprintf(format_buf, FORMAT_BUF_LEN - 32,
"node=%s type=DAEMON_ERR op=format-raw msg=NULL res=failed",
config->node_name);
else
snprintf(format_buf, MAX_AUDIT_MESSAGE_LENGTH,
"type=DAEMON_ERR op=format-raw msg=NULL res=failed");
} else {
int len, nlen;
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)
nlen = snprintf(format_buf, FORMAT_BUF_LEN - 32,
"node=%s type=%s msg=%.*s\n",
config->node_name, type, len, message);
else
nlen = snprintf(format_buf,
MAX_AUDIT_MESSAGE_LENGTH - 32,
"type=%s msg=%.*s", type, len, message);
/* Replace \n with space so it looks nicer. */
ptr = format_buf;
while ((ptr = strchr(ptr, 0x0A)) != NULL)
*ptr = ' ';
/* Trim trailing space off since it wastes space */
if (format_buf[nlen-1] == ' ')
format_buf[nlen-1] = 0;
}
return format_buf;
}
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;
tlen = 1 + strlen(enc) + 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) {
num += snprintf(ptr, tlen, "%s", enc);
free(enc);
} else
num += snprintf(ptr, tlen, "%s=%s", field_name, value);
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 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;
size_t mlen, len;
auparse_state_t *au;
char *message;
// Do raw format to get event started
format_raw(rep);
// How much room is left?
mlen = strlen(format_buf);
len = FORMAT_BUF_LEN - mlen;
if (len <= MIN_SPACE_LEFT)
return format_buf;
// create copy to parse up
format_buf[mlen] = 0x0A;
format_buf[mlen+1] = 0;
message = strdup(format_buf);
format_buf[mlen] = 0;
// init auparse
au = auparse_init(AUSOURCE_BUFFER, message);
if (au == NULL) {
free(message);
return format_buf;
}
auparse_set_escape_mode(au, AUPARSE_ESC_RAW);
sep_done = 0;
// Loop over all fields while possible to add field
rc = auparse_first_record(au);
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);
}
auparse_destroy_ext(au, AUPARSE_DESTROY_COMMON);
free(message);
}
return format_buf;
}
void format_event(struct auditd_event *e)
{
const char *buf;
switch (config->log_format)
{
case LF_RAW:
buf = format_raw(&e->reply);
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);
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(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 (fsync(log_fd) != 0) {
do_disk_error_action(
"fsync",
errno);
}
} else {
pthread_mutex_lock(&flush_lock);
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 % 3) < 2)
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_SUSPEND:
audit_msg(LOG_ERR,
"Audit daemon is suspending logging due to logfile size.");
logging_suspended = 1;
break;
case SZ_ROTATE:
if (config->num_logs > 1) {
audit_msg(LOG_NOTICE,
"Audit daemon rotating log files");
rotate_logs(0, 0);
}
break;
case SZ_KEEP_LOGS:
audit_msg(LOG_NOTICE,
"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;
rc = fstatfs(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;
if (admin)
action = config->admin_space_left_action;
else
action = config->space_left_action;
switch (action)
{
case FA_IGNORE:
break;
case FA_SYSLOG:
audit_msg(LOG_ALERT,
"Audit daemon is low on disk space for logging");
break;
case FA_ROTATE:
if (config->num_logs > 1) {
audit_msg(LOG_NOTICE,
"Audit daemon rotating log files");
rotate_logs(0, 0);
}
break;
case FA_EMAIL:
if (admin == 0) {
sendmail("Audit Disk Space Alert",
"The audit daemon is low on disk space for logging! Please take action\nto ensure no loss of service.",
config->action_mail_acct);
audit_msg(LOG_ALERT,
"Audit daemon is low on disk space for logging");
} else {
sendmail("Audit Admin Space Alert",
"The audit daemon is very low on disk space for logging! Immediate action\nis required to ensure no loss of service.",
config->action_mail_acct);
audit_msg(LOG_ALERT,
"Audit daemon is very low on disk space for logging");
}
break;
case FA_EXEC:
// Close the logging file in case the script zips or
// moves the file. We'll reopen in sigusr2 handler
fclose(log_file);
log_file = NULL;
log_fd = -1;
logging_suspended = 1;
if (admin)
safe_exec(config->admin_space_left_exe);
else
safe_exec(config->space_left_exe);
break;
case FA_SUSPEND:
audit_msg(LOG_ALERT,
"Audit daemon is suspending logging due to low disk space.");
logging_suspended = 1;
break;
case FA_SINGLE:
audit_msg(LOG_ALERT,
"The audit daemon is now changing the system to single user mode");
change_runlevel(SINGLE);
break;
case FA_HALT:
audit_msg(LOG_ALERT,
"The audit daemon is now halting the system");
change_runlevel(HALT);
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_NOTICE,
"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
fclose(log_file);
log_file = NULL;
log_fd = -1;
logging_suspended = 1;
safe_exec(config->disk_full_exe);
break;
case FA_SUSPEND:
audit_msg(LOG_ALERT,
"Audit daemon is suspending logging due to no space left on logging partition.");
logging_suspended = 1;
break;
case FA_SINGLE:
audit_msg(LOG_ALERT,
"The audit daemon is now changing the system to single user mode due to no space left on logging partition");
change_runlevel(SINGLE);
break;
case FA_HALT:
audit_msg(LOG_ALERT,
"The audit daemon is now halting the system due to no space left on logging partition");
change_runlevel(HALT);
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
fclose(log_file);
log_file = NULL;
log_fd = -1;
logging_suspended = 1;
safe_exec(config->disk_error_exe);
break;
case FA_SUSPEND:
audit_msg(LOG_ALERT,
"Audit daemon is suspending logging due to previously mentioned write error");
logging_suspended = 1;
break;
case FA_SINGLE:
audit_msg(LOG_ALERT,
"The audit daemon is now changing the system to single user mode due to previously mentioned write error");
change_runlevel(SINGLE);
break;
case FA_HALT:
audit_msg(LOG_ALERT,
"The audit daemon is now halting the system due to previously mentioned write error.");
change_runlevel(HALT);
break;
default:
audit_msg(LOG_ALERT,
"Unknown disk error action requested");
break;
}
}
static void rotate_logs_now(void)
{
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);
chmod(dir, config->log_group ? S_IRWXU|S_IRGRP|S_IXGRP : S_IRWXU);
chown(dir, 0, config->log_group ? config->log_group : 0);
// 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 (fchmod(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(log_fd, 0, config->log_group) < 0) {
audit_msg(LOG_WARNING, "Couldn't change ownership while "
"rotating log file (%s)", strerror(errno));
}
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.%u", config->log_file, i-1);
snprintf(newname, len, "%s.%u", 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;
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 (fcntl(lfd, F_SETFD, FD_CLOEXEC) == -1) {
audit_msg(LOG_ERR, "Error setting log file CLOEXEC flag (%s)",
strerror(errno));
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;
}
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;
}
static void change_runlevel(const char *level)
{
char *argv[3];
int pid;
struct sigaction sa;
static const char *init_pgm = "/sbin/init";
pid = fork();
if (pid < 0) {
audit_msg(LOG_ALERT,
"Audit daemon failed to fork switching runlevels");
return;
}
if (pid) /* Parent */
return;
/* Child */
sigfillset (&sa.sa_mask);
sigprocmask (SIG_UNBLOCK, &sa.sa_mask, 0);
argv[0] = (char *)init_pgm;
argv[1] = (char *)level;
argv[2] = NULL;
execve(init_pgm, argv, NULL);
audit_msg(LOG_ALERT, "Audit daemon failed to exec %s", init_pgm);
exit(1);
}
static void safe_exec(const char *exe)
{
char *argv[2];
int pid;
struct sigaction sa;
if (exe == NULL) {
audit_msg(LOG_ALERT,
"Safe_exec passed NULL for program to execute");
return;
}
pid = fork();
if (pid < 0) {
audit_msg(LOG_ALERT,
"Audit daemon failed to fork doing safe_exec");
return;
}
if (pid) /* Parent */
return;
/* Child */
sigfillset (&sa.sa_mask);
sigprocmask (SIG_UNBLOCK, &sa.sa_mask, 0);
argv[0] = (char *)exe;
argv[1] = NULL;
execve(exe, argv, NULL);
audit_msg(LOG_ALERT, "Audit daemon failed to exec %s", exe);
exit(1);
}
static void reconfigure(struct auditd_event *e)
{
struct daemon_conf *nconf = e->reply.conf;
struct daemon_conf *oconf = config;
uid_t uid = nconf->sender_uid;
pid_t pid = nconf->sender_pid;
const char *ctx = nconf->sender_ctx;
struct timeval tv;
char txt[MAX_AUDIT_MESSAGE_LENGTH];
char date[40];
unsigned int seq_num;
int need_size_check = 0, need_reopen = 0, need_space_check = 0;
snprintf(txt, sizeof(txt),
"config change requested by pid=%d auid=%u subj=%s",
pid, uid, ctx);
audit_msg(LOG_NOTICE, "%s", txt);
/* Do the reconfiguring. These are done in a specific
* order from least invasive to most invasive. We will
* start with general system parameters. */
// start with disk error action.
oconf->disk_error_action = nconf->disk_error_action;
free((char *)oconf->disk_error_exe);
oconf->disk_error_exe = nconf->disk_error_exe;
disk_err_warning = 0;
// number of logs
oconf->num_logs = nconf->num_logs;
// flush freq
oconf->freq = nconf->freq;
// priority boost
if (oconf->priority_boost != nconf->priority_boost) {
int rc;
oconf->priority_boost = nconf->priority_boost;
errno = 0;
rc = nice(-oconf->priority_boost);
if (rc == -1 && errno)
audit_msg(LOG_WARNING, "Cannot change priority in "
"reconfigure (%s)", strerror(errno));
}
// log format
oconf->log_format = nconf->log_format;
// Only update this if we are in background mode since
// foreground mode writes to stderr.
if ((oconf->write_logs != nconf->write_logs) &&
(oconf->daemonize == D_BACKGROUND)) {
oconf->write_logs = nconf->write_logs;
need_reopen = 1;
}
// log_group
if (oconf->log_group != nconf->log_group) {
oconf->log_group = nconf->log_group;
need_reopen = 1;
}
// action_mail_acct
if (strcmp(oconf->action_mail_acct, nconf->action_mail_acct)) {
free((void *)oconf->action_mail_acct);
oconf->action_mail_acct = nconf->action_mail_acct;
} else
free((void *)nconf->action_mail_acct);
// node_name
if (oconf->node_name_format != nconf->node_name_format ||
(oconf->node_name && nconf->node_name &&
strcmp(oconf->node_name, nconf->node_name) != 0)) {
oconf->node_name_format = nconf->node_name_format;
free((char *)oconf->node_name);
oconf->node_name = nconf->node_name;
}
// network listener
auditd_tcp_listen_reconfigure(nconf, oconf);
// distribute network events
oconf->distribute_network_events = nconf->distribute_network_events;
// Dispatcher items
oconf->q_depth = nconf->q_depth;
oconf->overflow_action = nconf->overflow_action;
oconf->max_restarts = nconf->max_restarts;
if (oconf->plugin_dir != nconf->plugin_dir ||
(oconf->plugin_dir && nconf->plugin_dir &&
strcmp(oconf->plugin_dir, nconf->plugin_dir) != 0)) {
free(oconf->plugin_dir);
oconf->plugin_dir = nconf->plugin_dir;
}
/* At this point we will work on the items that are related to
* a single log file. */
// max logfile action
if (oconf->max_log_size_action != nconf->max_log_size_action) {
oconf->max_log_size_action = nconf->max_log_size_action;
need_size_check = 1;
}
// max log size
if (oconf->max_log_size != nconf->max_log_size) {
oconf->max_log_size = nconf->max_log_size;
need_size_check = 1;
}
if (need_size_check) {
logging_suspended = 0;
check_log_file_size();
}
// flush technique
if (oconf->flush != nconf->flush) {
oconf->flush = nconf->flush;
need_reopen = 1;
}
// logfile
if (strcmp(oconf->log_file, nconf->log_file)) {
free((void *)oconf->log_file);
oconf->log_file = nconf->log_file;
need_reopen = 1;
need_space_check = 1; // might be on new partition
} else
free((void *)nconf->log_file);
if (need_reopen) {
fclose(log_file);
log_file = NULL;
fix_disk_permissions();
if (open_audit_log()) {
int saved_errno = errno;
audit_msg(LOG_ERR,
"Could not reopen a log after reconfigure");
logging_suspended = 1;
// Likely errors: ENOMEM, ENOSPC
do_disk_error_action("reconfig", saved_errno);
} else {
logging_suspended = 0;
check_log_file_size();
}
}
/* At this point we will start working on items that are
* related to the amount of space on the partition. */
// space left
if (oconf->space_left != nconf->space_left) {
oconf->space_left = nconf->space_left;
need_space_check = 1;
}
// space left percent
if (oconf->space_left_percent != nconf->space_left_percent) {
oconf->space_left_percent = nconf->space_left_percent;
need_space_check = 1;
}
// space left action
if (oconf->space_left_action != nconf->space_left_action) {
oconf->space_left_action = nconf->space_left_action;
need_space_check = 1;
}
// space left exe
if (oconf->space_left_exe || nconf->space_left_exe) {
if (nconf->space_left_exe == NULL)
; /* do nothing if new one is blank */
else if (oconf->space_left_exe == NULL && nconf->space_left_exe)
need_space_check = 1;
else if (strcmp(oconf->space_left_exe, nconf->space_left_exe))
need_space_check = 1;
free((char *)oconf->space_left_exe);
oconf->space_left_exe = nconf->space_left_exe;
}
// admin space left
if (oconf->admin_space_left != nconf->admin_space_left) {
oconf->admin_space_left = nconf->admin_space_left;
need_space_check = 1;
}
// admin space left percent
if (oconf->admin_space_left_percent != nconf->admin_space_left_percent){
oconf->admin_space_left_percent =
nconf->admin_space_left_percent;
need_space_check = 1;
}
// admin space action
if (oconf->admin_space_left_action != nconf->admin_space_left_action) {
oconf->admin_space_left_action = nconf->admin_space_left_action;
need_space_check = 1;
}
// admin space left exe
if (oconf->admin_space_left_exe || nconf->admin_space_left_exe) {
if (nconf->admin_space_left_exe == NULL)
; /* do nothing if new one is blank */
else if (oconf->admin_space_left_exe == NULL &&
nconf->admin_space_left_exe)
need_space_check = 1;
else if (strcmp(oconf->admin_space_left_exe,
nconf->admin_space_left_exe))
need_space_check = 1;
free((char *)oconf->admin_space_left_exe);
oconf->admin_space_left_exe = nconf->admin_space_left_exe;
}
// disk full action
if (oconf->disk_full_action != nconf->disk_full_action) {
oconf->disk_full_action = nconf->disk_full_action;
need_space_check = 1;
}
// disk full exe
if (oconf->disk_full_exe || nconf->disk_full_exe) {
if (nconf->disk_full_exe == NULL)
; /* do nothing if new one is blank */
else if (oconf->disk_full_exe == NULL && nconf->disk_full_exe)
need_space_check = 1;
else if (strcmp(oconf->disk_full_exe, nconf->disk_full_exe))
need_space_check = 1;
free((char *)oconf->disk_full_exe);
oconf->disk_full_exe = nconf->disk_full_exe;
}
if (need_space_check) {
/* note save suspended flag, then do space_left. If suspended
* is still 0, then copy saved suspended back. This avoids
* having to call check_log_file_size to restore it. */
int saved_suspend = logging_suspended;
setup_percentages(oconf, log_fd);
fs_space_warning = 0;
fs_admin_space_warning = 0;
fs_space_left = 1;
logging_suspended = 0;
check_excess_logs();
check_space_left();
if (logging_suspended == 0)
logging_suspended = saved_suspend;
}
reconfigure_dispatcher(oconf);
// Next document the results
srand(time(NULL));
seq_num = rand()%10000;
if (gettimeofday(&tv, NULL) == 0) {
snprintf(date, sizeof(date), "audit(%lu.%03u:%u)", tv.tv_sec,
(unsigned)(tv.tv_usec/1000), seq_num);
} else {
snprintf(date, sizeof(date),
"audit(%lu.%03d:%u)", (unsigned long)time(NULL),
0, seq_num);
}
e->reply.type = AUDIT_DAEMON_CONFIG;
e->reply.len = snprintf(e->reply.msg.data, MAX_AUDIT_MESSAGE_LENGTH-2,
"%s: op=reconfigure state=changed auid=%u pid=%d subj=%s res=success",
date, uid, pid, ctx );
e->reply.message = e->reply.msg.data;
free((char *)ctx);
}