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

Paul Moore
2021-08-24T22:28:23Z

[-- Attachment #1: Type: text/plain, Size: 1345 bytes --]

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.

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.

--
paul moore
www.paul-moore.com

[-- Attachment #2: iouring.4.c --]
[-- Type: text/x-csrc, Size: 15303 bytes --]

/*
* 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;
}

[-- Attachment #3: Type: text/plain, Size: 106 bytes --]