Re: [RFC PATCH v2 0/9] Add LSM access controls and auditing to io_uring

Richard Guy Briggs
2021-08-25T01:36:42Z

On 2021-08-24 18:27, Paul Moore wrote:
> On Tue, Aug 24, 2021 at 4:57 PM Richard Guy Briggs <rgb@redhat.com> wrote:
> > Thanks for the tests. I have a bunch of userspace patches to add to the
> > last set I posted and these tests will help exercise them. I also have
> > one more kernel patch to post... I'll dive back into that now. I had
> > wanted to post them before now but got distracted with AUDIT_TRIM
> > breakage.
>
> If it helps, last week I started working on a little test tool for the
> audit-testsuite and selinux-testsuite (see attached). It may not be
> final, but I don't expect too many changes to it before I post the
> test suite patches; it is definitely usable now. It's inspired by the
> previous tests, but it uses a much more test suite friendly fork/exec
> model for testing the sharing of io_urings across process boundaries.
>
> Would you mind sharing your latest userspace patches, if not publicly
> I would be okay with privately off-list; I'm putting together the test
> suite patches this week and it would be good to make sure I'm using
> your latest take on the userspace changes.

I intend to publish them but they need squashing and some documentation
first. And a run through with io_uring specific tests would be good to
catch anything obvious...

> Also, what is the kernel patch? Did you find a bug or is this some
> new functionality you think might be useful? Both can be important,
> but the bug is *really* important; even if you don't have a fix for
> that, just a description of the problem would be good.

It was a very small patch that I realize I had already talked about and
you justified not including sessionid along with auid. That was
addressed in a reply tacked on to your v1 patchset just now.

> paul moore

> /*
> * io_uring test tool to exercise LSM/SELinux and audit kernel code paths
> * Author: Paul Moore <paul@paul-moore.com>
> *
> * Copyright 2021 Microsoft Corporation
> *
> * At the time this code was written the best, and most current, source of info
> * on io_uring seemed to be the liburing sources themselves (link below). The
> * code below is based on the lessons learned from looking at the liburing
> * code.
> *
> * -> https://github.com/axboe/liburing
> *
> * The liburing LICENSE file contains the following:
> *
> * Copyright 2020 Jens Axboe
> *
> * Permission is hereby granted, free of charge, to any person obtaining a copy
> * of this software and associated documentation files (the "Software"), to
> * deal in the Software without restriction, including without limitation the
> * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
> * sell copies of the Software, and to permit persons to whom the Software is
> * furnished to do so, subject to the following conditions:
> *
> * The above copyright notice and this permission notice shall be included in
> * all copies or substantial portions of the Software.
> *
> * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
> * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
> * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
> * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
> * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
> * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
> * DEALINGS IN THE SOFTWARE.
> *
> */
>
> /*
> * BUILDING:
> *
> * gcc -o <binary> -g -O0 -luring -lrt
> *
> * RUNNING:
> *
> * The program can be run using the following command lines:
> *
> * % prog sqpoll
> * ... this invocation runs the io_uring SQPOLL test.
> *
> * % prog t1
> * ... this invocation runs the parent/child io_uring sharing test.
> *
> * % prog t1 <domain>
> * ... this invocation runs the parent/child io_uring sharing test with the
> * child process run in the specified SELinux domain.
> *
> */
>
> #include <stdlib.h>
> #include <stdio.h>
> #include <errno.h>
> #include <string.h>
> #include <fcntl.h>
> #include <unistd.h>
> #include <sys/mman.h>
> #include <sys/stat.h>
> #include <sys/wait.h>
>
> #include <liburing.h>
>
> struct urt_config {
> struct io_uring ring;
> struct io_uring_params ring_params;
> int ring_creds;
> };
>
> #define URING_ENTRIES 8
> #define URING_SHM_NAME "/iouring_test_4"
>
> int selinux_state = -1;
> #define SELINUX_CTX_MAX 512
> char selinux_ctx[SELINUX_CTX_MAX] = "\0";
>
> /**
> * Display an error message and exit
> * @param msg the error message
> *
> * Output @msg to stderr and exit with errno as the exit value.
> */
> void fatal(const char *msg)
> {
> const char *str = (msg ? msg : "unknown");
>
> if (!errno) {
> errno = 1;
> fprintf(stderr, "%s: unknown error\n", msg);
> } else
> perror(str);
>
> if (errno < 0)
> exit(-errno);
> exit(errno);
> }
>
> /**
> * Determine if SELinux is enabled and set the internal state
> *
> * Attempt to read from /proc/self/attr/current and determine if SELinux is
> * enabled, store the current context/domain in @selinux_ctx if SELinux is
> * enabled. We avoid using the libselinux API in order to increase portability
> * and make it easier for other LSMs to adopt this test.
> */
> int selinux_enabled(void)
> {
> int fd = -1;
> ssize_t ctx_len;
> char ctx[SELINUX_CTX_MAX];
>
> if (selinux_state >= 0)
> return selinux_state;
>
> /* attempt to get the current context */
> fd = open("/proc/self/attr/current", O_RDONLY);
> if (fd < 0)
> goto err;
> ctx_len = read(fd, ctx, SELINUX_CTX_MAX - 1);
> if (ctx_len <= 0)
> goto err;
> close(fd);
>
> /* save the current context */
> ctx[ctx_len] = '\0';
> strcpy(selinux_ctx, ctx);
>
> selinux_state = 1;
> return selinux_state;
>
> err:
> if (fd >= 0)
> close(fd);
>
> selinux_state = 0;
> return selinux_state;
> }
>
> /**
> * Return the current SELinux domain or "DISABLED" if SELinux is not enabled
> *
> * The returned string should not be free()'d.
> */
> const char *selinux_current(void)
> {
> int rc;
>
> rc = selinux_enabled();
> if (!rc)
> return "DISABLED";
>
> return selinux_ctx;
> }
>
> /**
> * Set the SELinux domain for the next exec()'d process
> * @param ctx the SELinux domain
> *
> * This is similar to the setexeccon() libselinux API but we do it manually to
> * help increase portability and make it easier for other LSMs to adopt this
> * test.
> */
> int selinux_exec(const char *ctx)
> {
> int fd = -1;
> ssize_t len;
>
> if (!ctx)
> return -EINVAL;
>
> fd = open("/proc/self/attr/exec", O_WRONLY);
> if (fd < 0)
> return -errno;
> len = write(fd, ctx, strlen(ctx) + 1);
> close(fd);
>
> return len;
> }
>
> /**
> * Setup the io_uring
> * @param ring the io_uring pointer
> * @param params the io_uring parameters
> * @param creds pointer to the current process' registered io_uring personality
> *
> * Create a new io_uring using @params and return it in @ring with the
> * registered personality returned in @creds. Returns 0 on success, negative
> * values on failure.
> */
> int uring_setup(struct io_uring *ring,
> struct io_uring_params *params, int *creds)
> {
> int rc;
>
> /* call into liburing to do the setup heavy lifting */
> rc = io_uring_queue_init_params(URING_ENTRIES, ring, params);
> if (rc < 0)
> fatal("io_uring_queue_init_params");
>
> /* register our creds/personality */
> rc = io_uring_register_personality(ring);
> if (rc < 0)
> fatal("io_uring_register_personality()");
> *creds = rc;
> rc = 0;
>
> printf(">>> io_uring created; fd = %d, personality = %d\n",
> ring->ring_fd, *creds);
>
> return rc;
> }
>
> /**
> * Import an existing io_uring based on the given file descriptor
> * @param fd the io_uring's file descriptor
> * @param ring the io_uring pointer
> * @param params the io_uring parameters
> *
> * This function takes an io_uring file descriptor in @fd as well as the
> * io_uring parameters in @params and creates a valid io_uring in @ring.
> * Returns 0 on success, negative values on failure.
> */
> int uring_import(int fd, struct io_uring *ring, struct io_uring_params *params)
> {
> int rc;
>
> memset(ring, 0, sizeof(*ring));
> ring->flags = params->flags;
> ring->features = params->features;
> ring->ring_fd = fd;
>
> ring->sq.ring_sz = params->sq_off.array +
> params->sq_entries * sizeof(unsigned);
> ring->cq.ring_sz = params->cq_off.cqes +
> params->cq_entries * sizeof(struct io_uring_cqe);
>
> ring->sq.ring_ptr = mmap(NULL, ring->sq.ring_sz, PROT_READ | PROT_WRITE,
> MAP_SHARED | MAP_POPULATE, fd,
> IORING_OFF_SQ_RING);
> if (ring->sq.ring_ptr == MAP_FAILED)
> fatal("import mmap(ring)");
>
> ring->cq.ring_ptr = mmap(0, ring->cq.ring_sz, PROT_READ | PROT_WRITE,
> MAP_SHARED | MAP_POPULATE,
> fd, IORING_OFF_CQ_RING);
> if (ring->cq.ring_ptr == MAP_FAILED) {
> ring->cq.ring_ptr = NULL;
> goto err;
> }
>
> ring->sq.khead = ring->sq.ring_ptr + params->sq_off.head;
> ring->sq.ktail = ring->sq.ring_ptr + params->sq_off.tail;
> ring->sq.kring_mask = ring->sq.ring_ptr + params->sq_off.ring_mask;
> ring->sq.kring_entries = ring->sq.ring_ptr +
> params->sq_off.ring_entries;
> ring->sq.kflags = ring->sq.ring_ptr + params->sq_off.flags;
> ring->sq.kdropped = ring->sq.ring_ptr + params->sq_off.dropped;
> ring->sq.array = ring->sq.ring_ptr + params->sq_off.array;
>
> ring->sq.sqes = mmap(NULL,
> params->sq_entries * sizeof(struct io_uring_sqe),
> PROT_READ | PROT_WRITE, MAP_SHARED | MAP_POPULATE,
> fd, IORING_OFF_SQES);
> if (ring->sq.sqes == MAP_FAILED)
> goto err;
>
> ring->cq.khead = ring->cq.ring_ptr + params->cq_off.head;
> ring->cq.ktail = ring->cq.ring_ptr + params->cq_off.tail;
> ring->cq.kring_mask = ring->cq.ring_ptr + params->cq_off.ring_mask;
> ring->cq.kring_entries = ring->cq.ring_ptr +
> params->cq_off.ring_entries;
> ring->cq.koverflow = ring->cq.ring_ptr + params->cq_off.overflow;
> ring->cq.cqes = ring->cq.ring_ptr + params->cq_off.cqes;
> if (params->cq_off.flags)
> ring->cq.kflags = ring->cq.ring_ptr + params->cq_off.flags;
>
> return 0;
>
> err:
> if (ring->sq.ring_ptr)
> munmap(ring->sq.ring_ptr, ring->sq.ring_sz);
> if (ring->cq.ring_ptr);
> munmap(ring->cq.ring_ptr, ring->cq.ring_sz);
> fatal("import mmap");
> }
>
> void uring_shutdown(struct io_uring *ring)
> {
> if (!ring)
> return;
> io_uring_queue_exit(ring);
> }
>
> /**
> * An io_uring test
> * @param ring the io_uring pointer
> * @param personality the registered personality to use or 0
> * @param path the file path to use for the test
> *
> * This function executes an io_uring test, see the function body for more
> * details. Returns 0 on success, negative values on failure.
> */
> int uring_op_a(struct io_uring *ring, int personality, const char *path)
> {
>
> #define __OP_A_BSIZE 512
> #define __OP_A_STR "Lorem ipsum dolor sit amet.\n"
>
> int rc;
> int fds[1];
> char buf1[__OP_A_BSIZE];
> char buf2[__OP_A_BSIZE];
> struct io_uring_sqe *sqe;
> struct io_uring_cqe *cqe;
> int str_sz = strlen(__OP_A_STR);
>
> memset(buf1, 0, __OP_A_BSIZE);
> memset(buf2, 0, __OP_A_BSIZE);
> strncpy(buf1, __OP_A_STR, str_sz);
>
> if (personality > 0)
> printf(">>> io_uring ops using personality = %d\n",
> personality);
>
> /*
> * open
> */
>
> sqe = io_uring_get_sqe(ring);
> if (!sqe)
> fatal("io_uring_get_sqe(open)");
> io_uring_prep_openat(sqe, AT_FDCWD, path,
> O_RDWR | O_TRUNC | O_CREAT, 0644);
> if (personality > 0)
> sqe->personality = personality;
>
> rc = io_uring_submit(ring);
> if (rc < 0)
> fatal("io_uring_submit(open)");
>
> rc = io_uring_wait_cqe(ring, &cqe);
> fds[0] = cqe->res;
> if (rc < 0)
> fatal("io_uring_wait_cqe(open)");
> if (fds[0] < 0)
> fatal("uring_open");
> io_uring_cqe_seen(ring, cqe);
>
> rc = io_uring_register_files(ring, fds, 1);
> if(rc)
> fatal("io_uring_register_files");
>
> printf(">>> io_uring open(): OK\n");
>
> /*
> * write
> */
>
> sqe = io_uring_get_sqe(ring);
> if (!sqe)
> fatal("io_uring_get_sqe(write1)");
> io_uring_prep_write(sqe, 0, buf1, str_sz, 0);
> io_uring_sqe_set_flags(sqe, IOSQE_FIXED_FILE);
> if (personality > 0)
> sqe->personality = personality;
>
> rc = io_uring_submit(ring);
> if (rc < 0)
> fatal("io_uring_submit(write)");
>
> rc = io_uring_wait_cqe(ring, &cqe);
> if (rc < 0)
> fatal("io_uring_wait_cqe(write)");
> if (cqe->res < 0)
> fatal("uring_write");
> if (cqe->res != str_sz)
> fatal("uring_write(length)");
> io_uring_cqe_seen(ring, cqe);
>
> printf(">>> io_uring write(): OK\n");
>
> /*
> * read
> */
>
> sqe = io_uring_get_sqe(ring);
> if (!sqe)
> fatal("io_uring_get_sqe(read1)");
> io_uring_prep_read(sqe, 0, buf2,__OP_A_BSIZE, 0);
> io_uring_sqe_set_flags(sqe, IOSQE_FIXED_FILE);
> if (personality > 0)
> sqe->personality = personality;
>
> rc = io_uring_submit(ring);
> if (rc < 0)
> fatal("io_uring_submit(read)");
>
> rc = io_uring_wait_cqe(ring, &cqe);
> if (rc < 0)
> fatal("io_uring_wait_cqe(read)");
> if (cqe->res < 0)
> fatal("uring_read");
> if (cqe->res != str_sz)
> fatal("uring_read(length)");
> io_uring_cqe_seen(ring, cqe);
>
> if (strncmp(buf1, buf2, str_sz))
> fatal("strncmp(buf1,buf2)");
>
> printf(">>> io_uring read(): OK\n");
>
> /*
> * close
> */
>
> sqe = io_uring_get_sqe(ring);
> if (!sqe)
> fatal("io_uring_get_sqe(close)");
> io_uring_prep_close(sqe, 0);
> if (personality > 0)
> sqe->personality = personality;
>
> rc = io_uring_submit(ring);
> if (rc < 0)
> fatal("io_uring_submit(close)");
>
> rc = io_uring_wait_cqe(ring, &cqe);
> if (rc < 0)
> fatal("io_uring_wait_cqe(close)");
> if (cqe->res < 0)
> fatal("uring_close");
> io_uring_cqe_seen(ring, cqe);
>
> rc = io_uring_unregister_files(ring);
> if (rc < 0)
> fatal("io_uring_unregister_files");
>
> printf(">>> io_uring close(): OK\n");
>
> return 0;
> }
>
> /**
> * The main entrypoint to the test program
> * @param argc number of command line options
> * @param argv the command line options array
> */
> int main(int argc, char *argv[])
> {
> int rc = 1;
> int ring_shm_fd;
> struct io_uring ring_storage, *ring;
> struct urt_config *cfg_p;
>
> enum { TST_UNKNOWN,
> TST_SQPOLL,
> TST_T1_PARENT, TST_T1_CHILD } tst_method;
>
> /* parse the command line and do some sanity checks */
> tst_method = TST_UNKNOWN;
> if (argc >= 2) {
> if (!strcmp(argv[1], "sqpoll"))
> tst_method = TST_SQPOLL;
> else if (!strcmp(argv[1], "t1") ||
> !strcmp(argv[1], "t1_parent"))
> tst_method = TST_T1_PARENT;
> else if (!strcmp(argv[1], "t1_child"))
> tst_method = TST_T1_CHILD;
> }
> if (tst_method == TST_UNKNOWN) {
> fprintf(stderr, "usage: %s <method> ... \n", argv[0]);
> exit(EINVAL);
> }
>
> /* simple header */
> printf(">>> running as PID = %d\n", getpid());
> printf(">>> LSM/SELinux = %s\n", selinux_current());
>
> /*
> * test setup (if necessary)
> */
> if (tst_method == TST_SQPOLL || tst_method == TST_T1_PARENT) {
> /* create an io_uring and prepare it for optional sharing */
> int flags;
>
> /* create a shm segment to hold the io_uring info */
> ring_shm_fd = shm_open(URING_SHM_NAME, O_CREAT | O_RDWR,
> S_IRUSR | S_IWUSR);
> if (ring_shm_fd < 0)
> fatal("shm_open(create)");
>
> rc = ftruncate(ring_shm_fd, sizeof(struct urt_config));
> if (rc < 0)
> fatal("ftruncate(shm)");
>
> cfg_p = mmap(NULL, sizeof(*cfg_p), PROT_READ | PROT_WRITE,
> MAP_SHARED, ring_shm_fd, 0);
> if (!cfg_p)
> fatal("mmap(shm)");
>
> /* create the io_uring */
> memset(&cfg_p->ring, 0, sizeof(cfg_p->ring));
> memset(&cfg_p->ring_params, 0, sizeof(cfg_p->ring_params));
> if (tst_method == TST_SQPOLL)
> cfg_p->ring_params.flags |= IORING_SETUP_SQPOLL;
> rc = uring_setup(&cfg_p->ring, &cfg_p->ring_params,
> &cfg_p->ring_creds);
> if (rc)
> fatal("uring_setup");
> ring = &cfg_p->ring;
>
> /* explicitly clear FD_CLOEXEC on the io_uring */
> flags = fcntl(cfg_p->ring.ring_fd, F_GETFD, 0);
> if (flags < 0)
> fatal("fcntl(ring_shm_fd,getfd)");
> flags &= ~FD_CLOEXEC;
> rc = fcntl(cfg_p->ring.ring_fd, F_SETFD, flags);
> if (rc)
> fatal("fcntl(ring_shm_fd,setfd)");
> } else if (tst_method = TST_T1_CHILD) {
> /* import a previously created and shared io_uring */
>
> /* open the existing shm segment with the io_uring info */
> ring_shm_fd = shm_open(URING_SHM_NAME, O_RDWR, 0);
> if (ring_shm_fd < 0)
> fatal("shm_open(existing)");
> cfg_p = mmap(NULL, sizeof(*cfg_p), PROT_READ | PROT_WRITE,
> MAP_SHARED, ring_shm_fd, 0);
> if (!cfg_p)
> fatal("mmap(shm)");
>
> /* import the io_uring */
> ring = &ring_storage;
> rc = uring_import(cfg_p->ring.ring_fd,
> ring, &cfg_p->ring_params);
> if (rc < 0)
> fatal("uring_import");
> }
>
> /*
> * fork/exec a child process (if necessary)
> */
> if (tst_method == TST_T1_PARENT) {
> pid_t pid;
>
> /* set the ctx for the next exec */
> if (argc >= 3) {
> printf(">>> set LSM/SELinux exec: %s\n",
> (selinux_exec(argv[2]) > 0 ? "OK" : "FAILED"));
> }
>
> /* fork/exec */
> pid = fork();
> if (!pid) {
> /* start the child */
> rc = execl(argv[0], argv[0], "t1_child", (char *)NULL);
> if (rc < 0)
> fatal("exec");
> } else {
> /* wait for the child to exit */
> int status;
> waitpid(pid, &status, 0);
> if (WIFEXITED(status))
> rc = WEXITSTATUS(status);
> }
> }
>
> /*
> * run test(s)
> */
> if (tst_method == TST_SQPOLL || tst_method == TST_T1_CHILD) {
> rc = uring_op_a(ring, cfg_p->ring_creds, "/tmp/iouring.4.txt");
> if (rc < 0)
> fatal("uring_op_a(\"/tmp/iouring.4.txt\")");
> }
>
> /*
> * cleanup
> */
> if (tst_method == TST_SQPOLL || tst_method == TST_T1_PARENT) {
> printf(">>> shutdown\n");
> uring_shutdown(&cfg_p->ring);
> shm_unlink(URING_SHM_NAME);
> } else if (tst_method == TST_T1_CHILD) {
> shm_unlink(URING_SHM_NAME);
> }
>
> return rc;
> }

- RGB

--
Richard Guy Briggs <rgb@redhat.com>
Sr. S/W Engineer, Kernel Security, Base Operating Systems
Remote, Ottawa, Red Hat Canada
IRC: rgb, SunRaycer
Voice: +1.647.777.2635, Internal: (81) 32635