diff --git a/tests/Makefile b/tests/Makefile index c026a02..1945db4 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -26,7 +26,8 @@ TESTS := \ syscalls_file \ syscall_module \ syscall_socketcall \ - user_msg + user_msg \ + fanotify # apply any ABI restrictions to the tests ifneq ($(MACHINE),$(filter i386 x86_64,$(MACHINE))) diff --git a/tests/fanotify/.gitignore b/tests/fanotify/.gitignore new file mode 100644 index 0000000..5c33b15 --- /dev/null +++ b/tests/fanotify/.gitignore @@ -0,0 +1 @@ +trigger diff --git a/tests/fanotify/Makefile b/tests/fanotify/Makefile new file mode 100644 index 0000000..477708e --- /dev/null +++ b/tests/fanotify/Makefile @@ -0,0 +1,9 @@ +TARGETS=$(patsubst %.c,%,$(wildcard *.c)) + +all: $(TARGETS) + +fanotify: fanotify.c + $(CC) $(CFLAGS) -o $@ $^ + +clean: + rm -f $(TARGETS) diff --git a/tests/fanotify/fanotify.c b/tests/fanotify/fanotify.c new file mode 100644 index 0000000..012b457 --- /dev/null +++ b/tests/fanotify/fanotify.c @@ -0,0 +1,170 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +/* Read all available fanotify events from the file descriptor 'fd' */ + +static void +handle_events(int fd) +{ + const struct fanotify_event_metadata *metadata; + struct fanotify_event_metadata buf[200]; + ssize_t len; + char path[PATH_MAX]; + ssize_t path_len; + char procfd_path[PATH_MAX]; + struct fanotify_response response; + + /* Loop while events can be read from fanotify file descriptor */ + + for(;;) { + + /* Read some events */ + + len = read(fd, (void *) &buf, sizeof(buf)); + if (len == -1 && errno != EAGAIN) { + perror("read"); + exit(EXIT_FAILURE); + } + + /* Check if end of available data reached */ + + if (len <= 0) + break; + + /* Point to the first event in the buffer */ + + metadata = buf; + + /* Loop over all events in the buffer */ + + while (FAN_EVENT_OK(metadata, len)) { + + /* Check that run-time and compile-time structures match */ + + if (metadata->vers != FANOTIFY_METADATA_VERSION) { + fprintf(stderr, + "Mismatch of fanotify metadata version.\n"); + exit(EXIT_FAILURE); + } + + /* metadata->fd contains either FAN_NOFD, indicating a + queue overflow, or a file descriptor (a nonnegative + integer). Here, we simply ignore queue overflow. */ + + if (metadata->fd >= 0) { + + /* Handle open permission event */ + + if (metadata->mask & FAN_OPEN_PERM) { + printf("FAN_OPEN_PERM: "); + + /* Allow file to be opened */ + + response.fd = metadata->fd; + response.response = FAN_ALLOW | FAN_AUDIT; + write(fd, &response, + sizeof(struct fanotify_response)); + } + + /* Handle closing of writable file event */ + + if (metadata->mask & FAN_CLOSE_WRITE) + printf("FAN_CLOSE_WRITE: "); + + /* Retrieve and print pathname of the accessed file */ + + snprintf(procfd_path, sizeof(procfd_path), + "/proc/self/fd/%d", metadata->fd); + path_len = readlink(procfd_path, path, + sizeof(path) - 1); + if (path_len == -1) { + perror("readlink"); + exit(EXIT_FAILURE); + } + + path[path_len] = '\0'; + printf("File %s\n", path); + + /* Close the file descriptor of the event */ + + close(metadata->fd); + } + + /* Advance to next event */ + + metadata = FAN_EVENT_NEXT(metadata, len); + } + } +} + +int +main(int argc, char *argv[]) +{ + int fd, poll_num; + nfds_t nfds; + struct pollfd fds[2]; + + /* Check mount point is supplied */ + if (argc != 2) { + fprintf(stderr, "Usage: %s MOUNT\n", argv[0]); + exit(EXIT_FAILURE); + } + + /* Create the file descriptor for accessing the fanotify API */ + fd = fanotify_init(FAN_CLOEXEC | FAN_CLASS_CONTENT | FAN_NONBLOCK | + FAN_ENABLE_AUDIT, + O_RDONLY); + if (fd == -1) { + perror("fanotify_init"); + exit(EXIT_FAILURE); + } + + /* Mark the mount for: + - permission events before opening files + - notification events after closing a write-enabled + file descriptor */ + if (fanotify_mark(fd, FAN_MARK_ADD | FAN_MARK_MOUNT, + FAN_OPEN_PERM | FAN_CLOSE_WRITE, -1, + argv[1]) == -1) { + perror("fanotify_mark"); + exit(EXIT_FAILURE); + } + + /* Prepare for polling */ + nfds = 1; + + /* Fanotify input */ + fds[0].fd = fd; + fds[0].events = POLLIN; + + /* This is the loop to wait for incoming events */ + printf("Listening for events.\n"); + + while (1) { + poll_num = poll(fds, nfds, -1); + if (poll_num == -1) { + if (errno == EINTR) /* Interrupted by a signal */ + continue; /* Restart poll() */ + + perror("poll"); /* Unexpected error */ + exit(EXIT_FAILURE); + } + + if (poll_num > 0) { + if (fds[0].revents & POLLIN) { + + /* Fanotify events are available */ + handle_events(fd); + } + } + } + + printf("Listening for events stopped.\n"); + exit(EXIT_SUCCESS); +} diff --git a/tests/fanotify/test b/tests/fanotify/test new file mode 100755 index 0000000..e4154da --- /dev/null +++ b/tests/fanotify/test @@ -0,0 +1,104 @@ +#!/usr/bin/perl + +use strict; + +use Test; +BEGIN { plan tests => 4 } + +use IPC::Open3; +use File::Temp qw/ tempfile unlink0 /; + +my $basedir = $0; +$basedir =~ s|(.*)/[^/]*|$1|; + +sub key_gen { + my @chars = ( "A" .. "Z", "a" .. "z" ); + my $key = "testsuite-" . time . "-"; + $key .= $chars[ rand @chars ] for 1 .. 8; + return $key; +} + +# SETUP +# ===== + +# Clean audit rules. +system("auditctl -D >& /dev/null"); + +# Create sinks. +( my $fh_out, my $stdout ) = tempfile( + TEMPLATE => '/tmp/audit-testsuite-out-XXXX', + UNLINK => 1 +); +( my $fh_err, my $stderr ) = tempfile( + TEMPLATE => '/tmp/audit-testsuite-err-XXXX', + UNLINK => 1 +); + +# Create test rule. +my $key = key_gen(); +my $testfile = "/tmp/$key"; +system("auditctl -w $testfile -k $key"); + +# Start fanotify watcher in the background. +my $fanotify_pid = open3( undef, undef, undef, "$basedir/fanotify /tmp" ); +sleep 3; + +# TRIGGER +# ======= + +# Open testfile to trigger fanotify events. +open( my $fh_test, ">", $testfile ); +close($fh_test); + +# CLEAN-UP +# ======== + +# Stop fanotify watcher. +if ( $fanotify_pid > 0 ) { + eval { + local $SIG{ALRM} = sub { kill 9, $fanotify_pid; }; + alarm 6; + waitpid( $fanotify_pid, 0 ); + alarm 0; + }; +} + +# Empty rules. +system("auditctl -D >& /dev/null"); + +# Remove testfile. +unlink0( $fh_test, $testfile ); + +# VERIFY +# ====== + +# Check that filter rule generated some events. +my $result = system("ausearch -i -k $key > $stdout 2> $stderr"); + +# Some events were triggered. +ok( $result == 0 ); + +# Check that correct events were generated. +my $fanotify_found = 0; +my $syscall_found = 0; +my $path_found = 0; +while ( my $line = <$fh_out> ) { + if ( $line =~ /^type=FANOTIFY .* resp=allow/ ) { + $fanotify_found = 1; + } + elsif ( $line =~ /^type=SYSCALL .* syscall=open(at)? success=yes/ ) { + $syscall_found = 1; + } + elsif ( $line =~ /^type=PATH .* name=$testfile/ ) { + $path_found = 1; + } +} + +# FANOTIFY event was found. +ok( $fanotify_found == 1 ); + +# SYSCALL event was found. +ok( $syscall_found == 1 ); + +# PATH event was found. +ok( $path_found == 1 );