| // SPDX-License-Identifier: GPL-2.0 | |
| /* | |
| * A mount that another namespace's rmdir detached, or that went down with | |
| * the detached tree it was in when the tree's last fd was closed, keeps | |
| * its submounts connected, and a connected submount is put by its | |
| * parent's final mntput(). A submount whose filesystem keeps a file open | |
| * on the parent then holds the parent's count above zero for good: | |
| * nothing in userspace refers to either mount any more and nothing can | |
| * release them. A loop device is the simplest such filesystem, its | |
| * backing file sits on the parent. | |
| */ | |
| #define _GNU_SOURCE | |
| #include <dirent.h> | |
| #include <errno.h> | |
| #include <fcntl.h> | |
| #include <sched.h> | |
| #include <stdio.h> | |
| #include <stdlib.h> | |
| #include <string.h> | |
| #include <sys/ioctl.h> | |
| #include <sys/mount.h> | |
| #include <sys/stat.h> | |
| #include <stdbool.h> | |
| #include <sys/wait.h> | |
| #include <unistd.h> | |
| #include <linux/fuse.h> | |
| #include <linux/keyctl.h> | |
| #include <linux/major.h> | |
| #include <linux/raid/md_u.h> | |
| #include <linux/raid/md_p.h> | |
| #include <linux/loop.h> | |
| #include <linux/magic.h> | |
| #include <sys/syscall.h> | |
| #include <sys/sysmacros.h> | |
| #include <sys/uio.h> | |
| #include "../wrappers.h" | |
| #include "../../kselftest_harness.h" | |
| #define IMAGE_SIZE (1440 * 1024) | |
| #define SECTOR 512 | |
| /* | |
| * The tmpfs of the fixture. A directory of its own from mkdtemp(), so that | |
| * the test needs no writable root directory and two instances don't take | |
| * each other's mounts down. | |
| */ | |
| static char base[64]; | |
| /* The configfs directory of the gadget, named after the test process. */ | |
| static char gadget[96]; | |
| /* A path below the base directory. Eight of them can be in use at a time. */ | |
| static const char *at(const char *rel) | |
| { | |
| static char buf[8][PATH_MAX]; | |
| static unsigned int next; | |
| char *p = buf[next++ % 8]; | |
| snprintf(p, PATH_MAX, "%s/%s", base, rel); | |
| return p; | |
| } | |
| /* A path below the gadget's configfs directory. */ | |
| static const char *gat(const char *rel) | |
| { | |
| static char buf[4][PATH_MAX]; | |
| static unsigned int next; | |
| char *p = buf[next++ % 4]; | |
| snprintf(p, PATH_MAX, "%s%s", gadget, rel); | |
| return p; | |
| } | |
| /* The loop devices this process has bound, to release them on every path. */ | |
| #define MAX_LOOPS 4 | |
| static int bound[MAX_LOOPS]; | |
| static int nr_bound; | |
| /* What a child tells its parent: the loop devices it has bound, -1 for none. */ | |
| struct report { | |
| int n[2]; | |
| }; | |
| /* A blank FAT12 floppy image: boot sector, two FATs, an empty root directory. */ | |
| static int write_fat12(int fd) | |
| { | |
| struct stat st; | |
| unsigned char sector[SECTOR] = { | |
| 0xeb, 0x3c, 0x90, 'M', 'S', 'W', 'I', 'N', '4', '.', '1', | |
| [11] = 0x00, 0x02, /* bytes per sector: 512 */ | |
| [13] = 1, /* sectors per cluster */ | |
| [14] = 1, 0, /* reserved sectors */ | |
| [16] = 2, /* FATs */ | |
| [17] = 0xe0, 0x00, /* root directory entries: 224 */ | |
| [19] = 0x40, 0x0b, /* total sectors: 2880 */ | |
| [21] = 0xf0, /* media descriptor */ | |
| [22] = 9, 0, /* sectors per FAT */ | |
| [24] = 18, 0, /* sectors per track */ | |
| [26] = 2, 0, /* heads */ | |
| [38] = 0x29, /* extended boot signature */ | |
| [39] = 0x12, 0x34, 0x56, 0x78, | |
| [43] = 'N', 'O', ' ', 'N', 'A', 'M', 'E', ' ', ' ', ' ', ' ', | |
| [54] = 'F', 'A', 'T', '1', '2', ' ', ' ', ' ', | |
| [510] = 0x55, 0xaa, | |
| }; | |
| unsigned char fat[SECTOR] = { 0xf0, 0xff, 0xff }; | |
| if (pwrite(fd, sector, SECTOR, 0) != SECTOR) | |
| return -1; | |
| /* the first FAT and the second one, one sector each is enough */ | |
| if (pwrite(fd, fat, SECTOR, 1 * SECTOR) != SECTOR || | |
| pwrite(fd, fat, SECTOR, 10 * SECTOR) != SECTOR) | |
| return -1; | |
| if (fstat(fd, &st) || S_ISBLK(st.st_mode)) | |
| return 0; | |
| return ftruncate(fd, IMAGE_SIZE); | |
| } | |
| /* | |
| * A blank FAT12 image with 4 KiB sectors and @sectors of them, for a device | |
| * with 4 KiB logical blocks (zram) or for a bigger image than a floppy. | |
| */ | |
| static int write_fat12_4k(int fd, unsigned int sectors) | |
| { | |
| unsigned int fat_sectors = (sectors * 3 / 2 + 4095) / 4096; | |
| unsigned char sector[4096] = { | |
| 0xeb, 0x3c, 0x90, 'M', 'S', 'W', 'I', 'N', '4', '.', '1', | |
| [11] = 0x00, 0x10, /* bytes per sector: 4096 */ | |
| [13] = 1, /* sectors per cluster */ | |
| [14] = 1, 0, /* reserved sectors */ | |
| [16] = 2, /* FATs */ | |
| [17] = 128, 0, /* root directory entries: one sector */ | |
| [19] = sectors & 0xff, sectors >> 8, | |
| [21] = 0xf8, /* media descriptor */ | |
| [22] = fat_sectors, 0, | |
| [24] = 63, 0, /* sectors per track */ | |
| [26] = 255, 0, /* heads */ | |
| [38] = 0x29, /* extended boot signature */ | |
| [39] = 0x12, 0x34, 0x56, 0x78, | |
| [43] = 'N', 'O', ' ', 'N', 'A', 'M', 'E', ' ', ' ', ' ', ' ', | |
| [54] = 'F', 'A', 'T', '1', '2', ' ', ' ', ' ', | |
| [510] = 0x55, 0xaa, | |
| }; | |
| unsigned char fat[4096] = { 0xf8, 0xff, 0xff }; | |
| struct stat st; | |
| if (pwrite(fd, sector, sizeof(sector), 0) != sizeof(sector)) | |
| return -1; | |
| if (pwrite(fd, fat, sizeof(fat), 1 * 4096) != sizeof(fat) || | |
| pwrite(fd, fat, sizeof(fat), (1 + fat_sectors) * 4096) != sizeof(fat)) | |
| return -1; | |
| if (fstat(fd, &st) || S_ISBLK(st.st_mode)) | |
| return 0; | |
| return ftruncate(fd, (off_t)sectors * 4096); | |
| } | |
| #define MINIX_BLOCK 1024 | |
| #define MINIX_BLOCKS 4096 /* a 4 MiB image */ | |
| #define MINIX_INODES 512 | |
| #define MINIX_ITABLE (MINIX_INODES * 32 / MINIX_BLOCK) | |
| #define MINIX_FIRSTDATA (2 + 1 + 1 + MINIX_ITABLE) /* boot, super, imap, zmap, inodes */ | |
| /* | |
| * A blank minix v1 image, for the holders that need a FIFO or a device | |
| * node on the dying mount, which vfat can't hold. Superblock in block 1, | |
| * one block each for the inode and zone bitmaps, the inode table, and | |
| * the root directory in the first data zone. | |
| */ | |
| static int write_minix(int fd) | |
| { | |
| struct { | |
| __u16 s_ninodes, s_nzones, s_imap_blocks, s_zmap_blocks; | |
| __u16 s_firstdatazone, s_log_zone_size; | |
| __u32 s_max_size; | |
| __u16 s_magic, s_state; | |
| } sb = { | |
| .s_ninodes = MINIX_INODES, | |
| .s_nzones = MINIX_BLOCKS, | |
| .s_imap_blocks = 1, | |
| .s_zmap_blocks = 1, | |
| .s_firstdatazone = MINIX_FIRSTDATA, | |
| .s_max_size = (7 + 512 + 512 * 512) * MINIX_BLOCK, | |
| .s_magic = MINIX_SUPER_MAGIC, | |
| .s_state = 1, /* MINIX_VALID_FS */ | |
| }; | |
| struct { | |
| __u16 i_mode, i_uid; | |
| __u32 i_size, i_time; | |
| __u8 i_gid, i_nlinks; | |
| __u16 i_zone[9]; | |
| } root = { | |
| .i_mode = S_IFDIR | 0755, | |
| .i_size = 2 * 16, | |
| .i_nlinks = 2, | |
| .i_zone = { MINIX_FIRSTDATA }, | |
| }; | |
| unsigned char imap[MINIX_BLOCK], zmap[MINIX_BLOCK], dir[MINIX_BLOCK] = {}; | |
| int i; | |
| /* bit 0 is reserved in both maps, the root inode and its zone are in use */ | |
| memset(imap, 0xff, sizeof(imap)); | |
| for (i = 2; i <= MINIX_INODES; i++) | |
| imap[i / 8] &= ~(1 << (i % 8)); | |
| memset(zmap, 0xff, sizeof(zmap)); | |
| for (i = 2; i <= MINIX_BLOCKS - MINIX_FIRSTDATA; i++) | |
| zmap[i / 8] &= ~(1 << (i % 8)); | |
| dir[0] = 1; | |
| dir[2] = '.'; | |
| dir[16] = 1; | |
| dir[18] = '.'; | |
| dir[19] = '.'; | |
| if (pwrite(fd, &sb, sizeof(sb), 1 * MINIX_BLOCK) != sizeof(sb) || | |
| pwrite(fd, imap, sizeof(imap), 2 * MINIX_BLOCK) != sizeof(imap) || | |
| pwrite(fd, zmap, sizeof(zmap), 3 * MINIX_BLOCK) != sizeof(zmap) || | |
| pwrite(fd, &root, sizeof(root), 4 * MINIX_BLOCK) != sizeof(root) || | |
| pwrite(fd, dir, sizeof(dir), MINIX_FIRSTDATA * MINIX_BLOCK) != sizeof(dir)) | |
| return -1; | |
| return ftruncate(fd, (off_t)MINIX_BLOCKS * MINIX_BLOCK); | |
| } | |
| static int read_sysfs(const char *path, char *buf, size_t size) | |
| { | |
| ssize_t n; | |
| int fd; | |
| fd = open(path, O_RDONLY); | |
| if (fd < 0) | |
| return -1; | |
| n = read(fd, buf, size - 1); | |
| close(fd); | |
| if (n < 0) | |
| return -1; | |
| buf[n] = '\0'; | |
| return 0; | |
| } | |
| /* Is @dev the mount source of this line of mountinfo? loop1 is not loop10. */ | |
| static bool line_has_source(const char *line, const char *dev) | |
| { | |
| const char *sep, *src, *end; | |
| sep = strstr(line, " - "); | |
| if (!sep) | |
| return false; | |
| src = strchr(sep + 3, ' '); /* skip the filesystem type */ | |
| if (!src) | |
| return false; | |
| src++; | |
| end = strchr(src, ' '); | |
| if (!end) | |
| return false; | |
| return (size_t)(end - src) == strlen(dev) && !strncmp(src, dev, end - src); | |
| } | |
| /* | |
| * Does a mount namespace of a process that /proc shows have a mount of @dev? | |
| * The mounts under test are in no namespace at this point on any kernel, so | |
| * this only checks that the test got as far as it thinks. | |
| */ | |
| static bool mounted_anywhere(const char *dev) | |
| { | |
| char path[PATH_MAX], line[4096]; | |
| struct dirent *de; | |
| bool found = false; | |
| DIR *proc; | |
| FILE *f; | |
| proc = opendir("/proc"); | |
| if (!proc) | |
| return false; | |
| while (!found && (de = readdir(proc))) { | |
| if (de->d_name[0] < '0' || de->d_name[0] > '9') | |
| continue; | |
| snprintf(path, sizeof(path), "/proc/%s/mountinfo", de->d_name); | |
| f = fopen(path, "re"); | |
| if (!f) | |
| continue; | |
| while (fgets(line, sizeof(line), f)) { | |
| if (line_has_source(line, dev)) { | |
| found = true; | |
| break; | |
| } | |
| } | |
| fclose(f); | |
| } | |
| closedir(proc); | |
| return found; | |
| } | |
| /* Wait up to @ms milliseconds for the loop device to give up its backing file. */ | |
| static bool loop_released(const char *sysfs, int ms) | |
| { | |
| char buf[PATH_MAX]; | |
| for (; ms > 0; ms -= 100) { | |
| if (read_sysfs(sysfs, buf, sizeof(buf)) < 0) | |
| return errno == ENOENT; | |
| usleep(100000); | |
| } | |
| return read_sysfs(sysfs, buf, sizeof(buf)) < 0 && errno == ENOENT; | |
| } | |
| /* Tell the loop device to give its file up, now or when its last user is gone. */ | |
| static void loop_clear(int n) | |
| { | |
| char dev[32]; | |
| int lfd; | |
| if (n < 0) | |
| return; | |
| snprintf(dev, sizeof(dev), "/dev/loop%d", n); | |
| lfd = open(dev, O_RDWR); | |
| if (lfd < 0) | |
| return; | |
| ioctl(lfd, LOOP_CLR_FD); | |
| close(lfd); | |
| } | |
| FIXTURE(loop_cycle) { | |
| char dev[32]; /* the loop device the child set up */ | |
| char sysfs[64]; /* its backing_file attribute */ | |
| int loops[MAX_LOOPS]; /* every loop device the case has bound */ | |
| int nr_loops; | |
| bool gadget; /* the gadget's configfs directory exists */ | |
| }; | |
| static void remember_loop(FIXTURE_DATA(loop_cycle) *self, int n) | |
| { | |
| if (n >= 0 && self->nr_loops < MAX_LOOPS) | |
| self->loops[self->nr_loops++] = n; | |
| } | |
| FIXTURE_SETUP(loop_cycle) | |
| { | |
| if (geteuid() != 0) | |
| SKIP(return, "test requires CAP_SYS_ADMIN"); | |
| if (access("/dev/loop-control", R_OK | W_OK)) | |
| SKIP(return, "test requires loop devices"); | |
| ASSERT_EQ(unshare(CLONE_NEWNS), 0); | |
| ASSERT_EQ(mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL), 0); | |
| snprintf(base, sizeof(base), "/tmp/loop_cycle.XXXXXX"); | |
| ASSERT_NE(mkdtemp(base), NULL); | |
| /* a failed assertion in the setup does not run the teardown */ | |
| ASSERT_EQ(mount("tmpfs", base, "tmpfs", 0, NULL), 0) | |
| rmdir(base); | |
| snprintf(gadget, sizeof(gadget), | |
| "/sys/kernel/config/usb_gadget/kselftest_cycle_%d", getpid()); | |
| self->dev[0] = '\0'; | |
| self->nr_loops = 0; | |
| self->gadget = false; | |
| } | |
| static int mount_configfs(void); | |
| static void gadget_remove(void); | |
| FIXTURE_TEARDOWN(loop_cycle) | |
| { | |
| if (self->gadget) | |
| gadget_remove(); | |
| /* whatever a failed assertion has left bound */ | |
| for (int i = 0; i < self->nr_loops; i++) | |
| loop_clear(self->loops[i]); | |
| umount2(base, MNT_DETACH); | |
| rmdir(base); | |
| } | |
| /* Child exit codes. */ | |
| enum { | |
| CHILD_OK, | |
| CHILD_NS, /* could not set up the namespace or the tmpfs */ | |
| CHILD_IMAGE, /* could not write the image */ | |
| CHILD_LOOP, /* could not set up the loop device */ | |
| CHILD_MOUNT, /* could not mount it (vfat and msdos both refused) */ | |
| CHILD_PIPE, /* the parent went away */ | |
| CHILD_HOLDER, /* could not set the holder up below the mount */ | |
| CHILD_SKIP, /* the kernel lacks what the holder needs */ | |
| CHILD_NOFS, /* the kernel lacks the filesystem of the image */ | |
| }; | |
| /* Bind the loop device that LOOP_CTL_GET_FREE names to @ifd; the device number. */ | |
| static int loop_bind_free(int ifd) | |
| { | |
| int cfd, lfd, n; | |
| char dev[32]; | |
| cfd = open("/dev/loop-control", O_RDWR); | |
| if (cfd < 0) | |
| return -1; | |
| n = ioctl(cfd, LOOP_CTL_GET_FREE); | |
| close(cfd); | |
| if (n < 0) | |
| return -1; | |
| snprintf(dev, sizeof(dev), "/dev/loop%d", n); | |
| lfd = open(dev, O_RDWR); | |
| if (lfd < 0) | |
| return -1; | |
| if (ioctl(lfd, LOOP_SET_FD, ifd)) | |
| n = -1; | |
| close(lfd); | |
| return n; | |
| } | |
| /* | |
| * Bind a free loop device to the open image @ifd; the device number. The | |
| * device is free when LOOP_CTL_GET_FREE names it and may be somebody else's | |
| * a moment later, so try again when it is busy. | |
| */ | |
| static int loop_bind(int ifd) | |
| { | |
| int n = -1; | |
| for (int i = 0; i < 64 && n < 0; i++) { | |
| n = loop_bind_free(ifd); | |
| if (n < 0 && errno != EBUSY) | |
| return -1; | |
| } | |
| if (n >= 0 && nr_bound < MAX_LOOPS) | |
| bound[nr_bound++] = n; | |
| return n; | |
| } | |
| /* Mount a FAT image, as vfat or as msdos; -1 with ENODEV if the kernel has neither. */ | |
| static int mount_fat(const char *dev, const char *mp) | |
| { | |
| int err; | |
| if (!mount(dev, mp, "vfat", 0, NULL)) | |
| return 0; | |
| err = errno; | |
| if (!mount(dev, mp, "msdos", 0, NULL)) | |
| return 0; | |
| if (err != ENODEV) | |
| errno = err; | |
| return -1; | |
| } | |
| /* | |
| * A child that gives up has to take down what it has set up: nobody else | |
| * knows about it. Unmount, then tell the loop devices to let go. The mounts | |
| * are released after this process is gone and the devices follow them. | |
| */ | |
| static int child_fails(int ret) | |
| { | |
| umount2(at("vol"), MNT_DETACH); | |
| umount2(at("vol2"), MNT_DETACH); | |
| umount2(at("p"), MNT_DETACH); | |
| umount2(at("img"), MNT_DETACH); | |
| for (int i = 0; i < nr_bound; i++) | |
| loop_clear(bound[i]); | |
| return ret; | |
| } | |
| /* Write an image to @img, bind a loop device to it and mount that at @mp; the device number. */ | |
| static int loop_mount(const char *img, const char *mp) | |
| { | |
| char dev[32]; | |
| int ifd, n; | |
| ifd = open(img, O_RDWR | O_CREAT | O_EXCL, 0600); | |
| if (ifd < 0 || write_fat12(ifd)) | |
| return -CHILD_IMAGE; | |
| n = loop_bind(ifd); | |
| close(ifd); /* the loop device holds the file from now on */ | |
| if (n < 0) | |
| return -CHILD_LOOP; | |
| snprintf(dev, sizeof(dev), "/dev/loop%d", n); | |
| if (mkdir(mp, 0755)) | |
| return -CHILD_MOUNT; | |
| if (mount_fat(dev, mp)) | |
| return errno == ENODEV ? -CHILD_NOFS : -CHILD_MOUNT; | |
| return n; | |
| } | |
| /* | |
| * Two tmpfs mounts, each carrying the image of the loop mount below the | |
| * other: the loop mount below vol has its image on vol2 and the other way | |
| * round. | |
| */ | |
| static int crossed_child(int to_parent, int from_parent) | |
| { | |
| struct report r; | |
| char c; | |
| if (unshare(CLONE_NEWNS) || mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL)) | |
| return CHILD_NS; | |
| if (mkdir(at("vol"), 0755) || mount("tmpfs", at("vol"), "tmpfs", 0, NULL) || | |
| mkdir(at("vol2"), 0755) || mount("tmpfs", at("vol2"), "tmpfs", 0, NULL)) | |
| return child_fails(CHILD_NS); | |
| r.n[0] = loop_mount(at("vol2/img"), at("vol/mnt")); | |
| if (r.n[0] < 0) | |
| return child_fails(-r.n[0]); | |
| r.n[1] = loop_mount(at("vol/img"), at("vol2/mnt")); | |
| if (r.n[1] < 0) | |
| return child_fails(-r.n[1]); | |
| if (write(to_parent, &r, sizeof(r)) != sizeof(r)) | |
| return child_fails(CHILD_PIPE); | |
| if (read(from_parent, &c, 1) != 1) | |
| return child_fails(CHILD_PIPE); | |
| return CHILD_OK; | |
| } | |
| /* | |
| * In its own mount namespace the child mounts a tmpfs on vol, | |
| * puts a filesystem image on it, binds a loop device to the image and | |
| * mounts that loop device below. The loop device's backing file is a | |
| * reference on the mount the image is on, held by the loop device, held | |
| * by the mounted filesystem, held by the mount below. | |
| */ | |
| static int loop_child(int to_parent, int from_parent) | |
| { | |
| struct report r = { { -1, -1 } }; | |
| char c; | |
| if (unshare(CLONE_NEWNS) || mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL)) | |
| return CHILD_NS; | |
| if (mkdir(at("vol"), 0755) || mount("tmpfs", at("vol"), "tmpfs", 0, NULL)) | |
| return child_fails(CHILD_NS); | |
| r.n[0] = loop_mount(at("vol/img"), at("vol/mnt")); | |
| if (r.n[0] < 0) | |
| return child_fails(-r.n[0]); | |
| if (write(to_parent, &r, sizeof(r)) != sizeof(r)) | |
| return child_fails(CHILD_PIPE); | |
| /* keep the namespace alive while the parent removes the directory */ | |
| if (read(from_parent, &c, 1) != 1) | |
| return child_fails(CHILD_PIPE); | |
| return CHILD_OK; | |
| } | |
| /* | |
| * The child did not report. Skip if the kernel lacks something, fail with | |
| * what the child said otherwise. A child that a signal killed has failed. | |
| */ | |
| #define CHILD_GAVE_UP(pid) do { \ | |
| int __status; \ | |
| \ | |
| ASSERT_EQ(waitpid(pid, &__status, 0), pid); \ | |
| if (WIFEXITED(__status) && WEXITSTATUS(__status) == CHILD_NOFS) \ | |
| SKIP(return, "test requires the filesystem of the image (FAT or minix)"); \ | |
| if (WIFEXITED(__status) && WEXITSTATUS(__status) == CHILD_SKIP) \ | |
| SKIP(return, "the kernel lacks what this holder needs"); \ | |
| ASSERT_TRUE(false) \ | |
| TH_LOG("child failed to set up: %s %d", \ | |
| WIFEXITED(__status) ? "exit status" : "signal", \ | |
| WIFEXITED(__status) ? WEXITSTATUS(__status) : WTERMSIG(__status)); \ | |
| } while (0) | |
| /* The child has to leave by itself and with nothing to complain about. */ | |
| #define CHILD_LEFT(pid) do { \ | |
| int __status; \ | |
| \ | |
| ASSERT_EQ(waitpid(pid, &__status, 0), pid); \ | |
| ASSERT_TRUE(WIFEXITED(__status)); \ | |
| ASSERT_EQ(WEXITSTATUS(__status), CHILD_OK); \ | |
| } while (0) | |
| /* | |
| * An exclusive open of the device fails while a filesystem holds it, and | |
| * the only filesystem that ever did is the one mounted below the dead | |
| * mount. Give a release in flight a moment. Then the device must clear | |
| * right away rather than only be marked for autoclear. | |
| */ | |
| static void assert_loop_released(struct __test_metadata *_metadata, | |
| FIXTURE_DATA(loop_cycle) *self) | |
| { | |
| int lfd; | |
| for (int i = 0; i < 20; i++) { | |
| lfd = open(self->dev, O_RDONLY | O_EXCL); | |
| if (lfd >= 0) | |
| break; | |
| usleep(100000); | |
| } | |
| EXPECT_GE(lfd, 0) | |
| TH_LOG("%s is still held by the loop mount below the dead mount: nothing refers to either mount and nothing can release them", | |
| self->dev); | |
| if (lfd >= 0) | |
| close(lfd); | |
| lfd = open(self->dev, O_RDWR); | |
| ASSERT_GE(lfd, 0); | |
| ASSERT_EQ(ioctl(lfd, LOOP_CLR_FD), 0); | |
| close(lfd); | |
| ASSERT_TRUE(loop_released(self->sysfs, 5000)) | |
| TH_LOG("%s kept its backing file after LOOP_CLR_FD: the filesystem on it is still mounted somewhere nobody can reach", | |
| self->dev); | |
| /* somebody else may bind it from now on, so the teardown leaves it alone */ | |
| for (int i = 0; i < self->nr_loops; i++) { | |
| char dev[32]; | |
| snprintf(dev, sizeof(dev), "/dev/loop%d", self->loops[i]); | |
| if (!strcmp(dev, self->dev)) | |
| self->loops[i] = -1; | |
| } | |
| } | |
| /* | |
| * rmdir of vol from here, where it is not a mountpoint, detaches | |
| * the child's tmpfs with the loop mount connected below it. Once the child | |
| * is gone nothing refers to either mount. The loop device must then be | |
| * free to give up its backing file, which only happens when the mounted | |
| * filesystem below the detached tmpfs has been released. | |
| */ | |
| TEST_F(loop_cycle, detached_loop_mount_released) | |
| { | |
| int to_parent[2], to_child[2]; | |
| struct report r = { { -1, -1 } }; | |
| char buf[PATH_MAX]; | |
| pid_t pid; | |
| ASSERT_EQ(pipe(to_parent), 0); | |
| ASSERT_EQ(pipe(to_child), 0); | |
| pid = fork(); | |
| ASSERT_GE(pid, 0); | |
| if (pid == 0) { | |
| close(to_parent[0]); | |
| close(to_child[1]); | |
| _exit(loop_child(to_parent[1], to_child[0])); | |
| } | |
| close(to_parent[1]); | |
| close(to_child[0]); | |
| if (read(to_parent[0], &r, sizeof(r)) != sizeof(r)) | |
| CHILD_GAVE_UP(pid); | |
| remember_loop(self, r.n[0]); | |
| snprintf(self->dev, sizeof(self->dev), "/dev/loop%d", r.n[0]); | |
| snprintf(self->sysfs, sizeof(self->sysfs), "/sys/block/loop%d/loop/backing_file", r.n[0]); | |
| ASSERT_EQ(read_sysfs(self->sysfs, buf, sizeof(buf)), 0); | |
| ASSERT_NE(strstr(buf, "/vol/img"), NULL); | |
| /* not a mountpoint in this namespace, so the directory can go */ | |
| ASSERT_EQ(rmdir(at("vol")), 0); | |
| /* the child leaves: its namespace and every reference it held are gone */ | |
| ASSERT_EQ(write(to_child[1], "", 1), 1); | |
| CHILD_LEFT(pid); | |
| close(to_parent[0]); | |
| close(to_child[1]); | |
| /* nothing can reach the two mounts any more */ | |
| ASSERT_EQ(access(at("vol"), F_OK), -1); | |
| ASSERT_FALSE(mounted_anywhere(self->dev)); | |
| assert_loop_released(_metadata, self); | |
| } | |
| /* | |
| * The same two mounts in a detached tree: a clone of vol from | |
| * open_tree(), the image opened through the clone so that the loop device | |
| * holds the clone, and the loop mount moved below the clone. The last | |
| * close of the tree's fd dissolves the tree with the loop mount left | |
| * connected below the dead clone, and nothing refers to either afterwards. | |
| */ | |
| TEST_F(loop_cycle, dissolved_tree_loop_mount_released) | |
| { | |
| int tfd, ifd, fsfd, mfd, n; | |
| char buf[PATH_MAX]; | |
| fsfd = sys_fsopen("vfat", 0); | |
| if (fsfd < 0) | |
| fsfd = sys_fsopen("msdos", 0); | |
| if (fsfd < 0) | |
| SKIP(return, "test requires a FAT filesystem"); | |
| ASSERT_EQ(mkdir(at("vol"), 0755), 0); | |
| ASSERT_EQ(mount("tmpfs", at("vol"), "tmpfs", 0, NULL), 0); | |
| tfd = sys_open_tree(AT_FDCWD, at("vol"), OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC); | |
| ASSERT_GE(tfd, 0); | |
| /* the image, opened through the clone: the loop device holds the clone */ | |
| ifd = openat(tfd, "img", O_RDWR | O_CREAT | O_EXCL, 0600); | |
| ASSERT_GE(ifd, 0); | |
| ASSERT_EQ(write_fat12(ifd), 0); | |
| n = loop_bind(ifd); | |
| close(ifd); | |
| ASSERT_GE(n, 0); | |
| remember_loop(self, n); | |
| snprintf(self->dev, sizeof(self->dev), "/dev/loop%d", n); | |
| snprintf(self->sysfs, sizeof(self->sysfs), "/sys/block/loop%d/loop/backing_file", n); | |
| /* the loop mount, moved below the clone */ | |
| ASSERT_EQ(mkdirat(tfd, "mnt", 0755), 0); | |
| ASSERT_EQ(sys_fsconfig(fsfd, FSCONFIG_SET_STRING, "source", self->dev, 0), 0); | |
| ASSERT_EQ(sys_fsconfig(fsfd, FSCONFIG_CMD_CREATE, NULL, NULL, 0), 0); | |
| mfd = sys_fsmount(fsfd, 0, 0); | |
| ASSERT_GE(mfd, 0); | |
| close(fsfd); | |
| ASSERT_EQ(sys_move_mount(mfd, "", tfd, "mnt", MOVE_MOUNT_F_EMPTY_PATH), 0); | |
| close(mfd); | |
| ASSERT_EQ(read_sysfs(self->sysfs, buf, sizeof(buf)), 0); | |
| ASSERT_NE(strstr(buf, "img"), NULL); | |
| /* the last fd of the tree: both mounts die, the loop mount connected */ | |
| close(tfd); | |
| /* nothing can reach the two mounts any more */ | |
| ASSERT_FALSE(mounted_anywhere(self->dev)); | |
| assert_loop_released(_metadata, self); | |
| } | |
| /* | |
| * The cycle in two steps: rmdir of vol leaves the loop mount below it | |
| * connected while its image's mount, vol2, is alive; then rmdir of vol2 | |
| * takes that one with the loop mount whose image is on the dead vol. Each | |
| * dead mount now owns a loop mount whose filesystem pins the other. | |
| */ | |
| TEST_F(loop_cycle, crossed_images_released) | |
| { | |
| int to_parent[2], to_child[2]; | |
| struct report r = { { -1, -1 } }; | |
| char sysfs[2][64], dev[2][32]; | |
| pid_t pid; | |
| ASSERT_EQ(pipe(to_parent), 0); | |
| ASSERT_EQ(pipe(to_child), 0); | |
| pid = fork(); | |
| ASSERT_GE(pid, 0); | |
| if (pid == 0) { | |
| close(to_parent[0]); | |
| close(to_child[1]); | |
| _exit(crossed_child(to_parent[1], to_child[0])); | |
| } | |
| close(to_parent[1]); | |
| close(to_child[0]); | |
| if (read(to_parent[0], &r, sizeof(r)) != sizeof(r)) | |
| CHILD_GAVE_UP(pid); | |
| for (int i = 0; i < 2; i++) { | |
| remember_loop(self, r.n[i]); | |
| snprintf(dev[i], sizeof(dev[i]), "/dev/loop%d", r.n[i]); | |
| snprintf(sysfs[i], sizeof(sysfs[i]), "/sys/block/loop%d/loop/backing_file", r.n[i]); | |
| } | |
| /* step one: the mount with the first loop mount below it goes */ | |
| ASSERT_EQ(rmdir(at("vol")), 0); | |
| /* step two: the other one, with the loop mount whose image is on the first */ | |
| ASSERT_EQ(rmdir(at("vol2")), 0); | |
| /* the child leaves: its namespace and every reference it held are gone */ | |
| ASSERT_EQ(write(to_child[1], "", 1), 1); | |
| CHILD_LEFT(pid); | |
| close(to_parent[0]); | |
| close(to_child[1]); | |
| /* nothing can reach the four mounts any more */ | |
| for (int i = 0; i < 2; i++) { | |
| ASSERT_FALSE(mounted_anywhere(dev[i])); | |
| strcpy(self->dev, dev[i]); | |
| strcpy(self->sysfs, sysfs[i]); | |
| assert_loop_released(_metadata, self); | |
| } | |
| } | |
| /* | |
| * The holders. Each keeps a file or a path on a mount P for as long as | |
| * its own filesystem or device lives, and each has that filesystem or | |
| * device mounted at C below P. Once another namespace's rmdir has | |
| * detached P with C connected below it and the child is gone, P is owned | |
| * by nobody, C by P, and P is kept by whatever the holder still holds. | |
| * | |
| * P is a loop mount so that its death can be observed: the loop device | |
| * gives its backing file up when P's superblock goes. The image sits on | |
| * a tmpfs next to P, not above it, so the loop device's own reference is | |
| * not part of the picture. | |
| */ | |
| enum holder { | |
| HOLDER_AUTOFS, /* a FIFO on P as the daemon's pipe */ | |
| HOLDER_ZRAM, /* a device node on P as the writeback device */ | |
| HOLDER_ECRYPTFS, /* a directory on P as the lower directory */ | |
| HOLDER_BINFMT_MISC, /* an executable on P as an 'F' interpreter */ | |
| HOLDER_FUSE, /* a file on P as a passthrough backing file */ | |
| HOLDER_ZLOOP, /* a directory on P for the zone files */ | |
| HOLDER_GADGET, /* a file on P as a mass storage LUN, over dummy_hcd */ | |
| HOLDER_MD, /* a file on P as an array's bitmap file */ | |
| }; | |
| #define HOLDER_IMG at("img/p.img") | |
| #define HOLDER_MNT at("p") | |
| #define HOLDER_BELOW at("p/c") | |
| static int write_file(const char *path, const char *s) | |
| { | |
| int fd = open(path, O_WRONLY); | |
| ssize_t n; | |
| if (fd < 0) | |
| return -1; | |
| n = write(fd, s, strlen(s)); | |
| close(fd); | |
| return n == (ssize_t)strlen(s) ? 0 : -1; | |
| } | |
| /* Bind a free loop device to @img; the device number. */ | |
| static int loop_attach(const char *img) | |
| { | |
| int ifd, n; | |
| ifd = open(img, O_RDWR); | |
| if (ifd < 0) | |
| return -1; | |
| n = loop_bind(ifd); | |
| close(ifd); | |
| return n; | |
| } | |
| static int holder_autofs(void) | |
| { | |
| char opts[64]; | |
| int pfd; | |
| if (mkfifo(at("p/pipe"), 0600)) | |
| return CHILD_HOLDER; | |
| pfd = open(at("p/pipe"), O_RDWR); | |
| if (pfd < 0) | |
| return CHILD_HOLDER; | |
| snprintf(opts, sizeof(opts), "fd=%d,minproto=5,maxproto=5", pfd); | |
| if (mount("autofs", HOLDER_BELOW, "autofs", 0, opts)) | |
| return errno == ENODEV ? CHILD_SKIP : CHILD_HOLDER; | |
| close(pfd); /* the mount keeps its own */ | |
| return CHILD_OK; | |
| } | |
| /* | |
| * zram's writeback device has to be a block device node, and that node has | |
| * to be on P. Point it at a second loop device. | |
| */ | |
| static void zram_reset(void); | |
| static int holder_zram(void) | |
| { | |
| char dev[32], buf[64]; | |
| struct stat st; | |
| int fd, n; | |
| if (access("/sys/block/zram0/backing_dev", W_OK)) | |
| return CHILD_SKIP; | |
| /* somebody else's device, leave it alone */ | |
| if (read_sysfs("/sys/block/zram0/initstate", buf, sizeof(buf)) || buf[0] != '0') | |
| return CHILD_SKIP; | |
| fd = open(at("img/wb.img"), O_RDWR | O_CREAT | O_EXCL, 0600); | |
| if (fd < 0 || ftruncate(fd, IMAGE_SIZE)) | |
| return CHILD_HOLDER; | |
| close(fd); | |
| n = loop_attach(at("img/wb.img")); | |
| if (n < 0) | |
| return CHILD_HOLDER; | |
| /* the minor is not the number of the device when loop has partitions */ | |
| snprintf(dev, sizeof(dev), "/dev/loop%d", n); | |
| if (stat(dev, &st) || mknod(at("p/wbdev"), S_IFBLK | 0600, st.st_rdev)) | |
| return CHILD_HOLDER; | |
| if (write_file("/sys/block/zram0/backing_dev", at("p/wbdev"))) | |
| return CHILD_HOLDER; | |
| snprintf(buf, sizeof(buf), "%d", 4 * 1024 * 1024); | |
| if (write_file("/sys/block/zram0/disksize", buf)) | |
| goto undo; | |
| fd = open("/dev/zram0", O_RDWR); | |
| if (fd < 0) | |
| goto undo; | |
| n = write_fat12_4k(fd, 1024); /* zram has 4 KiB blocks */ | |
| close(fd); /* the reset in undo is refused while the device is open */ | |
| if (n) | |
| goto undo; | |
| if (mount_fat("/dev/zram0", HOLDER_BELOW)) { | |
| n = errno == ENODEV ? CHILD_NOFS : CHILD_HOLDER; | |
| zram_reset(); | |
| return n; | |
| } | |
| return CHILD_OK; | |
| undo: | |
| zram_reset(); | |
| return CHILD_HOLDER; | |
| } | |
| /* Is @alg in /proc/crypto? A module is listed once a request has loaded it. */ | |
| static bool crypto_has(const char *alg) | |
| { | |
| char line[256], name[64]; | |
| bool found = false; | |
| FILE *f; | |
| f = fopen("/proc/crypto", "r"); | |
| if (!f) | |
| return true; /* no way to tell, assume it is */ | |
| while (!found && fgets(line, sizeof(line), f)) | |
| found = sscanf(line, "name : %63s", name) == 1 && !strcmp(name, alg); | |
| fclose(f); | |
| return found; | |
| } | |
| /* The kernel's auth token layout, which userspace has to match byte for byte. */ | |
| struct ecryptfs_auth_tok { | |
| __u16 version; | |
| __u16 token_type; | |
| __u32 flags; | |
| struct { | |
| __u32 flags, encrypted_key_size, decrypted_key_size; | |
| __u8 encrypted_key[512], decrypted_key[64]; | |
| } session_key; | |
| __u8 reserved[32]; | |
| struct { | |
| __u32 password_bytes; | |
| __s32 hash_algo; | |
| __u32 hash_iterations, session_key_encryption_key_bytes, flags; | |
| __u8 session_key_encryption_key[64]; | |
| __u8 signature[17]; | |
| __u8 salt[8]; | |
| } password; | |
| } __attribute__((packed)); | |
| #define ECRYPTFS_SIG "0123456789abcdef" | |
| static int holder_ecryptfs(void) | |
| { | |
| struct ecryptfs_auth_tok tok = { | |
| .version = 0x0004, | |
| .token_type = 0, /* ECRYPTFS_PASSWORD */ | |
| .password.session_key_encryption_key_bytes = 16, | |
| .password.flags = 0x02, /* ECRYPTFS_SESSION_KEY_ENCRYPTION_KEY_SET */ | |
| .password.signature = ECRYPTFS_SIG, | |
| }; | |
| /* a session keyring of this child's own, so that the key goes with it */ | |
| if (syscall(__NR_keyctl, KEYCTL_JOIN_SESSION_KEYRING, NULL) < 0) | |
| return errno == ENOSYS ? CHILD_SKIP : CHILD_HOLDER; | |
| if (syscall(__NR_add_key, "user", ECRYPTFS_SIG, &tok, sizeof(tok), | |
| KEY_SPEC_SESSION_KEYRING) < 0) | |
| return CHILD_HOLDER; | |
| if (mkdir(at("p/lower"), 0755)) | |
| return CHILD_HOLDER; | |
| if (mount(at("p/lower"), HOLDER_BELOW, "ecryptfs", 0, | |
| "ecryptfs_sig=" ECRYPTFS_SIG ",ecryptfs_cipher=aes,ecryptfs_key_bytes=16")) { | |
| /* EINVAL without the cipher; a module is loaded by the attempt */ | |
| if (errno == ENODEV || (errno == EINVAL && !crypto_has("aes"))) | |
| return CHILD_SKIP; | |
| return CHILD_HOLDER; | |
| } | |
| return CHILD_OK; | |
| } | |
| /* | |
| * binfmt_misc instances are per user namespace, so the mount below P is | |
| * made from a new one, which gets a copy of P. | |
| */ | |
| static int holder_binfmt_misc(void) | |
| { | |
| static const char interp[] = "#!/bin/true\n"; | |
| char reg[PATH_MAX]; | |
| int out; | |
| /* 'F' opens the interpreter at registration, nothing runs it here */ | |
| out = open(at("p/interp"), O_WRONLY | O_CREAT | O_EXCL, 0755); | |
| if (out < 0 || write(out, interp, sizeof(interp) - 1) != sizeof(interp) - 1) | |
| return CHILD_HOLDER; | |
| close(out); | |
| /* ENOSPC: user.max_user_namespaces is 0, EPERM: an LSM says no */ | |
| if (unshare(CLONE_NEWUSER | CLONE_NEWNS)) { | |
| if (errno == EINVAL || errno == ENOSPC || errno == EPERM) | |
| return CHILD_SKIP; | |
| return CHILD_HOLDER; | |
| } | |
| if (write_file("/proc/self/setgroups", "deny") || | |
| write_file("/proc/self/uid_map", "0 0 1") || | |
| write_file("/proc/self/gid_map", "0 0 1")) | |
| return CHILD_HOLDER; | |
| if (mount("binfmt_misc", HOLDER_BELOW, "binfmt_misc", 0, NULL)) | |
| return errno == ENODEV ? CHILD_SKIP : CHILD_HOLDER; | |
| snprintf(reg, sizeof(reg), ":cycle:E::cyc::%s:F", at("p/interp")); | |
| if (write_file(at("p/c/register"), reg)) | |
| return CHILD_HOLDER; | |
| return CHILD_OK; | |
| } | |
| /* | |
| * A fuse server that only ever answers FUSE_INIT, with passthrough on, and | |
| * then registers a file on P as a backing file. The registration alone | |
| * makes the fuse superblock hold the file. | |
| */ | |
| static int holder_fuse(void) | |
| { | |
| struct fuse_backing_map map = {}; | |
| struct fuse_in_header *ih; | |
| struct fuse_init_out init = { | |
| .major = FUSE_KERNEL_VERSION, | |
| .minor = FUSE_KERNEL_MINOR_VERSION, | |
| .flags = FUSE_INIT_EXT, | |
| .flags2 = FUSE_PASSTHROUGH >> 32, | |
| .max_write = 4096, | |
| .max_stack_depth = 1, | |
| }; | |
| struct fuse_out_header oh = { .len = sizeof(oh) + sizeof(init) }; | |
| struct iovec iov[2] = { { &oh, sizeof(oh) }, { &init, sizeof(init) } }; | |
| char opts[64], buf[8192]; | |
| int ffd, bfd; | |
| ssize_t n; | |
| bfd = open(at("p/backing"), O_RDWR | O_CREAT | O_EXCL, 0600); | |
| if (bfd < 0) | |
| return CHILD_HOLDER; | |
| ffd = open("/dev/fuse", O_RDWR); | |
| if (ffd < 0) | |
| return CHILD_SKIP; | |
| snprintf(opts, sizeof(opts), "fd=%d,rootmode=40000,user_id=0,group_id=0", ffd); | |
| if (mount("fuse", HOLDER_BELOW, "fuse", 0, opts)) | |
| return errno == ENODEV ? CHILD_SKIP : CHILD_HOLDER; | |
| n = read(ffd, buf, sizeof(buf)); | |
| ih = (void *)buf; | |
| if (n < (ssize_t)sizeof(*ih) || ih->opcode != FUSE_INIT) | |
| return CHILD_HOLDER; | |
| oh.unique = ih->unique; | |
| if (writev(ffd, iov, 2) != (ssize_t)oh.len) | |
| return CHILD_HOLDER; | |
| map.fd = bfd; | |
| if (ioctl(ffd, FUSE_DEV_IOC_BACKING_OPEN, &map) < 0) { | |
| /* EOPNOTSUPP: no FUSE_PASSTHROUGH, ENOTTY: no such ioctl */ | |
| if (errno == EPERM || errno == EOPNOTSUPP || errno == ENOTTY) | |
| return CHILD_SKIP; | |
| return CHILD_HOLDER; | |
| } | |
| close(bfd); /* the connection keeps its own */ | |
| return CHILD_OK; /* ffd stays open until the child exits */ | |
| } | |
| /* Wait for a device node the kernel is about to create. */ | |
| static int open_when_there(const char *dev, int flags, int ms) | |
| { | |
| int fd; | |
| for (; ms > 0; ms -= 100) { | |
| fd = open(dev, flags); | |
| if (fd >= 0) | |
| return fd; | |
| usleep(100000); | |
| } | |
| return -1; | |
| } | |
| /* | |
| * zloop keeps every zone file open. One conventional zone is enough for a | |
| * FAT image, the sequential one stays empty. | |
| */ | |
| static int holder_zloop(void) | |
| { | |
| char cmd[PATH_MAX]; | |
| int fd, ret = CHILD_HOLDER; | |
| if (access("/dev/zloop-control", W_OK)) | |
| return CHILD_SKIP; | |
| /* somebody else's device, leave it alone */ | |
| if (!access("/sys/block/zloop0", F_OK)) | |
| return CHILD_SKIP; | |
| if (mkdir(at("p/zl"), 0755) || mkdir(at("p/zl/0"), 0755)) | |
| return CHILD_HOLDER; | |
| snprintf(cmd, sizeof(cmd), | |
| "add id=0,capacity_mb=8,zone_size_mb=4,conv_zones=1,base_dir=%s", | |
| at("p/zl")); | |
| if (write_file("/dev/zloop-control", cmd)) | |
| return CHILD_HOLDER; | |
| fd = open_when_there("/dev/zloop0", O_RDWR, 5000); | |
| if (fd < 0 || write_fat12_4k(fd, 1024)) /* 4 KiB blocks, like P */ | |
| goto undo; | |
| close(fd); | |
| fd = -1; | |
| if (mount_fat("/dev/zloop0", HOLDER_BELOW)) { | |
| ret = errno == ENODEV ? CHILD_NOFS : CHILD_HOLDER; | |
| goto undo; | |
| } | |
| return CHILD_OK; | |
| undo: | |
| if (fd >= 0) | |
| close(fd); | |
| write_file("/dev/zloop-control", "remove id=0"); | |
| return ret; | |
| } | |
| static int mount_configfs(void) | |
| { | |
| if (access("/sys/kernel/config", F_OK)) | |
| return -1; | |
| if (mount("configfs", "/sys/kernel/config", "configfs", 0, NULL) && errno != EBUSY) | |
| return -1; | |
| return 0; | |
| } | |
| /* Take the gadget out of configfs again, in the reverse order of its creation. */ | |
| static void gadget_remove(void) | |
| { | |
| if (mount_configfs()) | |
| return; | |
| write_file(gat("/functions/mass_storage.0/lun.0/file"), "\n"); | |
| write_file(gat("/UDC"), "\n"); | |
| unlink(gat("/configs/c.1/mass_storage.0")); | |
| rmdir(gat("/configs/c.1/strings/0x409")); | |
| rmdir(gat("/configs/c.1")); | |
| rmdir(gat("/functions/mass_storage.0")); | |
| rmdir(gat("/strings/0x409")); | |
| rmdir(gat("")); | |
| } | |
| /* The disk usb-storage created for the gadget, by the LUN's inquiry string. */ | |
| static int find_gadget_disk(char *dev, size_t len, int ms) | |
| { | |
| char path[PATH_MAX], model[64]; | |
| struct dirent *de; | |
| DIR *d; | |
| for (; ms > 0; ms -= 100, usleep(100000)) { | |
| d = opendir("/sys/block"); | |
| if (!d) | |
| return -1; | |
| while ((de = readdir(d))) { | |
| if (strncmp(de->d_name, "sd", 2)) | |
| continue; | |
| snprintf(path, sizeof(path), "/sys/block/%s/device/model", de->d_name); | |
| if (read_sysfs(path, model, sizeof(model)) || | |
| strncmp(model, "File-Stor Gadget", 16)) | |
| continue; | |
| snprintf(dev, len, "/dev/%s", de->d_name); | |
| closedir(d); | |
| return 0; | |
| } | |
| closedir(d); | |
| } | |
| return -1; | |
| } | |
| /* | |
| * A mass storage gadget bound to the dummy UDC, so that this kernel is | |
| * also the USB host that sees the LUN as a SCSI disk. sd locks the | |
| * medium on open, which is what keeps the LUN's file from being ejected. | |
| */ | |
| #define GADGET_STEP(x) do { \ | |
| if (x) { \ | |
| fprintf(stderr, "gadget: %s failed: %s\n", #x, strerror(errno)); \ | |
| gadget_remove(); \ | |
| return CHILD_HOLDER; \ | |
| } \ | |
| } while (0) | |
| static int holder_gadget(void) | |
| { | |
| char dev[PATH_MAX]; | |
| int fd; | |
| if (mount_configfs()) | |
| return CHILD_SKIP; | |
| if (access("/sys/kernel/config/usb_gadget", F_OK) || | |
| access("/sys/class/udc/dummy_udc.0", F_OK)) | |
| return CHILD_SKIP; | |
| fd = open(at("p/lun.img"), O_RDWR | O_CREAT | O_EXCL, 0600); | |
| if (fd < 0 || write_fat12(fd)) | |
| return CHILD_HOLDER; | |
| close(fd); | |
| GADGET_STEP(mkdir(gat(""), 0755)); | |
| GADGET_STEP(write_file(gat("/idVendor"), "0x1d6b")); | |
| GADGET_STEP(write_file(gat("/idProduct"), "0x0104")); | |
| GADGET_STEP(mkdir(gat("/strings/0x409"), 0755)); | |
| GADGET_STEP(write_file(gat("/strings/0x409/serialnumber"), "1")); | |
| GADGET_STEP(write_file(gat("/strings/0x409/manufacturer"), "kselftest")); | |
| GADGET_STEP(write_file(gat("/strings/0x409/product"), "cycle")); | |
| GADGET_STEP(mkdir(gat("/configs/c.1"), 0755)); | |
| GADGET_STEP(mkdir(gat("/configs/c.1/strings/0x409"), 0755)); | |
| GADGET_STEP(write_file(gat("/configs/c.1/strings/0x409/configuration"), "c")); | |
| GADGET_STEP(mkdir(gat("/functions/mass_storage.0"), 0755)); | |
| GADGET_STEP(write_file(gat("/functions/mass_storage.0/lun.0/removable"), "1")); | |
| GADGET_STEP(write_file(gat("/functions/mass_storage.0/lun.0/file"), at("p/lun.img"))); | |
| GADGET_STEP(symlink(gat("/functions/mass_storage.0"), gat("/configs/c.1/mass_storage.0"))); | |
| /* EBUSY: the controller is somebody else's, leave it alone */ | |
| if (write_file(gat("/UDC"), "dummy_udc.0")) { | |
| fd = errno == EBUSY ? CHILD_SKIP : CHILD_HOLDER; | |
| gadget_remove(); | |
| return fd; | |
| } | |
| /* usb-storage waits a second before it scans the device */ | |
| if (find_gadget_disk(dev, sizeof(dev), 15000)) { | |
| /* without usb-storage and sd the LUN never shows up as a disk */ | |
| fd = (access("/sys/bus/usb/drivers/usb-storage", F_OK) || | |
| access("/sys/bus/scsi/drivers/sd", F_OK)) ? CHILD_SKIP : CHILD_HOLDER; | |
| gadget_remove(); | |
| return fd; | |
| } | |
| fd = open_when_there(dev, O_RDONLY, 5000); | |
| GADGET_STEP(fd < 0); | |
| close(fd); | |
| if (mount_fat(dev, HOLDER_BELOW)) { | |
| fd = errno == ENODEV ? CHILD_NOFS : CHILD_HOLDER; | |
| gadget_remove(); | |
| return fd; | |
| } | |
| return CHILD_OK; | |
| } | |
| #define BITMAP_MAGIC 0x6d746962 | |
| /* | |
| * A RAID1 of one loop device, not persistent, with its bitmap in a file | |
| * on P. The bitmap file needs a superblock the kernel accepts; sync_size, | |
| * uuid and events are not looked at for a non-persistent array. | |
| */ | |
| static int holder_md(void) | |
| { | |
| struct { | |
| __u32 magic, version; | |
| __u8 uuid[16]; | |
| __u64 events, events_cleared, sync_size; | |
| __u32 state, chunksize, daemon_sleep, write_behind; | |
| } bsb = { | |
| .magic = BITMAP_MAGIC, | |
| .version = 4, | |
| .chunksize = 64 * 1024, | |
| .daemon_sleep = 5, | |
| }; | |
| mdu_array_info_t info = { | |
| .level = 1, | |
| .raid_disks = 1, | |
| .size = 8 * 1024, /* KiB */ | |
| .not_persistent = 1, | |
| }; | |
| mdu_disk_info_t disk = { | |
| .major = 7, | |
| .state = (1 << MD_DISK_ACTIVE) | (1 << MD_DISK_SYNC), | |
| }; | |
| int fd, mdfd, bfd, n, ret = CHILD_HOLDER; | |
| char dev[32], buf[4096]; | |
| struct stat st; | |
| fd = open(at("img/md.img"), O_RDWR | O_CREAT | O_EXCL, 0600); | |
| if (fd < 0 || ftruncate(fd, 8 * 1024 * 1024)) | |
| return CHILD_HOLDER; | |
| close(fd); | |
| n = loop_attach(at("img/md.img")); | |
| if (n < 0) | |
| return CHILD_HOLDER; | |
| /* the minor is not the number of the device when loop has partitions */ | |
| snprintf(dev, sizeof(dev), "/dev/loop%d", n); | |
| if (stat(dev, &st)) | |
| return CHILD_HOLDER; | |
| disk.major = major(st.st_rdev); | |
| disk.minor = minor(st.st_rdev); | |
| bfd = open(at("p/bitmap"), O_RDWR | O_CREAT | O_EXCL, 0600); | |
| if (bfd < 0 || write(bfd, &bsb, sizeof(bsb)) != sizeof(bsb) || ftruncate(bfd, 4096)) | |
| return CHILD_HOLDER; | |
| if (read_sysfs("/proc/mdstat", buf, sizeof(buf))) { | |
| fprintf(stderr, "md: no /proc/mdstat\n"); | |
| return CHILD_SKIP; | |
| } | |
| if (!strstr(buf, "[raid1]")) { | |
| fprintf(stderr, "md: no raid1 personality: %s\n", buf); | |
| return CHILD_SKIP; | |
| } | |
| /* a node of our own: /dev may not have one and is not ours to change */ | |
| if (mknod(at("img/md0"), S_IFBLK | 0600, makedev(MD_MAJOR, 0))) | |
| return CHILD_HOLDER; | |
| mdfd = open(at("img/md0"), O_RDWR); | |
| if (mdfd < 0) | |
| return CHILD_SKIP; | |
| /* somebody else's array: SET_ARRAY_INFO would say EINVAL, not EBUSY */ | |
| if (!read_sysfs("/sys/block/md0/md/array_state", buf, sizeof(buf)) && | |
| strncmp(buf, "clear", 5)) { | |
| fprintf(stderr, "md: md0 is in use: %s", buf); | |
| close(mdfd); | |
| return CHILD_SKIP; | |
| } | |
| /* the bitmap ops are only installed once a bitmap type is chosen */ | |
| write_file("/sys/block/md0/md/bitmap_type", "bitmap"); | |
| /* EBUSY: somebody else's array, leave it alone */ | |
| if (ioctl(mdfd, SET_ARRAY_INFO, &info)) | |
| return errno == EBUSY ? CHILD_SKIP : CHILD_HOLDER; | |
| /* the array is ours from here on and has to be stopped on every path */ | |
| if (ioctl(mdfd, ADD_NEW_DISK, &disk)) | |
| goto undo; | |
| /* attach the bitmap file before the array runs, the way mdadm does */ | |
| if (ioctl(mdfd, SET_BITMAP_FILE, bfd)) { | |
| fprintf(stderr, "md: SET_BITMAP_FILE: %s\n", strerror(errno)); | |
| if (errno == EINVAL) | |
| ret = CHILD_SKIP; | |
| goto undo; | |
| } | |
| close(bfd); /* the array keeps its own */ | |
| if (ioctl(mdfd, RUN_ARRAY, NULL)) { | |
| fprintf(stderr, "md: RUN_ARRAY: %s\n", strerror(errno)); | |
| goto undo; | |
| } | |
| if (write_fat12(mdfd)) | |
| goto undo; | |
| close(mdfd); | |
| if (mount_fat(at("img/md0"), HOLDER_BELOW)) { | |
| ret = errno == ENODEV ? CHILD_NOFS : CHILD_HOLDER; | |
| mdfd = open(at("img/md0"), O_RDWR); | |
| goto undo; | |
| } | |
| return CHILD_OK; | |
| undo: | |
| if (mdfd >= 0) { | |
| ioctl(mdfd, STOP_ARRAY); | |
| close(mdfd); | |
| } | |
| return ret; | |
| } | |
| /* | |
| * A device keeps its file for as long as it is configured, so once nothing | |
| * below the dead mount is left the device has to be told to let go. With | |
| * the cycle unbroken C's filesystem still holds the device and every one | |
| * of these refuses. | |
| */ | |
| static int holder_let_go_once(enum holder holder) | |
| { | |
| int fd, ret; | |
| switch (holder) { | |
| case HOLDER_ZRAM: | |
| return write_file("/sys/block/zram0/reset", "1"); | |
| case HOLDER_ZLOOP: | |
| return write_file("/dev/zloop-control", "remove id=0"); | |
| case HOLDER_GADGET: | |
| if (mount_configfs()) | |
| return -1; | |
| /* a zero-length write is a no-op for configfs; a newline ejects */ | |
| return write_file(gat("/functions/mass_storage.0/lun.0/file"), "\n"); | |
| case HOLDER_MD: | |
| fd = open(at("img/md0"), O_RDONLY); | |
| if (fd < 0) { | |
| /* the child's node went with its tmpfs */ | |
| mknod(at("md0"), S_IFBLK | 0600, makedev(MD_MAJOR, 0)); | |
| fd = open(at("md0"), O_RDONLY); | |
| } | |
| if (fd < 0) | |
| return -1; | |
| ret = ioctl(fd, STOP_ARRAY); | |
| close(fd); | |
| return ret; | |
| default: | |
| return 0; | |
| } | |
| } | |
| static void zram_reset(void) | |
| { | |
| holder_let_go_once(HOLDER_ZRAM); | |
| } | |
| /* | |
| * In its own mount namespace the child puts the image on a tmpfs next to | |
| * P, mounts P from a loop device, and sets the holder up with its own | |
| * filesystem or device mounted at C below P. | |
| */ | |
| static int holder_child(int to_parent, int from_parent, enum holder holder) | |
| { | |
| bool fifo = holder == HOLDER_AUTOFS || holder == HOLDER_ZRAM; | |
| /* room for a 4 MiB zone file or a floppy image on P */ | |
| bool big = holder == HOLDER_ZLOOP || holder == HOLDER_GADGET; | |
| struct report r = { { -1, -1 } }; | |
| char dev[32], c; | |
| int ifd, n, ret; | |
| if (unshare(CLONE_NEWNS) || mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL)) | |
| return CHILD_NS; | |
| if (mkdir(at("img"), 0755) || mount("tmpfs", at("img"), "tmpfs", 0, NULL)) | |
| return child_fails(CHILD_NS); | |
| ifd = open(HOLDER_IMG, O_RDWR | O_CREAT | O_EXCL, 0600); | |
| if (ifd < 0 || (fifo ? write_minix(ifd) : | |
| big ? write_fat12_4k(ifd, 3072) : write_fat12(ifd))) | |
| return child_fails(CHILD_IMAGE); | |
| close(ifd); | |
| n = loop_attach(HOLDER_IMG); | |
| if (n < 0) | |
| return child_fails(CHILD_LOOP); | |
| snprintf(dev, sizeof(dev), "/dev/loop%d", n); | |
| if (mkdir(HOLDER_MNT, 0755)) | |
| return child_fails(CHILD_MOUNT); | |
| if (fifo ? mount(dev, HOLDER_MNT, "minix", 0, NULL) : mount_fat(dev, HOLDER_MNT)) | |
| return child_fails(errno == ENODEV ? CHILD_NOFS : CHILD_MOUNT); | |
| if (mkdir(HOLDER_BELOW, 0755)) | |
| return child_fails(CHILD_MOUNT); | |
| switch (holder) { | |
| case HOLDER_AUTOFS: | |
| ret = holder_autofs(); | |
| break; | |
| case HOLDER_ZRAM: | |
| ret = holder_zram(); | |
| break; | |
| case HOLDER_ECRYPTFS: | |
| ret = holder_ecryptfs(); | |
| break; | |
| case HOLDER_BINFMT_MISC: | |
| ret = holder_binfmt_misc(); | |
| break; | |
| case HOLDER_FUSE: | |
| ret = holder_fuse(); | |
| break; | |
| case HOLDER_ZLOOP: | |
| ret = holder_zloop(); | |
| break; | |
| case HOLDER_GADGET: | |
| ret = holder_gadget(); | |
| break; | |
| case HOLDER_MD: | |
| ret = holder_md(); | |
| break; | |
| default: | |
| ret = CHILD_HOLDER; | |
| } | |
| if (ret != CHILD_OK) | |
| return child_fails(ret); | |
| /* P's device first, then the one the holder has bound for itself */ | |
| r.n[0] = n; | |
| if (nr_bound > 1) | |
| r.n[1] = bound[1]; | |
| if (write(to_parent, &r, sizeof(r)) != sizeof(r) || | |
| read(from_parent, &c, 1) != 1) { | |
| /* the parent went away and will not tell the devices to let go */ | |
| umount2(HOLDER_BELOW, MNT_DETACH); | |
| for (int i = 0; i < 50 && holder_let_go_once(holder); i++) | |
| usleep(100000); | |
| if (holder == HOLDER_GADGET) | |
| gadget_remove(); | |
| return child_fails(CHILD_PIPE); | |
| } | |
| return CHILD_OK; | |
| } | |
| /* | |
| * The release of C's filesystem may still be in flight when the child is | |
| * gone, and a device that is still held refuses to let go, so try for a | |
| * while. With the cycle unbroken it refuses for good. | |
| */ | |
| static void holder_let_go(enum holder holder) | |
| { | |
| for (int i = 0; i < 50; i++) { | |
| if (!holder_let_go_once(holder)) | |
| return; | |
| usleep(100000); | |
| } | |
| } | |
| /* | |
| * rmdir of p from here, where it is a plain directory, detaches | |
| * P in the child's namespace with C connected below it. Once the child is | |
| * gone the holder's file on P is the only thing left that refers to P, | |
| * and it is dropped only when C's filesystem dies, which waits for P. | |
| * The loop device backing P tells whether that resolved. | |
| */ | |
| static void holder_cycle(struct __test_metadata *_metadata, | |
| FIXTURE_DATA(loop_cycle) *self, enum holder holder) | |
| { | |
| struct report r = { { -1, -1 } }; | |
| int to_parent[2], from_parent[2]; | |
| pid_t pid; | |
| ASSERT_EQ(pipe(to_parent), 0); | |
| ASSERT_EQ(pipe(from_parent), 0); | |
| pid = fork(); | |
| ASSERT_GE(pid, 0); | |
| if (pid == 0) { | |
| close(to_parent[0]); | |
| close(from_parent[1]); | |
| _exit(holder_child(to_parent[1], from_parent[0], holder)); | |
| } | |
| close(to_parent[1]); | |
| close(from_parent[0]); | |
| if (read(to_parent[0], &r, sizeof(r)) != sizeof(r)) | |
| CHILD_GAVE_UP(pid); | |
| self->gadget = holder == HOLDER_GADGET; | |
| remember_loop(self, r.n[0]); | |
| remember_loop(self, r.n[1]); | |
| snprintf(self->dev, sizeof(self->dev), "/dev/loop%d", r.n[0]); | |
| snprintf(self->sysfs, sizeof(self->sysfs), "/sys/block/loop%d/loop/backing_file", r.n[0]); | |
| ASSERT_EQ(rmdir(HOLDER_MNT), 0); | |
| ASSERT_EQ(write(from_parent[1], "x", 1), 1); | |
| CHILD_LEFT(pid); | |
| close(to_parent[0]); | |
| close(from_parent[1]); | |
| holder_let_go(holder); | |
| assert_loop_released(_metadata, self); | |
| /* the teardown takes the gadget and the holder's own loop device away */ | |
| } | |
| TEST_F(loop_cycle, autofs_pipe_on_dead_mount_released) | |
| { | |
| holder_cycle(_metadata, self, HOLDER_AUTOFS); | |
| } | |
| TEST_F(loop_cycle, zram_writeback_node_on_dead_mount_released) | |
| { | |
| holder_cycle(_metadata, self, HOLDER_ZRAM); | |
| } | |
| TEST_F(loop_cycle, ecryptfs_lower_on_dead_mount_released) | |
| { | |
| holder_cycle(_metadata, self, HOLDER_ECRYPTFS); | |
| } | |
| TEST_F(loop_cycle, binfmt_misc_interpreter_on_dead_mount_released) | |
| { | |
| holder_cycle(_metadata, self, HOLDER_BINFMT_MISC); | |
| } | |
| TEST_F(loop_cycle, fuse_backing_file_on_dead_mount_released) | |
| { | |
| holder_cycle(_metadata, self, HOLDER_FUSE); | |
| } | |
| TEST_F(loop_cycle, zloop_zone_files_on_dead_mount_released) | |
| { | |
| holder_cycle(_metadata, self, HOLDER_ZLOOP); | |
| } | |
| /* up to 15 s for the disk to show up and 12 s for a device that is held */ | |
| TEST_F_TIMEOUT(loop_cycle, mass_storage_lun_on_dead_mount_released, 120) | |
| { | |
| holder_cycle(_metadata, self, HOLDER_GADGET); | |
| } | |
| TEST_F(loop_cycle, md_bitmap_file_on_dead_mount_released) | |
| { | |
| holder_cycle(_metadata, self, HOLDER_MD); | |
| } | |
| TEST_HARNESS_MAIN |