Blob Blame History Raw
# -*- Autotest -*-

AT_BANNER([/proc helpers])

## -------------------- ##
## get_env_variable_ext ##
## -------------------- ##

AT_TESTFUN([get_env_variable_ext],
[[

#include "testsuite.h"

void test_delim(char delim)
{
    const char *test_data[][2] = {
            { "EMPTY", "" },
            { "SATYR", "awesome" },
            { "STUFF", "" },
            { "OPENSOURCE", "brilliant" },
            { "LIBREPORT", "great" },
            { "TRICK", "" },
            { "ABRT", "fabulous" },
            { "SENTINEL", NULL}
        };


    char fdname[] = "/tmp/libreprt-testsuite.XXXXXX";
    int fd = mkstemp(fdname);
    assert(fd >= 0);
    printf("Temporary file: %s\n", fdname);

    int fddup = dup(fd);
    assert(fddup >= 0);

    FILE *f = fdopen(fddup, "w");
    assert(f);

    for (size_t i = 0; i < ARRAY_SIZE(test_data); ++i)
    {
        if (test_data[i][1] == NULL)
            continue;

        fprintf(f, "%s=%s", test_data[i][0], test_data[i][1]);

        /* Do not add delimiter after the last entry */
        if (i < ARRAY_SIZE(test_data) - 1)
            fputc(delim, f);
    }

    fclose(f);

    for (size_t i = ARRAY_SIZE(test_data); i != 0; --i)
    {
        lseek(fd, 0, SEEK_SET);
        char *value = NULL;
        printf("Looking for '%s'\n", test_data[i-1][0]);
        TS_ASSERT_FUNCTION(get_env_variable_ext(fd, delim, test_data[i-1][0], &value));
        TS_ASSERT_STRING_EQ(value, test_data[i-1][1], "Environment value at 'i'");
        free(value);
    }

    close(fd);
}

TS_MAIN
{
    test_delim('\n');
    test_delim('\0');
}
TS_RETURN_MAIN
]])


## ---------------- ##
## get_env_variable ##
## ---------------- ##

AT_TESTFUN([get_env_variable],
[[

#include "testsuite.h"

TS_MAIN
{
    char cwd[257];
    getcwd(cwd, sizeof(cwd));

    char *value = NULL;
    TS_ASSERT_FUNCTION(get_env_variable(getpid(), "PWD", &value));
    TS_ASSERT_STRING_EQ(value, cwd, "Test environment variable - PWD");
    free(value);
}
TS_RETURN_MAIN
]])

## ----------- ##
## get_cmdline ##
## ----------- ##

AT_TESTFUN([get_cmdline], [[
#include "testsuite.h"
#include <err.h>

void test(const char *program, const char *args[], const char *expected)
{
    int inout[2];
    xpipe(inout);

    pid_t pid = fork();
    if (pid < 0) {
        err(EXIT_FAILURE, "fork");
    }

    if (pid == 0) {
        close(STDOUT_FILENO);
        xdup2(inout[1], STDOUT_FILENO);
        close(inout[0]);

        execv(program, (char **)args);
        err(EXIT_FAILURE, "exec(%s)", program);
    }

    close(inout[1]);
    int status = 0;
    if (safe_waitpid(pid, &status, 0) < 0) {
        err(EXIT_FAILURE, "waitpid");
    }

    if (WEXITSTATUS(status) != 0) {
        errx(EXIT_FAILURE, "Child not exited with 0");
    }

    const size_t buffer_size = strlen(expected) * 2;
    char cmdline[buffer_size];
    const ssize_t total = full_read(inout[0], cmdline, buffer_size);
    close(inout[0]);

    if (total < 0) {
        err(EXIT_FAILURE, "full_read");
    }

    cmdline[total] = '\0';
    TS_ASSERT_STRING_EQ(cmdline, expected, "/proc/[pid]/cmd");
}

TS_MAIN
{
    if (argc > 1) {
        char *cmdline = NULL;
        if (strcmp(argv[0], "get_cmdline") == 0) {
            cmdline = get_cmdline(getpid());
        }
        else if (strcmp(argv[0], "get_cmdline_at") == 0) {
            int pid_proc_fd = open("/proc/self", O_DIRECTORY);
            if (pid_proc_fd < 0) {
                err(EXIT_FAILURE, "open(/proc/self, O_DIRECTORY)");
            }
            cmdline = get_cmdline_at(pid_proc_fd);
            close(pid_proc_fd);
        }
        else {
            errx(EXIT_FAILURE, "Unsupported function type '%s'", argv[0]);
        }

        fprintf(stdout, "%s", cmdline);
        fflush(stdout);
        exit(EXIT_SUCCESS);
    }

    char *binary = malloc_readlink("/proc/self/exe");
    const char *args[] = { NULL, "!fo\" \"o", "@blah", "b\na'r", "g\rea\t", "regular", NULL };

#define EXPECTED " '!fo\\\" \\\"o' @blah 'b\\na\\'r' 'g\\rea\\t' regular"

    args[0] = "get_cmdline";
    test(binary, args, "get_cmdline" EXPECTED);

    args[0] = "get_cmdline_at";
    test(binary, args, "get_cmdline_at" EXPECTED);

    free(binary);
}
TS_RETURN_MAIN
]])

## -------------- ##
## get_executable ##
## -------------- ##

AT_TESTFUN([get_executable], [[
#include "testsuite.h"
#include <sys/sendfile.h>
#include <err.h>

#define PRELINK_BASENAME "/tmp/libreport.testsuite.get_executable"
#
void test(const char *program, const char *expected, const char *function, const char *argv1)
{
    int inout[2];
    xpipe(inout);

    pid_t pid = fork();
    if (pid < 0) {
        err(EXIT_FAILURE, "fork");
    }

    if (pid == 0) {
        close(STDOUT_FILENO);
        xdup2(inout[1], STDOUT_FILENO);
        close(inout[0]);

        const char *args[3] = { function, argv1, NULL };
        execv(program, (char **)args);
        err(EXIT_FAILURE, "execv(%s) : %d", program, errno);
    }

    close(inout[1]);
    int status = 0;
    if (safe_waitpid(pid, &status, 0) < 0) {
        err(EXIT_FAILURE, "waitpid");
    }

    if (WEXITSTATUS(status) != 0) {
        errx(EXIT_FAILURE, "Child not exited with 0");
    }

    const size_t buffer_size = strlen(expected) * 2;
    char executable[buffer_size];
    const ssize_t total = full_read(inout[0], executable, buffer_size);
    close(inout[0]);

    if (total < 0) {
        err(EXIT_FAILURE, "full_read");
    }

    executable[total] = '\0';
    TS_ASSERT_STRING_EQ(executable, expected, "/proc/[pid]/exe");
}

int copy_to_temporary(const char *source, char *dest)
{
    int dest_fd = mkstemp(dest);
    if (dest_fd < 0) {
        err(EXIT_FAILURE, "mkstemp(%s)", dest);
    }

    int src_fd = open(source, O_RDONLY);
    if (src_fd < 0) {
        err(EXIT_FAILURE, "open(%s, O_RDONLY)", source);
    }

    struct stat src_stat;
    if (fstat(src_fd, &src_stat) < 0) {
        err(EXIT_FAILURE, "fstat(%s)", source);
    }

    if (sendfile(dest_fd, src_fd, NULL, src_stat.st_size) < 0) {
        err(EXIT_FAILURE, "splice(%s, %s, %zu)", source, dest, src_stat.st_size);
    }

    close(src_fd);

    fchmod(dest_fd, src_stat.st_mode);
    return dest_fd;
}

TS_MAIN
{
    if (argc > 1) {
        if (strcmp(argv[1], "delete") == 0) {
            char *binary = malloc_readlink("/proc/self/exe");
            unlink(binary);
            if (access(binary, R_OK) != -1 && errno != !ENOENT) {
                err(EXIT_FAILURE, "failed to remove %s", binary);
            }
            free(binary);
        }

        char *executable = NULL;
        if (strcmp(argv[0], "get_executable") == 0) {
            executable = get_executable(getpid());
        }
        else if (strcmp(argv[0], "get_executable_at") == 0) {
            int pid_proc_fd = open("/proc/self", O_DIRECTORY);
            if (pid_proc_fd < 0) {
                err(EXIT_FAILURE, "open(/proc/self, O_DIRECTORY)");
            }
            executable = get_executable_at(pid_proc_fd);
            close(pid_proc_fd);
        }
        else {
            errx(EXIT_FAILURE, "Unsupported function type '%s'", argv[0]);
        }

        fprintf(stdout, "%s", executable);
        fflush(stdout);
        exit(EXIT_SUCCESS);
    }

    {
        char *binary = malloc_readlink("/proc/self/exe");

        test(binary, binary, "get_executable", "keep");
        test(binary, binary, "get_executable_at", "keep");

        free(binary);
    }

    {
        char binary[] = PRELINK_BASENAME ".#prelink#.XXXXXX";
        int binary_fd = copy_to_temporary("/proc/self/exe", binary);
        close(binary_fd);

        test(binary, PRELINK_BASENAME, "get_executable", "keep");

        unlink(binary);
    }

    {
        char binary[] = PRELINK_BASENAME ".#prelink#.XXXXXX";
        int binary_fd = copy_to_temporary("/proc/self/exe", binary);
        close(binary_fd);

        test(binary, PRELINK_BASENAME, "get_executable_at", "keep");

        unlink(binary);
    }

    {
        char binary[] = PRELINK_BASENAME ".#prelink#.XXXXXX";
        int binary_fd = copy_to_temporary("/proc/self/exe", binary);
        close(binary_fd);

        test(binary, PRELINK_BASENAME, "get_executable", "delete");

        if (unlink(binary) == 0) {
            errx(EXIT_FAILURE, "should be already removed %s", binary);
        }
    }

    {
        char binary[] = PRELINK_BASENAME ".#prelink#.XXXXXX";
        int binary_fd = copy_to_temporary("/proc/self/exe", binary);
        close(binary_fd);

        test(binary, PRELINK_BASENAME, "get_executable_at", "delete");

        if (unlink(binary) == 0) {
            errx(EXIT_FAILURE, "should be already removed %s", binary);
        }
    }

    {
        char binary[] = "/tmp/libreport.testsuite.get_executable.XXXXXX";
        int binary_fd = copy_to_temporary("/proc/self/exe", binary);
        close(binary_fd);

        test(binary, binary, "get_executable", "delete");

        if (unlink(binary) == 0) {
            errx(EXIT_FAILURE, "should be already removed %s", binary);
        }
    }

    {
        char binary[] = "/tmp/libreport.testsuite.get_executable.XXXXXX";
        int binary_fd = copy_to_temporary("/proc/self/exe", binary);
        close(binary_fd);

        test(binary, binary, "get_executable_at", "delete");

        if (unlink(binary) == 0) {
            errx(EXIT_FAILURE, "should be already removed %s", binary);
        }
    }
}
TS_RETURN_MAIN
]])

## ------- ##
## get_cwd ##
## ------- ##

AT_TESTFUN([get_cwd], [[
#include "testsuite.h"
#include <sys/sendfile.h>
#include <err.h>

TS_MAIN
{
    char wd[PATH_MAX];
    getcwd(wd, sizeof(wd));

    char *cwd = get_cwd(getpid());
    TS_ASSERT_STRING_EQ(cwd, wd, "get_cwd(getpid())");
    free(cwd);


    int pid_proc_fd = open("/proc/self", O_DIRECTORY | O_PATH);
    if (pid_proc_fd < 0) {
        err(EXIT_FAILURE, "open(/proc/self, O_DIRECTORY | O_PATH)");
    }

    char *cwd_at = get_cwd_at(pid_proc_fd);
    TS_ASSERT_STRING_EQ(cwd_at, wd, "get_cwd_at(open(/proc/self))");
    close(pid_proc_fd);
    free(cwd_at);
}
TS_RETURN_MAIN
]])

## ----------- ##
## get_rootdir ##
## ----------- ##

AT_TESTFUN([get_rootdir], [[
#include "testsuite.h"
#include <sys/sendfile.h>
#include <err.h>

TS_MAIN
{
    char *proc_self_root = malloc_readlink("/proc/self/root");

    char *root_dir = get_rootdir(getpid());
    TS_ASSERT_STRING_EQ(root_dir, proc_self_root, "get_rootdir(getpid())");
    free(root_dir);


    int pid_proc_fd = open("/proc/self", O_DIRECTORY | O_PATH);
    if (pid_proc_fd < 0) {
        err(EXIT_FAILURE, "open(/proc/self, O_DIRECTORY | O_PATH)");
    }

    char *root_dir_at = get_rootdir_at(pid_proc_fd);
    TS_ASSERT_STRING_EQ(root_dir_at, proc_self_root, "get_rootdir_at(open(/proc/self))");
    close(pid_proc_fd);
    free(root_dir_at);
}
TS_RETURN_MAIN
]])

## ------------ ##
## dump_fd_info ##
## ------------ ##

AT_TESTFUN([dump_fd_info], [[
#include "testsuite.h"
#include <sys/sendfile.h>
#include <err.h>

#define FILENAME_FORMAT "/tmp/libreport.testsuite.fdinfo.%d.%s"

pid_t prepare_process(void)
{
    int toparent[2];
    xpipe(toparent);

    char *binary = malloc_readlink("/proc/self/exe");
    pid_t pid = fork();
    if (pid < 0) {
        err(EXIT_FAILURE, "fork");
    }

    if (pid == 0) {
        close(STDOUT_FILENO);
        xdup2(toparent[1], STDOUT_FILENO);

        DIR *fddir = opendir("/proc/self/fd");
        struct dirent *dent;
        while ((dent = readdir(fddir))) {
            const int fd = atoi(dent->d_name);
            if (fd != STDOUT_FILENO) {
                close(fd);
            }
        }

        execl(binary, "wait", NULL);
        exit(EXIT_FAILURE);
    }

    close(toparent[1]);

    free(binary);

    /* Wait for child */
    char buf[8];
    if (full_read(toparent[0], buf, 8) < 0) {
        fprintf(stderr, "Failed to read from child: %s\n", strerror(errno));
        fflush(stderr);
    }

    close(toparent[0]);

    return pid;
}

void kill_process(pid_t pid)
{
    /* Notify child */
    kill(pid, SIGTERM);
    int status = 0;
    if (safe_waitpid(pid, &status, 0) < 0) {
        fprintf(stderr, "Couldn't wait for child\n");
    }
    else if (!WIFSIGNALED(status) || WTERMSIG(status) != SIGTERM) {
        fprintf(stderr, "Child was not TERMinated - %d\n", status);
    }
}

void check_file_contents(const char *fdinfo_filename)
{
    struct fd {
        int fd;
        const char *file;
    } fds[] = {
        { .fd = 0, .file = "/etc/services", },
        { .fd = 2, .file = "/etc/passwd", },
        { .fd = 3, .file = "/etc/group", },
    };

    char *file = xmalloc_xopen_read_close(fdinfo_filename, NULL);
    int fdno = 0;
    char *cursor = file;
    char *line = file;
    char *end = file + strlen(file);
    while (cursor < end) {
        cursor = strchrnul(line, '\n');
        if (*cursor != '\0') {
            *cursor = '\0';
        }
        ++cursor;

        if (fdno < (sizeof(fds)/sizeof(fds[0]))) {
            int fd = 0;
            char *file = NULL;
            const int res = sscanf(line, "%d:%ms", &fd, &file);
            TS_ASSERT_SIGNED_EQ(res, 2);
            TS_ASSERT_SIGNED_EQ(fd, fds[fdno].fd);
            TS_ASSERT_STRING_EQ(file, fds[fdno].file, "FD file name");
            free(file);
        }

        line = cursor;
        int fieldscnt = 0;
        while (line < end) {
            cursor = strchrnul(line, '\n');
            if (*cursor != '\0') {
                *cursor = '\0';
            }
            ++cursor;

            if (strcmp(line, "") == 0) {
                break;
            }

            int col = 0;
            for (; col < strlen(line); ++col) {
                if (line[col] == ':') {
                    break;
                }

                TS_ASSERT_TRUE(line[col] != ' ' && line[col] != '\t');
                if (!g_testsuite_last_ok) {
                    break;
                }
            }
            TS_ASSERT_SIGNED_NEQ(col, 0);
            TS_ASSERT_SIGNED_LT(col, strlen(line));
            if (g_testsuite_last_ok) {
                TS_ASSERT_CHAR_EQ(line[col], ':');
            }

            fieldscnt += g_testsuite_last_ok;
            line = cursor;
        }
        TS_ASSERT_SIGNED_GT(fieldscnt, 2);

        ++fdno;
        line = cursor;
    }

    TS_ASSERT_SIGNED_EQ(fdno, sizeof(fds)/sizeof(fds[0]));
    free(file);
}

TS_MAIN
{
    if (strcmp(argv[0], "wait") == 0) {
        FILE *services = fopen("/etc/services", "r");
        FILE *passwd = fopen("/etc/passwd", "r");
        FILE *group = fopen("/etc/group", "r");

        /* Notify parent */
        close(STDOUT_FILENO);

        /* Wait for parent */
        while (1) {
            sleep(1);
        }

        fclose(group);
        fclose(passwd);
        fclose(services);
        exit(EXIT_SUCCESS);
    }

    pid_t pid = prepare_process();

    char proc_dir_path[strlen("/proc/%d/fd") + sizeof(pid_t) * 3];
    if (sizeof(proc_dir_path) <= snprintf(proc_dir_path, sizeof(proc_dir_path), "/proc/%d/fd", pid)) {
        errx(EXIT_FAILURE, "too small buffer for proc dir path");
    }

    {
        TS_PRINTF("%s\n", "dump_fd_info");
        char fdinfo_filename[strlen(FILENAME_FORMAT) + sizeof(pid_t) * 3 + strlen("dump_fd_info")];
        if (sizeof(fdinfo_filename) <= snprintf(fdinfo_filename, sizeof(fdinfo_filename), FILENAME_FORMAT, pid, "dump_fd_info")) {
            errx(EXIT_FAILURE, "too small buffer for file name");
        }

        TS_ASSERT_FUNCTION(dump_fd_info(fdinfo_filename, proc_dir_path));

        struct stat st;
        TS_ASSERT_FUNCTION(stat(fdinfo_filename, &st));
        if (g_testsuite_last_ok) {
            TS_ASSERT_SIGNED_EQ(st.st_mode & 0777, 0600);
        }

        check_file_contents(fdinfo_filename);

        unlink(fdinfo_filename);
    }

    {
        TS_PRINTF("%s\n", "dump_fd_info_ext");
        char fdinfo_filename[strlen(FILENAME_FORMAT) + sizeof(pid_t) * 3 + strlen("dump_fd_info_ext")];
        if (sizeof(fdinfo_filename) <= snprintf(fdinfo_filename, sizeof(fdinfo_filename), FILENAME_FORMAT, pid, "dump_fd_info_ext")) {
            errx(EXIT_FAILURE, "too small buffer for file name");
        }

        const uid_t uid = getuid();
        const gid_t gid = getgid();
        TS_ASSERT_FUNCTION(dump_fd_info_ext(fdinfo_filename, proc_dir_path, uid, gid));

        struct stat st;
        TS_ASSERT_FUNCTION(stat(fdinfo_filename, &st));
        if (g_testsuite_last_ok) {
            TS_ASSERT_SIGNED_EQ(st.st_mode & 0777, 0600);
        }

        check_file_contents(fdinfo_filename);

        unlink(fdinfo_filename);
    }

    {
        TS_PRINTF("%s\n", "dump_fd_info_at");
        char fdinfo_filename[strlen(FILENAME_FORMAT) + sizeof(pid_t) * 3 + strlen("dump_fd_info_at")];
        if (sizeof(fdinfo_filename) <= snprintf(fdinfo_filename, sizeof(fdinfo_filename), FILENAME_FORMAT, pid, "dump_fd_info_at")) {
            errx(EXIT_FAILURE, "too small buffer for file name");
        }

        FILE *dest = fopen(fdinfo_filename, "w");
        const int pid_proc_fd = open_proc_pid_dir(pid);

        TS_ASSERT_FUNCTION(dump_fd_info_at(pid_proc_fd, dest));

        close(pid_proc_fd);
        fclose(dest);

        check_file_contents(fdinfo_filename);

        unlink(fdinfo_filename);
    }

    kill_process(pid);
}
TS_RETURN_MAIN
]])


## ------------- ##
## get_fs-u_g-id ##
## ------------- ##

AT_TESTFUN([get_fs-u_g-id], [[
#include "testsuite.h"
#include <sys/sendfile.h>
#include <err.h>

TS_MAIN
{
    char *proc_pid_status = xmalloc_xopen_read_close("/proc/self/status", NULL);
    TS_ASSERT_SIGNED_EQ(get_fsuid(proc_pid_status), getuid());
    TS_ASSERT_SIGNED_EQ(get_fsgid(proc_pid_status), getgid());
    free(proc_pid_status);
}
TS_RETURN_MAIN
]])


## ---------- ##
## get_ns_ids ##
## ---------- ##

AT_TESTFUN([get_ns_ids], [[
#include "testsuite.h"
#include <sys/sendfile.h>
#include <err.h>

void check(struct ns_ids *ids)
{
    const int nsfd = open("/proc/self/ns", O_DIRECTORY);
    assert(nsfd >= 0);

    for (size_t i = 0; i < ARRAY_SIZE(libreport_proc_namespaces); ++i) {
        struct stat st;
        if (fstatat(nsfd, libreport_proc_namespaces[i], &st, 0) < 0) {
            TS_ASSERT_SIGNED_OP_MESSAGE(ids->nsi_ids[i], ==, PROC_NS_UNSUPPORTED, libreport_proc_namespaces[i]);
        }
        else {
            TS_ASSERT_SIGNED_OP_MESSAGE(ids->nsi_ids[i], ==, st.st_ino, libreport_proc_namespaces[i]);
        }
    }

    DIR *nsdir = fdopendir(nsfd);
    assert(nsdir != NULL);

    struct dirent *dent = NULL;
    while ((dent = readdir(nsdir))) {
        if (dot_or_dotdot(dent->d_name))
            continue;

        size_t i = 0;
        for (; i < ARRAY_SIZE(libreport_proc_namespaces); ++i) {
            if (strcmp(libreport_proc_namespaces[i], dent->d_name) == 0) {
                break;
            }
        }
        TS_ASSERT_SIGNED_OP_MESSAGE(i, <, ARRAY_SIZE(libreport_proc_namespaces), dent->d_name);

        if (g_testsuite_last_ok) {
            struct stat st;
            TS_ASSERT_FUNCTION(fstatat(nsfd, dent->d_name, &st, 0));
            TS_ASSERT_SIGNED_OP_MESSAGE(ids->nsi_ids[i], ==, st.st_ino, dent->d_name);
        }
    }

    closedir(nsdir);
}

TS_MAIN
{
    TS_PRINTF("%s\n", "get_ns_ids");
    struct ns_ids pid_ids;
    TS_ASSERT_FUNCTION(get_ns_ids(getpid(), &pid_ids));
    check(&pid_ids);

    TS_PRINTF("%s\n", "get_ns_ids_at");
    const int pid_proc_dir = open_proc_pid_dir(getpid());
    struct ns_ids proc_ids;
    TS_ASSERT_FUNCTION(get_ns_ids_at(pid_proc_dir, &proc_ids));
    check(&proc_ids);
    close(pid_proc_dir);
}
TS_RETURN_MAIN
]])


## ------------------------------ ##
##  get_mountinfo_for_mount_point ##
## ------------------------------ ##


AT_TESTFUN([get_mountinfo_for_mount_point], [[
#include "testsuite.h"
#include <err.h>

void test_get_mountinfo_for_mount_point(
    const char *mount_point,
    const char *description,
    const char *input,
    const char *error,
    int exp_r,
    const struct mountinfo *exp_mntnf)
{
    TS_PRINTF("++++ %s\n", description);

    char *buf = NULL;

    FILE *mntnf_file = fmemopen((void *)input, strlen(input), "r");
    if (mntnf_file == NULL) {
        err(EXIT_FAILURE, "open_memstream");
    }

    struct mountinfo mntnf;
    int r;
    TS_ASSERT_STREAM_FD_CONTENTS_EQ_BEGIN(STDERR_FILENO);
    r = get_mountinfo_for_mount_point(mntnf_file, &mntnf, mount_point);
    TS_ASSERT_STREAM_FD_CONTENTS_EQ_END(error, description);
    TS_ASSERT_SIGNED_OP_MESSAGE(r, ==, exp_r, description);

    if (r == 0 && exp_mntnf != NULL) {
        TS_ASSERT_STRING_EQ(mntnf.mntnf_items[0], exp_mntnf->mntnf_items[0], "Item 0");
        TS_ASSERT_STRING_EQ(mntnf.mntnf_items[1], exp_mntnf->mntnf_items[1], "Item 1");
        TS_ASSERT_STRING_EQ(mntnf.mntnf_items[2], exp_mntnf->mntnf_items[2], "Item 2");
        TS_ASSERT_STRING_EQ(mntnf.mntnf_items[3], exp_mntnf->mntnf_items[3], "Item 3");
        TS_ASSERT_STRING_EQ(mntnf.mntnf_items[4], exp_mntnf->mntnf_items[4], "Item 4");
        TS_ASSERT_STRING_EQ(mntnf.mntnf_items[5], exp_mntnf->mntnf_items[5], "Item 5");
        TS_ASSERT_STRING_EQ(mntnf.mntnf_items[6], exp_mntnf->mntnf_items[6], "Item 6");
        TS_ASSERT_STRING_EQ(mntnf.mntnf_items[7], exp_mntnf->mntnf_items[7], "Item 7");
        TS_ASSERT_STRING_EQ(mntnf.mntnf_items[8], exp_mntnf->mntnf_items[8], "Item 8");
        TS_ASSERT_STRING_EQ(mntnf.mntnf_items[9], exp_mntnf->mntnf_items[9], "Item 9");
    }

    fclose(mntnf_file);
    free(buf);

    TS_PRINTF("---- %s\n", description);
}

#define TS_MOUNT_INFO(f0, f1, f2, f3, f4, f5, f6, f7, f8, f9) \
    ((struct mountinfo){ .mntnf_items = { (char *)f0, (char *)f1, (char *)f2, \
                                          (char *)f3, (char *)f4, (char *)f5, \
                                          (char *)f6, (char *)f7, (char *)f8, \
                                          (char *)f9 } })


TS_MAIN
{
    test_get_mountinfo_for_mount_point(
        /* mount point */ "/",
        /* description */ "Empty file",
        /* input       */ "",
        /* error       */ "Mountinfo line does not have enough fields 0\n",
        /* exp_r       */ 1,
        /* exp_mntnf   */ NULL
    );

    test_get_mountinfo_for_mount_point(
        /* mount point */ "/",
        /* description */ "Not-enough fields",
        /* input       */ "12 34 567:10 ",
        /* error       */ "Mountinfo line does not have enough fields 3\n",
        /* exp_r       */ 1,
        /* exp_mntnf   */ NULL
    );

    test_get_mountinfo_for_mount_point(
        /* mount point */ "/",
        /* description */ "Missing the mount point field",
        /* input       */ "12 34 567:10 /foo ",
        /* error       */ "Mountinfo line does not have the mount point field\n",
        /* exp_r       */ 2,
        /* exp_mntnf   */ NULL
    );

    test_get_mountinfo_for_mount_point(
        /* mount point */ "/",
        /* description */ "Not found mount point",
        /* input       */ "12 34 567:10 /foo /bar ",
        /* error       */ "",
        /* exp_r       */ -ENOKEY,
        /* exp_mntnf   */ NULL
    );

    test_get_mountinfo_for_mount_point(
        /* mount point */ "/",
        /* description */ "Unexpected end of file",
        /* input       */ "12 34 567:10 /foo / rw,noatime",
        /* error       */ "Unexpected end of file\n",
        /* exp_r       */ -ENODATA,
        /* exp_mntnf   */ NULL
    );

    test_get_mountinfo_for_mount_point(
        /* mount point */ "/",
        /* description */ "Escaped path: no mount point field",
        /* input       */ "12 34 567:10 /foo\\ bar ",
        /* error       */ "Mountinfo line does not have the mount point field\n",
        /* exp_r       */ 2,
        /* exp_mntnf   */ NULL
    );

    test_get_mountinfo_for_mount_point(
        /* mount point */ "/",
        /* description */ "Correct, matching line",
        /* input       */ "12 34 567:10 /foo / rw,noatime shared:1 - xfs /dev/sda1 rw,seclabel,attr2",
        /* error       */ "",
        /* exp_r       */ 0,
        /* exp_mntnf   */ &TS_MOUNT_INFO("12", "34", "567:10", "/foo", "/", "rw,noatime", "shared:1", "xfs", "/dev/sda1", "rw,seclabel,attr2")
    );

    test_get_mountinfo_for_mount_point(
        /* mount point */ "/",
        /* description */ "Correct, matching line, empty optional fields",
        /* input       */ "12 34 567:10 /foo / rw,noatime - xfs /dev/sda1 rw,seclabel,attr2",
        /* error       */ "",
        /* exp_r       */ 0,
        /* exp_mntnf   */ &TS_MOUNT_INFO("12", "34", "567:10", "/foo", "/", "rw,noatime", "", "xfs", "/dev/sda1", "rw,seclabel,attr2")
    );

    test_get_mountinfo_for_mount_point(
        /* mount point */ "/",
        /* description */ "Correct, matching line, several optional fields",
        /* input       */ "12 34 567:10 /foo / rw,noatime shared:1 master:2 unbindable:3 - xfs /dev/sda1 rw,seclabel,attr2",
        /* error       */ "",
        /* exp_r       */ 0,
        /* exp_mntnf   */ &TS_MOUNT_INFO("12", "34", "567:10", "/foo", "/", "rw,noatime", "shared:1 master:2 unbindable:3", "xfs", "/dev/sda1", "rw,seclabel,attr2")
    );

    test_get_mountinfo_for_mount_point(
        /* mount point */ "/",
        /* description */ "Escaped path: correct, matching line",
        /* input       */ "12 34 567:10 /foo\\ bar / rw,noatime shared:1 - xfs /dev/sda1 rw,seclabel,attr2",
        /* error       */ "",
        /* exp_r       */ 0,
        /* exp_mntnf   */ &TS_MOUNT_INFO("12", "34", "567:10", "/foo\\ bar", "/", "rw,noatime", "shared:1", "xfs", "/dev/sda1", "rw,seclabel,attr2")
    );

    test_get_mountinfo_for_mount_point(
        /* mount point */ "/",
        /* description */ "Escaped path: correct, matching line + escaped mount source",
        /* input       */ "12 34 567:10 /foo\\ bar / rw,noatime shared:1 - xfs /dev/sda\\ 1 rw,seclabel,attr2",
        /* error       */ "",
        /* exp_r       */ 0,
        /* exp_mntnf   */ &TS_MOUNT_INFO("12", "34", "567:10", "/foo\\ bar", "/", "rw,noatime", "shared:1", "xfs", "/dev/sda\\ 1", "rw,seclabel,attr2")
    );
}
TS_RETURN_MAIN
]])


## ------------------- ##
## dump_namespace_diff ##
## ------------------- ##

AT_TESTFUN([dump_namespace_diff], [[
#include "testsuite.h"
#include <err.h>

#define FILENAME_FORMAT "/tmp/libreport.testsuite.namespace_diff.%d.%s"

void check_file_contents(const char *filename)
{
    char *expected;
    struct stat st;

    char const *pid_for_children = "default";
    char const *cgroup = "default";

    if (stat("/proc/self/ns/cgroup", &st) < 0 && errno == ENOENT)
        cgroup = "unknown";

    if (stat("/proc/self/ns/pid_for_children", &st) < 0 && errno == ENOENT)
        pid_for_children = "unknown";

    expected = xasprintf("ipc : default\n"
                         "mnt : default\n"
                         "net : default\n"
                         "pid : default\n"
                         "uts : default\n"
                         "user : default\n"
                         "cgroup : %s\n"
                         "pid_for_children : %s\n", cgroup, pid_for_children);

    char *file = xmalloc_xopen_read_close(filename, NULL);
    TS_ASSERT_STRING_EQ(file, expected, "Namespaces");
    free(file);
    free(expected);
}

TS_MAIN
{
    {
        TS_PRINTF("%s\n", "dump_namespace_diff");
        char filename[strlen(FILENAME_FORMAT) + sizeof(pid_t) * 3 + strlen("dump_namespace_diff")];
        if (sizeof(filename) <= snprintf(filename, sizeof(filename), FILENAME_FORMAT, getpid(), "dump_namespace_diff")) {
            errx(EXIT_FAILURE, "too small buffer for file name");
        }

        TS_ASSERT_FUNCTION(dump_namespace_diff(filename, getpid(), getppid()));

        struct stat st;
        TS_ASSERT_FUNCTION(stat(filename, &st));
        if (g_testsuite_last_ok) {
            TS_ASSERT_SIGNED_EQ(st.st_mode & 0777, 0600);
        }

        check_file_contents(filename);

        unlink(filename);
    }

    {
        TS_PRINTF("%s\n", "dump_namespace_diff_ext");
        char filename[strlen(FILENAME_FORMAT) + sizeof(pid_t) * 3 + strlen("dump_namespace_diff_ext")];
        if (sizeof(filename) <= snprintf(filename, sizeof(filename), FILENAME_FORMAT, getpid(), "dump_namespace_diff_ext")) {
            errx(EXIT_FAILURE, "too small buffer for file name");
        }

        const uid_t uid = getuid();
        const gid_t gid = getgid();
        TS_ASSERT_FUNCTION(dump_namespace_diff_ext(filename, getpid(), getppid(), uid, gid));

        struct stat st;
        TS_ASSERT_FUNCTION(stat(filename, &st));
        if (g_testsuite_last_ok) {
            TS_ASSERT_SIGNED_EQ(st.st_mode & 0777, 0600);
        }

        check_file_contents(filename);

        unlink(filename);
    }

    {
        TS_PRINTF("%s\n", "dump_namespace_diff_at");
        char filename[strlen(FILENAME_FORMAT) + sizeof(pid_t) * 3 + strlen("dump_namespace_diff_at")];
        if (sizeof(filename) <= snprintf(filename, sizeof(filename), FILENAME_FORMAT, getppid(), "dump_namespace_diff_at")) {
            errx(EXIT_FAILURE, "too small buffer for file name");
        }

        FILE *dest = fopen(filename, "w");
        assert(dest != NULL);

        const int pid_proc_fd = open_proc_pid_dir(getppid());

        TS_ASSERT_FUNCTION(dump_namespace_diff_at(pid_proc_fd, pid_proc_fd, dest));

        close(pid_proc_fd);
        fclose(dest);

        check_file_contents(filename);

        unlink(filename);
    }
}
TS_RETURN_MAIN
]])


## -------------------- ##
## process_has_own_root ##
## -------------------- ##

AT_TESTFUN([process_has_own_root], [[
#include "testsuite.h"
#include <sys/sendfile.h>
#include <err.h>

void write_cmd_output_to_fd(int fd, const char *cmd)
{
    FILE *proc = popen(cmd, "r");
    if (proc == NULL) {
        err(EXIT_FAILURE, "popen(%s)", cmd);
    }

    char *output = xmalloc_fgetline(proc);
    TS_PRINTF("%s : %s\n", cmd, output);

    const int retcode = pclose(proc);
    if (retcode == -1) {
        err(EXIT_FAILURE, "pclose(%s)", cmd);
    }

    if (retcode != 0) {
        errx(EXIT_FAILURE, "non-0 status %d of '%s'", cmd);
    }

    if (output == NULL) {
        errx(EXIT_FAILURE, "no output of '%s'", cmd);
    }

    full_write_str(fd, output);
    free(output);
}

TS_MAIN
{
    char mock_pid_proc[] = "/tmp/libreport.testsuite.pid.XXXXXX";

    if (mkdtemp(mock_pid_proc) == NULL) {
        err(EXIT_FAILURE, "mkdtemp(%s)", mock_pid_proc);
    }

    const int mock_pid_proc_fd = open(mock_pid_proc, O_DIRECTORY);
    if (mock_pid_proc_fd < 0) {
        err(EXIT_FAILURE, "open(%s, O_DIRECTORY)", mock_pid_proc);
    }

    {
        /* TODO: add test for open file descriptors */
        const int r = process_has_own_root_at(mock_pid_proc_fd);
        TS_ASSERT_SIGNED_EQ(r, -ENOENT);
    }

    /* Please, notice that the mode is intentionally 0000 - no read, no write,
     * no execute access */
    int mntnf_fd = openat(mock_pid_proc_fd, "mountinfo", O_RDWR | O_CREAT | O_EXCL, 0000);

    {
        /* TODO: add test for open file descriptors */
        const int r = process_has_own_root_at(mock_pid_proc_fd);
        TS_ASSERT_SIGNED_EQ(r, -EACCES);
    }

    /* Make the file readable & writable */
    fchmod(mntnf_fd, 0600);

    {
        /* TODO: add test for open file descriptors */
        const int r = process_has_own_root_at(mock_pid_proc_fd);
        TS_ASSERT_SIGNED_EQ(r, -ENOKEY);
    }

    full_write_str(mntnf_fd, "36 35 98:0 /madeuproot /foo rw,noatime master:1 - ext3 /dev/myroot rw,errors=continue\n");
    full_write_str(mntnf_fd, "37 38 99:0 /mnt3 /mnt4 rw,noatime master:2 - ext3 /dev/boot rw,errors=continue\n");

    fsync(mntnf_fd);
    lseek(mntnf_fd, 0, SEEK_SET);

    TS_PRINTF("Made-up mountinfo created in %s\n", mock_pid_proc);

    {
        /* TODO: add test for open file descriptors */
        const int r = process_has_own_root_at(mock_pid_proc_fd);
        TS_ASSERT_SIGNED_EQ(r, -ENOKEY);
    }

    TS_PRINTF("Going to copy /proc/1/mountinfo to %s\n", mock_pid_proc);

    const int pid1_mntnf_fd = open("/proc/1/mountinfo", O_RDONLY);
    if (pid1_mntnf_fd < 0) {
        err(EXIT_FAILURE, "/proc/1/mountinfo");
    }

    TS_PRINTF("Copying /proc/1/mountinfo to %s\n", mock_pid_proc);

    {
        int r = 0;

        while ((r = sendfile(mntnf_fd, pid1_mntnf_fd, NULL, 65535)) > 0)
            ;

        if (r < 0) {
            err(EXIT_FAILURE, "Cannot copy /proc/1/mountinfo to %s", mock_pid_proc);
        }
    }

    close(pid1_mntnf_fd);

    fsync(mntnf_fd);
    lseek(mntnf_fd, 0, SEEK_SET);

    TS_PRINTF("Copied /proc/1/mountinfo to %s\n", mock_pid_proc);

    {
        /* TODO: add test for open file descriptors */
        const int r = process_has_own_root_at(mock_pid_proc_fd);
        TS_ASSERT_SIGNED_EQ(r, 0);
    }

    /* Test different source directory. Swap / with \ in the mock mountinfo. */
    fsync(mntnf_fd);
    lseek(mntnf_fd, 0, SEEK_SET);

    full_write_str(mntnf_fd, "12 34 567:89 /madeuproot / ");
    write_cmd_output_to_fd(mntnf_fd, "findmnt -F /proc/1/mountinfo -r -n -o VFS-OPTIONS,OPT-FIELDS -T /");
    full_write_str(mntnf_fd, " - ");
    write_cmd_output_to_fd(mntnf_fd, "findmnt -F /proc/1/mountinfo -r -n -o FSTYPE,SOURCE,FS-OPTIONS -T /");

    fsync(mntnf_fd);
    lseek(mntnf_fd, 0, SEEK_SET);

    {
        /* TODO: add test for open file descriptors */
        const int r = process_has_own_root_at(mock_pid_proc_fd);
        TS_ASSERT_SIGNED_EQ(r, 1);
    }

    close(mntnf_fd);

    if (unlinkat(mock_pid_proc_fd, "mountinfo", 0) < 0) {
        perror("unlinkat(fd, mountinfo)");
    }

    if (rmdir(mock_pid_proc) < 0) {
        perror("rmdir(/mock_pid_dir)");
    }
}
TS_RETURN_MAIN
]])