[PATCH] filesystem location based auditing

Amy Griffis amy.griffis at hp.com
Fri Feb 24 20:19:02 UTC 2006


Hello,

This patch provides the functionality which allows a user to specify
audit rules targeting specific filesystem locations.  It is an update
of the previously posted patch which provided functionality solely for
adding/removing rules with AUDIT_WATCH fields:

https://www.redhat.com/archives/linux-audit/2006-February/msg00034.html

The patch is intended to be a replacement for linux-2.6-audit-string-2.patch
in the current LSPP test kernel, and the git tree patch represented by
commit 7f54bafe959bcecda97a4a1e4a083a39fb4d9fc8 in branch amg.b0.
Since it depends on the inotify kernel API patch, the commits should
probably be re-ordered in the next branch.

This patch adds parent data structures for filesystem watches
specified in audit rules.  A 'parent' is analogous to the dentry
parent for a watched filesystem location.  When a parent is added, a
corresponding inotify watch is registered for it.  The inotify event
handler specified during during initialization (audit_init) determines
an action to take for various events received from inotify.

The actions taken fall into two groups.  When an event is received
which indicates a change at a watched filesystem location, audit
replaces rules in the AUDIT_FILTER_EXIT list with updated copies
containing the new inode # (or the invalid value on removal).  When
inotify indicates the parent is being removed from the filesystem,
audit removes the parent, its associated watches, and their
associated rules.

Part of this functionality was also previously posted here as a WIP:

https://www.redhat.com/archives/linux-audit/2006-February/msg00030.html

This patch improves on the WIP patch in the following ways:

    - Adds per-filterlist spinlocks to protect against concurrent
      filterlist writes/manipulations.

    - Improves code for copying rules for list_replace_rcu().

    - Removes the per-watch and per-parent semaphores, which weren't
      playing well with rcu.

    - Moves calls to add/remove inotify watches out to separate
      threads, which enables us to hold the filterlist lock through
      the duration of operations to add or remove audit rules.  I
      attempted a solution involving dropping the filterlist locks
      part way through, and found it to be difficult to handle the
      races.

The locking model implemented in this patch consists of a two-level
lock hierarchy:

    per-filterlist spinlock
        parentlist spinlock

The per-filterlist spinlock protects the filterlist, the rules it
contains, and the watch.rules and parent.watches lists that are
associated with the filterlist.  The contents of the watch structure
are write-once, so don't need an additional lock.

The parentlist spinlock protects the master_parents list and the data
in the parent structure, excluding the parent.watches list.

In this patch, only the AUDIT_FILTER_EXIT list lock is used, but I
added the others as I anticipate they will be used for rule updates
from SELinux policy reload.

This locking model seems to be sufficient to cover the potential race
conditions, but more granularity could be added for performance or
code clarity.  I decided to keep it as simple as possible for the
initial implementation.

Limitations:

This implementation has the following known limitations:

    - The dentry parent for a watched filesystem location must exist.
    - Only one watch may be specified per rule.

Todo:

I anticipate some changes may need to be made to audit_update_rule()
and audit_dupe_rule() to properly accommodate the SELinux filtering
that Darrel and Dustin are implementing.

Additionally, the update of rules resulting from the removal of a file
at a watched filesystem location happens too early in the syscall
path.  This causes the inode number for the rule to be invalidated
prior to syscall exit filtering, and the record that should have been
emitted is not.  I'm still working on a good solution for this.

TIA for reviewing this patch.

Regards,
Amy

 include/linux/audit.h |    1 
 kernel/audit.c        |   22 +
 kernel/audit.h        |   34 ++
 kernel/auditfilter.c  |  700 +++++++++++++++++++++++++++++++++++++++++++++-----
 kernel/auditsc.c      |   53 +--
 5 files changed, 709 insertions(+), 101 deletions(-)


diff --git a/include/linux/audit.h b/include/linux/audit.h
index c208554..d76fa58 100644
--- a/include/linux/audit.h
+++ b/include/linux/audit.h
@@ -148,6 +148,7 @@
 #define AUDIT_INODE	102
 #define AUDIT_EXIT	103
 #define AUDIT_SUCCESS   104	/* exit >= 0; value ignored */
+#define AUDIT_WATCH	105
 
 #define AUDIT_ARG0      200
 #define AUDIT_ARG1      (AUDIT_ARG0+1)
diff --git a/kernel/audit.c b/kernel/audit.c
index 4eb97b6..82d926e 100644
--- a/kernel/audit.c
+++ b/kernel/audit.c
@@ -55,6 +55,9 @@
 #include <net/netlink.h>
 #include <linux/skbuff.h>
 #include <linux/netlink.h>
+#include <linux/inotify.h>
+
+#include "audit.h"
 
 /* No auditing will take place until audit_initialized != 0.
  * (Initialization happens after skb_init is called.) */
@@ -99,6 +102,12 @@ static atomic_t    audit_lost = ATOMIC_I
 /* The netlink socket. */
 static struct sock *audit_sock;
 
+/* Inotify device. */
+struct inotify_device *audit_idev;
+
+/* Audit filter lists, initialized in audit_init() */
+extern struct audit_flist audit_filter_list[];
+
 /* The audit_freelist is a list of pre-allocated audit buffers (if more
  * than AUDIT_MAXFREE are in use, the audit buffer is freed instead of
  * being placed on the freelist). */
@@ -552,6 +561,14 @@ static void audit_receive(struct sock *s
 /* Initialize audit support at boot time. */
 static int __init audit_init(void)
 {
+	int i;
+
+	/* must be initialized before any audit_log calls */
+	for (i = 0; i < AUDIT_NR_FILTERS; i++) {
+		INIT_LIST_HEAD(&audit_filter_list[i].head);
+		spin_lock_init(&audit_filter_list[i].lock);
+	}
+
 	printk(KERN_INFO "audit: initializing netlink socket (%s)\n",
 	       audit_default ? "enabled" : "disabled");
 	audit_sock = netlink_kernel_create(NETLINK_AUDIT, 0, audit_receive,
@@ -564,6 +581,11 @@ static int __init audit_init(void)
 	audit_initialized = 1;
 	audit_enabled = audit_default;
 	audit_log(NULL, GFP_KERNEL, AUDIT_KERNEL, "initialized");
+
+	audit_idev = inotify_init(audit_handle_ievent);
+	if (IS_ERR(audit_idev))
+		audit_panic("cannot initialize inotify device");
+
 	return 0;
 }
 __initcall(audit_init);
diff --git a/kernel/audit.h b/kernel/audit.h
index 4b602cd..ecb69d0 100644
--- a/kernel/audit.h
+++ b/kernel/audit.h
@@ -22,6 +22,8 @@
 #include <linux/fs.h>
 #include <linux/audit.h>
 
+struct inotify_event;
+
 /* 0 = no checking
    1 = put_count checking
    2 = verbose put_count checking
@@ -52,6 +54,23 @@ enum audit_state {
 };
 
 /* Rule lists */
+struct audit_parent {
+	atomic_t		count;	 /* reference count */
+	unsigned int		flags;	 /* flag in-process removals */
+	unsigned long		ino;	 /* associated inode number */
+	u32			wd;	 /* inotify watch descriptor */
+	struct list_head	mlist;	 /* entry in master_parents */
+	struct list_head	watches; /* associated watches */
+};
+
+struct audit_watch {
+	atomic_t		count;	 /* reference count */
+	char			*path;	 /* watch insertion path */
+	struct list_head	rules;	 /* associated rules */
+	struct list_head	wlist;	 /* entry in audit_parent.watches list*/
+	struct audit_parent	*parent; /* associated parent */
+};
+
 struct audit_field {
 	u32			type;
 	u32			val;
@@ -67,18 +86,29 @@ struct audit_krule {
 	u32			buflen; /* for data alloc on list rules */
 	u32			field_count;
 	struct audit_field	*fields;
+	struct audit_watch	*watch;	 /* associated watch */
+	struct list_head	rlist;	 /* entry in audit_watch.rules list */
 };
 
 struct audit_entry {
 	struct list_head	list;
 	struct rcu_head		rcu;
-	struct audit_krule	rule;
+	struct audit_entry	*replacement; /* used with list_replace_rcu() */
+	unsigned int		flags;	 /* flag in-process adds and removals */
+	struct audit_krule	rule;	 /* audit rule data */
 };
 
+struct audit_flist {
+	struct list_head	head;
+	spinlock_t		lock;	 /* syncs filter data manipulation */
+};
 
 extern int audit_pid;
 extern int audit_comparator(const u32 left, const u32 op, const u32 right);
-
+extern int audit_compare_dname_path(const char *dname, const char *path);
+extern void audit_handle_ievent(struct inotify_event *event,
+				const char *dname, struct inode * inode,
+				void *ptr);
 extern void		    audit_send_reply(int pid, int seq, int type,
 					     int done, int multi,
 					     void *payload, int size);
diff --git a/kernel/auditfilter.c b/kernel/auditfilter.c
index 5735acd..697a688 100644
--- a/kernel/auditfilter.c
+++ b/kernel/auditfilter.c
@@ -22,26 +22,65 @@
 #include <linux/kernel.h>
 #include <linux/audit.h>
 #include <linux/kthread.h>
+#include <linux/fs.h>
+#include <linux/namei.h>
 #include <linux/netlink.h>
+#include <linux/inotify.h>
 #include "audit.h"
 
-/* There are three lists of rules -- one to search at task creation
- * time, one to search at syscall entry time, and another to search at
- * syscall exit time. */
-struct list_head audit_filter_list[AUDIT_NR_FILTERS] = {
-	LIST_HEAD_INIT(audit_filter_list[0]),
-	LIST_HEAD_INIT(audit_filter_list[1]),
-	LIST_HEAD_INIT(audit_filter_list[2]),
-	LIST_HEAD_INIT(audit_filter_list[3]),
-	LIST_HEAD_INIT(audit_filter_list[4]),
-	LIST_HEAD_INIT(audit_filter_list[5]),
-#if AUDIT_NR_FILTERS != 6
-#error Fix audit_filter_list initialiser
-#endif
-};
+/* Audit filter lists */
+struct audit_flist audit_filter_list[AUDIT_NR_FILTERS];
+
+static LIST_HEAD(master_parents);
+static DEFINE_SPINLOCK(master_parents_lock);
+
+/* Inotify device. */
+extern struct inotify_device *audit_idev;
+
+/* Inotify events we care about. */
+#define AUDIT_IN_WATCH IN_MOVE|IN_CREATE|IN_DELETE|IN_DELETE_SELF|IN_MOVE_SELF
+#define AUDIT_IN_SELF  IN_DELETE_SELF|IN_MOVE_SELF|IN_UNMOUNT
+
+/* Flags for stale filterlist data */
+#define AUDIT_ENTRY_ADD  0x01	/* Rule entry addition in progress. */
+#define AUDIT_ENTRY_DEL  0x02	/* Rule entry deletion in progress. */
+#define AUDIT_PARENT_DEL 0x01	/* Parent deletion in progress. */
+
+static inline void audit_get_parent(struct audit_parent *parent)
+{
+	atomic_inc(&parent->count);
+}
+
+static inline void audit_put_parent(struct audit_parent *parent)
+{
+	if (atomic_dec_and_test(&parent->count)) {
+		BUG_ON(!list_empty(&parent->watches));
+		kfree(parent);
+	}
+}
+
+static inline void audit_get_watch(struct audit_watch *watch)
+{
+	atomic_inc(&watch->count);
+}
+
+static inline void audit_put_watch(struct audit_watch *watch)
+{
+	if (atomic_dec_and_test(&watch->count)) {
+		BUG_ON(!list_empty(&watch->rules));
+		/* watches that were never added don't have a parent */
+		if (watch->parent)
+			audit_put_parent(watch->parent);
+		kfree(watch->path);
+		kfree(watch);
+	}
+}
 
 static inline void audit_free_rule(struct audit_entry *e)
 {
+	/* handle rules that don't have associated watches */
+	if (e->rule.watch)
+		audit_put_watch(e->rule.watch);
 	kfree(e->rule.fields);
 	kfree(e);
 }
@@ -52,6 +91,190 @@ static inline void audit_free_rule_rcu(s
 	audit_free_rule(e);
 }
 
+/* Remove all watches & rules associated with a parent that is going away. */
+static inline void audit_remove_parent_watches(struct audit_parent *parent)
+{
+	struct audit_watch *w, *nextw;
+	struct audit_krule *r, *nextr;
+	struct audit_entry *e;
+	struct audit_flist *flist = &audit_filter_list[AUDIT_FILTER_EXIT];
+
+	spin_lock(&flist->lock);
+	list_for_each_entry_safe(w, nextw, &parent->watches, wlist) {
+		list_for_each_entry_safe(r, nextr, &w->rules, rlist) {
+			e = container_of(r, struct audit_entry, rule);
+
+			/* make sure we have a valid copy */
+			while (e->replacement != NULL)
+				e = e->replacement;
+			if (e->flags & AUDIT_ENTRY_DEL)
+				continue;
+
+			list_del(&r->rlist);
+			list_del_rcu(&e->list);
+			e->flags |= AUDIT_ENTRY_DEL;
+			call_rcu(&e->rcu, audit_free_rule_rcu);
+			audit_log(NULL, GFP_ATOMIC, AUDIT_CONFIG_CHANGE,
+				 "audit implicitly removed rule from list=%d\n",
+				  AUDIT_FILTER_EXIT);
+		}
+		list_del(&w->wlist);
+		audit_put_watch(w);
+	}
+	spin_unlock(&flist->lock);
+}
+
+/* Actually remove the parent; inotify has acknowleged the removal. */
+static inline void audit_remove_parent(struct audit_parent *parent)
+{
+	BUG_ON(!list_empty(&parent->watches));
+	spin_lock(&master_parents_lock);
+	list_del(&parent->mlist);
+	audit_put_parent(parent);
+	spin_unlock(&master_parents_lock);
+}
+
+
+/* Register this parent's inotify watch. */
+static int audit_inotify_register(void *_data)
+{
+	void **data = _data;
+	struct audit_parent *parent;
+	char *path;
+	struct nameidata nd;
+	int err;
+	u32 wd;
+
+	parent = data[0];
+	path = data[1];
+	kfree(data);
+
+	err = path_lookup(path, LOOKUP_PARENT, &nd);
+	if (err)
+		goto handle_error;
+
+	wd = inotify_add_watch(audit_idev, nd.dentry->d_inode, AUDIT_IN_WATCH,
+			       parent);
+	if (wd < 0)
+		goto handle_error;
+
+	spin_lock(&master_parents_lock);
+	parent->wd = wd;
+	parent->ino = nd.dentry->d_inode->i_ino;
+	spin_unlock(&master_parents_lock);
+
+	path_release(&nd);
+	audit_put_parent(parent);
+	return 0;
+
+handle_error:
+	path_release(&nd);
+	audit_remove_parent_watches(parent);
+	audit_remove_parent(parent);
+
+	audit_put_parent(parent);
+	return 0;
+}
+
+/* Unregister this parent's inotify watch.  Generates an IN_IGNORED event. */
+static int audit_inotify_unregister(void *data)
+{
+	struct audit_parent *parent = data;
+	s32 wd;
+
+	spin_lock(&master_parents_lock);
+	wd = parent->wd;
+	spin_unlock(&master_parents_lock);
+
+	if (inotify_ignore(audit_idev, wd))
+		printk(KERN_ERR
+		       "%s:%d: unable to remove inotify watch for inode %lu\n",
+		       __FILE__, __LINE__, parent->ino);
+	audit_put_parent(parent);
+	return 0;
+}
+
+/* Initialize a parent watch entry. */
+static inline struct audit_parent *audit_init_parent(char *path,
+						     unsigned long ino)
+{
+	struct audit_parent *parent;
+	void **data;
+	struct task_struct *task;
+
+	parent = kmalloc(sizeof(*parent), GFP_ATOMIC);
+	if (unlikely(!parent))
+		return ERR_PTR(-ENOMEM);
+
+	memset(parent, 0, sizeof(*parent));
+	INIT_LIST_HEAD(&parent->watches);
+	atomic_set(&parent->count, 0);
+	parent->ino = ino;
+	audit_get_parent(parent);
+
+	/* Spawn a thread to register the parent's inotify watch without
+	 * the filterlist spinlock. */
+	data = kmalloc(2 * sizeof(void *), GFP_ATOMIC);
+	if (!data) {
+		audit_put_parent(parent);
+		return ERR_PTR(-ENOMEM);
+	}
+	data[0] = parent;
+	data[1] = path;
+	audit_get_parent(parent);
+	task = kthread_run(audit_inotify_register, data,
+			   "audit_inotify_register");
+	if (IS_ERR(task)) {
+		audit_put_parent(parent);
+		return ERR_PTR(PTR_ERR(task));
+	}
+
+	return parent;
+}
+
+/* Initialize a watch entry. */
+static inline struct audit_watch *audit_init_watch(char *path)
+{
+	struct audit_watch *watch;
+
+	watch = kmalloc(sizeof(*watch), GFP_KERNEL);
+	if (unlikely(!watch))
+		return ERR_PTR(-ENOMEM);
+
+	memset(watch, 0, sizeof(*watch));
+	INIT_LIST_HEAD(&watch->rules);
+	atomic_set(&watch->count, 0);
+	watch->path = path;
+	audit_get_watch(watch);
+
+	return watch;
+}
+
+/* Initialize an audit filterlist entry. */
+static inline struct audit_entry *audit_init_entry(u32 field_count,
+						   gfp_t gfp_mask)
+{
+	struct audit_entry *entry;
+	struct audit_field *fields;
+
+	entry = kmalloc(sizeof(*entry), gfp_mask);
+	if (unlikely(!entry))
+		return NULL;
+
+	fields = kmalloc(sizeof(*fields) * field_count, gfp_mask);
+	if (unlikely(!fields)) {
+		kfree(entry);
+		return NULL;
+	}
+
+	memset(entry, 0, sizeof(struct audit_entry));
+	memset(fields, 0, sizeof(struct audit_field) * field_count);
+
+	entry->rule.fields = fields;
+
+	return entry;
+}
+
 /* Unpack a filter field's string representation from user-space
  * buffer. */
 static char *audit_unpack_string(void **bufp, size_t *remain, size_t len)
@@ -79,12 +302,33 @@ static char *audit_unpack_string(void **
 	return str;
 }
 
+/* Translate a watch string to kernel respresentation. */
+static int audit_to_watch(char *path, struct audit_krule *krule, int fidx)
+{
+	struct audit_field *f = &krule->fields[fidx];
+	struct audit_watch *watch;
+
+	if (path[0] != '/' || path[f->val-1] == '/' ||
+	    krule->listnr != AUDIT_FILTER_EXIT ||
+	    f->op & ~AUDIT_EQUAL)
+		return -EINVAL;
+
+	watch = audit_init_watch(path);
+	if (unlikely(IS_ERR(watch)))
+		return PTR_ERR(watch);
+
+	audit_get_watch(watch);
+	krule->watch = watch;
+	f->val = (unsigned int)-1;
+
+	return 0;
+}
+
 /* Common user-space to kernel rule translation. */
 static inline struct audit_entry *audit_to_entry_common(struct audit_rule *rule)
 {
 	unsigned listnr;
 	struct audit_entry *entry;
-	struct audit_field *fields;
 	int i, err;
 
 	err = -EINVAL;
@@ -108,23 +352,14 @@ static inline struct audit_entry *audit_
 		goto exit_err;
 
 	err = -ENOMEM;
-	entry = kmalloc(sizeof(*entry), GFP_KERNEL);
-	if (unlikely(!entry))
+	entry = audit_init_entry(rule->field_count, GFP_KERNEL);
+	if (!entry)
 		goto exit_err;
-	fields = kmalloc(sizeof(*fields) * rule->field_count, GFP_KERNEL);
-	if (unlikely(!fields)) {
-		kfree(entry);
-		goto exit_err;
-	}
-
-	memset(&entry->rule, 0, sizeof(struct audit_krule));
-	memset(fields, 0, sizeof(struct audit_field));
 
 	entry->rule.flags = rule->flags & AUDIT_FILTER_PREPEND;
 	entry->rule.listnr = listnr;
 	entry->rule.action = rule->action;
 	entry->rule.field_count = rule->field_count;
-	entry->rule.fields = fields;
 
 	for (i = 0; i < AUDIT_BITMASK_SIZE; i++)
 		entry->rule.mask[i] = rule->mask[i];
@@ -150,15 +385,16 @@ static struct audit_entry *audit_rule_to
 	for (i = 0; i < rule->field_count; i++) {
 		struct audit_field *f = &entry->rule.fields[i];
 
-		if (rule->fields[i] & AUDIT_UNUSED_BITS) {
-			err = -EINVAL;
-			goto exit_free;
-		}
-
 		f->op = rule->fields[i] & (AUDIT_NEGATE|AUDIT_OPERATORS);
 		f->type = rule->fields[i] & ~(AUDIT_NEGATE|AUDIT_OPERATORS);
 		f->val = rule->values[i];
 
+		if (f->type & AUDIT_UNUSED_BITS ||
+		    f->type == AUDIT_WATCH) {
+			err = -EINVAL;
+			goto exit_free;
+		}
+
 		entry->rule.vers_ops = (f->op & AUDIT_OPERATORS) ? 2 : 1;
 		if (f->op & AUDIT_NEGATE)
 			f->op |= AUDIT_NOT_EQUAL;
@@ -182,8 +418,9 @@ static struct audit_entry *audit_data_to
 	int err = 0;
 	struct audit_entry *entry;
 	void *bufp;
-	/* size_t remain = datasz - sizeof(struct audit_rule_data); */
+	size_t remain = datasz - sizeof(struct audit_rule_data);
 	int i;
+	char *path;
 
 	entry = audit_to_entry_common((struct audit_rule *)data);
 	if (IS_ERR(entry))
@@ -201,10 +438,20 @@ static struct audit_entry *audit_data_to
 
 		f->op = data->fieldflags[i] & AUDIT_OPERATORS;
 		f->type = data->fields[i];
+		f->val = data->values[i];
 		switch(f->type) {
-		/* call type-specific conversion routines here */
-		default:
-			f->val = data->values[i];
+		case AUDIT_WATCH:
+			path = audit_unpack_string(&bufp, &remain, f->val);
+			if (IS_ERR(path))
+				goto exit_free;
+			entry->rule.buflen += f->val;
+
+			err = audit_to_watch(path, &entry->rule, i);
+			if (err) {
+				kfree(path);
+				goto exit_free;
+			}
+			break;
 		}
 	}
 
@@ -234,7 +481,8 @@ static struct audit_rule *audit_krule_to
 	struct audit_rule *rule;
 	int i;
 
-	rule = kmalloc(sizeof(*rule), GFP_KERNEL);
+	/* use GFP_ATOMIC because we're under rcu_read_lock() */
+	rule = kmalloc(sizeof(*rule), GFP_ATOMIC);
 	if (unlikely(!rule))
 		return ERR_PTR(-ENOMEM);
 	memset(rule, 0, sizeof(*rule));
@@ -265,7 +513,8 @@ static struct audit_rule_data *audit_kru
 	void *bufp;
 	int i;
 
-	data = kmalloc(sizeof(*data) + krule->buflen, GFP_KERNEL);
+	/* use GFP_ATOMIC because we're under rcu_read_lock() */
+	data = kmalloc(sizeof(*data) + krule->buflen, GFP_ATOMIC);
 	if (unlikely(!data))
 		return ERR_PTR(-ENOMEM);
 	memset(data, 0, sizeof(*data));
@@ -280,7 +529,10 @@ static struct audit_rule_data *audit_kru
 		data->fields[i] = f->type;
 		data->fieldflags[i] = f->op;
 		switch(f->type) {
-		/* call type-specific conversion routines here */
+		case AUDIT_WATCH:
+			data->buflen += data->values[i] =
+				audit_pack_string(&bufp, krule->watch->path);
+			break;
 		default:
 			data->values[i] = f->val;
 		}
@@ -290,6 +542,12 @@ static struct audit_rule_data *audit_kru
 	return data;
 }
 
+/* Compare two watches.  Considered success if rules don't match. */
+static inline int audit_compare_watch(struct audit_watch *a, struct audit_watch *b)
+{
+	return strcmp(a->path, b->path);
+}
+
 /* Compare two rules in kernel format.  Considered success if rules
  * don't match. */
 static int audit_compare_rule(struct audit_krule *a, struct audit_krule *b)
@@ -308,7 +566,10 @@ static int audit_compare_rule(struct aud
 			return 1;
 
 		switch(a->fields[i].type) {
-		/* call type-specific comparison routines here */
+		case AUDIT_WATCH:
+			if (audit_compare_watch(a->watch, b->watch))
+				return 1;
+			break;
 		default:
 			if (a->fields[i].val != b->fields[i].val)
 				return 1;
@@ -322,45 +583,264 @@ static int audit_compare_rule(struct aud
 	return 0;
 }
 
+/* Copy an audit rule entry to be replaced.
+ * Caller must hold filterlist lock. */
+static inline struct audit_entry *audit_dupe_rule(struct audit_entry *old)
+{
+	u32 fcount = old->rule.field_count;
+	struct audit_entry *new;
+	struct audit_field *fields;
+	struct audit_watch *watch;
+
+	new = audit_init_entry(fcount, GFP_ATOMIC);
+	if (unlikely(!new))
+		return ERR_PTR(-ENOMEM);
+
+	fields = new->rule.fields;
+	memcpy(&new->rule, &old->rule, sizeof(struct audit_krule));
+	memcpy(fields, old->rule.fields, sizeof(struct audit_field) * fcount);
+	new->rule.fields = fields;
+
+	watch = old->rule.watch;
+	audit_get_watch(watch);
+	list_add(&new->rule.rlist, &watch->rules);
+	list_del(&old->rule.rlist);
+
+	return new;
+}
+
+/* Update an audit rule field.  If the rule is part of a filterlist, caller
+ * must hold that filterlist's lock. */
+static void audit_update_rule(struct audit_krule *krule, u32 type, u32 val)
+{
+	int i;
+	struct audit_entry *old, *new;
+
+	for (i = 0; i < AUDIT_MAX_FIELDS; i++) {
+		if (krule->fields[i].type != type)
+			continue;
+
+		old = container_of(krule, struct audit_entry, rule);
+
+		/* rule is not in filterlist yet */
+		if (old->flags & AUDIT_ENTRY_ADD) {
+			krule->fields[i].val = val;
+			return;
+		}
+
+		/* make sure we have a valid copy */
+		while (old->replacement != NULL)
+			old = old->replacement;
+		if (old->flags & AUDIT_ENTRY_DEL)
+			return;
+
+		new = audit_dupe_rule(old);
+		if (unlikely(IS_ERR(new))) {
+			audit_panic("cannot allocate memory for rule update");
+			return;
+		}
+		new->rule.fields[i].val = val;
+
+		old->flags |= AUDIT_ENTRY_DEL;
+		old->replacement = new;
+		list_replace_rcu(&old->list, &new->list);
+		call_rcu(&old->rcu, audit_free_rule_rcu);
+	}
+}
+
+/* Find an existing parent entry for this watch, or create a new one.
+ * Caller must hold exit filterlist lock. */
+static inline struct audit_parent *audit_find_parent(char *path)
+{
+	int err;
+	struct nameidata nd;
+	struct audit_parent *p, *parent;
+	unsigned long ino;
+
+	err = path_lookup(path, LOOKUP_PARENT, &nd);
+	if (err) {
+		path_release(&nd);
+		parent = ERR_PTR(err);
+		goto out;
+	}
+
+	/* walk list locked for safe compare of ino field */
+	spin_lock(&master_parents_lock);
+	list_for_each_entry(p, &master_parents, mlist) {
+		if (p->ino != nd.dentry->d_inode->i_ino ||
+		    p->flags & AUDIT_PARENT_DEL)
+			continue;
+
+		spin_unlock(&master_parents_lock);
+		path_release(&nd);
+		parent = p;
+		goto out;
+	}
+	ino = nd.dentry->d_inode->i_ino;
+	spin_unlock(&master_parents_lock);
+	path_release(&nd);
+
+	/* Initialize parent with this inode #; the registration thread will
+	 * catch any changes. */
+	parent = audit_init_parent(path, ino);
+	if (unlikely(IS_ERR(parent)))
+		goto out;
+
+	spin_lock(&master_parents_lock);
+	list_add(&parent->mlist, &master_parents);
+	spin_unlock(&master_parents_lock);
+
+out:
+	return parent;
+}
+
+/* Find a matching watch entry, or add this one.
+ * Caller must hold exit filterlist lock. */
+static inline int audit_add_watch(struct audit_krule *krule)
+{
+	struct audit_parent *parent;
+	struct audit_watch *w, *watch = krule->watch;
+	struct nameidata nd;
+
+	parent = audit_find_parent(watch->path);
+	if (IS_ERR(parent))
+		return PTR_ERR(parent);
+
+	list_for_each_entry(w, &parent->watches, wlist) {
+		if (audit_compare_watch(watch, w))
+			continue;
+
+		audit_put_watch(watch); /* krule's ref */
+		audit_put_watch(watch); /* destroy */
+
+		audit_get_watch(w);
+		krule->watch = watch = w;
+		goto add_rule;
+	}
+
+	audit_get_parent(parent);
+	watch->parent = parent;
+	list_add(&watch->wlist, &parent->watches);
+
+add_rule:
+	list_add(&krule->rlist, &watch->rules);
+
+	if (path_lookup(watch->path, 0, &nd) == 0)
+		audit_update_rule(krule, AUDIT_WATCH,
+				  nd.dentry->d_inode->i_ino);
+	path_release(&nd);
+	return 0;
+}
+
 /* Add rule to given filterlist if not a duplicate.  Protected by
  * audit_netlink_sem. */
 static inline int audit_add_rule(struct audit_entry *entry,
-				  struct list_head *list)
+				 struct audit_flist *flist)
 {
 	struct audit_entry *e;
+	int err;
+
+	/* The *_rcu iterator is needed to protect from filterlist
+	 * updates or removals. */
+	rcu_read_lock();
+	list_for_each_entry_rcu(e, &flist->head, list) {
+		if (e->flags & AUDIT_ENTRY_DEL)
+			continue;
+		if (!audit_compare_rule(&entry->rule, &e->rule)) {
+			err = -EEXIST;
+			rcu_read_unlock();
+			goto error;
+		}
+	}
+	rcu_read_unlock();
+
+	spin_lock(&flist->lock);
+	entry->flags |= AUDIT_ENTRY_ADD;
 
-	/* Do not use the _rcu iterator here, since this is the only
-	 * addition routine. */
-	list_for_each_entry(e, list, list) {
-		if (!audit_compare_rule(&entry->rule, &e->rule))
-			return -EEXIST;
+	if (entry->rule.watch) {
+		err = audit_add_watch(&entry->rule);
+		if (err)
+			goto error;
 	}
 
 	if (entry->rule.flags & AUDIT_FILTER_PREPEND) {
-		list_add_rcu(&entry->list, list);
+		list_add_rcu(&entry->list, &flist->head);
 	} else {
-		list_add_tail_rcu(&entry->list, list);
+		list_add_tail_rcu(&entry->list, &flist->head);
 	}
 
+	entry->flags &= ~AUDIT_ENTRY_ADD;
+	spin_unlock(&flist->lock);
+
 	return 0;
+
+error:
+	if (entry->rule.watch)
+		audit_put_watch(entry->rule.watch);
+	return err;
+}
+
+/* Remove given krule from its associated watch's rules list and clean up any
+ * last instances of associated watch and parent.
+ * Caller must hold exit filterlist lock */
+static inline void audit_remove_watch(struct audit_krule *krule)
+{
+	struct audit_watch *watch = krule->watch;
+	struct audit_parent *parent = watch->parent;
+	struct task_struct *task;
+
+	list_del(&krule->rlist);
+	if (list_empty(&watch->rules)) {
+		list_del(&watch->wlist);
+		audit_put_watch(watch);
+
+		if (list_empty(&parent->watches)) {
+			/* This flag only read when user adds a watch,
+			 * which is prevented by audit_netlink_sem. */
+			parent->flags |= AUDIT_PARENT_DEL;
+
+			/* Spawn a thread to unregister the parent's inotify
+			 * watch without the filterlist spinlock. */
+			audit_get_parent(parent);
+			task = kthread_run(audit_inotify_unregister, parent,
+					   "audit_inotify_unregister");
+			if (IS_ERR(task))
+				printk(KERN_ERR
+			"%s:%d: unable to remove inotify watch for inode %lu\n",
+				__FILE__, __LINE__, parent->ino);
+		}
+	}
 }
 
 /* Remove an existing rule from filterlist.  Protected by
  * audit_netlink_sem. */
 static inline int audit_del_rule(struct audit_entry *entry,
-				 struct list_head *list)
+				 struct audit_flist *flist)
 {
 	struct audit_entry  *e;
+	int ret = 0;
 
-	/* Do not use the _rcu iterator here, since this is the only
-	 * deletion routine. */
-	list_for_each_entry(e, list, list) {
-		if (!audit_compare_rule(&entry->rule, &e->rule)) {
-			list_del_rcu(&e->list);
-			call_rcu(&e->rcu, audit_free_rule_rcu);
-			return 0;
+	spin_lock(&flist->lock);
+	list_for_each_entry(e, &flist->head, list) {
+		if (e->flags & AUDIT_ENTRY_DEL ||
+		    audit_compare_rule(&entry->rule, &e->rule))
+			continue;
+
+		if (e->rule.watch) {
+			audit_remove_watch(&e->rule);
+			audit_put_watch(entry->rule.watch);
 		}
+
+		list_del_rcu(&e->list);
+		e->flags |= AUDIT_ENTRY_DEL;
+		call_rcu(&e->rcu, audit_free_rule_rcu);
+		spin_unlock(&flist->lock);
+
+		return ret;
 	}
+	spin_unlock(&flist->lock);
+	if (entry->rule.watch)
+		audit_put_watch(entry->rule.watch);
 	return -ENOENT;		/* No matching rule */
 }
 
@@ -379,10 +859,12 @@ static int audit_list(void *_dest)
 
 	down(&audit_netlink_sem);
 
-	/* The *_rcu iterators not needed here because we are
-	   always called with audit_netlink_sem held. */
+	/* The *_rcu iterator is needed to protect from filesystem
+	 * updates or removals. */
 	for (i=0; i<AUDIT_NR_FILTERS; i++) {
-		list_for_each_entry(entry, &audit_filter_list[i], list) {
+		rcu_read_lock();
+		list_for_each_entry_rcu(entry, &audit_filter_list[i].head,
+					list) {
 			struct audit_rule *rule;
 
 			rule = audit_krule_to_rule(&entry->rule);
@@ -392,6 +874,7 @@ static int audit_list(void *_dest)
 					 rule, sizeof(*rule));
 			kfree(rule);
 		}
+		rcu_read_unlock();
 	}
 	audit_send_reply(pid, seq, AUDIT_LIST, 1, 1, NULL, 0);
 	
@@ -413,19 +896,21 @@ static int audit_list_rules(void *_dest)
 
 	down(&audit_netlink_sem);
 
-	/* The *_rcu iterators not needed here because we are
-	   always called with audit_netlink_sem held. */
+	/* The *_rcu iterator is needed to protect from filesystem
+	 * updates or removals. */
 	for (i=0; i<AUDIT_NR_FILTERS; i++) {
-		list_for_each_entry(e, &audit_filter_list[i], list) {
+		rcu_read_lock();
+		list_for_each_entry_rcu(e, &audit_filter_list[i].head, list) {
 			struct audit_rule_data *data;
 
 			data = audit_krule_to_data(&e->rule);
 			if (unlikely(!data))
 				break;
 			audit_send_reply(pid, seq, AUDIT_LIST_RULES, 0, 1,
-					 data, sizeof(*data));
+					 data, sizeof(*data) + data->buflen);
 			kfree(data);
 		}
+		rcu_read_unlock();
 	}
 	audit_send_reply(pid, seq, AUDIT_LIST_RULES, 1, 1, NULL, 0);
 
@@ -516,6 +1001,58 @@ int audit_receive_filter(int type, int p
 	return err;
 }
 
+/* Update inode numbers in audit rules based on filesystem event. */
+static inline void audit_update_ino(struct audit_parent *parent,
+				    const char *dname, u32 ino)
+{
+	struct audit_watch *w;
+	struct audit_krule *r, *next;
+	struct audit_flist *flist = &audit_filter_list[AUDIT_FILTER_EXIT];
+	struct audit_buffer *ab;
+
+	spin_lock(&flist->lock);
+	list_for_each_entry(w, &parent->watches, wlist) {
+		if (audit_compare_dname_path(dname, w->path))
+			continue;
+
+		list_for_each_entry_safe(r, next, &w->rules, rlist)
+			audit_update_rule(r, AUDIT_WATCH, ino);
+
+		ab = audit_log_start(NULL, GFP_ATOMIC, AUDIT_CONFIG_CHANGE);
+		audit_log_format(ab, "audit updated rules specifying watch=");
+		audit_log_untrustedstring(ab, w->path);
+		audit_log_format(ab, " with ino=%u\n", ino);
+		audit_log_end(ab);
+		break;
+	}
+	spin_unlock(&flist->lock);
+}
+
+/**
+ * audit_handle_ievent - handler for Inotify events
+ * @event: information about the event
+ * @dname: dentry name associated with event
+ * @inode: inode associated with event
+ * @ptr: kernel's version of a watch descriptor
+ */
+void audit_handle_ievent(struct inotify_event *event, const char *dname,
+			  struct inode *inode, void *ptr)
+{
+	struct audit_parent *parent = (struct audit_parent *)ptr;
+
+	if (event->mask & (IN_CREATE|IN_MOVED_TO) && inode)
+		audit_update_ino(parent, dname, (unsigned int)inode->i_ino);
+	else if (event->mask & (IN_DELETE|IN_MOVED_FROM))
+		audit_update_ino(parent, dname, (unsigned int)-1);
+	/* Note: Inotify doesn't remove the watch for the IN_MOVE_SELF event.
+	 * Work around this by leaving the parent around with an empty
+	 * watchlist.  It will be re-used if new watches are added. */
+	else if (event->mask & (AUDIT_IN_SELF))
+		audit_remove_parent_watches(parent);
+	else if (event->mask & IN_IGNORED)
+		audit_remove_parent(parent);
+}
+
 int audit_comparator(const u32 left, const u32 op, const u32 right)
 {
 	switch (op) {
@@ -536,7 +1073,39 @@ int audit_comparator(const u32 left, con
 	}
 }
 
+/* Compare given dentry name with last component in given path,
+ * return of 0 indicates a match. */
+int audit_compare_dname_path(const char *dname, const char *path)
+{
+	int dlen, plen;
+	const char *p;
 
+	if (!dname || !path)
+		return 1;
+
+	dlen = strlen(dname);
+	plen = strlen(path);
+	if (plen < dlen)
+		return 1;
+
+	/* disregard trailing slashes */
+	p = path + plen - 1;
+	while ((*p == '/') && (p > path))
+		p--;
+
+	/* find last path component */
+	p = p - dlen + 1;
+	if (p < path)
+		return 1;
+	else if (p > path) {
+		if (*--p != '/')
+			return 1;
+		else
+			p++;
+	}
+
+	return strncmp(p, dname, dlen);
+}
 
 static int audit_filter_user_rules(struct netlink_skb_parms *cb,
 				   struct audit_krule *rule,
@@ -581,7 +1150,8 @@ int audit_filter_user(struct netlink_skb
 	int ret = 1;
 
 	rcu_read_lock();
-	list_for_each_entry_rcu(e, &audit_filter_list[AUDIT_FILTER_USER], list) {
+	list_for_each_entry_rcu(e, &audit_filter_list[AUDIT_FILTER_USER].head,
+				list) {
 		if (audit_filter_user_rules(cb, &e->rule, &state)) {
 			if (state == AUDIT_DISABLED)
 				ret = 0;
@@ -599,10 +1169,10 @@ int audit_filter_type(int type)
 	int result = 0;
 	
 	rcu_read_lock();
-	if (list_empty(&audit_filter_list[AUDIT_FILTER_TYPE]))
+	if (list_empty(&audit_filter_list[AUDIT_FILTER_TYPE].head))
 		goto unlock_and_return;
 
-	list_for_each_entry_rcu(e, &audit_filter_list[AUDIT_FILTER_TYPE],
+	list_for_each_entry_rcu(e, &audit_filter_list[AUDIT_FILTER_TYPE].head,
 				list) {
 		int i;
 		for (i = 0; i < e->rule.field_count; i++) {
diff --git a/kernel/auditsc.c b/kernel/auditsc.c
index 8ff51b3..4626c35 100644
--- a/kernel/auditsc.c
+++ b/kernel/auditsc.c
@@ -60,7 +60,7 @@
 
 #include "audit.h"
 
-extern struct list_head audit_filter_list[];
+extern struct audit_flist audit_filter_list[];
 
 /* No syscall auditing will take place unless audit_enabled != 0. */
 extern int audit_enabled;
@@ -241,7 +241,8 @@ static int audit_filter_rules(struct tas
 			}
 			break;
 		case AUDIT_INODE:
-			if (ctx) {
+		case AUDIT_WATCH:
+			if (ctx && f->val != (unsigned int)-1) {
 				for (j = 0; j < ctx->name_count; j++) {
 					if (audit_comparator(ctx->names[j].ino, f->op, f->val) ||
 					    audit_comparator(ctx->names[j].pino, f->op, f->val)) {
@@ -286,7 +287,8 @@ static enum audit_state audit_filter_tas
 	enum audit_state   state;
 
 	rcu_read_lock();
-	list_for_each_entry_rcu(e, &audit_filter_list[AUDIT_FILTER_TASK], list) {
+	list_for_each_entry_rcu(e, &audit_filter_list[AUDIT_FILTER_TASK].head,
+				list) {
 		if (audit_filter_rules(tsk, &e->rule, NULL, &state)) {
 			rcu_read_unlock();
 			return state;
@@ -342,7 +344,7 @@ static inline struct audit_context *audi
 
 	if (context->in_syscall && !context->auditable) {
 		enum audit_state state;
-		state = audit_filter_syscall(tsk, context, &audit_filter_list[AUDIT_FILTER_EXIT]);
+		state = audit_filter_syscall(tsk, context, &audit_filter_list[AUDIT_FILTER_EXIT].head);
 		if (state == AUDIT_RECORD_CONTEXT)
 			context->auditable = 1;
 	}
@@ -789,7 +791,7 @@ void audit_syscall_entry(struct task_str
 
 	state = context->state;
 	if (state == AUDIT_SETUP_CONTEXT || state == AUDIT_BUILD_CONTEXT)
-		state = audit_filter_syscall(tsk, context, &audit_filter_list[AUDIT_FILTER_ENTRY]);
+		state = audit_filter_syscall(tsk, context, &audit_filter_list[AUDIT_FILTER_ENTRY].head);
 	if (likely(state == AUDIT_DISABLED))
 		return;
 
@@ -1033,37 +1035,20 @@ void __audit_inode_child(const char *dna
 		return;
 
 	/* determine matching parent */
-	if (dname)
-		for (idx = 0; idx < context->name_count; idx++)
-			if (context->names[idx].pino == pino) {
-				const char *n;
-				const char *name = context->names[idx].name;
-				int dlen = strlen(dname);
-				int nlen = name ? strlen(name) : 0;
-
-				if (nlen < dlen)
-					continue;
-				
-				/* disregard trailing slashes */
-				n = name + nlen - 1;
-				while ((*n == '/') && (n > name))
-					n--;
-
-				/* find last path component */
-				n = n - dlen + 1;
-				if (n < name)
-					continue;
-				else if (n > name) {
-					if (*--n != '/')
-						continue;
-					else
-						n++;
-				}
+	if (!dname)
+		goto no_match;
+	for (idx = 0; idx < context->name_count; idx++)
+		if (context->names[idx].pino == pino) {
+			const char *name = context->names[idx].name;
 
-				if (strncmp(n, dname, dlen) == 0)
-					goto update_context;
-			}
+			if (!name)
+				continue;
+
+			if (audit_compare_dname_path(dname, name) == 0)
+				goto update_context;
+		}
 
+no_match:
 	/* catch-all in case match not found */
 	idx = context->name_count++;
 	context->names[idx].name  = NULL;




More information about the Linux-audit mailing list