diff --git a/.devcontainer/Containerfile b/.devcontainer/Containerfile new file mode 100644 index 0000000..5c90062 --- /dev/null +++ b/.devcontainer/Containerfile @@ -0,0 +1,20 @@ +FROM debian +COPY pkgs.list / +RUN apt-get update \ + && apt-get install -y --no-install-recommends $(cat /pkgs.list) \ + && rm /pkgs.list +RUN chmod -s /usr/bin/newuidmap \ + && setcap cap_setuid=ep /usr/bin/newuidmap \ + && chmod -s /usr/bin/newgidmap \ + && setcap cap_setgid=ep /usr/bin/newgidmap +RUN groupadd --gid 1000 dev \ + && useradd --uid 1000 --gid 1000 --shell /bin/bash --create-home \ + -K SUB_UID_MIN=32768 \ + -K SUB_UID_MAX=49151 \ + -K SUB_UID_COUNT=2048 \ + -K SUB_GID_MIN=32768 \ + -K SUB_GID_MAX=49151 \ + -K SUB_GID_COUNT=2048 \ + dev +USER dev +WORKDIR /home/dev diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..4620cdd --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,14 @@ +{ + "name": "dev", + "build": { + "dockerfile": "Containerfile" + }, + "customizations": { + "vscode": { + "extensions": [ + "ms-vscode.cpptools" + ] + } + }, + "runArgs": [ "--security-opt", "seccomp=unconfined", "--security-opt", "label=disable", "--security-opt", "apparmor=unconfined" ] +} diff --git a/.devcontainer/pkgs.list b/.devcontainer/pkgs.list new file mode 100644 index 0000000..177bf02 --- /dev/null +++ b/.devcontainer/pkgs.list @@ -0,0 +1,21 @@ +attr +binutils +ca-certificates +debootstrap +gcc +gcc-aarch64-linux-gnu +gcc-x86-64-linux-gnu +git +htop +libc6-dev +libc6-dev-amd64-cross +libc6-dev-arm64-cross +libcap2-bin +make +man-db +openssl +patchelf +procps +strace +uidmap +wesperanto diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..42b0124 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,94 @@ +name: build +on: push +jobs: + build: + strategy: + matrix: + arch: + - x86_64 + - aarch64 + fail-fast: false + name: ${{ matrix.arch }} + runs-on: ${{ matrix.arch == 'aarch64' && 'ubuntu-latest-arm' || 'ubuntu-latest' }} + steps: + - name: setup arm runner + if: matrix.arch == 'aarch64' + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends podman uidmap slirp4netns dbus-user-session + id="$(id -u)" + sudo systemctl start user@$id + export DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$id/bus + systemctl --user start dbus + mkdir -p "$HOME/.config/containers" + echo 'unqualified-search-registries = ["docker.io"]' > "$HOME/.config/containers/registries.conf" + - uses: actions/checkout@v4 + - name: build dev container + run: podman build -t dev -f .devcontainer/Containerfile .devcontainer + - name: start dev container + run: >- + podman run --rm + --security-opt seccomp=unconfined + --security-opt label=disable + --security-opt apparmor=unconfined + --uidmap 0:1:1000 + --uidmap 1000:0:1 + --uidmap 1001:1001:64536 + --gidmap 0:1:1000 + --gidmap 1000:0:1 + --gidmap 1001:1001:64536 + -v "$PWD:/workdir" + -w /workdir + -d --name dev + dev tail -f /dev/null + - name: build + run: podman exec dev make all + - name: test + run: podman exec dev make test + - name: stop dev container + run: podman stop dev + - name: upload artifact + uses: actions/upload-artifact@v4 + with: + name: fake_xattr_${{ matrix.arch }} + path: fake_xattr + release: + if: github.ref == 'refs/heads/main' + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: download artifact (x86_64) + uses: actions/download-artifact@v4 + with: + name: fake_xattr_x86_64 + path: fake_xattr_x86_64 + - name: download artifact (aarch64) + uses: actions/download-artifact@v4 + with: + name: fake_xattr_aarch64 + path: fake_xattr_aarch64 + - name: setup env context + run: | + repo='${{ github.repository }}' + commit='${{ github.sha }}' + echo "artifact_prefix=${repo#*/}-${commit::8}" | tee -a "$GITHUB_ENV" + - name: pack artifacts + run: | + for arch in x86_64 aarch64; do + tar -c --transform "s|^fake_xattr_|${artifact_prefix}-|" "fake_xattr_$arch" | gzip > "${artifact_prefix}-${arch}.tar.gz" + done + - name: publish release + run: | + if gh release view latest > /dev/null 2>&1; then + gh release delete -y latest + fi + git tag --force latest + git push --force origin latest + gh release create latest \ + --verify-tag \ + --notes "created by GitHub actions run [${{ github.run_id }}](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})" \ + "${artifact_prefix}-x86_64.tar.gz" \ + "${artifact_prefix}-aarch64.tar.gz" \ + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c79d24 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +*.o + +# generated +syscall_lookup.c + +# executables +fake_xattr +do_syscall + +# tests +test_xattr_db +test_seccomp_unotify + +.tmp diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..15ea2be --- /dev/null +++ b/Makefile @@ -0,0 +1,74 @@ +MAKEFLAGS += --no-builtin-rules + +.DELETE_ON_ERROR: +.SILENT: +.SECONDEXPANSION: + +CFLAGS := -std=c11 -Wall -Wextra -Wshadow -Wdeclaration-after-statement -Wno-multichar -Werror -O2 + +ifdef TEST_DEBUG +CFLAGS += -DTEST_DEBUG=$(TEST_DEBUG) +endif + +generated := syscall_lookup.c +executables := fake_xattr do_syscall +tests := test_xattr_db test_seccomp_unotify + +xattr_db_objects := xattr_db.o zalloc.o +seccomp_unotify_objects := seccomp_unotify.o clone_vfork.o syscall_lookup.o +test_objects := mem_account.o fd_account.o child_account.o + +fake_xattr_objects := main.o mem_account.o $(seccomp_unotify_objects) $(xattr_db_objects) +do_syscall_objects := do_syscall.o syscall_lookup.o zalloc.o + +test_xattr_db_objects := test_xattr_db.o $(xattr_db_objects) $(test_objects) +test_seccomp_unotify_objects := test_seccomp_unotify.o $(seccomp_unotify_objects) $(test_objects) + +objects := $(foreach executable,$(executables) $(tests),$($(executable)_objects)) +uniq_objects := +$(foreach object,$(objects),$(if $(filter $(object),$(uniq_objects)),,$(eval uniq_objects += $(object)))) + +outputs := $(generated) $(objects) $(executables) $(tests) + +.PHONY: all clean test $(addprefix @,$(tests)) + +all: $(executables) + +clean: + echo rm $(outputs) + rm -rf "$$(readlink .tmp)" + rm -f $(outputs) .tmp + +@test_seccomp_unotify: do_syscall .tmp .tmp/chroot + +define test_target +@$(1): $(1) + unshare --map-root-user --map-auto ./$$< +endef + +$(foreach test,$(tests),$(eval $(call test_target,$(test)))) + +test: integration_test $(addprefix @,$(tests)) all .tmp + ./$< + +$(foreach object,$(filter-out $(generated),$(objects:.o=.c)),$(eval $(shell $(CC) -MM $(object) | tr -d '\\'))) + +.tmp: + echo MKTEMP $@ + ln -sf "$$(mktemp -d)" $@ + +.tmp/chroot: test_chroot_import_bin do_syscall | .tmp + [ -e $@ ] || mkdir $@ + ./$< $@ $(wordlist 2,$(words $^),$^) $$(command -v sh) $$(command -v cat) $$(command -v head) $$(command -v tail) + +$(executables) $(tests): %: $$($$@_objects) + echo LINK $^ '->' $@ + $(CC) $(CFLAGS) -o $@ $^ + +$(generated): %.c: gen_% + echo GEN $@ + CC=$(CC) ./$< > $@ + +$(uniq_objects): %.o: %.c + echo CC $< '->' $@ + $(CC) $(CFLAGS) -o $@ -c $< diff --git a/README.md b/README.md new file mode 100644 index 0000000..0d1cd3c --- /dev/null +++ b/README.md @@ -0,0 +1,72 @@ +# seccomp_fake_xattr + +Emulate extended attribute ([xattr](https://man7.org/linux/man-pages/man7/xattr.7.html)) operations in user space. + +All xattr related syscalls are intercepted using the seccomp user-space notification mechanism ([seccomp_unotify](https://man7.org/linux/man-pages/man2/seccomp_unotify.2.html)) and their results emulated by a user-space handler. +The handler stores its own xattr database in memory, without ever accessing extended attributes on the underlying file system. + +The main use of this is for system / distribution image builds. +It allows the build to set extended attributes that it would normally not have access to (i.e. if the build is running inside an unprivileged container or on a host with security modules loaded) in such a way that later tar archive / disk image / etc creation steps see and includes these attributes correctly. + + +## Usage + +``` +./fake_xattr [command] [args...] +``` + +`[command]` and all of its child processes will then run with their xattr syscalls intercepted and emulated. + +> [!IMPORTANT] +> In order to install seccomp filters for its child processes, `fake_xattr` needs the `CAP_SYS_ADMIN` capability in its user namspace. +> Therefore, as an unprivileged user, it is necessary to run it with `unshare --map-root-user --map-auto ./fake_xattr` or similar means. + + +## Build + +To build the main tool run + +``` +make fake_xattr +``` + +This requires a standard linux build environment to be present. See [Dev Container](#dev-container) for the recommended way of setting this up. + +### Test + +To build and run the unit and integration tests run + +``` +make test +``` + +## Dev Container + +To simplify the build process this project includes a dev container. This can either be used directly by [VS Code](https://code.visualstudio.com/docs/devcontainers/containers) and [GitHub Codespaces](https://docs.github.com/en/codespaces) or as a regular container. + +To use it as a regular container run + +``` +podman build -t dev .devcontainer +podman run --rm \ + --security-opt seccomp=unconfined \ + --security-opt label=disable \ + --security-opt apparmor=unconfined \ + --userns keep-id:uid=1000,gid=1000 \ + -v "$PWD:/home/dev/workdir" \ + -w /home/dev/workdir \ + -it dev +``` + +> [!NOTE] +> The `--userns keep-id:uid=1000,gid=1000` is needed because the dev container is configured to drop privileges to a dev user (`uid=1000 gid=1000`), while podman by default will map your host system user to `uid=0 gid=0`. +> Thus the work directory would not otherwise be writable by the dev user. + +> [!TIP] +> Older versions of podman do not support the `--userns keep-id:uid=1000,gid=1000` parameter. +> For these versions you will need to use the long form: +> `--uidmap 0:1:1000 --uidmap 1000:0:1 --uidmap 1001:1001:64536 --gidmap 0:1:1000 --gidmap 1000:0:1 --gidmap 1001:1001:64536` +> This does exactly the same, just in a more verbose format. + +> [!TIP] +> Running the dev container with docker instead of podman may work, but is not supported and you will need to setup a uid_map from your host user to the dev user inside the container. diff --git a/array_size.h b/array_size.h new file mode 100644 index 0000000..9916974 --- /dev/null +++ b/array_size.h @@ -0,0 +1 @@ +#define array_size(X) (sizeof(X) / sizeof(*X)) diff --git a/child_account.c b/child_account.c new file mode 100644 index 0000000..7839473 --- /dev/null +++ b/child_account.c @@ -0,0 +1,15 @@ +#include + +#include "child_account.h" + +size_t get_children(int *list, size_t size) +{ + FILE *file; + size_t len; + + file = fopen("/proc/thread-self/children", "r"); + len = 0; + for (int child; fscanf(file, "%d", &child) != EOF; ++len) if (len < size) list[len] = child; + fclose(file); + return len; +} diff --git a/child_account.h b/child_account.h new file mode 100644 index 0000000..839da0b --- /dev/null +++ b/child_account.h @@ -0,0 +1,3 @@ +#include + +size_t get_children(int *list, size_t size); diff --git a/clone_vfork.c b/clone_vfork.c new file mode 100644 index 0000000..a1c1a67 --- /dev/null +++ b/clone_vfork.c @@ -0,0 +1,30 @@ +#define _GNU_SOURCE +#include +#include +#include + +#include "clone_vfork.h" + +#define CLONE_STACK_SIZE 0x100000 + +pid_t clone_vfork(int (*func)(void *), void *arg, int flags) +{ + void *clone_stack; + pid_t pid; + + clone_stack = mmap( + NULL, + CLONE_STACK_SIZE, + PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, + -1, + 0 + ); + + if (!clone_stack) return -1; + + pid = clone(func, clone_stack + CLONE_STACK_SIZE, CLONE_VM | CLONE_VFORK | CLONE_CLEAR_SIGHAND | flags, arg); + munmap(clone_stack, CLONE_STACK_SIZE); + + return pid; +} diff --git a/clone_vfork.h b/clone_vfork.h new file mode 100644 index 0000000..cc38aea --- /dev/null +++ b/clone_vfork.h @@ -0,0 +1,3 @@ +#include + +pid_t clone_vfork(int (*func)(void *), void *arg, int flags); diff --git a/debug.h b/debug.h new file mode 100644 index 0000000..c9f9386 --- /dev/null +++ b/debug.h @@ -0,0 +1,8 @@ +#include +#include + +extern int debug; + +#define __STR(x) #x +#define _STR(x) __STR(x) +#define debug_printf(fmt, ...) if (debug) fprintf(stderr, "\033[2mdebug: " __FILE__ ":" _STR(__LINE__) " (%s) [%d]: " fmt "\033[0m\n", __func__, getpid(), ##__VA_ARGS__) diff --git a/do_syscall.c b/do_syscall.c new file mode 100644 index 0000000..5321b60 --- /dev/null +++ b/do_syscall.c @@ -0,0 +1,166 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include + +#include "syscall_lookup.h" +#include "zalloc.h" + +int debug = 0; + +static int resolve_syscall(const char *syscall_descrpitor) +{ + if (!syscall_descrpitor) + { + errno = EINVAL; + return -1; + } + if (*syscall_descrpitor == '#') return atoi(syscall_descrpitor + 1); + for (size_t i = 0; i < syscall_lookup_len; ++i) + { + if (strcmp(syscall_descrpitor, syscall_lookup[i]) == 0) return i; + } + + errno = ENOSYS; + return -1; +} + +enum input_type { INPUT_TYPE_INT = ':tni', INPUT_TYPE_STR = ':rts', INPUT_TYPE_HEX = ':xeh', INPUT_TYPE_BUF = ':fub' }; + +static uint8_t from_hex_lookup[] = { + ['0'] = 0x00, ['1'] = 0x01, ['2'] = 0x02, ['3'] = 0x03, ['4'] = 0x04, ['5'] = 0x05, ['6'] = 0x06, ['7'] = 0x07, ['8'] = 0x08, ['9'] = 0x09, + ['A'] = 0x0a, ['B'] = 0x0b, ['C'] = 0x0c, ['D'] = 0x0d, ['E'] = 0x0e, ['F'] = 0x0f, + ['a'] = 0x0a, ['b'] = 0x0b, ['c'] = 0x0c, ['d'] = 0x0d, ['e'] = 0x0e, ['f'] = 0x0f +}; + +static char to_hex_lookup[] = { + [0x00] = '0', [0x01] = '1', [0x02] = '2', [0x03] = '3', [0x04] = '4', [0x05] = '5', [0x06] = '6', [0x07] = '7', + [0x08] = '8', [0x09] = '9', [0x0a] = 'a', [0x0b] = 'b', [0x0c] = 'c', [0x0d] = 'd', [0x0e] = 'e', [0x0f] = 'f' +}; + +static void print_hex(const char *buf, size_t len) +{ + // "00 00 00 00 00 00 00 00 ........" + + char line_buf[36]; + size_t num_lines; + size_t line_len; + + num_lines = (len + 7) / 8; + for (size_t line = 0; line < num_lines; ++line) + { + memset(line_buf, ' ', 35); + line_buf[35] = '\0'; + line_len = (line == num_lines - 1) ? len % 8 : 8; + for (size_t i = 0; i < line_len; ++i) + { + line_buf[i * 3] = to_hex_lookup[buf[i] >> 4 & 0x0f]; + line_buf[(i * 3) + 1] = to_hex_lookup[buf[i] & 0x0f]; + line_buf[i + 27] = (buf[i] >= 0x20 && buf[i] <= 0x7e) ? buf[i] : '.'; + } + puts(line_buf); + buf += 8; + } +} + +static int setup_input(const char *input_descriptor, size_t *input) +{ + enum input_type input_type = *((int *) input_descriptor); + input_descriptor += sizeof(int); + + switch (input_type) + { + size_t len; + char *buf; + + case INPUT_TYPE_INT: + int value = atoi(input_descriptor); + *input = (size_t) value; + break; + case INPUT_TYPE_STR: + len = strlen(input_descriptor) + 1; + buf = zalloc(len); + memcpy(buf, input_descriptor, len); + *input = (size_t) buf; + break; + case INPUT_TYPE_HEX: + size_t hex_len = strlen(input_descriptor); + if (hex_len % 2) + { + errno = EINVAL; + return -1; + } + len = hex_len / 2; + buf = zalloc(len); + for (size_t i = 0; i < len; ++i) + { + buf[i] = (from_hex_lookup[(int) input_descriptor[i * 2]] << 4) | from_hex_lookup[(int) input_descriptor[(i * 2) + 1]]; + } + *input = (size_t) buf; + break; + case INPUT_TYPE_BUF: + len = (size_t) atoi(input_descriptor + sizeof(int)); + buf = zalloc(len); + *input = (size_t) buf; + break; + default: + errno = EINVAL; + return -1; + } + + return 0; +} + +static void cleanup_input(const char *input_descriptor, size_t input) +{ + enum input_type input_type = *((int *) input_descriptor); + input_descriptor += sizeof(int); + + switch (input_type) + { + case INPUT_TYPE_BUF: + enum input_type output_type = *((int *) input_descriptor); + if (output_type == INPUT_TYPE_STR) puts((char *) input); + else if (output_type == INPUT_TYPE_HEX) + { + size_t len = (size_t) atoi(input_descriptor + sizeof(int)); + print_hex((char *) input, len); + } + __attribute__ ((fallthrough)); + case INPUT_TYPE_STR: + case INPUT_TYPE_HEX: + free((void *) input); + break; + default: + break; + } +} + +int main(int argc, char **argv) +{ + size_t arg[8] = { 0 }; + int ret; + + int syscall_nr = resolve_syscall(argv[1]); + if (syscall_nr == -1) err(1, "resolve_syscall"); + arg[0] = (size_t) syscall_nr; + + for (int i = 2; i < argc; ++i) + { + int status = setup_input(argv[i], &arg[i-1]); + if (status == -1) err(1, "setup_input"); + } + + ret = syscall(arg[0], arg[1], arg[2], arg[3], arg[4], arg[5], arg[6], arg[7]); + + for (int i = 2; i < argc; ++i) cleanup_input(argv[i], arg[i-1]); + + printf("%d\n", ret); + + return 0; +} diff --git a/fd_account.c b/fd_account.c new file mode 100644 index 0000000..8f8996a --- /dev/null +++ b/fd_account.c @@ -0,0 +1,35 @@ +#define _GNU_SOURCE +#include +#include + +#include "fd_account.h" + +static int filter(const struct dirent *entry) +{ + return *entry->d_name != '.'; +} + +size_t get_fd_list(int *fd_list, size_t size) +{ + struct dirent **dirent_list; + int n; + + n = scandir("/proc/self/fd", &dirent_list, filter, versionsort); + if (n == -1) return -1; + + for (size_t i = 0; i < (size_t) n; ++i) + { + if (i < size) fd_list[i] = atoi(dirent_list[i]->d_name); + free(dirent_list[i]); + } + free(dirent_list); + + return n; +} + +int compare_fd_list(int *fd_list_a, size_t len_a, int *fd_list_b, size_t len_b) +{ + if (len_a != len_b) return 1; + for (size_t i = 0; i < len_a; ++i) if(fd_list_a[i] != fd_list_b[i]) return 1; + return 0; +} diff --git a/fd_account.h b/fd_account.h new file mode 100644 index 0000000..0bd0598 --- /dev/null +++ b/fd_account.h @@ -0,0 +1,4 @@ +#include + +size_t get_fd_list(int *fd_list, size_t size); +int compare_fd_list(int *fd_list_a, size_t len_a, int *fd_list_b, size_t len_b); diff --git a/gen_syscall_lookup b/gen_syscall_lookup new file mode 100755 index 0000000..81a34f4 --- /dev/null +++ b/gen_syscall_lookup @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +set -eufo pipefail + +cat << EOF +#include + +#include "array_size.h" +#include "syscall_lookup.h" + +const char *syscall_lookup[] = { +EOF + +echo '#include ' | $CC -E -dM - | grep '^#define SYS_' | cut -d ' ' -f 2 | while read -r syscall; do + printf '\t[%s] = "%s",\n' $syscall ${syscall#SYS_} +done + +cat << EOF +}; + +const size_t syscall_lookup_len = array_size(syscall_lookup); +EOF diff --git a/integration_test b/integration_test new file mode 100755 index 0000000..b33303c --- /dev/null +++ b/integration_test @@ -0,0 +1,166 @@ +#!/usr/bin/env bash + +set -eufo pipefail + +if [ "${1-}" != run_tests ]; then + export FAKE_XATTR_CHECK_MEM=1 + exec unshare --map-root-user --map-auto --mount --propagation unchanged ./fake_xattr "$0" run_tests +fi + +echo "running integration tests:" + +progress_animation() { + label="${1-}" + max="${2-}" + + reset_cursor() { + printf '\033[?25h' >&2 + } + + if [ -t 2 ]; then + trap reset_cursor EXIT + printf '\033[?25l\033[s\033[K' >&2 + printf '\033[u\033[K%s ' "$label" >&2 + cntr=0 + symbols=(▱▱▱ ▰▱▱ ▱▰▱ ▱▱▰) + num_symbols="${#symbols[@]}" + while read -r line; do + [ -t 1 ] || printf '%s\n' "$line" + cntr="$(( cntr + 1 ))" + if [ -n "$max" ]; then + percent="$(( ( cntr * 100 ) / max ))" + status="$(printf '%2d%%' "$percent")" + else + status="${symbols[$(( cntr % num_symbols ))]}" + fi + printf '\033[u\033[K%s \033[94m%s\033[0m' "$label" "$status" >&2 + done + printf '\033[u\033[K\033[?25h' >&2 + else + cat + fi +} + +shuf_words() { + LC_ALL=C grep -xE '[[:alnum:]]+' < /usr/share/dict/words | shuf "$@" +} + +test_run() ( + exec 3>&1 + exec 1>&2 + + cd "$1" + mkdir files expected_xattr + + shuf_words -n 32 > file_names + + for i in {1..32}; do + attr_name="$(shuf_words -n "$(shuf -n 1 -i 1-8)" | tr '\n' '.' | sed 's/.$//' | head -c 200)" + echo "$attr_name" + done > attr_names + + # by pigeon hole principal we can be sure to also test overwriting of at least one file / attr + for i in {1..64}; do + file_name=$(shuf -n 1 file_names) + touch "files/$file_name" + + for i in {1..64}; do + attr_name="$(shuf -n 1 attr_names)" + + data_len="$(shuf -n 1 -i 0-64)" + data="0x$(head -c "$data_len" /dev/urandom | od -An -t x1 | tr -d ' \n' || true)" + + setfattr -n "$attr_name" -v "$data" "files/$file_name" + + touch "expected_xattr/$file_name" + { sed "/^$attr_name=/d" < "expected_xattr/$file_name"; echo "$attr_name=$data"; } | sort > "expected_xattr/_$file_name" + mv "expected_xattr/_$file_name" "expected_xattr/$file_name" + + diff -Nau --color "expected_xattr/$file_name" <(getfattr -d -e hex -m - "files/$file_name" | sed '/^#/d;/^$/d' | sort) + done + + cut -d = -f 1 < "expected_xattr/$file_name" | shuf -n "$(shuf -n 1 -i 0-32)" | while read attr_name; do + setfattr -x "$attr_name" "files/$file_name" + + sed "/^$attr_name=/d" < "expected_xattr/$file_name" | sort > "expected_xattr/_$file_name" + mv "expected_xattr/_$file_name" "expected_xattr/$file_name" + + diff -Nau --color "expected_xattr/$file_name" <(getfattr -d -e hex -m - "files/$file_name" | sed '/^#/d;/^$/d' | sort) + done + + echo >&3 + done +) + +if [ ! -e .tmp ]; then + echo 'ERROR: .tmp does not exist. Please run `make .tmp` first.' >&2 + exit 1 +fi + +rm -rf .tmp/integration_test +mkdir -p .tmp/integration_test/mnt_a .tmp/integration_test/mnt_b + +mount -t tmpfs none .tmp/integration_test/mnt_a +mount -t tmpfs none .tmp/integration_test/mnt_b + +mnt_a_dev="$(stat -c '%Hd:%Ld' .tmp/integration_test/mnt_a)" + +test_run .tmp/integration_test/mnt_a | progress_animation '[1/4] random xattr' 64 > /dev/null +printf '[1/4] random xattr: \033[92mpassed\033[0m\n' + +test_run .tmp/integration_test/mnt_b | progress_animation '[2/4] parallel mnt' 64 > /dev/null +printf '[2/4] parallel mnt: \033[92mpassed\033[0m\n' + +umount .tmp/integration_test/mnt_a +mount -t tmpfs none .tmp/integration_test/mnt_a + +if [ "$(stat -c '%Hd:%Ld' .tmp/integration_test/mnt_a)" != "$mnt_a_dev" ]; then + echo "tmpfs did not reuse device id as expected, test meaningless, abort!" >&2 + exit 1 +fi + +test_run .tmp/integration_test/mnt_a | progress_animation '[3/4] reused dev id' 64 > /dev/null +printf '[3/4] reused dev id: \033[92mpassed\033[0m\n' + +umount .tmp/integration_test/mnt_a +umount .tmp/integration_test/mnt_b + +mount -t tmpfs none .tmp/integration_test/mnt_a + +if ! unshare --mount --propagation unchanged env container=lxc debootstrap --variant=minbase --include="iputils-ping selinux-basics selinux-policy-default" stable .tmp/integration_test/mnt_a | progress_animation '[4/4] selinux: creating debian chroot for test' > /dev/null; then + echo + cat .tmp/integration_test/mnt_a/debootstrap/debootstrap.log >&2 + exit 1 +fi + +if ! getcap .tmp/integration_test/mnt_a/usr/bin/ping | grep cap_net_raw > /dev/null; then + echo + echo "file capability sanity check failed" >&2 + exit 1 +fi + +mount --rbind /proc .tmp/integration_test/mnt_a/proc +mount --rbind /sys .tmp/integration_test/mnt_a/sys +mount --rbind /dev .tmp/integration_test/mnt_a/dev + +mkdir .tmp/integration_test/mnt_a/loop +mount --bind .tmp/integration_test/mnt_a .tmp/integration_test/mnt_a/loop + +chroot .tmp/integration_test/mnt_a setfiles -v -r /loop /etc/selinux/default/contexts/files/file_contexts /loop | progress_animation '[4/4] selinux: testing selinux file labelling' > /dev/null + +umount .tmp/integration_test/mnt_a/loop + +umount -l .tmp/integration_test/mnt_a/proc +umount -l .tmp/integration_test/mnt_a/sys +umount -l .tmp/integration_test/mnt_a/dev + +if [ "$(getfattr -d -m - .tmp/integration_test/mnt_a/home 2> /dev/null | sed '/^#/d;/^$/d')" != 'security.selinux="system_u:object_r:home_root_t:s0"' ]; then + echo + echo "selinux labeling sanity check failed" >&2 + exit 1 +fi + +printf '[4/4] selinux: \033[92mpassed\033[0m\n' + +umount .tmp/integration_test/mnt_a +rmdir .tmp/integration_test/mnt_a .tmp/integration_test/mnt_b .tmp/integration_test diff --git a/main.c b/main.c new file mode 100644 index 0000000..b3a9f8f --- /dev/null +++ b/main.c @@ -0,0 +1,301 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "array_size.h" +#include "debug.h" +#include "mem_account.h" +#include "path.h" +#include "seccomp_unotify.h" +#include "xattr_db.h" +#include "zalloc.h" + +static int get_file_id(xattr_db_file_id *file_id, struct seccomp_data *data, int proc_dir_fd, int proc_mem_fd) +{ + int dir_fd = AT_FDCWD; + int statx_flags; + path_t path; + ssize_t path_len; + path_t readlink_buf; + struct statx file_statx; + + dir_fd = AT_FDCWD; + statx_flags = 0; + + if (data->nr == SYS_fsetxattr || data->nr == SYS_fgetxattr || data->nr == SYS_flistxattr || data->nr == SYS_fremovexattr) + { + dir_fd = proc_dir_fd; + path_len = snprintf(path, sizeof(path), "fd/%d", (int) data->args[0]); + if (path_len < 0 || path_len >= (ssize_t) sizeof(path)) return -1; + + if (debug) + { + memset(readlink_buf, 0, sizeof(readlink_buf)); + readlinkat(dir_fd, path, readlink_buf, sizeof(readlink_buf) - 1); + debug_printf("path=%s", readlink_buf); + } + } + else + { + if (pread(proc_mem_fd, path, sizeof(path), data->args[0]) == -1) return -1; + if (strnlen(path, sizeof(path)) >= sizeof(path)) + { + errno = ENAMETOOLONG; + return -1; + } + + debug_printf("path=%s", path); + } + + if (data->nr == SYS_lsetxattr) statx_flags = AT_SYMLINK_NOFOLLOW; + + if(statx(dir_fd, path, statx_flags, STATX_BASIC_STATS | STATX_BTIME, &file_statx) == -1) return -1; + + file_id->ino = file_statx.stx_ino; + file_id->dev.major = file_statx.stx_dev_major; + file_id->dev.minor = file_statx.stx_dev_minor; + file_id->btime.sec = file_statx.stx_btime.tv_sec; + file_id->btime.nsec = file_statx.stx_btime.tv_nsec; + + return 0; +} + +static int handle_setxattr(void *ctx, struct seccomp_data *data, int proc_dir_fd, int proc_mem_fd) +{ + xattr_db_file_id file_id; + xattr_db_attr_name attr_name; + char *data_buf; + size_t data_len; + ssize_t read_len; + int xattr_flags; + int ret; + + if (get_file_id(&file_id, data, proc_dir_fd, proc_mem_fd) == -1) return -1; + + if (pread(proc_mem_fd, attr_name, sizeof(attr_name), data->args[1]) == -1) return -1; + if (strnlen(attr_name, sizeof(attr_name)) >= sizeof(attr_name)) + { + errno = ERANGE; + return -1; + } + + debug_printf("name=%s", attr_name); + + data_len = data->args[3]; + if (data_len > XATTR_SIZE_MAX) + { + errno = ERANGE; + return -1; + } + + data_buf = zalloc(data_len); + + read_len = pread(proc_mem_fd, data_buf, data_len, data->args[2]); + if ((size_t) read_len != data_len) + { + if (read_len >= 0) errno = EFAULT; + free(data_buf); + return -1; + } + + xattr_flags = data->args[4]; + ret = xattr_db_set((xattr_db_ctx *) ctx, file_id, attr_name, data_buf, data_len, xattr_flags & XATTR_CREATE, xattr_flags & XATTR_REPLACE); + + free(data_buf); + + return ret; +} + +static int handle_getxattr(void *ctx, struct seccomp_data *data, int proc_dir_fd, int proc_mem_fd) +{ + xattr_db_file_id file_id; + xattr_db_attr_name attr_name; + char *data_buf; + size_t data_size; + ssize_t len; + ssize_t write_len; + + if (get_file_id(&file_id, data, proc_dir_fd, proc_mem_fd) == -1) return -1; + + if (pread(proc_mem_fd, attr_name, sizeof(attr_name), data->args[1]) == -1) return -1; + if (strnlen(attr_name, sizeof(attr_name)) >= sizeof(attr_name)) + { + errno = ERANGE; + return -1; + } + + debug_printf("name=%s", attr_name); + + data_size = data->args[3]; + if (data_size > XATTR_SIZE_MAX) data_size = XATTR_SIZE_MAX; + + data_buf = zalloc(data_size); + + len = xattr_db_get((xattr_db_ctx *) ctx, file_id, attr_name, data_buf, data_size); + if (len == -1 || len > (ssize_t) data_size) + { + if (data_size != 0) len = -1; + goto _return; + } + + write_len = pwrite(proc_mem_fd, data_buf, len, data->args[2]); + if (write_len != len) + { + if (len >= 0) errno = EFAULT; + len = -1; + } + + _return: + free(data_buf); + return len; +} + +static int handle_listxattr(void *ctx, struct seccomp_data *data, int proc_dir_fd, int proc_mem_fd) +{ + xattr_db_file_id file_id; + char *list_buf; + size_t list_size; + ssize_t len; + ssize_t write_len; + + if (get_file_id(&file_id, data, proc_dir_fd, proc_mem_fd) == -1) return -1; + + list_size = data->args[2]; + if (list_size > XATTR_LIST_MAX) list_size = XATTR_LIST_MAX; + + list_buf = zalloc(list_size); + + len = xattr_db_list((xattr_db_ctx *) ctx, file_id, list_buf, list_size); + if (len == -1 || len > (ssize_t) list_size) + { + if (list_size != 0) len = -1; + goto _return; + } + + write_len = pwrite(proc_mem_fd, list_buf, len, data->args[1]); + if (write_len != len) + { + if (len >= 0) errno = EFAULT; + len = -1; + } + + _return: + free(list_buf); + return len; +} + +static int handle_removexattr(void *ctx, struct seccomp_data *data, int proc_dir_fd, int proc_mem_fd) +{ + xattr_db_file_id file_id; + xattr_db_attr_name attr_name; + + if (get_file_id(&file_id, data, proc_dir_fd, proc_mem_fd) == -1) return -1; + + if (pread(proc_mem_fd, attr_name, sizeof(attr_name), data->args[1]) == -1) return -1; + if (strnlen(attr_name, sizeof(attr_name)) >= sizeof(attr_name)) + { + errno = ERANGE; + return -1; + } + + debug_printf("name=%s", attr_name); + + return xattr_db_remove((xattr_db_ctx *) ctx, file_id, attr_name); +} + +static seccomp_unotify_handler xattr_syscall_handlers[] = { + [SYS_setxattr] = handle_setxattr, + [SYS_lsetxattr] = handle_setxattr, + [SYS_fsetxattr] = handle_setxattr, + [SYS_getxattr] = handle_getxattr, + [SYS_lgetxattr] = handle_getxattr, + [SYS_fgetxattr] = handle_getxattr, + [SYS_listxattr] = handle_listxattr, + [SYS_llistxattr] = handle_listxattr, + [SYS_flistxattr] = handle_listxattr, + [SYS_removexattr] = handle_removexattr, + [SYS_lremovexattr] = handle_removexattr, + [SYS_fremovexattr] = handle_removexattr, +}; + +static void on_sigchld(int) +{ + pid_t pid; + + debug_printf("SIGCHLD recieved\n"); + + while ((pid = waitpid(-1, NULL, WNOHANG)) > 0) + { + debug_printf("reaped zombie process %d\n", pid); + } +} + +int debug = 0; + +int main(int, char **argv) +{ + char *debug_env; + char *check_mem_env; + struct sigaction sa; + struct clone_args clone_args; + pid_t pid; + xattr_db_ctx *db; + int wstatus; + int ret; + + debug_env = getenv("FAKE_XATTR_DEBUG"); + if (debug_env && *debug_env == '1') debug = 1; + + debug_printf("%s debug mode", argv[0]); + + sa = (struct sigaction) { + .sa_handler = on_sigchld, + .sa_flags = SA_NOCLDSTOP + }; + + sigaction(SIGCHLD, &sa, NULL); + + prctl(PR_SET_CHILD_SUBREAPER, 1); + + clone_args = (struct clone_args) { + .flags = CLONE_CLEAR_SIGHAND + }; + + pid = syscall(SYS_clone3, &clone_args, sizeof(clone_args)); + if (pid == -1) err(1, "clone"); + + if (pid == 0) + { + db = xattr_db_init(); + ret = seccomp_unotify_vfork_exec(db, xattr_syscall_handlers, array_size(xattr_syscall_handlers), argv[1], argv + 1, environ); + debug_printf(); + xattr_db_free(db); + + debug_printf("max heap memory usage: %lu bytes", max_mem_account); + if (mem_account) + { + debug_printf("memory leak detected (%lu bytes)", mem_account); + check_mem_env = getenv("FAKE_XATTR_CHECK_MEM"); + if (check_mem_env && *check_mem_env == '1') raise(SIGSEGV); + } + + return ret; + } + else + { + while (waitpid(pid, &wstatus, __WCLONE) == -1) if (errno != EINTR) err(1, "waitpid"); + return WIFEXITED(wstatus) ? WEXITSTATUS(wstatus) : -1; + } +} diff --git a/mem_account.c b/mem_account.c new file mode 100644 index 0000000..5ea103d --- /dev/null +++ b/mem_account.c @@ -0,0 +1,111 @@ +#include + +extern void * __libc_malloc(size_t); +extern void * __libc_calloc(size_t, size_t); +extern void * __libc_realloc(void *, size_t); +extern void * __libc_reallocarray(void *, size_t, size_t); +extern void * __libc_memalign(size_t, size_t); +extern void * __libc_valloc(size_t); +extern void * __libc_pvalloc(size_t); +extern void * __libc_free(void *); +extern size_t malloc_usable_size (void *); + +size_t mem_account = 0; +size_t max_mem_account = 0; + +void * malloc(size_t size) +{ + void *ptr; + size_t usable_size; + + ptr = __libc_malloc(size); + usable_size = malloc_usable_size(ptr); + mem_account += usable_size; + if (mem_account > max_mem_account) max_mem_account = mem_account; + return ptr; +} + +void * calloc(size_t num, size_t size) +{ + void *ptr; + size_t usable_size; + + ptr = __libc_calloc(num, size); + usable_size = malloc_usable_size(ptr); + mem_account += usable_size; + if (mem_account > max_mem_account) max_mem_account = mem_account; + return ptr; +} + +void * realloc(void *old_ptr, size_t size) +{ + size_t old_usable_size; + void *new_ptr; + size_t new_usable_size; + + old_usable_size = malloc_usable_size(old_ptr); + new_ptr = __libc_realloc(old_ptr, size); + new_usable_size = malloc_usable_size(new_ptr); + mem_account += new_usable_size - old_usable_size; + if (mem_account > max_mem_account) max_mem_account = mem_account; + return new_ptr; +} + +void * reallocarray(void *old_ptr, size_t num, size_t size) +{ + size_t old_usable_size; + void *new_ptr; + size_t new_usable_size; + + old_usable_size = malloc_usable_size(old_ptr); + new_ptr = __libc_reallocarray(old_ptr, num, size); + new_usable_size = malloc_usable_size(new_ptr); + mem_account += new_usable_size - old_usable_size; + if (mem_account > max_mem_account) max_mem_account = mem_account; + return new_ptr; +} + +void * memalign(size_t alignment, size_t size) +{ + void *ptr; + size_t usable_size; + + ptr = __libc_memalign(alignment, size); + usable_size = malloc_usable_size(ptr); + mem_account += usable_size; + if (mem_account > max_mem_account) max_mem_account = mem_account; + return ptr; +} + +void * valloc(size_t size) +{ + void *ptr; + size_t usable_size; + + ptr = __libc_valloc(size); + usable_size = malloc_usable_size(ptr); + mem_account += usable_size; + if (mem_account > max_mem_account) max_mem_account = mem_account; + return ptr; +} + +void * pvalloc(size_t size) +{ + void *ptr; + size_t usable_size; + + ptr = __libc_pvalloc(size); + usable_size = malloc_usable_size(ptr); + mem_account += usable_size; + if (mem_account > max_mem_account) max_mem_account = mem_account; + return ptr; +} + +void free(void *ptr) +{ + size_t usable_size; + + usable_size = malloc_usable_size(ptr); + __libc_free(ptr); + mem_account -= usable_size; +} diff --git a/mem_account.h b/mem_account.h new file mode 100644 index 0000000..b9f1ec0 --- /dev/null +++ b/mem_account.h @@ -0,0 +1,4 @@ +#include + +extern size_t mem_account; +extern size_t max_mem_account; diff --git a/path.h b/path.h new file mode 100644 index 0000000..8d2e0c3 --- /dev/null +++ b/path.h @@ -0,0 +1,3 @@ +#include + +typedef char path_t[PATH_MAX]; diff --git a/seccomp_unotify.c b/seccomp_unotify.c new file mode 100644 index 0000000..01ca39f --- /dev/null +++ b/seccomp_unotify.c @@ -0,0 +1,462 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "clone_vfork.h" +#include "debug.h" +#include "path.h" +#include "seccomp_unotify.h" +#include "syscall_lookup.h" + +#define _return(x) { _return_code = x; goto _on_return; } + +int enter_proc_mntns(int target_proc_dir_fd, int self_proc_dir_fd) +{ + int _return_code; + path_t readlink_buf; + int self_proc_mntns_fd; + int target_proc_mntns_fd; + struct stat self_proc_mntns_stat; + struct stat target_proc_mntns_stat; + + _return_code = 0; + + if (debug) + { + memset(readlink_buf, 0, sizeof(readlink_buf)); + readlinkat(self_proc_dir_fd, "ns/mnt", readlink_buf, sizeof(readlink_buf) - 1); + debug_printf("self mount namespace: %s", readlink_buf); + + memset(readlink_buf, 0, sizeof(readlink_buf)); + readlinkat(target_proc_dir_fd, "ns/mnt", readlink_buf, sizeof(readlink_buf) - 1); + debug_printf("target mount namespace: %s", readlink_buf); + } + + self_proc_mntns_fd = -1; + target_proc_mntns_fd = -1; + + self_proc_mntns_fd = openat(self_proc_dir_fd, "ns/mnt", O_RDONLY); + if (self_proc_mntns_fd == -1) _return(-1); + + target_proc_mntns_fd = openat(target_proc_dir_fd, "ns/mnt", O_RDONLY); + if (target_proc_mntns_fd == -1) _return(-1); + + if (fstat(self_proc_mntns_fd, &self_proc_mntns_stat) == -1) _return(-1); + if (fstat(target_proc_mntns_fd, &target_proc_mntns_stat) == -1) _return(-1); + + if (target_proc_mntns_stat.st_dev != self_proc_mntns_stat.st_dev || target_proc_mntns_stat.st_ino != self_proc_mntns_stat.st_ino) + { + debug_printf("setns %s", readlink_buf); + _return_code = setns(target_proc_mntns_fd, CLONE_NEWNS); + } + + _on_return: + if (self_proc_mntns_fd != -1) close(self_proc_mntns_fd); + if (target_proc_mntns_fd != -1) close(target_proc_mntns_fd); + return _return_code; +} + +static int fchroot(int fd) +{ + if (fchdir(fd) == -1) return -1; + return chroot("."); +} + +static int enter_proc_root(int target_proc_dir_fd, int self_proc_dir_fd) +{ + int _return_code; + path_t readlink_buf; + int self_proc_root_fd; + int target_proc_root_fd; + struct stat self_proc_root_stat; + struct stat target_proc_root_stat; + + _return_code = 0; + + if (debug) + { + memset(readlink_buf, 0, sizeof(readlink_buf)); + readlinkat(self_proc_dir_fd, "root", readlink_buf, sizeof(readlink_buf) - 1); + debug_printf("self root directory: %s", readlink_buf); + + memset(readlink_buf, 0, sizeof(readlink_buf)); + readlinkat(target_proc_dir_fd, "root", readlink_buf, sizeof(readlink_buf) - 1); + debug_printf("target root directory: %s", readlink_buf); + } + + self_proc_root_fd = -1; + target_proc_root_fd = -1; + + self_proc_root_fd = openat(self_proc_dir_fd, "root", O_RDONLY); + if (self_proc_root_fd == -1) _return(-1); + + target_proc_root_fd = openat(target_proc_dir_fd, "root", O_RDONLY); + if (target_proc_root_fd == -1) _return(-1); + + if (fstat(self_proc_root_fd, &self_proc_root_stat) == -1) _return(-1); + if (fstat(target_proc_root_fd, &target_proc_root_stat) == -1) _return(-1); + + if (target_proc_root_stat.st_dev != self_proc_root_stat.st_dev || target_proc_root_stat.st_ino != self_proc_root_stat.st_ino) + { + debug_printf("chroot %s", readlink_buf); + _return_code = fchroot(target_proc_root_fd); + } + + _on_return: + if (self_proc_root_fd != -1) close(self_proc_root_fd); + if (target_proc_root_fd != -1) close(target_proc_root_fd); + return _return_code; +} + +static int enter_proc_cwd(int target_proc_dir_fd, int self_proc_dir_fd) +{ + int _return_code; + path_t readlink_buf; + int self_proc_cwd_fd; + int target_proc_cwd_fd; + struct stat self_proc_cwd_stat; + struct stat target_proc_cwd_stat; + + _return_code = 0; + + if (debug) + { + memset(readlink_buf, 0, sizeof(readlink_buf)); + readlinkat(self_proc_dir_fd, "cwd", readlink_buf, sizeof(readlink_buf) - 1); + debug_printf("self working directory: %s", readlink_buf); + + memset(readlink_buf, 0, sizeof(readlink_buf)); + readlinkat(target_proc_dir_fd, "cwd", readlink_buf, sizeof(readlink_buf) - 1); + debug_printf("target working directory: %s", readlink_buf); + } + + self_proc_cwd_fd = -1; + target_proc_cwd_fd = -1; + + self_proc_cwd_fd = openat(self_proc_dir_fd, "cwd", O_RDONLY); + if (self_proc_cwd_fd == -1) _return(-1); + + target_proc_cwd_fd = openat(target_proc_dir_fd, "cwd", O_RDONLY); + if (target_proc_cwd_fd == -1) _return(-1); + + if (fstat(self_proc_cwd_fd, &self_proc_cwd_stat) == -1) _return(-1); + if (fstat(target_proc_cwd_fd, &target_proc_cwd_stat) == -1) _return(-1); + + if (target_proc_cwd_stat.st_dev != self_proc_cwd_stat.st_dev || target_proc_cwd_stat.st_ino != self_proc_cwd_stat.st_ino) + { + debug_printf("chdir %s", readlink_buf); + _return_code = fchdir(target_proc_cwd_fd); + } + + _on_return: + if (self_proc_cwd_fd != -1) close(self_proc_cwd_fd); + if (target_proc_cwd_fd != -1) close(target_proc_cwd_fd); + return _return_code; +} + +struct handle_syscall_vfork_arg { + volatile int *ret; + void *ctx; + seccomp_unotify_handler *syscall_handlers; + int proc_dir_fd; + struct seccomp_data *data; +}; + +static int handle_syscall_vfork(void *_arg) +{ + struct handle_syscall_vfork_arg *arg; + + volatile int *ret; + void *ctx; + seccomp_unotify_handler *syscall_handlers; + int proc_dir_fd; + struct seccomp_data *data; + + int _return_code; + int proc_self_dir_fd; + int proc_target_mem_fd; + + debug_printf("vforked"); + + arg = _arg; + ret = arg->ret; + ctx = arg->ctx; + syscall_handlers = arg->syscall_handlers; + proc_dir_fd = arg->proc_dir_fd; + data = arg->data; + + _return_code = 0; + proc_self_dir_fd = -1; + proc_target_mem_fd = -1; + + proc_self_dir_fd = open("/proc/self", O_PATH | O_DIRECTORY); + if (proc_self_dir_fd == -1) _return(-1); + + if ( + enter_proc_mntns(proc_dir_fd, proc_self_dir_fd) == -1 || + enter_proc_root(proc_dir_fd, proc_self_dir_fd) == -1 || + enter_proc_cwd(proc_dir_fd, proc_self_dir_fd) == -1 + ) _return(-1); + + proc_target_mem_fd = openat(proc_dir_fd, "mem", O_RDWR); + if (proc_target_mem_fd == -1) _return(-1); + + if (syscall_handlers[data->nr]) + { + _return_code = syscall_handlers[data->nr](ctx, data, proc_dir_fd, proc_target_mem_fd); + } else + { + errno = ENOSYS; + _return_code = -1; + } + + _on_return: + if (proc_self_dir_fd != -1) close(proc_self_dir_fd); + if (proc_target_mem_fd != -1) close(proc_target_mem_fd); + *ret = _return_code; + return 0; +} + +static int handle_syscall(void *ctx, seccomp_unotify_handler *syscall_handlers, int proc_dir_fd, struct seccomp_data *data) +{ + volatile int status; + int wstatus; + pid_t pid; + + status = 0; + pid = clone_vfork(handle_syscall_vfork, & (struct handle_syscall_vfork_arg) { &status, ctx, syscall_handlers, proc_dir_fd, data }, 0); + if (pid == -1) + { + warn("clone"); + return -1; + } + + if (waitpid(pid, &wstatus, __WCLONE) == -1) + { + warn("waitpid"); + return -1; + } + if (!WIFEXITED(wstatus) || WEXITSTATUS(wstatus) != 0) return -1; + + return status; +} + +static int supervisor(pid_t target_pid, int seccomp_notify_fd, void *ctx, seccomp_unotify_handler *syscall_handlers) +{ + struct seccomp_notif_sizes sizes; + struct seccomp_notif *req; + struct seccomp_notif_resp *resp; + siginfo_t siginfo; + struct pollfd poll_seccomp_notify_fd; + path_t proc_dir_path; + ssize_t proc_dir_path_len; + int proc_dir_fd; + int status; + int ret; + + ret = -1; + + if (syscall(SYS_seccomp, SECCOMP_GET_NOTIF_SIZES, 0, &sizes) == -1) err(1, "seccomp(SECCOMP_GET_NOTIF_SIZES)"); + if (sizes.seccomp_notif < sizeof(struct seccomp_notif)) sizes.seccomp_notif = sizeof(struct seccomp_notif); + if (sizes.seccomp_notif_resp < sizeof(struct seccomp_notif_resp)) sizes.seccomp_notif_resp = sizeof(struct seccomp_notif_resp); + + req = alloca(sizes.seccomp_notif); + resp = alloca(sizes.seccomp_notif_resp); + + debug_printf("listening for seccomp notify events"); + + poll_seccomp_notify_fd = (struct pollfd) { + .fd = seccomp_notify_fd, + .events = POLLIN + }; + + while (1) + { + if (target_pid) + { + waitid(P_PID, target_pid, &siginfo, WEXITED | WNOHANG); + if (siginfo.si_pid == target_pid) + { + debug_printf("target %d exited with status %d", siginfo.si_pid, siginfo.si_status); + + ret = siginfo.si_status; + target_pid = 0; + } + } + + if (poll(&poll_seccomp_notify_fd, 1, -1) == -1) + { + if (errno == EINTR) continue; + err(1, "poll"); + } + + debug_printf("seccomp_notify_fd polled (0x%04x)", poll_seccomp_notify_fd.revents); + + if (poll_seccomp_notify_fd.revents & POLLHUP) + { + debug_printf("seccomp_notify_fd POLLHUP event recieved"); + break; + } + if (!(poll_seccomp_notify_fd.revents & POLLIN)) continue; + + memset(req, 0, sizes.seccomp_notif); + if (ioctl(seccomp_notify_fd, SECCOMP_IOCTL_NOTIF_RECV, req) == -1) + { + if (errno == EINTR) continue; + err(1, "ioctl(SECCOMP_IOCTL_NOTIF_RECV)"); + } + + debug_printf("event recieved (pid=%d, syscall=%s@%d, id=%llx, flags=%x)", req->pid, syscall_lookup[req->data.nr], req->data.nr, req->id, req->flags); + + proc_dir_path_len = snprintf(proc_dir_path, sizeof(proc_dir_path), "/proc/%u", req->pid); + if (proc_dir_path_len < 0 || proc_dir_path_len >= (ssize_t) sizeof(proc_dir_path)) err(1, "snprintf"); + + proc_dir_fd = open(proc_dir_path, O_PATH | O_DIRECTORY); + if (proc_dir_fd == -1) continue; + + if (ioctl(seccomp_notify_fd, SECCOMP_IOCTL_NOTIF_ID_VALID, &req->id) == -1) + { + close(proc_dir_fd); + continue; + } + + status = handle_syscall(ctx, syscall_handlers, proc_dir_fd, &req->data); + close(proc_dir_fd); + + memset(resp, 0, sizes.seccomp_notif_resp); + resp->id = req->id; + resp->val = status; + resp->error = (status == -1) ? -errno : 0; + if (ioctl(seccomp_notify_fd, SECCOMP_IOCTL_NOTIF_SEND, resp) == -1) + { + if (errno == ENOENT) continue; + err(1, "ioctl(SECCOMP_IOCTL_NOTIF_SEND)"); + } + + debug_printf("response send (id=%llx, value=%lld, error=%d)", resp->id, resp->val, resp->error); + } + + return ret; +} + +struct target_vfork_arg { + volatile int *seccomp_notify_fd; + seccomp_unotify_handler *syscall_handlers; + size_t len_syscall_handlers; + char *file; + char **argv; + char **envp; +}; + +static int target_vfork(void *_arg) +{ + struct target_vfork_arg *arg; + + volatile int *seccomp_notify_fd; + seccomp_unotify_handler *syscall_handlers; + size_t len_syscall_handlers; + char *file; + char **argv; + char **envp; + + int len; + int *filtered_syscalls; + struct sock_filter *bpf_filter; + struct sock_fprog bpf_prog; + + debug_printf("vforked"); + + arg = _arg; + seccomp_notify_fd = arg->seccomp_notify_fd; + syscall_handlers = arg->syscall_handlers; + len_syscall_handlers = arg->len_syscall_handlers; + file = arg->file; + argv = arg->argv; + envp = arg->envp; + + len = 0; + for (size_t i = 0; i < len_syscall_handlers; ++i) if (syscall_handlers[i]) ++len; + + filtered_syscalls = alloca(len * sizeof(int)); + + for (size_t i = 0, j = 0; i < len_syscall_handlers; ++i) + { + if (syscall_handlers[i]) + { + filtered_syscalls[j] = i; + ++j; + } + } + + bpf_filter = alloca(sizeof(struct sock_filter[len + 3])); + + bpf_filter[0] = (struct sock_filter) BPF_STMT(BPF_LD | BPF_W | BPF_ABS, (offsetof(struct seccomp_data, nr))); + bpf_filter[len+1] = (struct sock_filter) BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW); + bpf_filter[len+2] = (struct sock_filter) BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_USER_NOTIF); + + for (int i = 0; i < len; ++i) + { + bpf_filter[i+1] = (struct sock_filter) BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, filtered_syscalls[i], len-i, 0); + } + + bpf_prog = (struct sock_fprog) { + .len = len + 3, + .filter = bpf_filter + }; + + *seccomp_notify_fd = syscall(SYS_seccomp, SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, &bpf_prog); + if (*seccomp_notify_fd == -1) err(1, "seccomp(SECCOMP_SET_MODE_FILTER)"); + + if(unshare(CLONE_FILES) == -1) err(1, "unshare"); + close(*seccomp_notify_fd); + + if (execvpe(file, argv, envp) == -1) err(1, "execvp"); + + return 0; +} + +static void on_sigchld(int) +{ + debug_printf("SIGCHLD recieved\n"); +} + +int seccomp_unotify_vfork_exec(void *ctx, seccomp_unotify_handler *syscall_handlers, size_t len_syscall_handlers, char *file, char **argv, char **envp) +{ + struct sigaction sa; + struct sigaction old_sa; + volatile int seccomp_notify_fd; + pid_t pid; + int ret; + + sa = (struct sigaction) { + .sa_handler = on_sigchld, + .sa_flags = SA_NOCLDSTOP + }; + + sigaction(SIGCHLD, &sa, &old_sa); + + seccomp_notify_fd = -1; + pid = clone_vfork(target_vfork, & (struct target_vfork_arg) { &seccomp_notify_fd, syscall_handlers, len_syscall_handlers, file, argv, envp }, CLONE_FILES | SIGCHLD); + if (pid == -1) err(1, "clone"); + + if (seccomp_notify_fd == -1) err(1, "target_vfork"); + ret = supervisor(pid, seccomp_notify_fd, ctx, syscall_handlers); + close(seccomp_notify_fd); + + sigaction(SIGCHLD, &old_sa, NULL); + + return ret; +} diff --git a/seccomp_unotify.h b/seccomp_unotify.h new file mode 100644 index 0000000..6f3f5d7 --- /dev/null +++ b/seccomp_unotify.h @@ -0,0 +1,6 @@ +#include +#include + +typedef int (*seccomp_unotify_handler)(void *ctx, struct seccomp_data *data, int proc_dir_fd, int proc_mem_fd); + +int seccomp_unotify_vfork_exec(void *ctx, seccomp_unotify_handler *syscall_handlers, size_t len_syscall_handlers, char *file, char **argv, char **envp); diff --git a/syscall_lookup.h b/syscall_lookup.h new file mode 100644 index 0000000..85b86d7 --- /dev/null +++ b/syscall_lookup.h @@ -0,0 +1,4 @@ +#include + +extern const char *syscall_lookup[]; +extern const size_t syscall_lookup_len; diff --git a/syscall_table_size.h b/syscall_table_size.h new file mode 100644 index 0000000..ed6bdd9 --- /dev/null +++ b/syscall_table_size.h @@ -0,0 +1 @@ +#define SYSCALL_TABLE_SIZE 512 diff --git a/test.h b/test.h new file mode 100644 index 0000000..2bc9260 --- /dev/null +++ b/test.h @@ -0,0 +1,138 @@ +#include +#include +#include +#include +#include +#include + +#include "array_size.h" +#include "child_account.h" +#include "debug.h" +#include "fd_account.h" +#include "mem_account.h" + +#ifndef TEST_DEBUG +#define TEST_DEBUG 0 +#endif + +#ifndef TEST_BUF_SIZE +#define TEST_LIST_SIZE 64 +#define TEST_BUF_SIZE 64 +#endif + +int debug = TEST_DEBUG; + +typedef struct test { + char *name; + void (* func)(); +} test_set[]; + +#define TEST(X) ((struct test) { #X, test_ ## X }) + +static void fmt_int_list(char *buf, size_t size, const int *list, size_t list_len) +{ + char *ptr = buf; + + size_t len; + + len = snprintf(ptr, size, "[ "); + size -= len; + ptr += len; + + for (size_t i = 0; i < list_len; ++i) + { + len = snprintf(ptr, size - 5, "%d%s", list[i], (i != list_len - 1) ? ", " : ""); + if (len > size - 6) + { + len = snprintf(ptr, size, "..."); + i = list_len; + } + size -= len; + ptr += len; + } + + len = snprintf(ptr, size, " ]"); +} + +int run_test_set(test_set tests, size_t num_tests, char *file_name) +{ + size_t num_passed; + pid_t test_pid; + int original_fd_list[TEST_LIST_SIZE]; + int fd_list[TEST_LIST_SIZE]; + size_t fd_list_len; + char fd_list_fmt_buf[TEST_BUF_SIZE]; + int child_list[TEST_LIST_SIZE]; + size_t child_list_len; + char child_list_fmt_buf[TEST_BUF_SIZE]; + int status; + int passed; + + printf("%s\n", file_name); + + num_passed = 0; + for (size_t i = 0; i < num_tests; ++i) + { + fflush(stdout); + fflush(stderr); + + assert((test_pid = fork()) != -1); + + if (test_pid == 0) + { + size_t original_fd_list_len = get_fd_list(original_fd_list, array_size(original_fd_list)); + + size_t original_mem_account = mem_account; + tests[i].func(); + if (mem_account != original_mem_account) + { + fprintf(stderr, "memory leak detected during test (%lu bytes)\n", mem_account - original_mem_account); + exit(1); + } + + fd_list_len = get_fd_list(fd_list, array_size(fd_list)); + + if(compare_fd_list(original_fd_list, original_fd_list_len, fd_list, fd_list_len) != 0) + { + fprintf(stderr, "file descriptor leak detected during test\n"); + + fmt_int_list(fd_list_fmt_buf, array_size(fd_list_fmt_buf), original_fd_list, original_fd_list_len); + fprintf(stderr, "fd list before test = %s\n", fd_list_fmt_buf); + + fmt_int_list(fd_list_fmt_buf, array_size(fd_list_fmt_buf), fd_list, fd_list_len); + fprintf(stderr, "fd list after test = %s\n", fd_list_fmt_buf); + + exit(1); + } + + child_list_len = get_children(child_list, array_size(child_list)); + + if(child_list_len > 0) + { + fprintf(stderr, "child processes not terminated or reaped during test\n"); + + fmt_int_list(child_list_fmt_buf, array_size(child_list_fmt_buf), child_list, child_list_len); + fprintf(stderr, "child processes = %s\n", child_list_fmt_buf); + + exit(1); + } + + exit(0); + } + + assert(wait(&status) != -1); + passed = WIFEXITED(status) && WEXITSTATUS(status) == 0; + num_passed += passed; + printf("[%lu/%lu] %s: %s\n", i+1, num_tests, tests[i].name, passed ? "\033[92mpassed\033[0m" : "\033[91mfailed\033[0m"); +#ifdef TEST_FAIL_FAST + if (status != 0) break; +#endif + } + printf("%s: %lu tests passed, %lu tests failed\n", file_name, num_passed, num_tests - num_passed); + return num_passed != num_tests; +} + +#define RUN_TESTS(X) int main() { return run_test_set(X, array_size(X), __FILE__); } + +void test_always_pass() { } +void test_always_fail() { abort(); } diff --git a/test_chroot_import_bin b/test_chroot_import_bin new file mode 100755 index 0000000..70181c0 --- /dev/null +++ b/test_chroot_import_bin @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +set -eufo pipefail + +chroot="$1" +shift + +while [ $# -gt 0 ]; do + file="$1" + shift + + interpreter="$(patchelf --print-interpreter "$file")" + cp "$interpreter" "$chroot/" + + ldd "$file" | grep -oP '(?<==> )[^ ]*' | while read -r lib; do + [ "$(basename "$lib")" != "$(basename "$interpreter")" ] || continue + + target_lib="$chroot/$(basename "$lib")" + cp "$lib" "$target_lib" + patchelf --set-rpath . "$target_lib" + done + + target_file="$chroot/$(basename "$file")" + cp "$file" "$target_file" + + patchelf --set-rpath . --set-interpreter "$(basename "$interpreter")" "$target_file" +done diff --git a/test_seccomp_unotify.c b/test_seccomp_unotify.c new file mode 100644 index 0000000..ffacdcd --- /dev/null +++ b/test_seccomp_unotify.c @@ -0,0 +1,195 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include + +#include "array_size.h" +#include "path.h" +#include "seccomp_unotify.h" +#include "test.h" + +static int handle_getuid(void *, struct seccomp_data *data, int, int) +{ + assert(data->nr == SYS_getuid); + return 42; +} + +static int handle_fake_uid(void *ctx, struct seccomp_data *data, int, int) +{ + if (data->nr == SYS_getuid) + { + return *((int *) ctx); + } + + if (data->nr == SYS_setuid) + { + *((int *) ctx) = (int) data->args[0]; + return 0; + } + + errno = ENOSYS; + return -1; +} + +static int handle_uname(void *ctx, struct seccomp_data *data, int, int proc_mem_fd) +{ + struct utsname utsname; + ssize_t len; + + assert(data->nr == SYS_uname); + + uname(&utsname); + strncpy(utsname.version, ctx, sizeof(utsname.version) - 1); + + len = pwrite(proc_mem_fd, &utsname, sizeof(utsname), data->args[0]); + if (len != sizeof(utsname)) + { + if (len >= 0) errno = EFAULT; + return -1; + } + + return 0; +} + +static int handle_getxattr(void *, struct seccomp_data *data, int, int proc_mem_fd) +{ + char name[XATTR_NAME_MAX + 1]; + path_t path; + struct statx file_statx; + char buf[TEST_BUF_SIZE]; + size_t len; + ssize_t written_len; + + if (pread(proc_mem_fd, name, sizeof(name), data->args[1]) == -1) return -1; + if (strnlen(name, sizeof(name)) >= sizeof(name)) + { + errno = ERANGE; + return -1; + } + + debug_printf("name=%s", name); + + if (strcmp(name, "stat.ino") != 0) + { + errno = ENODATA; + return -1; + } + + if (pread(proc_mem_fd, path, sizeof(path), data->args[0]) == -1) return -1; + if (strnlen(path, sizeof(path)) >= sizeof(path)) + { + errno = ENAMETOOLONG; + return -1; + } + + debug_printf("path=%s", path); + + if (statx(AT_FDCWD, path, 0, STATX_INO, &file_statx) == -1) return -1; + + snprintf(buf, sizeof(buf), "%llu", file_statx.stx_ino); + + len = strlen(buf); + if (data->args[3] == 0) return len; + if (data->args[3] < len) + { + errno = ERANGE; + return -1; + } + + written_len = pwrite(proc_mem_fd, buf, len, data->args[2]); + if ((size_t) written_len != len) + { + if (written_len >= 0) errno = EFAULT; + return -1; + } + + return len; +} + +static seccomp_unotify_handler empty_syscall_handlers[] = { }; + +static seccomp_unotify_handler getuid_syscall_handlers[] = { + [SYS_getuid] = handle_getuid +}; + +static seccomp_unotify_handler fakeuid_syscall_handlers[] = { + [SYS_setuid] = handle_fake_uid, + [SYS_getuid] = handle_fake_uid +}; + +static seccomp_unotify_handler uname_syscall_handlers[] = { + [SYS_uname] = handle_uname +}; + +static seccomp_unotify_handler getxattr_syscall_handlers[] = { + [SYS_getxattr] = handle_getxattr +}; + +static int seccomp_unotify_sh(char *cmd, void *ctx, seccomp_unotify_handler *syscall_handlers, size_t len_syscall_handlers) +{ + return seccomp_unotify_vfork_exec(ctx, syscall_handlers, len_syscall_handlers, "/bin/sh", (char * []) { "/bin/sh", "-c", cmd, NULL }, (char * []) { NULL }); +} + +static void test_no_opt() +{ + assert(seccomp_unotify_sh("true", NULL, empty_syscall_handlers, array_size(empty_syscall_handlers)) == 0); +} + +static void test_exit_code() +{ + assert(seccomp_unotify_sh("exit 42", NULL, empty_syscall_handlers, array_size(empty_syscall_handlers)) == 42); +} + +static void test_handler() +{ + assert(seccomp_unotify_sh("[ $(./do_syscall getuid) = 42 ]", NULL, getuid_syscall_handlers, array_size(getuid_syscall_handlers)) == 0); +} + +static void test_ctx() +{ + int uid = 0; + assert(seccomp_unotify_sh("[ $(./do_syscall getuid) = 0 ] && [ $(./do_syscall setuid int:42) = 0 ] && [ $(./do_syscall getuid) = 42 ]", &uid, fakeuid_syscall_handlers, array_size(fakeuid_syscall_handlers)) == 0); +} + +static void test_write_mem() +{ + assert(seccomp_unotify_sh("[ $(uname -v) = " __FILE__ " ]", __FILE__, uname_syscall_handlers, array_size(uname_syscall_handlers)) == 0); +} + +static void test_file_access() +{ + assert(seccomp_unotify_sh("touch .tmp/test_file && [ $(./do_syscall getxattr str:.tmp/test_file str:stat.ino buf:str:64 int:64 | head -n 1) = $(stat -c '\%i' .tmp/test_file) ]", NULL, getxattr_syscall_handlers, array_size(getxattr_syscall_handlers)) == 0); +} + +static void test_cwd() +{ + assert(seccomp_unotify_sh("do_syscall_path=$(realpath do_syscall) && mkdir -p .tmp/test_dir && cd .tmp/test_dir && touch test_file && [ $($do_syscall_path getxattr str:test_file str:stat.ino buf:str:64 int:64 | head -n 1) = $(stat -c '\%i' test_file) ]", NULL, getxattr_syscall_handlers, array_size(getxattr_syscall_handlers)) == 0); +} + +static void test_chroot() +{ + assert(seccomp_unotify_sh("touch .tmp/chroot/test_file && [ $(chroot .tmp/chroot /do_syscall getxattr str:/test_file str:stat.ino buf:str:64 int:64 | head -n 1) = $(stat -c '\%i' .tmp/chroot/test_file) ]", NULL, getxattr_syscall_handlers, array_size(getxattr_syscall_handlers)) == 0); +} + +static void test_mntns() +{ + assert(seccomp_unotify_sh("touch .tmp/test_file .tmp/test_mnt && [ $(unshare --map-root-user --mount --propagation unchanged sh -c 'mount --bind .tmp/test_file .tmp/test_mnt && ./do_syscall getxattr str:.tmp/test_mnt str:stat.ino buf:str:64 int:64' | head -n 1) = $(stat -c '\%i' .tmp/test_file) ]", NULL, getxattr_syscall_handlers, array_size(getxattr_syscall_handlers)) == 0); +} + +test_set tests = { + TEST(no_opt), + TEST(exit_code), + TEST(handler), + TEST(ctx), + TEST(write_mem), + TEST(file_access), + TEST(cwd), + TEST(chroot), + TEST(mntns) +}; + +RUN_TESTS(tests) diff --git a/test_xattr_db.c b/test_xattr_db.c new file mode 100644 index 0000000..933af8d --- /dev/null +++ b/test_xattr_db.c @@ -0,0 +1,171 @@ +#include +#include + +#include "test.h" +#include "xattr_db.h" + +void test_struct_size() +{ + static_assert(sizeof(xattr_db_file_id) == 28); + static_assert(sizeof(xattr_db_attr_name) == 256); +} + +void test_alloc_free() +{ + xattr_db_ctx *xattr_db = xattr_db_init(); + xattr_db_free(xattr_db); +} + +void test_set_get() +{ + char get_data[TEST_BUF_SIZE]; + char set_data[] = { 'h', 'e', 'l', 'l', 'o' }; + size_t len; + + xattr_db_ctx *xattr_db = xattr_db_init(); + xattr_db_file_id file_id = { .dev = { .major = 0, .minor = 0 }, .ino = 1 }; + + xattr_db_attr_name attr_name = "test"; + xattr_db_set(xattr_db, file_id, attr_name, set_data, sizeof(set_data), 0, 0); + len = xattr_db_get(xattr_db, file_id, attr_name, get_data, sizeof(get_data)); + + assert(len == sizeof(set_data)); + assert(memcmp(set_data, get_data, len) == 0); + + xattr_db_free(xattr_db); +} + +void test_overwrite() +{ + char set_data_a[] = { 'h', 'e', 'l', 'l', 'o' }; + char set_data_b[] = { 'r', 'e', 'p', 'l', 'a', 'c', 'e', 'd' }; + char get_data[TEST_BUF_SIZE]; + ssize_t len; + + xattr_db_ctx *xattr_db = xattr_db_init(); + xattr_db_file_id file_id = { .dev = { .major = 0, .minor = 0 }, .ino = 1 }; + + xattr_db_attr_name attr_name = "test"; + + xattr_db_set(xattr_db, file_id, attr_name, set_data_a, sizeof(set_data_a), 0, 0); + xattr_db_set(xattr_db, file_id, attr_name, set_data_b, sizeof(set_data_b), 0, 0); + + len = xattr_db_get(xattr_db, file_id, attr_name, get_data, sizeof(get_data)); + + assert(len == sizeof(set_data_b)); + assert(memcmp(set_data_b, get_data, len) == 0); + + xattr_db_free(xattr_db); +} + +void test_list() +{ + char set_data[] = { 'h', 'e', 'l', 'l', 'o' }; + char expected_list[] = "testA\0testB\0testC"; + char list[TEST_BUF_SIZE]; + ssize_t len; + + xattr_db_ctx *xattr_db = xattr_db_init(); + xattr_db_file_id file_id = { .dev = { .major = 0, .minor = 0 }, .ino = 1 }; + + xattr_db_attr_name attr_name[] = { "testA", "testC", "testB", "testA" }; + for (size_t i = 0; i < array_size(attr_name); ++i) + { + xattr_db_set(xattr_db, file_id, attr_name[i], set_data, sizeof(set_data), 0, 0); + } + + len = xattr_db_list(xattr_db, file_id, list, sizeof(list)); + assert(len == sizeof(expected_list)); + assert(memcmp(expected_list, list, len) == 0); + + len = xattr_db_list(xattr_db, file_id, NULL, 0); + assert(len > 0); + assert(errno == ERANGE); + + xattr_db_free(xattr_db); +} + +void test_remove() +{ + char get_data[TEST_BUF_SIZE]; + char set_data[] = { 'h', 'e', 'l', 'l', 'o' }; + ssize_t len; + + xattr_db_ctx *xattr_db = xattr_db_init(); + xattr_db_file_id file_id = { .dev = { .major = 0, .minor = 0 }, .ino = 1 }; + + xattr_db_attr_name attr_name = "test"; + xattr_db_set(xattr_db, file_id, attr_name, set_data, sizeof(set_data), 0, 0); + len = xattr_db_get(xattr_db, file_id, attr_name, get_data, sizeof(get_data)); + + assert(len == sizeof(set_data)); + assert(memcmp(set_data, get_data, len) == 0); + + assert(xattr_db_remove(xattr_db, file_id, attr_name) == 0); + + len = xattr_db_get(xattr_db, file_id, attr_name, get_data, sizeof(get_data)); + assert(len == -1); + assert(errno == ENODATA); + + len = xattr_db_list(xattr_db, file_id, NULL, 0); + assert(len == 0); + + xattr_db_free(xattr_db); +} + +void test_get_errors() +{ + xattr_db_ctx *xattr_db = xattr_db_init(); + xattr_db_file_id file_id = { .dev = { .major = 0, .minor = 0 }, .ino = 1 }; + xattr_db_attr_name attr_name = "test"; + char set_data[] = { 'h', 'e', 'l', 'l', 'o' }; + char get_data[64]; + ssize_t len; + + len = xattr_db_get(xattr_db, file_id, attr_name, get_data, sizeof(get_data)); + assert(len == -1); + assert(errno == ENODATA); + + xattr_db_set(xattr_db, file_id, attr_name, set_data, sizeof(set_data), 0, 0); + + len = xattr_db_get(xattr_db, file_id, attr_name, NULL, 0); + assert(len > 0); + assert(errno == ERANGE); + + xattr_db_free(xattr_db); +} + +void test_set_errors() +{ + xattr_db_ctx *xattr_db = xattr_db_init(); + xattr_db_file_id file_id = { .dev = { .major = 0, .minor = 0 }, .ino = 1 }; + xattr_db_attr_name attr_name = "test"; + char set_data[] = { 'h', 'e', 'l', 'l', 'o' }; + int err; + + err = xattr_db_set(xattr_db, file_id, attr_name, set_data, sizeof(set_data), 0, 1); + assert(err == -1); + assert(errno == ENODATA); + + err = xattr_db_set(xattr_db, file_id, attr_name, set_data, sizeof(set_data), 1, 0); + assert(err == 0); + + err = xattr_db_set(xattr_db, file_id, attr_name, set_data, sizeof(set_data), 1, 0); + assert(err == -1); + assert(errno == EEXIST); + + xattr_db_free(xattr_db); +} + +test_set tests = { + TEST(struct_size), + TEST(alloc_free), + TEST(set_get), + TEST(overwrite), + TEST(list), + TEST(remove), + TEST(get_errors), + TEST(set_errors) +}; + +RUN_TESTS(tests) diff --git a/xattr_db.c b/xattr_db.c new file mode 100644 index 0000000..866b413 --- /dev/null +++ b/xattr_db.c @@ -0,0 +1,224 @@ +#define _GNU_SOURCE +#include +#include +#include + +#include "debug.h" +#include "xattr_db.h" +#include "zalloc.h" + +struct xattr_db_attr_list { + struct xattr_db_attr_list *next; + xattr_db_attr_name name; + struct { + size_t len; + char * buf; + } data; +}; + +static void xattr_db_attr_list_free(struct xattr_db_attr_list *head) +{ + while (head) + { + struct xattr_db_attr_list *next = head->next; + if (head->data.buf) free(head->data.buf); + free(head); + head = next; + } +} + +static struct xattr_db_attr_list * xattr_db_attr_list_entry(struct xattr_db_attr_list **head, xattr_db_attr_name name, int create, int replace) +{ + struct xattr_db_attr_list *entry; + + while (*head) + { + int cmp = strncmp(name, (*head)->name, sizeof(xattr_db_attr_name)); + if (cmp == 0) + { + if (!create) return *head; + else + { + errno = EEXIST; + return NULL; + } + } + else if (cmp < 0) break; + else head = &(*head)->next; + } + + if (replace) + { + errno = ENODATA; + return NULL; + } + + entry = zalloc(sizeof(struct xattr_db_attr_list)); + entry->next = *head; + memcpy(entry->name, name, sizeof(xattr_db_attr_name)); + *head = entry; + + debug_printf("new xattr key \"%s\" added", name); + + return entry; +} + +int xattr_db_attr_list_remove(struct xattr_db_attr_list **head, xattr_db_attr_name name) +{ + struct xattr_db_attr_list *entry; + + while (*head) + { + if (strncmp(name, (*head)->name, sizeof(xattr_db_attr_name)) == 0) break; + else head = &(*head)->next; + } + + entry = *head; + if (!entry) + { + errno = ENODATA; + return -1; + } + + *head = entry->next; + if (entry->data.buf) free(entry->data.buf); + free(entry); + + return 0; +} + +struct xattr_db_tree_node { + xattr_db_file_id file_id; + struct xattr_db_attr_list *head; +}; + +static void xattr_db_tree_node_free(struct xattr_db_tree_node *node) +{ + if (node->head) xattr_db_attr_list_free(node->head); + free(node); +} + +struct xattr_db_ctx { + void *tree; + struct xattr_db_tree_node *scratch; +}; + +static int xattr_db_file_id_compare(const struct xattr_db_tree_node *a, const struct xattr_db_tree_node *b) +{ + return memcmp(&a->file_id, &b->file_id, sizeof(xattr_db_file_id)); +} + +xattr_db_ctx * xattr_db_init() +{ + return zalloc(sizeof(struct xattr_db_ctx)); +} + +void xattr_db_free(xattr_db_ctx *ctx) +{ + tdestroy(ctx->tree, (void (*)(void *)) xattr_db_tree_node_free); + if (ctx->scratch) xattr_db_tree_node_free(ctx->scratch); + free(ctx); +} + +static struct xattr_db_tree_node * xattr_db_tree_get(xattr_db_ctx *ctx, xattr_db_file_id file_id) +{ + struct xattr_db_tree_node *node; + + if (!ctx->scratch) ctx->scratch = zalloc(sizeof (struct xattr_db_tree_node)); + + ctx->scratch->file_id = file_id; + node = *((struct xattr_db_tree_node **) tsearch(ctx->scratch, &ctx->tree, (int (*)(const void *, const void *)) xattr_db_file_id_compare)); + + if (node == ctx->scratch) + { + debug_printf("new database entry created for inode %lu on dev %u:%u", node->file_id.ino, node->file_id.dev.major, node->file_id.dev.minor); + ctx->scratch = NULL; + } + else debug_printf("found database entry for inode %lu on dev %u:%u", node->file_id.ino, node->file_id.dev.major, node->file_id.dev.minor); + + return node; +} + +int xattr_db_set(xattr_db_ctx *ctx, xattr_db_file_id file_id, xattr_db_attr_name name, const char *data, size_t len, int create, int replace) +{ + struct xattr_db_tree_node *node; + struct xattr_db_attr_list *entry; + + node = xattr_db_tree_get(ctx, file_id); + entry = xattr_db_attr_list_entry(&node->head, name, create, replace); + + if (!entry) return -1; + + if(entry->data.buf) free(entry->data.buf); + + entry->data.len = len; + entry->data.buf = zalloc(len); + memcpy(entry->data.buf, data, len); + + return 0; +} + +ssize_t xattr_db_get(xattr_db_ctx *ctx, xattr_db_file_id file_id, xattr_db_attr_name name, char *data, size_t size) +{ + size_t len; + struct xattr_db_tree_node *node; + struct xattr_db_attr_list *entry; + + node = xattr_db_tree_get(ctx, file_id); + entry = xattr_db_attr_list_entry(&node->head, name, 0, 1); + + if (!entry) return -1; + + len = entry->data.len; + if (len > size) + { + errno = ERANGE; + return len; + } + + memcpy(data, entry->data.buf, len); + return len; +} + +ssize_t xattr_db_list(xattr_db_ctx *ctx, xattr_db_file_id file_id, char *buf, size_t size) +{ + struct xattr_db_tree_node *node; + struct xattr_db_attr_list *head; + size_t total_len = 0; + int _errno; + + _errno = 0; + + node = xattr_db_tree_get(ctx, file_id); + head = node->head; + + while (head) + { + size_t len = strnlen(head->name, sizeof(xattr_db_attr_name) - 1) + 1; + if (len <= size) + { + memcpy(buf, head->name, len); + buf += len; + size -= len; + } + else + { + _errno = ERANGE; + size = 0; + } + + total_len += len; + head = head->next; + } + + if (_errno) errno = _errno; + return total_len; +} + +int xattr_db_remove(xattr_db_ctx *ctx, xattr_db_file_id file_id, xattr_db_attr_name name) +{ + struct xattr_db_tree_node *node; + + node = xattr_db_tree_get(ctx, file_id); + return xattr_db_attr_list_remove(&node->head, name); +} diff --git a/xattr_db.h b/xattr_db.h new file mode 100644 index 0000000..f8710cd --- /dev/null +++ b/xattr_db.h @@ -0,0 +1,27 @@ +#include +#include +#include +#include + +typedef struct { + struct { + uint32_t major; + uint32_t minor; + } __attribute__((aligned(4),packed)) dev; + uint64_t ino; + struct { + uint64_t sec; + uint32_t nsec; + } __attribute__((aligned(4),packed)) btime; +} __attribute__((aligned(4),packed)) xattr_db_file_id; + +typedef char xattr_db_attr_name[XATTR_NAME_MAX + 1]; +typedef struct xattr_db_ctx xattr_db_ctx; + +xattr_db_ctx * xattr_db_init(); +void xattr_db_free(xattr_db_ctx *ctx); + +int xattr_db_set(xattr_db_ctx *ctx, xattr_db_file_id file_id, xattr_db_attr_name name, const char *data, size_t len, int create, int replace); +ssize_t xattr_db_get(xattr_db_ctx *ctx, xattr_db_file_id file_id, xattr_db_attr_name name, char *data, size_t size); +ssize_t xattr_db_list(xattr_db_ctx *ctx, xattr_db_file_id file_id, char *buf, size_t size); +int xattr_db_remove(xattr_db_ctx *ctx, xattr_db_file_id file_id, xattr_db_attr_name name); diff --git a/zalloc.c b/zalloc.c new file mode 100644 index 0000000..d57621f --- /dev/null +++ b/zalloc.c @@ -0,0 +1,22 @@ +#include +#include +#include + +#include "debug.h" +#include "zalloc.h" + +void * zalloc(size_t size) +{ + void *ptr; + + debug_printf("allocating %lu bytes", size); + + ptr = malloc(size); + if (!ptr) + { + perror("malloc"); + abort(); + } + memset(ptr, 0, size); + return ptr; +} diff --git a/zalloc.h b/zalloc.h new file mode 100644 index 0000000..3d1ce5b --- /dev/null +++ b/zalloc.h @@ -0,0 +1,3 @@ +#include + +void * zalloc(size_t size);