diff --git a/mk/tests.mk b/mk/tests.mk index f53f1cc..dc3ac5a 100644 --- a/mk/tests.mk +++ b/mk/tests.mk @@ -6,6 +6,7 @@ TEST_DIR = tests/unit TEST_SRCS = $(TEST_DIR)/test-runner.c \ $(TEST_DIR)/test-fd-table.c \ $(TEST_DIR)/test-path.c \ + $(TEST_DIR)/test-mount.c \ $(TEST_DIR)/test-cli.c \ $(TEST_DIR)/test-identity.c \ $(TEST_DIR)/test-syscall-nr.c \ @@ -30,6 +31,8 @@ endif # Unit tests link only the pure-computation sources (no LKL) TEST_SUPPORT_SRCS = $(SRC_DIR)/fd-table.c \ $(SRC_DIR)/path.c \ + $(SRC_DIR)/mount.c \ + $(TEST_DIR)/test-mount-stubs.c \ $(SRC_DIR)/cli.c \ $(SRC_DIR)/identity.c \ $(SRC_DIR)/syscall-nr.c \ diff --git a/src/mount.c b/src/mount.c index f149d36..e5ad92d 100644 --- a/src/mount.c +++ b/src/mount.c @@ -10,13 +10,19 @@ #include #include #include +#include #include "kbox/mount.h" #include "lkl-wrap.h" +#include "syscall-nr.h" /* MS_BIND from without pulling the full header. */ #define KBOX_MS_BIND 0x1000 +/* asm-generic O_* values for LKL openat (same on x86_64 and aarch64). */ +#define LKL_O_CREAT 0100 +#define LKL_O_EXCL 0200 + /* Bind-mount spec parser. */ int kbox_parse_bind_spec(const char *spec, struct kbox_bind_spec *out) @@ -118,7 +124,90 @@ int kbox_apply_recommended_mounts(const struct kbox_sysnrs *s, return 0; } -/* Bind mounts. */ +/* Bind mounts. + * + * The source is a host path; stat it to determine whether the bind-mount + * target inside LKL should be a directory or a regular file. Anything + * other than a regular file or directory is rejected up front. + */ + +/* Verify that the existing inode at target has the expected type. */ +static int verify_existing_target(const struct kbox_sysnrs *s, + const char *target, + unsigned int expected_mode_type) +{ + struct kbox_lkl_stat lkl_st; + long ret; + + ret = kbox_lkl_newfstatat(s, AT_FDCWD_LINUX, target, &lkl_st, 0); + if (ret < 0) { + fprintf(stderr, "stat(%s): %s\n", target, kbox_err_text(ret)); + return -1; + } + if ((lkl_st.st_mode & S_IFMT) != expected_mode_type) { + fprintf(stderr, + "bind mount: target %s exists but has wrong type " + "(expected 0%o, got 0%o)\n", + target, expected_mode_type, lkl_st.st_mode & S_IFMT); + return -1; + } + return 0; +} + +static int create_bind_target(const struct kbox_sysnrs *s, + const char *source, + const char *target) +{ + struct stat st; + long ret; + + if (lstat(source, &st) < 0) { + fprintf(stderr, "bind mount: cannot stat source %s: %s\n", source, + strerror(errno)); + return -1; + } + + if (S_ISLNK(st.st_mode)) { + fprintf(stderr, "bind mount: source %s must not be a symlink\n", + source); + return -1; + } + + if (S_ISDIR(st.st_mode)) { + ret = kbox_lkl_mkdir(s, target, 0755); + if (ret == -EEXIST) + return verify_existing_target(s, target, S_IFDIR); + if (ret < 0) { + fprintf(stderr, "mkdir(%s): %s\n", target, kbox_err_text(ret)); + return -1; + } + return 0; + } + + if (S_ISREG(st.st_mode)) { + long cret; + + ret = kbox_lkl_openat(s, AT_FDCWD_LINUX, target, + LKL_O_CREAT | LKL_O_EXCL, 0644); + if (ret == -EEXIST) + return verify_existing_target(s, target, S_IFREG); + if (ret < 0) { + fprintf(stderr, "creat(%s): %s\n", target, kbox_err_text(ret)); + return -1; + } + cret = kbox_lkl_close(s, ret); + if (cret < 0) { + fprintf(stderr, "close(%s): %s\n", target, kbox_err_text(cret)); + return -1; + } + return 0; + } + + fprintf(stderr, + "bind mount: source %s is neither a regular file nor a directory\n", + source); + return -1; +} int kbox_apply_bind_mounts(const struct kbox_sysnrs *s, const struct kbox_bind_spec *specs, @@ -127,13 +216,12 @@ int kbox_apply_bind_mounts(const struct kbox_sysnrs *s, int i; long ret; + if (!s || (!specs && count > 0) || count < 0) + return -1; + for (i = 0; i < count; i++) { - ret = kbox_lkl_mkdir(s, specs[i].target, 0755); - if (ret < 0 && ret != -EEXIST) { - fprintf(stderr, "mkdir(%s): %s\n", specs[i].target, - kbox_err_text(ret)); + if (create_bind_target(s, specs[i].source, specs[i].target) < 0) return -1; - } ret = kbox_lkl_mount(s, specs[i].source, specs[i].target, NULL, KBOX_MS_BIND, NULL); diff --git a/tests/unit/test-mount-stubs.c b/tests/unit/test-mount-stubs.c new file mode 100644 index 0000000..6c485b8 --- /dev/null +++ b/tests/unit/test-mount-stubs.c @@ -0,0 +1,74 @@ +/* SPDX-License-Identifier: MIT */ +/* Stubs for LKL functions referenced by mount.c but not exercised + * in the unit tests (only kbox_parse_bind_spec is tested). + */ + +#include + +#include "lkl-wrap.h" +#include "syscall-nr.h" + +long kbox_lkl_mkdir(const struct kbox_sysnrs *s, const char *path, int mode) +{ + (void) s; + (void) path; + (void) mode; + return -ENOSYS; +} + +long kbox_lkl_mount(const struct kbox_sysnrs *s, + const char *src, + const char *target, + const char *fstype, + long flags, + const void *data) +{ + (void) s; + (void) src; + (void) target; + (void) fstype; + (void) flags; + (void) data; + return -ENOSYS; +} + +long kbox_lkl_openat(const struct kbox_sysnrs *s, + long dirfd, + const char *path, + long flags, + long mode) +{ + (void) s; + (void) dirfd; + (void) path; + (void) flags; + (void) mode; + return -ENOSYS; +} + +long kbox_lkl_close(const struct kbox_sysnrs *s, long fd) +{ + (void) s; + (void) fd; + return 0; +} + +long kbox_lkl_newfstatat(const struct kbox_sysnrs *s, + long dirfd, + const char *path, + void *buf, + long flags) +{ + (void) s; + (void) dirfd; + (void) path; + (void) buf; + (void) flags; + return -ENOSYS; +} + +const char *kbox_err_text(long code) +{ + (void) code; + return "stub"; +} diff --git a/tests/unit/test-mount.c b/tests/unit/test-mount.c new file mode 100644 index 0000000..dd6af93 --- /dev/null +++ b/tests/unit/test-mount.c @@ -0,0 +1,103 @@ +/* SPDX-License-Identifier: MIT */ +#include + +#include "kbox/mount.h" +#include "test-runner.h" + +static void fill_char(char *buf, size_t len, char c) +{ + memset(buf, c, len); + buf[len] = '\0'; +} + +/* --- kbox_parse_bind_spec tests --- */ + +static void test_parse_basic(void) +{ + struct kbox_bind_spec spec; + ASSERT_EQ(kbox_parse_bind_spec("/host/dir:/guest/dir", &spec), 0); + ASSERT_STREQ(spec.source, "/host/dir"); + ASSERT_STREQ(spec.target, "/guest/dir"); +} + +static void test_parse_null_spec(void) +{ + struct kbox_bind_spec spec; + ASSERT_EQ(kbox_parse_bind_spec(NULL, &spec), -1); +} + +static void test_parse_null_out(void) +{ + ASSERT_EQ(kbox_parse_bind_spec("/a:/b", NULL), -1); +} + +static void test_parse_no_colon(void) +{ + struct kbox_bind_spec spec; + ASSERT_EQ(kbox_parse_bind_spec("/no/colon/here", &spec), -1); +} + +static void test_parse_empty_source(void) +{ + struct kbox_bind_spec spec; + ASSERT_EQ(kbox_parse_bind_spec(":/guest", &spec), -1); +} + +static void test_parse_empty_target(void) +{ + struct kbox_bind_spec spec; + ASSERT_EQ(kbox_parse_bind_spec("/host:", &spec), -1); +} + +static void test_parse_colon_in_middle(void) +{ + struct kbox_bind_spec spec; + ASSERT_EQ(kbox_parse_bind_spec("a:b", &spec), 0); + ASSERT_STREQ(spec.source, "a"); + ASSERT_STREQ(spec.target, "b"); +} + +static void test_parse_multiple_colons(void) +{ + struct kbox_bind_spec spec; + ASSERT_EQ(kbox_parse_bind_spec("/a:/b:c", &spec), 0); + ASSERT_STREQ(spec.source, "/a"); + ASSERT_STREQ(spec.target, "/b:c"); +} + +static void test_parse_source_too_long(void) +{ + struct kbox_bind_spec spec; + /* source component >= sizeof(spec.source) must be rejected */ + char big[4096 + 1 + 2]; /* 4096 'x' + ':' + '/' + NUL */ + fill_char(big, 4096, 'x'); + big[4096] = ':'; + big[4097] = '/'; + big[4098] = '\0'; + ASSERT_EQ(kbox_parse_bind_spec(big, &spec), -1); +} + +static void test_parse_target_too_long(void) +{ + struct kbox_bind_spec spec; + /* target component >= sizeof(spec.target) must be rejected */ + char big[2 + 4096 + 1]; /* '/' + ':' + 4096 'y' + NUL */ + big[0] = '/'; + big[1] = ':'; + fill_char(big + 2, 4096, 'y'); + ASSERT_EQ(kbox_parse_bind_spec(big, &spec), -1); +} + +void test_mount_init(void) +{ + TEST_REGISTER(test_parse_basic); + TEST_REGISTER(test_parse_null_spec); + TEST_REGISTER(test_parse_null_out); + TEST_REGISTER(test_parse_no_colon); + TEST_REGISTER(test_parse_empty_source); + TEST_REGISTER(test_parse_empty_target); + TEST_REGISTER(test_parse_colon_in_middle); + TEST_REGISTER(test_parse_multiple_colons); + TEST_REGISTER(test_parse_source_too_long); + TEST_REGISTER(test_parse_target_too_long); +} diff --git a/tests/unit/test-runner.c b/tests/unit/test-runner.c index ed105c1..52d3b33 100644 --- a/tests/unit/test-runner.c +++ b/tests/unit/test-runner.c @@ -91,6 +91,7 @@ void test_pass(void) /* Portable test suites (all hosts) */ extern void test_fd_table_init(void); extern void test_path_init(void); +extern void test_mount_init(void); extern void test_cli_init(void); extern void test_identity_init(void); extern void test_syscall_nr_init(void); @@ -120,6 +121,7 @@ int main(int argc, char *argv[]) /* Portable suites */ test_fd_table_init(); test_path_init(); + test_mount_init(); test_cli_init(); test_identity_init(); test_syscall_nr_init();