/* * stik - minimal removable device mount/unmount * https://rawnix.org * * Copyright (c) 2025 zorz@gmx.com * SPDX-License-Identifier: BSD-2-Clause * * Mounts removable devices to /media/. * Supports vfat, exfat, ntfs, ext2/3/4, btrfs, xfs, f2fs, iso9660, udf. * * Usage: stik mount [-nr] /dev/sdX * stik umount /dev/sdX * * Installed setuid root; only root and members of the group plugdev * may use it. Everything below assumes hostile input from an * unprivileged caller: no subprocesses, no shell, no environment trust. * * Filesystems that carry ownership on disk (ext*, btrfs, xfs, f2fs) are * mounted through an idmapped mount that maps on-disk uid/gid 0 to the * caller, so the caller can write to media formatted by root without * anything on disk being modified. Requires Linux 5.12+; falls back to a * plain mount with a warning. */ #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* ------------------------------------------------------------------ * * syscall numbers and constants; kernel headers may predate them * ------------------------------------------------------------------ */ #ifndef SYS_fsopen #define SYS_fsopen 430 #endif #ifndef SYS_fsconfig #define SYS_fsconfig 431 #endif #ifndef SYS_fsmount #define SYS_fsmount 432 #endif #ifndef SYS_move_mount #define SYS_move_mount 429 #endif #ifndef SYS_mount_setattr #define SYS_mount_setattr 442 #endif #define FSCONFIG_SET_STRING 1 #define FSCONFIG_CMD_CREATE 6 #define MOUNT_ATTR_RDONLY 0x00000001 #define MOUNT_ATTR_NOSUID 0x00000002 #define MOUNT_ATTR_NODEV 0x00000004 #define MOUNT_ATTR_IDMAP 0x00100000 #define MOVE_MOUNT_F_EMPTY_PATH 0x00000004 struct stik_mount_attr { uint64_t attr_set; uint64_t attr_clr; uint64_t propagation; uint64_t userns_fd; }; static int sys_fsopen(const char *fsname) { return (int)syscall(SYS_fsopen, fsname, 0U); } static int sys_fsconfig(int fd, unsigned cmd, const char *key, const char *value) { return (int)syscall(SYS_fsconfig, fd, cmd, key, value, 0); } static int sys_fsmount(int fd, unsigned attr) { return (int)syscall(SYS_fsmount, fd, 0U, attr); } static int sys_move_mount(int from_fd, const char *from, int to_fd, const char *to, unsigned flags) { return (int)syscall(SYS_move_mount, from_fd, from, to_fd, to, flags); } static int sys_mount_setattr(int fd, const char *path, unsigned flags, struct stik_mount_attr *attr) { return (int)syscall(SYS_mount_setattr, fd, path, flags, attr, sizeof(*attr)); } /* ------------------------------------------------------------------ */ #define STIK_VERSION "1.2" #define MEDIA_DIR "/media" #define STIK_GROUP "plugdev" static const char *prog = "stik"; static void warnx_(const char *fmt, ...) { va_list ap; fprintf(stderr, "%s: ", prog); va_start(ap, fmt); vfprintf(stderr, fmt, ap); va_end(ap); fputc('\n', stderr); } static void info(const char *fmt, ...) { va_list ap; fputs("==> ", stdout); va_start(ap, fmt); vfprintf(stdout, fmt, ap); va_end(ap); fputc('\n', stdout); fflush(stdout); } /* ------------------------------------------------------------------ * * filesystem probing: read the superblock ourselves. No lsblk, no * popen, no shell, no util-linux dependency. * ------------------------------------------------------------------ */ enum optstyle { OPT_NATIVE, /* ownership lives on disk; idmap candidate */ OPT_DOS, /* uid=,gid=,fmask=,dmask= */ OPT_UIDGID /* uid=,gid= only */ }; struct fsinfo { const char *names[4]; /* NULL-terminated candidates, best first */ enum optstyle style; int rdonly; }; static uint32_t le32(const unsigned char *p) { return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24); } /* * Returns 0 on success, -1 on read error, 1 if nothing recognisable was * found (reason filled in). */ static int probe_fs(const char *device, struct fsinfo *fi, char *reason, size_t rlen) { unsigned char b[4096], x[16]; int fd; ssize_t n; memset(fi, 0, sizeof(*fi)); fd = open(device, O_RDONLY | O_CLOEXEC | O_NONBLOCK); if (fd < 0) { snprintf(reason, rlen, "cannot read %s: %s", device, strerror(errno)); return -1; } n = pread(fd, b, sizeof(b), 0); if (n < (ssize_t)sizeof(b)) { snprintf(reason, rlen, "short read on %s", device); close(fd); return -1; } /* encrypted containers: fail with something actionable */ if (memcmp(b, "LUKS\xba\xbe", 6) == 0) { snprintf(reason, rlen, "%s is a LUKS volume; unlock it first", device); close(fd); return 1; } /* exFAT and NTFS carry their signature at offset 3, before FAT's */ if (memcmp(b + 3, "EXFAT ", 8) == 0) { fi->names[0] = "exfat"; fi->style = OPT_DOS; close(fd); return 0; } if (memcmp(b + 3, "NTFS ", 8) == 0) { fi->names[0] = "ntfs3"; fi->names[1] = "ntfs"; fi->style = OPT_DOS; close(fd); return 0; } if (b[510] == 0x55 && b[511] == 0xAA && (memcmp(b + 0x36, "FAT", 3) == 0 || memcmp(b + 0x52, "FAT32", 5) == 0)) { fi->names[0] = "vfat"; fi->style = OPT_DOS; close(fd); return 0; } if (memcmp(b, "XFSB", 4) == 0) { fi->names[0] = "xfs"; fi->style = OPT_NATIVE; close(fd); return 0; } /* ext2/3/4: superblock at 1024, magic 0xEF53 at sb offset 0x38 */ if (b[1024 + 0x38] == 0x53 && b[1024 + 0x39] == 0xEF) { uint32_t compat = le32(b + 1024 + 0x5C); uint32_t incompat = le32(b + 1024 + 0x60); uint32_t rocompat = le32(b + 1024 + 0x64); if ((incompat & 0x02C0u) || (rocompat & 0x0478u)) { fi->names[0] = "ext4"; } else if (compat & 0x0004u) { fi->names[0] = "ext3"; fi->names[1] = "ext4"; } else { fi->names[0] = "ext2"; fi->names[1] = "ext4"; } fi->style = OPT_NATIVE; close(fd); return 0; } /* f2fs: magic 0xF2F52010 at 1024 */ if (le32(b + 1024) == 0xF2F52010u) { fi->names[0] = "f2fs"; fi->style = OPT_NATIVE; close(fd); return 0; } /* btrfs: "_BHRfS_M" at 0x10040 */ if (pread(fd, x, 8, 0x10040) == 8 && memcmp(x, "_BHRfS_M", 8) == 0) { fi->names[0] = "btrfs"; fi->style = OPT_NATIVE; close(fd); return 0; } /* optical / image volumes at the ISO descriptor */ if (pread(fd, x, 8, 0x8000) == 8) { if (memcmp(x + 1, "CD001", 5) == 0) { fi->names[0] = "iso9660"; fi->style = OPT_UIDGID; fi->rdonly = 1; close(fd); return 0; } if (memcmp(x + 1, "BEA01", 5) == 0) { fi->names[0] = "udf"; fi->style = OPT_UIDGID; fi->rdonly = 1; close(fd); return 0; } } /* * Nothing matched: say something more useful than "unknown". * Names ending in a digit take a "p" before the partition number * (nvme0n1p1, mmcblk0p1), the rest do not (sda1). */ { size_t l = strlen(device); const char *sep = (l > 0 && device[l - 1] >= '0' && device[l - 1] <= '9') ? "p" : ""; if (pread(fd, x, 8, 512) == 8 && memcmp(x, "EFI PART", 8) == 0) { snprintf(reason, rlen, "no filesystem on %s: it holds a GPT " "partition table (try %s%s1)", device, device, sep); close(fd); return 1; } if (b[510] == 0x55 && b[511] == 0xAA) { snprintf(reason, rlen, "no filesystem on %s: it holds a partition " "table (try %s%s1)", device, device, sep); close(fd); return 1; } } { snprintf(reason, rlen, "no recognised filesystem on %s", device); } close(fd); return 1; } /* ------------------------------------------------------------------ * * device gating: only removable / hotplug-bus block devices * ------------------------------------------------------------------ */ static int sysfs_flag_is_one(const char *path) { char c; int fd; ssize_t n; fd = open(path, O_RDONLY | O_CLOEXEC); if (fd < 0) return 0; n = read(fd, &c, 1); close(fd); return (n == 1 && c == '1'); } /* * A device qualifies if the block device (or its parent disk) reports * removable, or if it sits behind a hotplug bus. USB hard disks report * removable=0, SD cards likewise, so the bus path matters too. */ /* * The first line of a small sysfs file, newline stripped. 0 on success. */ static int sysfs_read(const char *path, char *buf, size_t len) { int fd; ssize_t n; fd = open(path, O_RDONLY | O_CLOEXEC); if (fd < 0) return -1; n = read(fd, buf, len - 1); close(fd); if (n <= 0) return -1; buf[n] = '\0'; buf[strcspn(buf, "\n")] = '\0'; return 0; } /* * An MMC card is a hotplug SD card or, on many laptops and boards, the * soldered-in eMMC that holds the system -- including partitions that * are normally not mounted, such as the EFI system partition. The card * type tells them apart: only "SD" is removable media. */ static int mmc_is_sd(const char *devname) { char path[PATH_MAX], type[16]; snprintf(path, sizeof(path), "/sys/class/block/%s/device/type", devname); if (sysfs_read(path, type, sizeof(type)) != 0) { /* partition: the card belongs to the parent disk */ snprintf(path, sizeof(path), "/sys/class/block/%s/../device/type", devname); if (sysfs_read(path, type, sizeof(type)) != 0) return 0; } return strcmp(type, "SD") == 0; } static int device_is_eligible(const char *devname, dev_t rdev) { char path[PATH_MAX], link[PATH_MAX], num[32], want[32]; ssize_t n; snprintf(path, sizeof(path), "/sys/class/block/%s", devname); n = readlink(path, link, sizeof(link) - 1); if (n < 0) return 0; link[n] = '\0'; /* * The checks below go by name, the mount by device node: make sure * both are the same device, so a node called sdb that is really * something else cannot borrow sdb's eligibility. */ snprintf(path, sizeof(path), "/sys/class/block/%s/dev", devname); snprintf(want, sizeof(want), "%u:%u", major(rdev), minor(rdev)); if (sysfs_read(path, num, sizeof(num)) != 0 || strcmp(num, want) != 0) return 0; if (strstr(link, "/usb") != NULL || strstr(link, "/firewire") != NULL) return 1; if (strstr(link, "/mmc_host/") != NULL) return mmc_is_sd(devname); snprintf(path, sizeof(path), "/sys/class/block/%s/removable", devname); if (sysfs_flag_is_one(path)) return 1; /* partition: the parent directory is the whole disk */ snprintf(path, sizeof(path), "/sys/class/block/%s/../removable", devname); if (sysfs_flag_is_one(path)) return 1; return 0; } /* ------------------------------------------------------------------ * * caller identity * ------------------------------------------------------------------ */ static void resolve_caller(uid_t *uid, gid_t *gid) { const char *e; struct passwd *pw; *uid = getuid(); *gid = getgid(); /* Invoked by real root (e.g. under doas): honour the original user. */ if (*uid != 0) return; e = getenv("DOAS_USER"); if (e == NULL || *e == '\0') e = getenv("SUDO_USER"); if (e != NULL && *e != '\0') { pw = getpwnam(e); if (pw != NULL) { *uid = pw->pw_uid; *gid = pw->pw_gid; } } } /* * Only root and members of plugdev may use stik. The real ids are the * caller's; the supplementary groups are not changed by setuid. A * missing group means nobody but root. */ static int caller_allowed(void) { struct group *gr; gid_t *groups; int i, n; if (getuid() == 0) return 1; gr = getgrnam(STIK_GROUP); if (gr == NULL) return 0; if (getgid() == gr->gr_gid) return 1; n = getgroups(0, NULL); if (n <= 0) return 0; groups = calloc((size_t)n, sizeof(*groups)); if (groups == NULL) return 0; n = getgroups(n, groups); for (i = 0; i < n; i++) { if (groups[i] == gr->gr_gid) { free(groups); return 1; } } free(groups); return 0; } /* ------------------------------------------------------------------ * * idmapped mounts * ------------------------------------------------------------------ */ static int write_file(const char *path, const char *data) { int fd; ssize_t n; size_t len = strlen(data); fd = open(path, O_WRONLY | O_CLOEXEC); if (fd < 0) return -1; n = write(fd, data, len); close(fd); return (n == (ssize_t)len) ? 0 : -1; } /* * Create a user namespace whose uid/gid 0 maps to the caller, and return * a file descriptor to it. Used purely as an idmap for the mount; the * helper process exits immediately afterwards. */ static int make_idmap_ns(uid_t uid, gid_t gid) { int ready[2], hold[2], fd = -1; char path[64], map[64]; char c; pid_t pid; if (pipe(ready) != 0) return -1; if (pipe(hold) != 0) { close(ready[0]); close(ready[1]); return -1; } pid = fork(); if (pid < 0) { close(ready[0]); close(ready[1]); close(hold[0]); close(hold[1]); return -1; } if (pid == 0) { close(ready[0]); close(hold[1]); if (unshare(CLONE_NEWUSER) != 0) _exit(1); if (write(ready[1], "x", 1) != 1) _exit(1); /* stay alive until the parent has grabbed the ns fd */ if (read(hold[0], &c, 1) < 0) _exit(1); _exit(0); } close(ready[1]); close(hold[0]); if (read(ready[0], &c, 1) != 1) goto out; snprintf(path, sizeof(path), "/proc/%d/uid_map", (int)pid); snprintf(map, sizeof(map), "0 %u 1\n", (unsigned)uid); if (write_file(path, map) != 0) goto out; snprintf(path, sizeof(path), "/proc/%d/gid_map", (int)pid); snprintf(map, sizeof(map), "0 %u 1\n", (unsigned)gid); if (write_file(path, map) != 0) goto out; snprintf(path, sizeof(path), "/proc/%d/ns/user", (int)pid); fd = open(path, O_RDONLY | O_CLOEXEC); out: close(ready[0]); close(hold[1]); (void)waitpid(pid, NULL, 0); return fd; } /* ------------------------------------------------------------------ * * mounting * ------------------------------------------------------------------ */ struct mopts { uid_t uid; gid_t gid; enum optstyle style; int rdonly; int no_idmap; }; /* * New mount API path. Returns 0 on success, -1 on failure (errno set), * or -2 if the API itself is unavailable and the caller should fall back. */ static int mount_new_api(const char *device, const char *fstype, const char *mountpoint, const struct mopts *mo, int *idmapped) { char buf[32]; struct stik_mount_attr attr; unsigned mattr = MOUNT_ATTR_NOSUID | MOUNT_ATTR_NODEV; int fs, mnt, ns; int saved; *idmapped = 0; fs = sys_fsopen(fstype); if (fs < 0) { if (errno == ENOSYS) return -2; return -1; } if (sys_fsconfig(fs, FSCONFIG_SET_STRING, "source", device) != 0) goto fail_fs; if (mo->style == OPT_DOS || mo->style == OPT_UIDGID) { snprintf(buf, sizeof(buf), "%u", (unsigned)mo->uid); if (sys_fsconfig(fs, FSCONFIG_SET_STRING, "uid", buf) != 0) goto fail_fs; snprintf(buf, sizeof(buf), "%u", (unsigned)mo->gid); if (sys_fsconfig(fs, FSCONFIG_SET_STRING, "gid", buf) != 0) goto fail_fs; } if (mo->style == OPT_DOS) { if (sys_fsconfig(fs, FSCONFIG_SET_STRING, "fmask", "133") != 0) goto fail_fs; if (sys_fsconfig(fs, FSCONFIG_SET_STRING, "dmask", "022") != 0) goto fail_fs; } if (sys_fsconfig(fs, FSCONFIG_CMD_CREATE, NULL, NULL) != 0) goto fail_fs; if (mo->rdonly) mattr |= MOUNT_ATTR_RDONLY; mnt = sys_fsmount(fs, mattr); saved = errno; close(fs); if (mnt < 0) { errno = saved; return -1; } /* * Ownership on disk only matters for filesystems that store it. * Map on-disk 0 to the caller so root-owned media is usable * without touching a single inode. */ if (mo->style == OPT_NATIVE && !mo->no_idmap && mo->uid != 0) { ns = make_idmap_ns(mo->uid, mo->gid); if (ns < 0) { warnx_("could not create id mapping: %s", strerror(errno)); } else { memset(&attr, 0, sizeof(attr)); attr.attr_set = MOUNT_ATTR_IDMAP; attr.userns_fd = (uint64_t)ns; if (sys_mount_setattr(mnt, "", AT_EMPTY_PATH, &attr) == 0) *idmapped = 1; else warnx_("id mapping unsupported here (%s); " "on-disk ownership applies", strerror(errno)); close(ns); } } if (sys_move_mount(mnt, "", AT_FDCWD, mountpoint, MOVE_MOUNT_F_EMPTY_PATH) != 0) { saved = errno; close(mnt); errno = saved; return -1; } close(mnt); return 0; fail_fs: saved = errno; close(fs); errno = saved; return -1; } /* Classic mount(2), for kernels without the new mount API. */ static int mount_legacy(const char *device, const char *fstype, const char *mountpoint, const struct mopts *mo) { char opts[256]; unsigned long flags = MS_NODEV | MS_NOSUID; opts[0] = '\0'; if (mo->style == OPT_DOS) snprintf(opts, sizeof(opts), "uid=%u,gid=%u,fmask=133,dmask=022", (unsigned)mo->uid, (unsigned)mo->gid); else if (mo->style == OPT_UIDGID) snprintf(opts, sizeof(opts), "uid=%u,gid=%u", (unsigned)mo->uid, (unsigned)mo->gid); if (mo->rdonly) flags |= MS_RDONLY; return mount(device, mountpoint, fstype, flags, opts[0] ? opts : NULL); } /* ------------------------------------------------------------------ * * /proc/mounts lookup * ------------------------------------------------------------------ */ static char * find_mountpoint(const char *device) { FILE *fp; struct mntent *ent; char *mountpoint = NULL; fp = setmntent("/proc/mounts", "r"); if (fp == NULL) return NULL; while ((ent = getmntent(fp)) != NULL) { if (strcmp(ent->mnt_fsname, device) == 0) { mountpoint = strdup(ent->mnt_dir); break; } } endmntent(fp); return mountpoint; } /* ------------------------------------------------------------------ * * argument validation * ------------------------------------------------------------------ */ /* * Accept exactly /dev/ where name is a single path component of * safe characters. This is what keeps "/dev/../something" and friends * out of the mountpoint construction below. */ static const char * validate_device(const char *device) { const char *name; size_t i; if (strncmp(device, "/dev/", 5) != 0) return NULL; name = device + 5; if (*name == '\0' || strlen(name) > 64) return NULL; for (i = 0; name[i] != '\0'; i++) { if (!((name[i] >= 'a' && name[i] <= 'z') || (name[i] >= 'A' && name[i] <= 'Z') || (name[i] >= '0' && name[i] <= '9') || name[i] == '-' || name[i] == '_')) return NULL; } return name; } static int prepare_mountpoint(const char *devname, char *out, size_t outlen) { struct stat st; if (mkdir(MEDIA_DIR, 0755) != 0 && errno != EEXIST) { warnx_("cannot create %s: %s", MEDIA_DIR, strerror(errno)); return -1; } snprintf(out, outlen, MEDIA_DIR "/%s", devname); if (mkdir(out, 0755) != 0 && errno != EEXIST) { warnx_("cannot create %s: %s", out, strerror(errno)); return -1; } /* must be a real directory, not a symlink someone left behind */ if (lstat(out, &st) != 0 || !S_ISDIR(st.st_mode)) { warnx_("%s is not a directory", out); return -1; } return 0; } /* ------------------------------------------------------------------ */ static int cmd_mount(const char *device, const char *devname, int rdonly, int no_idmap) { struct fsinfo fi; struct mopts mo; char mountpoint[PATH_MAX]; char reason[256]; char *existing; int i, rc, idmapped = 0, saved_errno = 0; existing = find_mountpoint(device); if (existing != NULL) { warnx_("%s is already mounted at %s", device, existing); free(existing); return 10; } rc = probe_fs(device, &fi, reason, sizeof(reason)); if (rc != 0) { warnx_("%s", reason); return 4; } memset(&mo, 0, sizeof(mo)); resolve_caller(&mo.uid, &mo.gid); mo.style = fi.style; mo.rdonly = rdonly || fi.rdonly; mo.no_idmap = no_idmap; if (prepare_mountpoint(devname, mountpoint, sizeof(mountpoint)) != 0) return 11; info("Mounting %s (%s) to %s", device, fi.names[0], mountpoint); rc = -1; for (i = 0; i < 4 && fi.names[i] != NULL; i++) { rc = mount_new_api(device, fi.names[i], mountpoint, &mo, &idmapped); if (rc == -2) { rc = mount_legacy(device, fi.names[i], mountpoint, &mo); if (rc != 0) rc = -1; else if (mo.style == OPT_NATIVE && !no_idmap && mo.uid != 0) warnx_("kernel too old for id mapping; " "on-disk ownership applies"); } if (rc == 0) break; saved_errno = errno; } if (rc != 0) { errno = saved_errno; warnx_("mount failed: %s", strerror(errno)); rmdir(mountpoint); return 6; } if (mo.style == OPT_NATIVE && !idmapped && !no_idmap && mo.uid != 0) warnx_("files owned by other users remain unwritable; " "run as root to change ownership"); info("Mounted at %s%s", mountpoint, mo.rdonly ? " (read-only)" : ""); return 0; } static int cmd_umount(const char *device, const char *devname, int gone) { char expected[PATH_MAX]; char *mountpoint; mountpoint = find_mountpoint(device); if (mountpoint == NULL) { warnx_("%s is not mounted", device); return 7; } snprintf(expected, sizeof(expected), MEDIA_DIR "/%s", devname); /* * Only ever unmount what stik would have mounted. Without this an * unprivileged caller could unmount /boot or / by naming the right * device node. */ if (strcmp(mountpoint, expected) != 0) { warnx_("%s is mounted at %s, not under " MEDIA_DIR "; refusing", device, mountpoint); free(mountpoint); return 12; } info("Unmounting %s from %s...", device, mountpoint); /* * A stick pulled without unmounting leaves its mount behind with * no device under it; detach it rather than fail on I/O. */ if (umount2(mountpoint, gone ? MNT_DETACH : 0) != 0) { warnx_("unmount failed: %s", strerror(errno)); free(mountpoint); return 8; } if (rmdir(mountpoint) != 0 && errno != ENOTEMPTY && errno != ENOENT) warnx_("could not remove %s: %s", mountpoint, strerror(errno)); free(mountpoint); info("Unmounted."); return 0; } static void usage(void) { fprintf(stderr, "usage: %s mount [-nr] /dev/sdX\n" " %s umount /dev/sdX\n" " %s -h | -V\n" "\n" " -n no id mapping; use ownership as stored on disk\n" " -r mount read-only\n", prog, prog, prog); } int main(int argc, char *argv[]) { const char *action = NULL, *device = NULL, *devname; struct stat st; int rdonly = 0, no_idmap = 0, is_umount; int i; for (i = 1; i < argc; i++) { if (argv[i][0] == '-' && argv[i][1] != '\0') { /* single-letter flags, alone or together: -r -n -rn */ const char *f; for (f = argv[i] + 1; *f != '\0'; f++) { switch (*f) { case 'r': rdonly = 1; break; case 'n': no_idmap = 1; break; case 'h': usage(); return 0; case 'V': printf("stik %s\n", STIK_VERSION); return 0; default: warnx_("unknown option '-%c'", *f); return 1; } } } else if (action == NULL) { action = argv[i]; } else if (device == NULL) { device = argv[i]; } else { usage(); return 1; } } if (action == NULL || device == NULL) { usage(); return 1; } if (!caller_allowed()) { warnx_("permission denied: you must be in the group " STIK_GROUP); return 14; } devname = validate_device(device); if (devname == NULL) { warnx_("device must be /dev/"); return 2; } is_umount = strcmp(action, "umount") == 0 || strcmp(action, "unmount") == 0; if (stat(device, &st) != 0) { /* * The stick was pulled while mounted: its node is gone. * umount may still detach the leftover mount, which it * only ever does at /media/. */ if (is_umount && errno == ENOENT) return cmd_umount(device, devname, 1); warnx_("%s not found or not a block device", device); return 3; } if (!S_ISBLK(st.st_mode)) { warnx_("%s not found or not a block device", device); return 3; } if (!device_is_eligible(devname, st.st_rdev)) { warnx_("%s is not a removable device", device); return 13; } if (strcmp(action, "mount") == 0) return cmd_mount(device, devname, rdonly, no_idmap); if (is_umount) return cmd_umount(device, devname, 0); warnx_("invalid action '%s'", action); usage(); return 9; }