include $(TOPDIR)/rules.mk
PKG_NAME:=uvol
-PKG_VERSION:=1.1
+PKG_VERSION:=1.1.1
PKG_RELEASE:=1
PKG_MAINTAINER:=Daniel Golle <daniel@makrotopia.org>
let json_param = "";
if (cmd in lvm_json_cmds)
json_param = "--reportformat json --units b ";
- let stdout = fs.popen(sprintf("LVM_SUPPRESS_FD_WARNINGS=1 %s %s %s%s", lvm_exec, cmd, json_param, join(" ", args)));
+ let stdout = fs.popen(sprintf("LVM_SUPPRESS_FD_WARNINGS=1 %s %s %s%s", lvm_exec, cmd, json_param, join(" ", map(args, shell_quote))));
let tmp;
if (stdout) {
tmp = stdout.read("all");
} catch(e) {
printf("Failed to parse lvm cli output: %s\n%s\n", e, e.stacktrace[0].context);
}
- return null;
+ // callers read .retval without checking, so failure is reported as a
+ // return value rather than as an absent object
+ return { retval: -1 };
}
function pvs() {
if (!rootdev)
return null;
- let tmp = lvm("pvs", "-o", "vg_name", "-S", sprintf("\"pv_name=~^/dev/%s.*\$\"", rootdev));
- if (tmp.report.pv[0])
+ let tmp = lvm("pvs", "-o", "vg_name", "-S", sprintf("pv_name=~^/dev/%s.*$", rootdev));
+ if (tmp?.report?.pv?.[0])
return tmp.report.pv[0].vg_name;
else
return null;
}
function vgs(vg_name) {
- let tmp = lvm("vgs", "-o", "vg_extent_size,vg_extent_count,vg_free_count", "-S", sprintf("\"vg_name=%s\"", vg_name));
+ let tmp = lvm("vgs", "-o", "vg_extent_size,vg_extent_count,vg_free_count", "-S", sprintf("vg_name=%s", vg_name));
let ret = null;
- if (tmp && tmp.report.vg) {
+ if (tmp?.report?.vg) {
ret = tmp.report.vg;
for (let r in ret) {
r.vg_extent_size = +(rtrim(r.vg_extent_size, "B"));
return null;
}
+// lvm matches lvname with its own regex, so a name is escaped before it goes
+// in. name_valid() limits names to [A-Za-z0-9._-], of which only '.' means
+// anything to that parser; widen this if the permitted set ever widens.
+function lvname_quote(vol_name) {
+ return replace(`${vol_name}`, /\./g, "\\.");
+}
+
function lvs(vg_name, vol_name, extra_exp) {
let ret = [];
- if (!vol_name)
- vol_name = ".*";
+ vol_name = vol_name ? lvname_quote(vol_name) : ".*";
- let lvexpr = sprintf("\"lvname=~^[rw][owp]_%s\$ && vg_name=%s%s%s\"",
+ let lvexpr = sprintf("lvname=~^[rw][owp]_%s$ && vg_name=%s%s%s",
vol_name, vg_name, extra_exp?" && ":"", extra_exp?extra_exp:"");
let tmp = lvm("lvs", "-o", "lv_active,lv_name,lv_full_name,lv_size,lv_path,lv_dm_path", "-S", lvexpr);
- if (tmp && tmp.report.lv) {
+ if (tmp?.report?.lv) {
ret = tmp.report.lv;
for (let r in ret) {
r.lv_size = +(rtrim(r.lv_size, "B"));
function lvs_deleting(vg_name) {
let ret = [];
let tmp = lvm("lvs", "-o", "lv_active,lv_name,lv_full_name,lv_dm_path", "-S",
- sprintf("\"lvname=~^dd_.* && vg_name=%s\"", vg_name));
- if (tmp && tmp.report.lv) {
+ sprintf("lvname=~^dd_.* && vg_name=%s", vg_name));
+ if (tmp?.report?.lv) {
ret = tmp.report.lv;
for (let r in ret)
r.lv_active = (r.lv_active == "active");
function lvs_incomplete(vg_name, vol_name) {
let ret = [];
let tmp = lvm("lvs", "-o", "lv_active,lv_name,lv_full_name,lv_size", "-S",
- sprintf("\"lvname=~^w[op]_%s\$ && vg_name=%s\"", vol_name ?? ".*", vg_name));
- if (tmp && tmp.report.lv) {
+ sprintf("lvname=~^w[op]_%s$ && vg_name=%s",
+ vol_name ? lvname_quote(vol_name) : ".*", vg_name));
+ if (tmp?.report?.lv) {
ret = tmp.report.lv;
for (let r in ret) {
r.lv_active = (r.lv_active == "active");
function lvm_init(ctx) {
cursor = ctx.cursor;
fs = ctx.fs;
+ shell_quote = ctx.shell_quote;
+ umount_dev = ctx.umount_dev;
if (type(fs.access) == "function" && !fs.access(lvm_exec, "x"))
return false;
let devname = getdev(lv);
if (devname) {
unregister(devname);
- system(sprintf("umount /dev/%s 2>/dev/null", devname));
+ umount_dev(devname);
}
}
let use_f2fs = (lv.lv_size > (100 * 1024 * 1024));
if (use_f2fs) {
- let mkfs_ret = system(sprintf("/usr/sbin/mkfs.f2fs -f -l \"%s\" \"%s\"", vol_name, lv.lv_path));
+ let mkfs_ret = system([ "/usr/sbin/mkfs.f2fs", "-f", "-l", vol_name, lv.lv_path ]);
if (mkfs_ret != 0 && mkfs_ret != 134) {
lvchange_r = lvm("lvchange", "-a", "n", lv.lv_full_name);
if (lvchange_r.retval != 0)
return mkfs_ret;
}
} else {
- let mkfs_ret = system(sprintf("/usr/sbin/mke2fs -F -t ext4 -O has_journal -L \"%s\" \"%s\"", vol_name, lv.lv_path));
+ let mkfs_ret = system([ "/usr/sbin/mke2fs", "-F", "-t", "ext4", "-O", "has_journal", "-L", vol_name, lv.lv_path ]);
if (mkfs_ret != 0) {
lvchange_r = lvm("lvchange", "-a", "n", lv.lv_full_name);
if (lvchange_r.retval != 0)
return null;
}
+function tool_path(name) {
+ let dirs = split(getenv("PATH") ?? "", ":");
+ push(dirs, "/usr/sbin", "/usr/bin", "/sbin", "/bin");
+ for (let dir in dirs) {
+ if (!dir)
+ continue;
+ let path = sprintf("%s/%s", dir, name);
+ if (fs.access(path, "x"))
+ return path;
+ }
+ return null;
+}
+
// Grow a rw volume in place; the fs (ext4 or f2fs, chosen at create by size) is
// grown with its own tool. Shrink is refused. The fs-grow tool is an optional
// dependency: if absent, report and refuse rather than grow the LV past the fs.
}
// the fs-grow tool first, so the LV is never left larger than its filesystem
- if (system(sprintf("command -v %s >/dev/null 2>&1", tool)) != 0) {
+ let tool_exec = tool_path(tool);
+ if (!tool_exec) {
warn(sprintf("uvol: %s not found; install %s to grow this %s volume\n",
tool, (fstype == "f2fs") ? "f2fs-tools" : "e2fsprogs", fstype));
return 95;
if (ret.retval != 0)
return ret.retval;
- if (fstype == "f2fs")
- return system(sprintf("resize.f2fs /dev/%s", dev));
-
- return system(sprintf("resize2fs /dev/%s", dev));
+ return system([ tool_exec, sprintf("/dev/%s", dev) ]);
}
// Reap volumes marked for deferred deletion (dd_ prefix). A volume can only be
return dirname;
}
+function ubirename(from_name, to_name) {
+ return system([ "ubirename", sprintf("/dev/%s", ubidev), from_name, to_name ]);
+}
+
+function ubirmvol(volnum) {
+ return system([ "ubirmvol", sprintf("/dev/%s", ubidev), "-n", sprintf("%d", +volnum) ]);
+}
+
+function ubiblock(action, vol_dev) {
+ return system([ "ubiblock", action, sprintf("/dev/%s", vol_dev) ]);
+}
+
function ubi_get_dev(vol_name) {
let wcstring = sprintf("uvol-[rw][owpd]-%s", vol_name);
for (vol_dir in fs.glob(sprintf("/sys/class/ubi/%s_*", ubidev))) {
function mkubifs(vol_dev) {
let temp_mp = mkdtemp();
- system(sprintf("mount -t ubifs /dev/%s %s", vol_dev, temp_mp));
- system(sprintf("umount %s", temp_mp));
+ system([ "mount", "-t", "ubifs", sprintf("/dev/%s", vol_dev), temp_mp ]);
+ system([ "umount", temp_mp ]);
fs.rmdir(temp_mp);
return 0;
}
register = ctx.register;
unregister = ctx.unregister;
+ umount_dev = ctx.umount_dev;
return true;
}
if (vol_dev)
return 17;
- let ret = system(sprintf("ubimkvol /dev/%s -N \"uvol-%s-%s\" -s %d", ubidev, mode, vol_name, vol_size));
+ let ret = system([ "ubimkvol", sprintf("/dev/%s", ubidev), "-N",
+ sprintf("uvol-%s-%s", mode, vol_name), "-s", sprintf("%d", vol_size) ]);
if (ret != 0)
return ret;
if (!vol_dev)
return 2;
- let ret = system(sprintf("ubiupdatevol -t /dev/%s", vol_dev));
+ let ret = system([ "ubiupdatevol", "-t", sprintf("/dev/%s", vol_dev) ]);
if (ret != 0)
return ret;
if (ret != 0)
return ret;
- let ret = system(sprintf("ubirename /dev/%s \"uvol-wp-%s\" \"uvol-wd-%s\"", ubidev, vol_name, vol_name));
+ let ret = ubirename(sprintf("uvol-wp-%s", vol_name), sprintf("uvol-wd-%s", vol_name));
if (ret != 0)
return ret;
if (req_lebs < cur_lebs)
return 22;
- return system(sprintf("ubirsvol /dev/%s -N \"uvol-rw-%s\" -s %d", ubidev, vol_name, vol_size));
+ return system([ "ubirsvol", sprintf("/dev/%s", ubidev), "-N",
+ sprintf("uvol-rw-%s", vol_name), "-s", sprintf("%d", vol_size) ]);
}
function ubi_get_deleting() {
if (v.lebs != want)
continue;
}
- let volnum = split(v.dev, "_")[1];
- system(sprintf("ubirmvol /dev/%s -n %d 2>/dev/null", ubidev, volnum));
+ ubirmvol(split(v.dev, "_")[1]);
}
return 0;
}
let blkdev = sprintf("ubiblock%s", substr(vol_dev, 3));
let isblock = fs.access(sprintf("/dev/%s", blkdev), "r");
- if (isblock && system(sprintf("ubiblock --remove /dev/%s 2>/dev/null", vol_dev)) != 0)
+ if (isblock && ubiblock("--remove", vol_dev) != 0)
continue;
if (!isblock)
- system(sprintf("umount /dev/%s 2>/dev/null", vol_dev));
+ umount_dev(vol_dev);
- if (system(sprintf("ubirmvol /dev/%s -n %d 2>/dev/null", ubidev, volnum)) != 0)
+ if (ubirmvol(volnum) != 0)
continue;
unregister(isblock ? blkdev : vol_dev);
return 2;
let vol_ubiname = read_file(sprintf("/sys/class/ubi/%s/name", vol_dev));
- let ret = system(sprintf("ubirename /dev/%s \"%s\" \"uvol-dd-%s\"", ubidev, vol_ubiname, vol_name));
+ let ret = ubirename(vol_ubiname, sprintf("uvol-dd-%s", vol_name));
if (ret != 0)
return ret;
return 16;
if (vol_mode == "rd") {
- let ret = system(sprintf("ubirename /dev/%s \"uvol-rd-%s\" \"uvol-ro-%s\"", ubidev, vol_name, vol_name));
+ let ret = ubirename(sprintf("uvol-rd-%s", vol_name), sprintf("uvol-ro-%s", vol_name));
if (ret != 0)
return ret;
- ret = system(sprintf("ubiblock --create /dev/%s", vol_dev));
+ ret = ubiblock("--create", vol_dev);
if (ret != 0)
return ret;
return register(vol_name, sprintf("ubiblock%s", substr(vol_dev, 3)), true);
} else if (vol_mode == "wd") {
- let ret = system(sprintf("ubirename /dev/%s \"uvol-wd-%s\" \"uvol-rw-%s\"", ubidev, vol_name, vol_name));
+ let ret = ubirename(sprintf("uvol-wd-%s", vol_name), sprintf("uvol-rw-%s", vol_name));
if (ret != 0)
return ret;
return 16;
else if (vol_mode == "ro") {
unregister(sprintf("ubiblock%s", substr(vol_dev, 3)));
- system(sprintf("umount /dev/ubiblock%s 2>&1 >/dev/null", substr(vol_dev, 3)));
- system(sprintf("ubiblock --remove /dev/%s", vol_dev));
- let ret = system(sprintf("ubirename /dev/%s \"uvol-ro-%s\" \"uvol-rd-%s\"", ubidev, vol_name, vol_name));
+ umount_dev(sprintf("ubiblock%s", substr(vol_dev, 3)));
+ ubiblock("--remove", vol_dev);
+ let ret = ubirename(sprintf("uvol-ro-%s", vol_name), sprintf("uvol-rd-%s", vol_name));
return ret;
} else if (vol_mode == "rw") {
unregister(vol_dev);
- system(sprintf("umount /dev/%s 2>&1 >/dev/null", vol_dev));
- let ret = system(sprintf("ubirename /dev/%s \"uvol-rw-%s\" \"uvol-wd-%s\"", ubidev, vol_name, vol_name));
+ umount_dev(vol_dev);
+ let ret = ubirename(sprintf("uvol-rw-%s", vol_name), sprintf("uvol-wd-%s", vol_name));
return ret;
}
return 0;
let vol_mode = substr(vol_ubiname, 5, 2);
if (vol_mode == "ro") {
- system(sprintf("ubiblock --create /dev/%s", vol_dev));
+ ubiblock("--create", vol_dev);
register(vol_name, sprintf("ubiblock%s", substr(vol_dev, 3)), true);
} else {
register(vol_name, vol_dev, false);
if (vol_mode != "wo")
return 22;
- let ret = system(sprintf("ubiupdatevol -s %d /dev/%s -", write_size, vol_dev));
+ let ret = system([ "ubiupdatevol", "-s", sprintf("%d", write_size),
+ sprintf("/dev/%s", vol_dev), "-" ]);
if (ret)
return ret;
if (verify && !verify(sprintf("/dev/%s", vol_dev)))
return 74;
- system(sprintf("ubirename /dev/%s \"uvol-wo-%s\" \"uvol-rd-%s\"", ubidev, vol_name, vol_name));
+ ubirename(sprintf("uvol-wo-%s", vol_name), sprintf("uvol-rd-%s", vol_name));
return 0;
}
let uci = require("uci");
let cursor = uci ? uci.cursor() : null;
+let shell_quote = function(word) {
+ return "'" + replace(`${word}`, /'/g, "'\\''") + "'";
+};
+
+// volumes are taken down whether or not anything mounted them, so umount is
+// expected to fail and stays quiet. That needs a shell, hence the quoting.
+let umount_dev = function(dev) {
+ return system(sprintf("umount %s 2>/dev/null", shell_quote(sprintf("/dev/%s", dev))));
+};
+
+// volume names reach tool arguments and lock file paths; anything outside
+// this set is refused before it gets there. A leading '.' is reserved for
+// internal volumes such as .meta, and a leading '-' would be taken for an
+// option by the backend tools.
+let name_valid = function(name, allow_internal) {
+ if (type(name) != "string")
+ return false;
+ if (allow_internal && name == ".meta")
+ return true;
+ return !!match(name, /^[A-Za-z0-9_][A-Za-z0-9._-]*$/);
+};
+
let ctx = {};
ctx.cursor = cursor;
ctx.fs = fs;
include("/usr/lib/uvol/mount.uc");
ctx.register = uvol_mount.register;
ctx.unregister = uvol_mount.unregister;
+ctx.shell_quote = shell_quote;
+ctx.umount_dev = umount_dev;
let backend = null;
let tried_backends = [];
return 22;
}
+let json_output = false;
+if (ARGV[0] == "-j") {
+ json_output = true;
+ shift(ARGV);
+}
+
+// only the read-only verbs may name an internal volume such as .meta; the
+// check runs before the lock, which is keyed on the volume name too
+let ro_name_cmds = [ "status", "device", "size", "list" ];
+let rw_name_cmds = [ "create", "remove", "up", "down", "resize", "write" ];
+let ro_name = index(ro_name_cmds, cmd) >= 0;
+if (index(rw_name_cmds, cmd) >= 0 || (ro_name && ARGV[0] != null)) {
+ if (!name_valid(ARGV[0], ro_name)) {
+ printf("invalid volume name\n");
+ exit(22);
+ }
+}
+
// serialise state-mutating commands device-wide; released on process exit.
// create and write additionally serialise per volume, as create may reclaim
// an incomplete volume of the same name and size while a writer is still
exit(0);
}
-let json_output = false;
-if (ARGV[0] == "-j") {
- json_output = true;
- shift(ARGV);
-}
-
let legacy_output = function(var) {
let out = "";
if (type(var) == "array") {
};
let ca_digest = function(cmd, size, path) {
- let src = path ? sprintf("head -c %d %s", size, path) : sprintf("head -c %d", size);
+ let src = path ? sprintf("head -c %d %s", size, shell_quote(path)) : sprintf("head -c %d", size);
let f = fs.popen(sprintf("%s | %s", src, cmd), "r");
if (!f)
return null;