* [PATCH remotes/origin/next 1/5] dm: lvm: Initial Logical Volume Management support
2026-09-17 7:40 [PATCH remotes/origin/next 0/5] dm: lvm: Initial Logical Volume Management support Tobias Waldekranz
@ 2026-09-17 7:40 ` Tobias Waldekranz
2026-09-17 7:40 ` [PATCH remotes/origin/next 2/5] dm: lvm: Add fuzz testers for metadata parser and binary headers Tobias Waldekranz
` (3 subsequent siblings)
4 siblings, 0 replies; 7+ messages in thread
From: Tobias Waldekranz @ 2026-09-17 7:40 UTC (permalink / raw)
To: barebox
Add support for creating device mappings (previously added in the
original device mapper series) based on information stored in LVM2
metadata blocks.
Initially, only linear mappings are supported. I.e., no RAID arrays or
thin provisioning LVs, etc.
This means that Barebox can then access the filesystems (typically, or
whatever data) stored on these logical volumes.
Signed-off-by: Tobias Waldekranz <tobias@waldekranz.com>
---
drivers/block/dm/Kconfig | 10 +
drivers/block/dm/Makefile | 1 +
drivers/block/dm/lvm/Makefile | 2 +
drivers/block/dm/lvm/lvm-core.c | 714 ++++++++++++++++++++++++++++++++
drivers/block/dm/lvm/lvm-md.c | 423 +++++++++++++++++++
drivers/block/dm/lvm/lvm-md.h | 109 +++++
drivers/block/dm/lvm/lvm2.h | 79 ++++
include/lvm.h | 100 +++++
8 files changed, 1438 insertions(+)
create mode 100644 drivers/block/dm/lvm/Makefile
create mode 100644 drivers/block/dm/lvm/lvm-core.c
create mode 100644 drivers/block/dm/lvm/lvm-md.c
create mode 100644 drivers/block/dm/lvm/lvm-md.h
create mode 100644 drivers/block/dm/lvm/lvm2.h
create mode 100644 include/lvm.h
diff --git a/drivers/block/dm/Kconfig b/drivers/block/dm/Kconfig
index 93f29c84a1..35f1cbcf99 100644
--- a/drivers/block/dm/Kconfig
+++ b/drivers/block/dm/Kconfig
@@ -18,3 +18,13 @@ config DM_BLK_VERITY
help
Transparent integrity checking of underlying device using a
pre-computed Merkle tree.
+
+config DM_LVM
+ bool "LVM2 support"
+ depends on DM_BLK_LINEAR
+ select QSORT
+ help
+ Support for the Logical Volume Manager (LVM2) on-disk format.
+ Parses the metadata of physical volumes to assemble volume
+ groups and lets logical volumes be activated as device mapper
+ devices. Only linear logical volumes are currently supported.
diff --git a/drivers/block/dm/Makefile b/drivers/block/dm/Makefile
index 3650f4c856..d3d8a87d05 100644
--- a/drivers/block/dm/Makefile
+++ b/drivers/block/dm/Makefile
@@ -2,3 +2,4 @@
obj-$(CONFIG_DM_BLK) += dm-core.o
obj-$(CONFIG_DM_BLK_LINEAR) += dm-linear.o
obj-$(CONFIG_DM_BLK_VERITY) += dm-verity.o
+obj-$(CONFIG_DM_LVM) += lvm/
diff --git a/drivers/block/dm/lvm/Makefile b/drivers/block/dm/lvm/Makefile
new file mode 100644
index 0000000000..b85f2714d1
--- /dev/null
+++ b/drivers/block/dm/lvm/Makefile
@@ -0,0 +1,2 @@
+# SPDX-License-Identifier: GPL-2.0-only
+obj-y += lvm-core.o lvm-md.o
diff --git a/drivers/block/dm/lvm/lvm-core.c b/drivers/block/dm/lvm/lvm-core.c
new file mode 100644
index 0000000000..befa534815
--- /dev/null
+++ b/drivers/block/dm/lvm/lvm-core.c
@@ -0,0 +1,714 @@
+// SPDX-License-Identifier: GPL-2.0-only
+// SPDX-FileCopyrightText: 2026 Tobias Waldekranz <tobias@waldekranz.com>
+
+#include <block.h>
+#include <disks.h>
+#include <driver.h>
+#include <lvm.h>
+#include <qsort.h>
+#include <stdio.h>
+#include <string.h>
+#include <xfuncs.h>
+
+#include <asm/unaligned.h>
+
+#include <linux/ctype.h>
+#include <linux/err.h>
+#include <linux/kernel.h>
+#include <linux/sprintf.h>
+#include <linux/types.h>
+
+#include "lvm2.h"
+#include "lvm-md.h"
+
+struct lvm_pv_priv {
+ struct lvm_pv pv;
+
+ struct cdev *cdev;
+
+ /* Only populated for a standalone PV returned by lvm_pv_alloc();
+ * the PVs hanging off a VG share the originating PV's metadata.
+ */
+ char *text;
+ struct lvm_md *md;
+};
+#define to_pv_priv(_pv) container_of((_pv), struct lvm_pv_priv, pv)
+
+struct lvm_seg {
+ struct lvm_pv *pv;
+
+ sector_t start; /* logical start, in sectors */
+ blkcnt_t len; /* in sectors */
+ sector_t phys; /* physical start on the PV, in sectors */
+};
+
+struct lvm_lv_priv {
+ struct lvm_lv lv;
+
+ struct lvm_seg *segs;
+ size_t num_segs;
+};
+#define to_lv_priv(_lv) container_of((_lv), struct lvm_lv_priv, lv)
+
+static void lvm_uuid_strcpy(char dst[LVM_UUID_LEN + 1], const char *src, size_t len)
+{
+ size_t i = 0;
+
+ while (len-- && i < LVM_UUID_LEN) {
+ if (isalnum(*src))
+ dst[i++] = *src;
+ src++;
+ }
+ dst[i] = '\0';
+}
+
+/* Look for a valid LVM2 label in any of the first 4 sectors of cdev,
+ * return the first one found along with the offset to the PV header.
+ */
+static int lvm_read_label(struct cdev *cdev, u8 *sector, u32 *pv_offset)
+{
+ struct lvm2_label *label = (void *)sector;
+ int s;
+
+ for (s = 0; s < LVM2_LABEL_SCAN_SECTORS; s++) {
+ if (cdev_read(cdev, sector, SECTOR_SIZE,
+ (loff_t)s << SECTOR_SHIFT, 0) != SECTOR_SIZE)
+ return -EIO;
+
+ if (memcmp(label->id, LVM2_LABEL_ID, sizeof(label->id)))
+ continue;
+
+ /* A label that was copied or relocated says it lives
+ * somewhere else, and must not be used.
+ */
+ if (get_unaligned_le64(&label->sector) != (u64)s)
+ continue;
+
+ /* The CRC covers everything after the crc field itself. */
+ if (lvm2_crc(&label->pv_offset,
+ SECTOR_SIZE - offsetof(struct lvm2_label, pv_offset))
+ != get_unaligned_le32(&label->crc))
+ continue;
+
+ if (memcmp(label->type, LVM2_LABEL_TYPE, sizeof(label->type)))
+ continue;
+
+ *pv_offset = get_unaligned_le32(&label->pv_offset);
+ if (*pv_offset >= SECTOR_SIZE - sizeof(struct lvm2_pv_header))
+ continue;
+
+ return 0;
+ }
+
+ return -ENOENT;
+}
+
+/* Read the committed text metadata document from one metadata area,
+ * validating its checksum. Returns a NUL-terminated, freshly allocated
+ * buffer and its length, or an error.
+ */
+static int lvm_read_mda(struct cdev *cdev, u64 mda_offset, u64 mda_size,
+ char **textp, size_t *lenp)
+{
+ struct lvm2_md_header *hdr;
+ struct lvm2_md_area *area;
+ u8 hbuf[SECTOR_SIZE];
+ u64 off, size, wrap;
+ char *text;
+ u32 flags;
+
+ if (cdev_read(cdev, hbuf, sizeof(hbuf), mda_offset, 0) != sizeof(hbuf))
+ return -EIO;
+
+ hdr = (void *)hbuf;
+ if (memcmp(hdr->magic, LVM2_MDA_MAGIC, sizeof(hdr->magic)))
+ return -EILSEQ;
+
+ /* The checksum covers the whole header, i.e. the raw location
+ * descriptors as well, but not the checksum itself.
+ */
+ if (lvm2_crc(hdr->magic, SECTOR_SIZE - offsetof(struct lvm2_md_header, magic))
+ != get_unaligned_le32(&hdr->checksum))
+ return -EILSEQ;
+
+ if (get_unaligned_le32(&hdr->version) != LVM2_MDA_VERSION)
+ return -ENOTSUPP;
+
+ /* The first descriptor points at the committed metadata. Any
+ * further ones describe a document that LVM was in the middle
+ * of committing, which we have no business activating.
+ */
+ area = &hdr->area[0];
+ off = get_unaligned_le64(&area->offset);
+ size = get_unaligned_le64(&area->size);
+ flags = get_unaligned_le32(&area->flags);
+
+ if (!size || (flags & LVM2_RAW_LOCN_IGNORED))
+ return -ENOENT;
+
+ /* The metadata lives in a ring buffer that starts right after
+ * the header, and a document may wrap around the end of it.
+ */
+ if (off < SECTOR_SIZE || off >= mda_size || size > mda_size)
+ return -EILSEQ;
+
+ text = malloc(size + 1);
+ if (!text)
+ return -ENOMEM;
+
+ if (off + size > mda_size) {
+ wrap = mda_size - off;
+ if (cdev_read(cdev, text, wrap, mda_offset + off, 0) != (ssize_t)wrap)
+ goto err_free_text;
+ if (cdev_read(cdev, text + wrap, size - wrap, mda_offset + SECTOR_SIZE, 0)
+ != (ssize_t)(size - wrap))
+ goto err_free_text;
+ } else {
+ if (cdev_read(cdev, text, size, mda_offset + off, 0) != (ssize_t)size)
+ goto err_free_text;
+ }
+
+ if (lvm2_crc(text, size) != get_unaligned_le32(&area->checksum))
+ goto err_free_text;
+
+ text[size] = '\0';
+ *textp = text;
+ *lenp = size;
+ return 0;
+
+err_free_text:
+ free(text);
+ return -EILSEQ;
+}
+
+/* Read label + pv_header + committed metadata from cdev. On success
+ * the raw 32-byte PV uuid is returned in uuid[] along with the parsed
+ * metadata document.
+ */
+static int lvm_pv_probe(struct cdev *cdev, char uuid[LVM_UUID_LEN + 1],
+ char **textp, struct lvm_md **mdp)
+{
+ u64 mda_off[8], mda_size[8];
+ struct lvm2_pv_header *pvh;
+ int err, i, num_mda = 0;
+ struct lvm2_area *area;
+ u8 sector[SECTOR_SIZE];
+ u32 pv_offset;
+ char *text;
+ size_t len;
+
+ err = lvm_read_label(cdev, sector, &pv_offset);
+ if (err)
+ return err;
+
+ pvh = (void *)(sector + pv_offset);
+ lvm_uuid_strcpy(uuid, (char *)pvh->uuid, sizeof(pvh->uuid));
+
+ /* Layout is:
+ *
+ * [DATA-AREA-0]
+ * ...
+ * [DATA-AREA-N]
+ * [ZERO-AREA ]
+ * [META-AREA-0]
+ * ...
+ * [META-AREA-N]
+ * [ZERO-AREA ]
+ *
+ * Start by seeking past the data-areas list...
+ */
+ for (area = pvh->area;
+ (u8 *)(area + 1) <= sector + SECTOR_SIZE
+ && get_unaligned_le64(&area->offset);
+ area++)
+ ;
+
+ /* ...and then the zero separator, to find the meta-areas. */
+ for (area++;
+ (u8 *)(area + 1) <= sector + SECTOR_SIZE
+ && num_mda < (int)ARRAY_SIZE(mda_off);
+ area++) {
+ mda_off[num_mda] = get_unaligned_le64(&area->offset);
+ mda_size[num_mda] = get_unaligned_le64(&area->size);
+ if (!mda_off[num_mda])
+ break;
+
+ num_mda++;
+ }
+
+ if (!num_mda)
+ return -ENOENT;
+
+ for (i = 0; i < num_mda; i++) {
+ err = lvm_read_mda(cdev, mda_off[i], mda_size[i], &text, &len);
+ if (err)
+ continue;
+
+ err = lvm_md_parse_alloc(text, len, mdp);
+ if (err) {
+ free(text);
+ continue;
+ }
+
+ *textp = text;
+ return 0;
+ }
+
+ return -EILSEQ;
+}
+
+static int lvm_pv_alloc(struct cdev *cdev, struct lvm_pv **pvptr)
+{
+ struct lvm_pv_priv *pvp;
+ int err;
+
+ pvp = xzalloc(sizeof(*pvp));
+ pvp->cdev = cdev;
+
+ err = lvm_pv_probe(cdev, pvp->pv.uuid, &pvp->text, &pvp->md);
+ if (err) {
+ free(pvp);
+ return err;
+ }
+
+ *pvptr = &pvp->pv;
+ return 0;
+}
+
+static void lvm_pv_free(struct lvm_pv *pv)
+{
+ struct lvm_pv_priv *pvp;
+
+ if (!pv)
+ return;
+
+ pvp = to_pv_priv(pv);
+ lvm_md_free(pvp->md);
+ free(pvp->text);
+ free(pvp->pv.name);
+ free(pvp);
+}
+
+static struct cdev *lvm_cdev_by_uuid(const char *uuid)
+{
+ char found[LVM_UUID_LEN + 1];
+ struct lvm2_pv_header *pvh;
+ u8 sector[SECTOR_SIZE];
+ struct cdev *cdev;
+ u32 pv_offset;
+
+ for_each_cdev(cdev) {
+ if (!cdev_is_block_device(cdev))
+ continue;
+
+ if (lvm_read_label(cdev, sector, &pv_offset))
+ continue;
+
+ pvh = (void *)(sector + pv_offset);
+ lvm_uuid_strcpy(found, (char *)pvh->uuid, sizeof(pvh->uuid));
+
+ if (!strcmp(found, uuid))
+ return cdev;
+ }
+
+ return NULL;
+}
+
+static struct lvm_pv *lvm_vg_add_pv(struct lvm_vg *vg, const struct lvm_md *md,
+ const lvm_tok_t *pvkey, struct lvm_pv *origin)
+{
+ const lvm_tok_t *id, *pvsect = lvm_md_val(md, pvkey);
+ struct lvm_pv_priv *pvp;
+ struct lvm_pv *pv;
+ u64 v;
+
+ if (!pvsect || pvsect->type != LVM_TOK_SECTION)
+ return NULL;
+
+ pvp = xzalloc(sizeof(*pvp));
+ pv = &pvp->pv;
+ pv->vg = vg;
+ pv->name = lvm_md_tok_xstrdup(md, pvkey);
+
+ id = lvm_md_find(md, pvsect, "id");
+ if (id && id->type == LVM_TOK_STRING)
+ lvm_uuid_strcpy(pv->uuid, md->text + id->start, id->end - id->start);
+
+ if (!lvm_md_u64(md, pvsect, "dev_size", &v))
+ pv->dev_size = v;
+ if (!lvm_md_u64(md, pvsect, "pe_start", &v))
+ pv->pe_start = v;
+ if (!lvm_md_u64(md, pvsect, "pe_count", &v))
+ pv->pe_count = v;
+
+ /* The originating PV's device is known for free. Any other
+ * PVs are resolved later via lvm_pv_cdev().
+ */
+ if (origin && !strcmp(pv->uuid, origin->uuid))
+ pvp->cdev = to_pv_priv(origin)->cdev;
+
+ vg->pvs = xrealloc(vg->pvs, (vg->num_pvs + 1) * sizeof(*vg->pvs));
+ vg->pvs[vg->num_pvs++] = pv;
+ return pv;
+}
+
+static struct lvm_pv *lvm_vg_pv_by_name(struct lvm_vg *vg, const char *name,
+ size_t len)
+{
+ size_t i;
+
+ for (i = 0; i < vg->num_pvs; i++) {
+ if (!strncmp(vg->pvs[i]->name, name, len) &&
+ vg->pvs[i]->name[len] == '\0')
+ return vg->pvs[i];
+ }
+
+ return NULL;
+}
+
+/* Parse one segment of an LV. Returns 0 on a supported (linear)
+ * segment, or a negative error for anything we cannot map.
+ */
+static int lvm_lv_add_seg(struct lvm_lv_priv *lpriv, struct lvm_vg *vg,
+ const struct lvm_md *md, const lvm_tok_t *segsect)
+{
+ u64 pe_count, pe_start, pe_offset, stripe_count;
+ const lvm_tok_t *stripes, *pvtok, *offtok;
+ struct lvm_seg *seg;
+ struct lvm_pv *pv;
+ char *type;
+ int err;
+
+ type = lvm_md_strdup(md, segsect, "type");
+ if (!type)
+ return -EINVAL;
+
+ if (lvm_md_u64(md, segsect, "start_extent", &pe_start) ||
+ lvm_md_u64(md, segsect, "extent_count", &pe_count)) {
+ err = -EINVAL;
+ goto out;
+ }
+
+ err = -ENOTSUPP;
+
+ if (strcmp(type, "striped") ||
+ lvm_md_u64(md, segsect, "stripe_count", &stripe_count) ||
+ stripe_count != 1)
+ goto out;
+
+ stripes = lvm_md_find(md, segsect, "stripes");
+ if (!stripes || stripes->type != LVM_TOK_ARRAY)
+ goto out;
+
+ pvtok = lvm_md_first(md, stripes);
+ offtok = pvtok ? lvm_md_next(md, stripes, pvtok) : NULL;
+ if (!pvtok || !offtok || lvm_md_tok_u64(md, offtok, &pe_offset))
+ goto out;
+
+ pv = lvm_vg_pv_by_name(vg, md->text + pvtok->start,
+ pvtok->end - pvtok->start);
+ if (!pv)
+ goto out;
+
+ seg = &lpriv->segs[lpriv->num_segs++];
+ seg->pv = pv;
+ seg->start = pe_start * vg->pe_size;
+ seg->len = pe_count * vg->pe_size;
+ seg->phys = pv->pe_start + pe_offset * vg->pe_size;
+ lpriv->lv.size += seg->len;
+ err = 0;
+out:
+ free(type);
+ return err;
+}
+
+static int lvm_seg_cmp(const void *_sega, const void *_segb)
+{
+ const struct lvm_seg *sega = _sega, *segb = _segb;
+
+ if (sega->start < segb->start)
+ return -1;
+ if (sega->start > segb->start)
+ return 1;
+ return 0;
+}
+
+static void lvm_vg_add_lv(struct lvm_vg *vg, const struct lvm_md *md,
+ const lvm_tok_t *lvkey)
+{
+ const lvm_tok_t *id, *lvsect = lvm_md_val(md, lvkey);
+ struct lvm_lv_priv *lvp;
+ const lvm_tok_t *seg;
+ struct lvm_lv *lv;
+ u64 num_segs = 0;
+ int i;
+
+ if (!lvsect || lvsect->type != LVM_TOK_SECTION)
+ return;
+
+ lvp = xzalloc(sizeof(*lvp));
+ lv = &lvp->lv;
+ lv->vg = vg;
+ lv->type = LVM_LV_LINEAR;
+ lv->name = lvm_md_tok_xstrdup(md, lvkey);
+
+ id = lvm_md_find(md, lvsect, "id");
+ if (id && id->type == LVM_TOK_STRING)
+ lvm_uuid_strcpy(lv->uuid, md->text + id->start, id->end - id->start);
+
+ lvm_md_u64(md, lvsect, "segment_count", &num_segs);
+ if (num_segs)
+ lvp->segs = xzalloc(num_segs * sizeof(*lvp->segs));
+
+ for (i = 1; i <= (int)num_segs; i++) {
+ seg = lvm_md_findf(md, lvsect, "segment%d", i);
+ if (!seg || seg->type != LVM_TOK_SECTION ||
+ lvm_lv_add_seg(lvp, vg, md, seg)) {
+ /* Unsupported mapping: keep the LV visible but
+ * mark it so that activation is refused.
+ */
+ lv->type = LVM_LV_UNKNOWN;
+ break;
+ }
+ }
+
+ if (lv->type == LVM_LV_LINEAR && lvp->num_segs)
+ qsort(lvp->segs, lvp->num_segs, sizeof(*lvp->segs),
+ lvm_seg_cmp);
+
+ vg->lvs = xrealloc(vg->lvs, (vg->num_lvs + 1) * sizeof(*vg->lvs));
+ vg->lvs[vg->num_lvs++] = lv;
+}
+
+static int lvm_vg_alloc(struct lvm_pv *pv, struct lvm_vg **vgp)
+{
+ const lvm_tok_t *vgsect, *vgkey, *pvs, *lvs, *key;
+ struct lvm_pv_priv *priv = to_pv_priv(pv);
+ const struct lvm_md *md = priv->md;
+ struct lvm_vg *vg;
+ char *id;
+ u64 v;
+
+ if (!md)
+ return -EINVAL;
+
+ vgsect = lvm_md_vgsect(md, &vgkey);
+ if (!vgsect)
+ return -EINVAL;
+
+ vg = xzalloc(sizeof(*vg));
+ vg->name = lvm_md_tok_xstrdup(md, vgkey);
+
+ if (!lvm_md_u64(md, vgsect, "seqno", &v))
+ vg->seqno = v;
+ if (!lvm_md_u64(md, vgsect, "extent_size", &v))
+ vg->pe_size = v;
+
+ id = lvm_md_strdup(md, vgsect, "id");
+ if (id) {
+ lvm_uuid_strcpy(vg->uuid, id, strlen(id));
+ free(id);
+ }
+
+ pvs = lvm_md_find(md, vgsect, "physical_volumes");
+ if (pvs && pvs->type == LVM_TOK_SECTION) {
+ lvm_md_for_each(md, key, pvs)
+ lvm_vg_add_pv(vg, md, key, pv);
+ }
+
+ lvs = lvm_md_find(md, vgsect, "logical_volumes");
+ if (lvs && lvs->type == LVM_TOK_SECTION) {
+ lvm_md_for_each(md, key, lvs)
+ lvm_vg_add_lv(vg, md, key);
+ }
+
+ *vgp = vg;
+ return 0;
+}
+
+void lvm_vg_free(struct lvm_vg *vg)
+{
+ size_t i;
+
+ if (!vg)
+ return;
+
+ for (i = 0; i < vg->num_lvs; i++) {
+ struct lvm_lv_priv *lvp = to_lv_priv(vg->lvs[i]);
+
+ free((char *)lvp->lv.name);
+ free(lvp->segs);
+ free(lvp);
+ }
+ free(vg->lvs);
+
+ for (i = 0; i < vg->num_pvs; i++) {
+ struct lvm_pv_priv *ppriv = to_pv_priv(vg->pvs[i]);
+
+ free(ppriv->pv.name);
+ free(ppriv);
+ }
+ free(vg->pvs);
+
+ free((char *)vg->name);
+ free(vg);
+}
+
+struct cdev *lvm_pv_cdev(struct lvm_pv *pv)
+{
+ struct lvm_pv_priv *pvp = to_pv_priv(pv);
+
+ /* Resolve and cache the backing device on first use. */
+ if (!pvp->cdev)
+ pvp->cdev = lvm_cdev_by_uuid(pv->uuid);
+
+ return pvp->cdev;
+}
+
+struct lvm_lv *lvm_vg_lv_by_name(struct lvm_vg *vg, const char *name)
+{
+ size_t i;
+
+ for (i = 0; i < vg->num_lvs; i++) {
+ if (!strcmp(vg->lvs[i]->name, name))
+ return vg->lvs[i];
+ }
+
+ return NULL;
+}
+
+char *lvm_lv_dm_ctable(struct lvm_lv *lv)
+{
+ struct lvm_lv_priv *lvp = to_lv_priv(lv);
+ char *table = NULL;
+ struct lvm_seg *s;
+ struct cdev *cdev;
+ size_t i;
+
+ if (lv->type != LVM_LV_LINEAR)
+ return ERR_PTR(-ENOTSUPP);
+
+ for (i = 0, s = lvp->segs; i < lvp->num_segs; i++, s++) {
+ cdev = s->pv ? lvm_pv_cdev(s->pv) : NULL;
+ if (!cdev) {
+ free(table);
+ return ERR_PTR(-ENODEV);
+ }
+
+ table = xrasprintf(table, "%llu %llu linear /dev/%s %llu\n",
+ (u64)s->start, (u64)s->len,
+ cdev_name(cdev), (u64)s->phys);
+ }
+
+ if (!table)
+ return ERR_PTR(-EINVAL);
+
+ return table;
+}
+
+struct lvm_vg_iter {
+ struct lvm_vg **vgs;
+ int num, cur;
+};
+
+void lvm_vg_iter_free(struct lvm_vg_iter *iter)
+{
+ for (; iter->cur < iter->num; iter->cur++)
+ lvm_vg_free(iter->vgs[iter->cur]);
+
+ free(iter->vgs);
+ free(iter);
+}
+
+struct lvm_vg *lvm_vg_iter_next(struct lvm_vg_iter *iter)
+{
+ if (iter->cur >= iter->num)
+ return NULL;
+
+ return iter->vgs[iter->cur++];
+}
+
+struct lvm_vg_iter *lvm_vg_iter_new(void)
+{
+ struct lvm_vg_iter *iter;
+ struct cdev *cdev;
+ struct lvm_pv *pv;
+ struct lvm_vg *vg;
+ int err, i;
+
+ iter = xzalloc(sizeof(*iter));
+
+ for_each_cdev(cdev) {
+ if (!cdev_is_block_device(cdev))
+ continue;
+
+ if (lvm_pv_alloc(cdev, &pv))
+ continue;
+
+ err = lvm_vg_alloc(pv, &vg);
+ lvm_pv_free(pv);
+ if (err)
+ continue;
+
+ for (i = 0; i < iter->num; i++) {
+ /* Use the most recently updated VG metadata
+ * when multiple versions are available.
+ */
+ if (strcmp(vg->uuid, iter->vgs[i]->uuid))
+ continue;
+
+ if (vg->seqno > iter->vgs[i]->seqno) {
+ lvm_vg_free(iter->vgs[i]);
+ iter->vgs[i] = vg;
+ }
+
+ goto next;
+ }
+
+ iter->vgs = xrealloc(iter->vgs, (iter->num + 1) * sizeof(*iter->vgs));
+ iter->vgs[iter->num++] = vg;
+next:
+ }
+
+ return iter;
+}
+
+int lvm_vg_alloc_by_name(const char *name, struct lvm_vg **vgp)
+{
+ struct lvm_vg_iter *iter;
+ struct lvm_vg *vg;
+
+ iter = lvm_vg_iter_new();
+ while ((vg = lvm_vg_iter_next(iter))) {
+ if (!strcmp(vg->name, name)) {
+ lvm_vg_iter_free(iter);
+ *vgp = vg;
+ return 0;
+ }
+ }
+
+ lvm_vg_iter_free(iter);
+ return -ENOENT;
+}
+
+int lvm_vg_alloc_by_cdev(struct cdev *cdev, struct lvm_vg **vgp)
+{
+ struct lvm_pv *pv;
+ struct lvm_vg *vg;
+ int err;
+
+ if (!cdev_is_block_device(cdev))
+ return -EINVAL;
+
+ err = lvm_pv_alloc(cdev, &pv);
+ if (err)
+ return err;
+
+ err = lvm_vg_alloc(pv, &vg);
+ lvm_pv_free(pv);
+ if (err)
+ return err;
+
+ *vgp = vg;
+ return 0;
+}
diff --git a/drivers/block/dm/lvm/lvm-md.c b/drivers/block/dm/lvm/lvm-md.c
new file mode 100644
index 0000000000..6b183193e0
--- /dev/null
+++ b/drivers/block/dm/lvm/lvm-md.c
@@ -0,0 +1,423 @@
+// SPDX-License-Identifier: GPL-2.0-only
+// SPDX-FileCopyrightText: 2026 Tobias Waldekranz <tobias@waldekranz.com>
+
+#include <stdio.h>
+#include <string.h>
+#include <xfuncs.h>
+
+#include <linux/kstrtox.h>
+
+#include "lvm-md.h"
+
+struct lvm_md_parser {
+ const char *text;
+ size_t len;
+ size_t pos;
+
+ lvm_tok_t *toks;
+ int next;
+
+ int super; /* Index of the current container (section/array) */
+ int key; /* Index of the most recent key in the current section */
+ bool value; /* An '=' was seen; the next scalar/array is its value */
+};
+
+static int lvm_md_tok_new(struct lvm_md_parser *p, enum lvm_tok_type type, int start)
+{
+ lvm_tok_t *t;
+
+ p->toks = xrealloc(p->toks, (p->next + 1) * sizeof(*p->toks));
+
+ t = &p->toks[p->next];
+ t->type = type;
+ t->start = start;
+ t->end = -1;
+ t->size = 0;
+ t->parent = -1;
+ return p->next++;
+}
+
+static void lvm_md_link(struct lvm_md_parser *p, int idx)
+{
+ lvm_tok_t *t = &p->toks[idx];
+
+ if (p->super >= 0 && p->toks[p->super].type == LVM_TOK_ARRAY) {
+ t->parent = p->super;
+ p->toks[p->super].size++;
+ } else if (p->value) {
+ t->parent = p->key;
+ p->value = false;
+ p->key = -1;
+ } else {
+ t->parent = p->super;
+ if (p->super >= 0)
+ p->toks[p->super].size++;
+ p->key = idx;
+ }
+}
+
+static int lvm_md_parse_string(struct lvm_md_parser *p)
+{
+ int idx, start;
+
+ /* Skip opening quote */
+ start = p->pos + 1;
+
+ for (p->pos++; p->pos < p->len; p->pos++) {
+ char c = p->text[p->pos];
+
+ if (c == '\\' && p->pos + 1 < p->len) {
+ p->pos++;
+ continue;
+ }
+ if (c == '"') {
+ idx = lvm_md_tok_new(p, LVM_TOK_STRING, start);
+ p->toks[idx].end = p->pos;
+ lvm_md_link(p, idx);
+ return 0;
+ }
+ }
+
+ /* Unterminated string */
+ return -EINVAL;
+}
+
+static bool lvm_md_is_primchar(char c)
+{
+ switch (c) {
+ case '0'...'9':
+ case 'a'...'z':
+ case 'A'...'Z':
+ case '.':
+ case '_':
+ case '-':
+ case '+':
+ return true;
+ }
+
+ return false;
+}
+
+static int lvm_md_parse_primitive(struct lvm_md_parser *p)
+{
+ int start = p->pos;
+ int idx;
+
+ while (p->pos < p->len && lvm_md_is_primchar(p->text[p->pos]))
+ p->pos++;
+
+ idx = lvm_md_tok_new(p, LVM_TOK_PRIMITIVE, start);
+ p->toks[idx].end = p->pos;
+ lvm_md_link(p, idx);
+
+ /* Reexamine the delimiter in the main loop */
+ p->pos--;
+ return 0;
+}
+
+static int lvm_md_parse(struct lvm_md_parser *p)
+{
+ int idx, s, par;
+ int err;
+
+ /* Implicit anonymous root section. */
+ idx = lvm_md_tok_new(p, LVM_TOK_SECTION, 0);
+ p->toks[idx].end = p->len;
+ p->super = idx;
+ p->key = -1;
+ p->value = false;
+
+ for (; p->pos < p->len; p->pos++) {
+ char c = p->text[p->pos];
+
+ switch (c) {
+ case ' ':
+ case '\t':
+ case '\r':
+ case '\n':
+ case ',':
+ break;
+ case '#':
+ while (p->pos < p->len && p->text[p->pos] != '\n')
+ p->pos++;
+ break;
+ case '\0':
+ /* A NUL terminates the document */
+ p->pos = p->len;
+ break;
+ case '"':
+ err = lvm_md_parse_string(p);
+ if (err)
+ return err;
+ break;
+ case '=':
+ if (p->key < 0)
+ return -EINVAL;
+ p->value = true;
+ break;
+ case '{':
+ /* The preceding key names this section. */
+ if (p->key < 0)
+ return -EINVAL;
+ idx = lvm_md_tok_new(p, LVM_TOK_SECTION, p->pos);
+ p->toks[idx].parent = p->key;
+ p->super = idx;
+ p->key = -1;
+ p->value = false;
+ break;
+ case '[':
+ idx = lvm_md_tok_new(p, LVM_TOK_ARRAY, p->pos);
+ lvm_md_link(p, idx);
+ p->super = idx;
+ p->key = -1;
+ p->value = false;
+ break;
+ case '}':
+ case ']':
+ s = p->super;
+ if (s < 0 || p->value)
+ return -EINVAL;
+ if (p->toks[s].type !=
+ (c == '}' ? LVM_TOK_SECTION : LVM_TOK_ARRAY))
+ return -EINVAL;
+ p->toks[s].end = p->pos + 1;
+
+ /* Pop back to the enclosing container. The token
+ * just closed hangs off either a key (the common
+ * case) or directly off an enclosing array.
+ */
+ par = p->toks[s].parent;
+ if (par >= 0 && p->toks[par].type == LVM_TOK_ARRAY)
+ p->super = par;
+ else
+ p->super = (par >= 0) ? p->toks[par].parent : -1;
+ p->key = -1;
+ p->value = false;
+ break;
+ default:
+ if (!lvm_md_is_primchar(c))
+ return -EINVAL;
+
+ err = lvm_md_parse_primitive(p);
+ if (err)
+ return err;
+ break;
+ }
+ }
+
+ if (p->super != 0 || p->value)
+ return -EINVAL; /* Unbalanced braces or dangling '=' */
+
+ return p->next;
+}
+
+int lvm_md_parse_alloc(const char *text, size_t len, struct lvm_md **mdp)
+{
+ struct lvm_md_parser p = {
+ .text = text,
+ .len = len,
+ };
+ struct lvm_md *md;
+ int ret;
+
+ ret = lvm_md_parse(&p);
+ if (ret < 0) {
+ free(p.toks);
+ return ret;
+ }
+
+ md = xzalloc(sizeof(*md));
+ md->text = text;
+ md->tokens = p.toks;
+ md->num_tokens = p.next;
+
+ *mdp = md;
+ return 0;
+}
+
+void lvm_md_free(struct lvm_md *md)
+{
+ if (!md)
+ return;
+
+ free(md->tokens);
+ free(md);
+}
+
+const lvm_tok_t *lvm_md_vgsect(const struct lvm_md *md, const lvm_tok_t **keyp)
+{
+ const lvm_tok_t *key, *val;
+
+ lvm_md_for_each(md, key, &md->tokens[0]) {
+ val = lvm_md_val(md, key);
+ if (val && val->type == LVM_TOK_SECTION) {
+ if (keyp)
+ *keyp = key;
+ return val;
+ }
+ }
+
+ return NULL;
+}
+
+static bool lvm_md_tok_eq(const struct lvm_md *md, const lvm_tok_t *tok, const char *str)
+{
+ size_t len = tok->end - tok->start;
+
+ return strlen(str) == len && !strncmp(md->text + tok->start, str, len);
+}
+
+static const lvm_tok_t *lvm_md_skip(const struct lvm_md *md, const lvm_tok_t *tok)
+{
+ const lvm_tok_t *end = md->tokens + md->num_tokens;
+ int max = tok->end;
+
+ do {
+ tok++;
+ } while (tok < end && tok->start < max);
+
+ return (tok < end) ? tok : NULL;
+}
+
+const lvm_tok_t *lvm_md_first(const struct lvm_md *md, const lvm_tok_t *parent)
+{
+ if (!parent)
+ return NULL;
+ if (parent->type != LVM_TOK_SECTION && parent->type != LVM_TOK_ARRAY)
+ return NULL;
+ if (parent->size == 0)
+ return NULL;
+ if (parent + 1 >= md->tokens + md->num_tokens)
+ return NULL;
+
+ return parent + 1;
+}
+
+const lvm_tok_t *lvm_md_next(const struct lvm_md *md, const lvm_tok_t *parent,
+ const lvm_tok_t *child)
+{
+ const lvm_tok_t *value, *next;
+
+ if (!parent || !child)
+ return NULL;
+
+ /* In a section a key is followed by its value subtree; in an
+ * array the element is itself the value.
+ */
+ value = (parent->type == LVM_TOK_SECTION) ? child + 1 : child;
+ if (value >= md->tokens + md->num_tokens)
+ return NULL;
+
+ next = lvm_md_skip(md, value);
+ if (!next || next->start >= parent->end)
+ return NULL;
+
+ return next;
+}
+
+const lvm_tok_t *lvm_md_val(const struct lvm_md *md, const lvm_tok_t *key)
+{
+ if (!key || key + 1 >= md->tokens + md->num_tokens)
+ return NULL;
+
+ return key + 1;
+}
+
+const lvm_tok_t *lvm_md_find(const struct lvm_md *md, const lvm_tok_t *sec,
+ const char *key)
+{
+ const lvm_tok_t *k;
+
+ if (!sec || sec->type != LVM_TOK_SECTION)
+ return NULL;
+
+ lvm_md_for_each(md, k, sec) {
+ if (lvm_md_tok_eq(md, k, key))
+ return lvm_md_val(md, k);
+ }
+
+ return NULL;
+}
+
+const lvm_tok_t *lvm_md_findf(const struct lvm_md *md, const lvm_tok_t *sec,
+ const char *keyfmt, ...)
+{
+ const lvm_tok_t *k;
+ va_list ap;
+ char *key;
+
+ va_start(ap, keyfmt);
+ key = xvasprintf(keyfmt, ap);
+ va_end(ap);
+
+ k = lvm_md_find(md, sec, key);
+ free(key);
+ return k;
+}
+
+static bool lvm_md_tok_is_str(const lvm_tok_t *tok)
+{
+ return tok && (tok->type == LVM_TOK_STRING ||
+ tok->type == LVM_TOK_PRIMITIVE);
+}
+
+static char *lvm_md_tok_strdup(const struct lvm_md *md, const lvm_tok_t *tok)
+{
+ int len;
+ char *s;
+
+ if (!lvm_md_tok_is_str(tok))
+ return NULL;
+
+ len = tok->end - tok->start;
+ s = malloc(len + 1);
+ if (!s)
+ return NULL;
+
+ memcpy(s, md->text + tok->start, len);
+ s[len] = '\0';
+ return s;
+}
+
+char *lvm_md_tok_xstrdup(const struct lvm_md *md, const lvm_tok_t *tok)
+{
+ char *cpy;
+
+ if (!lvm_md_tok_is_str(tok))
+ return NULL;
+
+ cpy = lvm_md_tok_strdup(md, tok);
+ if (!cpy)
+ panic("lvm: out of memory");
+
+ return cpy;
+}
+
+int lvm_md_tok_u64(const struct lvm_md *md, const lvm_tok_t *tok, u64 *out)
+{
+ char buf[32];
+ int len;
+
+ if (!lvm_md_tok_is_str(tok))
+ return -EINVAL;
+
+ len = tok->end - tok->start;
+ if (len <= 0 || len >= (int)sizeof(buf))
+ return -EINVAL;
+
+ memcpy(buf, md->text + tok->start, len);
+ buf[len] = '\0';
+ return kstrtou64(buf, 0, out);
+}
+
+char *lvm_md_strdup(const struct lvm_md *md, const lvm_tok_t *sec,
+ const char *key)
+{
+ return lvm_md_tok_strdup(md, lvm_md_find(md, sec, key));
+}
+
+int lvm_md_u64(const struct lvm_md *md, const lvm_tok_t *sec,
+ const char *key, u64 *out)
+{
+ return lvm_md_tok_u64(md, lvm_md_find(md, sec, key), out);
+}
diff --git a/drivers/block/dm/lvm/lvm-md.h b/drivers/block/dm/lvm/lvm-md.h
new file mode 100644
index 0000000000..3dd5ebaf2a
--- /dev/null
+++ b/drivers/block/dm/lvm/lvm-md.h
@@ -0,0 +1,109 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-FileCopyrightText: 2026 Tobias Waldekranz <tobias@waldekranz.com> */
+
+#ifndef _LVM_MD_H
+#define _LVM_MD_H
+
+/*
+ * LVM metadata parser, modeled on the jsmn JSON tokenizer.
+ *
+ * The text metadata is a sequence of statements. A statement is
+ * either an assignment:
+ *
+ * key = value
+ *
+ * where the value is a string ("..."), a primitive (a bare integer or
+ * identifier) or an array ([v0, v1, ...]); or a named section:
+ *
+ * key { ... }
+ *
+ * whose body is itself a sequence of statements. Sections nest. The
+ * document as a whole is treated as the body of an implicit,
+ * anonymous root section, which is always tokens[0].
+ */
+enum lvm_tok_type {
+ LVM_TOK_UNDEFINED = 0,
+ LVM_TOK_SECTION,
+ LVM_TOK_ARRAY,
+ LVM_TOK_STRING,
+ LVM_TOK_PRIMITIVE,
+};
+
+typedef struct lvm_tok {
+ enum lvm_tok_type type;
+ int start; /* Offset of first byte in text */
+ int end; /* Offset one past the last byte in text */
+ int size; /* # of child keys (section) or elements (array) */
+ int parent; /* Index of parent token, -1 for the root */
+} lvm_tok_t;
+
+struct lvm_md {
+ const char *text;
+ lvm_tok_t *tokens;
+ size_t num_tokens;
+};
+
+/* Tokenize text into a dynamically allocated context. As tokens are
+ * represented as spans, text must remain valid for the lifetime of
+ * the returned context.
+ */
+int lvm_md_parse_alloc(const char *text, size_t len, struct lvm_md **md);
+void lvm_md_free(struct lvm_md *md);
+
+/* Return the only named section in the root, which describes the
+ * VG. The key token, holding the VG name, is returned in keyp.
+ */
+const lvm_tok_t *lvm_md_vgsect(const struct lvm_md *md, const lvm_tok_t **keyp);
+
+/* Return the value token belonging to key. */
+const lvm_tok_t *lvm_md_val(const struct lvm_md *md, const lvm_tok_t *key);
+
+/* Look up key in the section sec and return its value token, or
+ * NULL.
+ */
+const lvm_tok_t *lvm_md_find(const struct lvm_md *md, const lvm_tok_t *sec,
+ const char *key);
+const lvm_tok_t *lvm_md_findf(const struct lvm_md *md, const lvm_tok_t *sec,
+ const char *keyfmt, ...) __printf(3, 4);
+
+/* The first child of a section (its first key) or array (its first
+ * element), or NULL if empty.
+ */
+const lvm_tok_t *lvm_md_first(const struct lvm_md *md, const lvm_tok_t *parent);
+
+/* The child following child within parent, or NULL once exhausted.
+ * For a section, children are the keys; for an array, the elements.
+ */
+const lvm_tok_t *lvm_md_next(const struct lvm_md *md, const lvm_tok_t *parent,
+ const lvm_tok_t *child);
+
+/* Iterate the keys of a section or the elements of an array. */
+#define lvm_md_for_each(_md, _child, _parent) \
+ for ((_child) = lvm_md_first((_md), (_parent)); \
+ (_child); \
+ (_child) = lvm_md_next((_md), (_parent), (_child)))
+
+/* Retrurn a copy of the text of a string/primitive value token, or
+ * NULL on error.
+ */
+char *lvm_md_tok_xstrdup(const struct lvm_md *md, const lvm_tok_t *tok);
+
+/* Parse a primitive (or string) value token as an unsigned
+ * integer.
+ */
+int lvm_md_tok_u64(const struct lvm_md *md, const lvm_tok_t *tok, u64 *out);
+
+/* Return a copy of key's value from sec, if available, otherwize
+ * NULL.
+ */
+char *lvm_md_strdup(const struct lvm_md *md, const lvm_tok_t *sec,
+ const char *key);
+
+/* Return key's numerical value from sec in out, if available and
+ * properly formatted. Returns 0 on success, negative error code on
+ * error.
+ */
+int lvm_md_u64(const struct lvm_md *md, const lvm_tok_t *sec,
+ const char *key, u64 *out);
+
+#endif /* _LVM_MD_H */
diff --git a/drivers/block/dm/lvm/lvm2.h b/drivers/block/dm/lvm/lvm2.h
new file mode 100644
index 0000000000..313aa71b8e
--- /dev/null
+++ b/drivers/block/dm/lvm/lvm2.h
@@ -0,0 +1,79 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-FileCopyrightText: 2026 Tobias Waldekranz <tobias@waldekranz.com> */
+
+#ifndef _LVM2_H
+#define _LVM2_H
+
+#include <linux/types.h>
+
+#define LVM2_LABEL_ID "LABELONE"
+#define LVM2_LABEL_TYPE "LVM2 001"
+#define LVM2_MDA_MAGIC " LVM2 x[5A%r0N*>"
+#define LVM2_MDA_VERSION 1
+#define LVM2_LABEL_SCAN_SECTORS 4
+#define LVM2_RAW_LOCN_IGNORED 0x00000001
+
+struct lvm2_label {
+ u8 id[8]; /* LVM2_LABEL_ID */
+ __le64 sector; /* Sector number of this label */
+ __le32 crc;
+ __le32 pv_offset; /* Byte offset to pv_header within sector */
+ u8 type[8]; /* LVM2_LABEL_TYPE */
+} __packed;
+
+struct lvm2_area {
+ __le64 offset;
+ __le64 size;
+} __packed;
+
+struct lvm2_pv_header {
+ u8 uuid[32];
+ __le64 size;
+
+ /* Zero terminated list of data areas, followed by zero
+ * terminated list of metadata areas.
+ */
+ struct lvm2_area area[0];
+} __packed;
+
+struct lvm2_md_area {
+ __le64 offset; /* Byte offset from start of MDA area */
+ __le64 size; /* Includes trailing NUL */
+ __le32 checksum;
+ __le32 flags;
+} __packed;
+
+/* The CRC32 variant used to protect the label and the metadata, see
+ * lib/misc/crc.c in lvm2. Note the nonstandard initial value.
+ */
+static inline u32 lvm2_crc(const void *buf, size_t len)
+{
+ static const u32 tab[16] = {
+ 0x00000000, 0x1db71064, 0x3b6e20c8, 0x26d930ac,
+ 0x76dc4190, 0x6b6b51f4, 0x4db26158, 0x5005713c,
+ 0xedb88320, 0xf00f9344, 0xd6d6a3e8, 0xcb61b38c,
+ 0x9b64c2b0, 0x86d3d2d4, 0xa00ae278, 0xbdbdf21c,
+ };
+ u32 crc = 0xf597a6cf;
+ const u8 *p = buf;
+
+ while (len--) {
+ crc ^= *p++;
+ crc = (crc >> 4) ^ tab[crc & 0xf];
+ crc = (crc >> 4) ^ tab[crc & 0xf];
+ }
+
+ return crc;
+}
+
+struct lvm2_md_header {
+ __le32 checksum;
+ u8 magic[16]; /* LVM2_MDA_MAGIC */
+ __le32 version; /* LVM2_MDA_VERSION */
+ __le64 start; /* Byte offset of MDA area on device */
+ __le64 size; /* Size of MDA area */
+
+ struct lvm2_md_area area[0];
+} __packed;
+
+#endif /* _LVM2_H */
diff --git a/include/lvm.h b/include/lvm.h
new file mode 100644
index 0000000000..d1d4144254
--- /dev/null
+++ b/include/lvm.h
@@ -0,0 +1,100 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-FileCopyrightText: 2026 Tobias Waldekranz <tobias@waldekranz.com> */
+
+#ifndef _LVM_H
+#define _LVM_H
+
+#include <linux/types.h>
+
+#include <disks.h>
+
+struct cdev;
+
+struct lvm_pv;
+struct lvm_lv;
+
+#define LVM_UUID_LEN 32
+
+/* Delinearized representation of a Volume Group (VG) with references
+ * to associated Physical (PVs) and Locical (LVs) Volumes
+ */
+struct lvm_vg {
+ char *name;
+ char uuid[LVM_UUID_LEN + 1];
+
+ u64 seqno;
+
+ /* Sectors per physical extent */
+ blkcnt_t pe_size;
+
+ struct lvm_pv **pvs;
+ size_t num_pvs;
+
+ struct lvm_lv **lvs;
+ size_t num_lvs;
+};
+
+struct lvm_lv *lvm_vg_lv_by_name(struct lvm_vg *vg, const char *name);
+
+/* Iterator over all available VGs, constructed by scanning all block
+ * devices known to the system. If multiple PVs contain metadata for
+ * the same VG, only the most recently updated version is returned to
+ * the caller of lvm_vg_iter_next(). Callers must ensure that any
+ * returned VGs are freed by calling lvm_vg_free().
+ */
+struct lvm_vg_iter;
+
+struct lvm_vg *lvm_vg_iter_next(struct lvm_vg_iter *iter);
+struct lvm_vg_iter *lvm_vg_iter_new(void);
+void lvm_vg_iter_free(struct lvm_vg_iter *iter);
+
+/* Return a VG based on its name. Internally this uses a VG iterator
+ * and is thus more expensive than lvm_vg_alloc_by_cdev(), with the
+ * upside that it is based on the most up-to-date metadata available.
+ */
+int lvm_vg_alloc_by_name(const char *name, struct lvm_vg **vgp);
+
+/* Return the VG described by the metadata on the PV backed by
+ * cdev.
+ */
+int lvm_vg_alloc_by_cdev(struct cdev *cdev, struct lvm_vg **vgp);
+
+void lvm_vg_free(struct lvm_vg *vg);
+
+struct lvm_pv {
+ struct lvm_vg *vg;
+
+ char *name;
+ char uuid[LVM_UUID_LEN + 1];
+
+ blkcnt_t dev_size;
+ blkcnt_t pe_start;
+ u32 pe_count;
+};
+
+/* Return the backing device for pv */
+struct cdev *lvm_pv_cdev(struct lvm_pv *pv);
+
+enum lvm_lv_type {
+ LVM_LV_UNKNOWN,
+ LVM_LV_LINEAR,
+};
+
+struct lvm_lv {
+ struct lvm_vg *vg;
+ enum lvm_lv_type type;
+ char *name;
+ char uuid[LVM_UUID_LEN + 1];
+
+ blkcnt_t size;
+};
+
+/* Create a device mapper configuration table for the specified LV,
+ * suitable for consumption by dm_create(). The caller takes ownership
+ * of the returned string. Returns an ERR_PTR() on failure, e.g. if
+ * the LV uses an unsupported mapping or references a PV that is not
+ * present.
+ */
+char *lvm_lv_dm_ctable(struct lvm_lv *lv);
+
+#endif /* _LVM_H */
--
2.43.0
^ permalink raw reply [flat|nested] 7+ messages in thread* [PATCH remotes/origin/next 2/5] dm: lvm: Add fuzz testers for metadata parser and binary headers
2026-09-17 7:40 [PATCH remotes/origin/next 0/5] dm: lvm: Initial Logical Volume Management support Tobias Waldekranz
2026-09-17 7:40 ` [PATCH remotes/origin/next 1/5] " Tobias Waldekranz
@ 2026-09-17 7:40 ` Tobias Waldekranz
2026-09-17 7:40 ` [PATCH remotes/origin/next 3/5] test: self: lvm: Add tests " Tobias Waldekranz
` (2 subsequent siblings)
4 siblings, 0 replies; 7+ messages in thread
From: Tobias Waldekranz @ 2026-09-17 7:40 UTC (permalink / raw)
To: barebox
For the header parser, we help the fuzzer along by ensuring enough of
the basic structure is in place that allows it to reach deeper into
the code, rather than immediately bailing out on missing magic values
or incorrect CRCs.
Signed-off-by: Tobias Waldekranz <tobias@waldekranz.com>
---
drivers/block/dm/lvm/Makefile | 1 +
drivers/block/dm/lvm/lvm-fuzz.c | 182 ++++++++++++++++++++++++++++++++
images/Makefile.sandbox | 2 +
test/testdata/fuzz/lvm-md.dict | 29 +++++
test/testdata/fuzz/lvm.dict | 30 ++++++
5 files changed, 244 insertions(+)
create mode 100644 drivers/block/dm/lvm/lvm-fuzz.c
create mode 100644 test/testdata/fuzz/lvm-md.dict
create mode 100644 test/testdata/fuzz/lvm.dict
diff --git a/drivers/block/dm/lvm/Makefile b/drivers/block/dm/lvm/Makefile
index b85f2714d1..b0be22db20 100644
--- a/drivers/block/dm/lvm/Makefile
+++ b/drivers/block/dm/lvm/Makefile
@@ -1,2 +1,3 @@
# SPDX-License-Identifier: GPL-2.0-only
obj-y += lvm-core.o lvm-md.o
+obj-$(CONFIG_FUZZ) += lvm-fuzz.o
diff --git a/drivers/block/dm/lvm/lvm-fuzz.c b/drivers/block/dm/lvm/lvm-fuzz.c
new file mode 100644
index 0000000000..3eaec8c14b
--- /dev/null
+++ b/drivers/block/dm/lvm/lvm-fuzz.c
@@ -0,0 +1,182 @@
+// SPDX-License-Identifier: GPL-2.0-only
+// SPDX-FileCopyrightText: 2026 Tobias Waldekranz <tobias@waldekranz.com>
+
+#include <block.h>
+#include <fuzz.h>
+#include <lvm.h>
+
+#include "lvm2.h"
+#include "lvm-md.h"
+
+static int fuzz_lvm_md(const char *text, size_t size)
+{
+ struct lvm_md *md;
+
+ if (!lvm_md_parse_alloc(text, size, &md))
+ lvm_md_free(md);
+
+ return 0;
+}
+fuzz_test_str("lvm-md", fuzz_lvm_md);
+
+static void lvm_fuzz_fixup_mda(u8 *img, size_t size, u64 mda_offset, u64 mda_size)
+{
+ struct lvm2_md_header *hdr;
+ struct lvm2_md_area *area;
+ u64 off, len, wrap;
+ u8 *text;
+
+ if (mda_offset >= size || mda_size < SECTOR_SIZE ||
+ mda_size > size - mda_offset)
+ return;
+
+ hdr = (void *)(img + mda_offset);
+ memcpy(hdr->magic, LVM2_MDA_MAGIC, sizeof(hdr->magic));
+ put_unaligned_le32(LVM2_MDA_VERSION, &hdr->version);
+
+ for (area = hdr->area;
+ (u8 *)(area + 1) <= (u8 *)hdr + SECTOR_SIZE; area++) {
+ off = get_unaligned_le64(&area->offset);
+ len = get_unaligned_le64(&area->size);
+
+ if (!off && !len)
+ break;
+ if (!len || off >= mda_size || len > mda_size)
+ continue;
+
+ text = malloc(len);
+ if (!text)
+ return;
+
+ if (off + len > mda_size) {
+ wrap = mda_size - off;
+
+ if (SECTOR_SIZE + (len - wrap) > mda_size)
+ goto next;
+
+ memcpy(text, img + mda_offset + off, wrap);
+ memcpy(text + wrap,
+ img + mda_offset + SECTOR_SIZE,
+ len - wrap);
+ } else {
+ memcpy(text, img + mda_offset + off, len);
+ }
+
+ put_unaligned_le32(lvm2_crc(text, len), &area->checksum);
+next:
+ free(text);
+ }
+
+ /* Last, as it covers the descriptors fixed up above. */
+ put_unaligned_le32(lvm2_crc(hdr->magic,
+ SECTOR_SIZE
+ - offsetof(struct lvm2_md_header, magic)),
+ &hdr->checksum);
+}
+
+static void lvm_fuzz_fixup_image(u8 *img, size_t size)
+{
+ struct lvm2_pv_header *pvh;
+ struct lvm2_label *label;
+ struct lvm2_area *area;
+ u8 *sector = NULL;
+ u32 pv_offset;
+ int s;
+
+ for (s = 0; s < LVM2_LABEL_SCAN_SECTORS; s++) {
+ if ((size_t)(s + 1) << SECTOR_SHIFT > size)
+ break;
+
+ label = (void *)(img + ((size_t)s << SECTOR_SHIFT));
+ if (!memcmp(label->id, LVM2_LABEL_ID, sizeof(label->id))) {
+ sector = (u8 *)label;
+ break;
+ }
+ }
+
+ /* Without a label there is nothing to find, so plant one in the
+ * sector that LVM uses by default, leaving the rest of it as it
+ * is.
+ */
+ if (!sector) {
+ if (size < 2 * SECTOR_SIZE)
+ return;
+
+ s = 1;
+ sector = img + SECTOR_SIZE;
+ label = (void *)sector;
+ memcpy(label->id, LVM2_LABEL_ID, sizeof(label->id));
+ }
+
+ memcpy(label->type, LVM2_LABEL_TYPE, sizeof(label->type));
+ put_unaligned_le64(s, &label->sector);
+
+ pv_offset = get_unaligned_le32(&label->pv_offset);
+ if (pv_offset < SECTOR_SIZE) {
+ pvh = (void *)(sector + pv_offset);
+
+ /* Same walk as lvm_pv_probe(): past the data areas, past
+ * the zero separator, then one fixup per metadata area.
+ */
+ for (area = pvh->area;
+ (u8 *)(area + 1) <= sector + SECTOR_SIZE
+ && get_unaligned_le64(&area->offset);
+ area++)
+ ;
+
+ for (area++;
+ (u8 *)(area + 1) <= sector + SECTOR_SIZE
+ && get_unaligned_le64(&area->offset);
+ area++)
+ lvm_fuzz_fixup_mda(img, size,
+ get_unaligned_le64(&area->offset),
+ get_unaligned_le64(&area->size));
+ }
+
+ put_unaligned_le32(lvm2_crc(&label->pv_offset,
+ SECTOR_SIZE - offsetof(struct lvm2_label, pv_offset)),
+ &label->crc);
+}
+
+static int fuzz_lvm(const u8 *data, size_t size)
+{
+ static struct ramdisk *ramdisk;
+ struct block_device *blk;
+ struct lvm_vg *vg;
+ char *table;
+ size_t i;
+ u8 *img;
+
+ if (size < 2 * SECTOR_SIZE)
+ return 0;
+
+ if (!ramdisk)
+ ramdisk = ramdisk_init(SECTOR_SIZE);
+ if (!ramdisk)
+ return -ENODEV;
+
+ img = xmemdup(data, size);
+
+ /* Help the fuzzer out by injecting a proper LVM label and
+ * valid CRCs, so it can reach further into the parser.
+ */
+ lvm_fuzz_fixup_image(img, size);
+
+ ramdisk_setup_rw(ramdisk, img, size);
+ blk = ramdisk_get_block_device(ramdisk);
+
+ if (!lvm_vg_alloc_by_cdev(&blk->cdev, &vg)) {
+ for (i = 0; i < vg->num_lvs; i++) {
+ table = lvm_lv_dm_ctable(vg->lvs[i]);
+ if (!IS_ERR(table))
+ free(table);
+ }
+
+ lvm_vg_free(vg);
+ }
+
+ ramdisk_setup_rw(ramdisk, NULL, 0);
+ free(img);
+ return 0;
+}
+fuzz_test("lvm", fuzz_lvm);
diff --git a/images/Makefile.sandbox b/images/Makefile.sandbox
index d0dc503fd0..0419fb8e18 100644
--- a/images/Makefile.sandbox
+++ b/images/Makefile.sandbox
@@ -16,6 +16,8 @@ fuzzer-$(CONFIG_EFI_PE_PARSER) += pe
fuzzer-$(CONFIG_TLV) += tlv
fuzzer-$(CONFIG_FS_FAT) += fat
fuzzer-$(CONFIG_STATE) += state-direct
+fuzzer-$(CONFIG_DM_LVM) += lvm
+fuzzer-$(CONFIG_DM_LVM) += lvm-md
ifeq ($(CONFIG_SANDBOX),y)
diff --git a/test/testdata/fuzz/lvm-md.dict b/test/testdata/fuzz/lvm-md.dict
new file mode 100644
index 0000000000..40b378e7f8
--- /dev/null
+++ b/test/testdata/fuzz/lvm-md.dict
@@ -0,0 +1,29 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+#
+# libfuzzer dictionary for the "lvm-md" fuzzer, which exercises the
+# LVM2 metadata tokenizer. Pass it with -dict=, e.g.
+#
+# images/fuzz-lvm-md -dict=test/testdata/fuzz/lvm-md.dict
+
+# Separators
+# (libfuzzer only supports escaping backslash, quote and xAB)
+"#"
+" "
+"\x09"
+"\x0a"
+"\x0d"
+"="
+"{"
+"}"
+"["
+"]"
+","
+"\""
+
+"sec_name {"
+"prim"
+"complex+prim.0"
+"\"string literal\""
+"\"FLAG\""
+"\"unterminated"
+"unopened\""
diff --git a/test/testdata/fuzz/lvm.dict b/test/testdata/fuzz/lvm.dict
new file mode 100644
index 0000000000..7be7d04574
--- /dev/null
+++ b/test/testdata/fuzz/lvm.dict
@@ -0,0 +1,30 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+#
+# libfuzzer dictionary for the "lvm" fuzzer, which exercises the LVM2
+# header parser. Pass it with -dict=, e.g.
+#
+# images/fuzz-lvm -dict=test/testdata/fuzz/lvm.dict
+
+"LABELONE"
+"LVM2 001"
+" LVM2 x[5A%r0N*>"
+
+# Small le32 constants
+"\x00\x00\x00\x00"
+"\x01\x00\x00\x00"
+"\x02\x00\x00\x00"
+"\x04\x00\x00\x00"
+"\x08\x00\x00\x00"
+"\x10\x00\x00\x00"
+"\x40\x00\x00\x00"
+"\x80\x00\x00\x00"
+
+# Small le64 constants
+"\x00\x00\x00\x00\x00\x00\x00\x00"
+"\x01\x00\x00\x00\x00\x00\x00\x00"
+"\x02\x00\x00\x00\x00\x00\x00\x00"
+"\x04\x00\x00\x00\x00\x00\x00\x00"
+"\x08\x00\x00\x00\x00\x00\x00\x00"
+"\x10\x00\x00\x00\x00\x00\x00\x00"
+"\x40\x00\x00\x00\x00\x00\x00\x00"
+"\x80\x00\x00\x00\x00\x00\x00\x00"
--
2.43.0
^ permalink raw reply [flat|nested] 7+ messages in thread* [PATCH remotes/origin/next 3/5] test: self: lvm: Add tests for metadata parser and binary headers
2026-09-17 7:40 [PATCH remotes/origin/next 0/5] dm: lvm: Initial Logical Volume Management support Tobias Waldekranz
2026-09-17 7:40 ` [PATCH remotes/origin/next 1/5] " Tobias Waldekranz
2026-09-17 7:40 ` [PATCH remotes/origin/next 2/5] dm: lvm: Add fuzz testers for metadata parser and binary headers Tobias Waldekranz
@ 2026-09-17 7:40 ` Tobias Waldekranz
2026-09-17 7:40 ` [PATCH remotes/origin/next 4/5] commands: lvm: inspect VGs, activate LVs Tobias Waldekranz
2026-09-17 7:40 ` [PATCH remotes/origin/next 5/5] test: py: lvm: Add basic LV activation test Tobias Waldekranz
4 siblings, 0 replies; 7+ messages in thread
From: Tobias Waldekranz @ 2026-09-17 7:40 UTC (permalink / raw)
To: barebox
Create a PV/VG generator that can generate images with various
non-standard transformations, then use it to test various types of
both valid and invalid images.
Signed-off-by: Tobias Waldekranz <tobias@waldekranz.com>
---
test/self/Kconfig | 14 +
test/self/Makefile | 2 +
test/self/lvm.c | 818 +++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 834 insertions(+)
create mode 100644 test/self/lvm.c
diff --git a/test/self/Kconfig b/test/self/Kconfig
index 352deadf05..6db4349e23 100644
--- a/test/self/Kconfig
+++ b/test/self/Kconfig
@@ -49,6 +49,7 @@ config SELFTEST_ENABLE_ALL
select SELFTEST_IDR
select SELFTEST_TLV
select SELFTEST_DM
+ select SELFTEST_LVM if BLOCK
select SELFTEST_TALLOC
select SELFTEST_BLSPEC if BLSPEC && DEFAULT_ENVIRONMENT
help
@@ -153,6 +154,19 @@ config SELFTEST_TLV
select BASE64
select BOARD_LXA
+config SELFTEST_LVM
+ bool "LVM selftest"
+ depends on BLOCK
+ select DM_LVM
+ select RAMDISK_BLK
+ help
+ Tests the parsing of LVM2 metadata, using physical volume
+ images that are assembled in memory and backed by ramdisks.
+ This covers the tokenizer, the recovery from damaged labels
+ and metadata areas, the selection of the most recent metadata
+ when the physical volumes of a group disagree, and the mapping
+ of logical volumes onto the extents they are made of.
+
config SELFTEST_DM
bool "Device mapper selftest"
select DISK
diff --git a/test/self/Makefile b/test/self/Makefile
index 2bfdbb9949..821b9fa5f3 100644
--- a/test/self/Makefile
+++ b/test/self/Makefile
@@ -27,6 +27,8 @@ obj-$(CONFIG_SELFTEST_TLV) += tlv.o tlv.dtb.o
obj-$(CONFIG_SELFTEST_DM) += dm.o
obj-$(CONFIG_SELFTEST_BLSPEC) += blspec.o
bbenv-$(CONFIG_SELFTEST_BLSPEC) += defaultenv-blspec-test
+obj-$(CONFIG_SELFTEST_LVM) += lvm.o
+CFLAGS_lvm.o += -I$(srctree)/drivers/block/dm/lvm
ifdef REGENERATE_KEYTOC
diff --git a/test/self/lvm.c b/test/self/lvm.c
new file mode 100644
index 0000000000..71f63914e2
--- /dev/null
+++ b/test/self/lvm.c
@@ -0,0 +1,818 @@
+// SPDX-License-Identifier: GPL-2.0-only
+// SPDX-FileCopyrightText: 2026 Tobias Waldekranz <tobias@waldekranz.com>
+
+#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+
+#include <block.h>
+#include <bselftest.h>
+#include <driver.h>
+#include <linux/err.h>
+#include <linux/kernel.h>
+#include <linux/sizes.h>
+#include <lvm.h>
+#include <ramdisk.h>
+#include <stdio.h>
+#include <string.h>
+#include <xfuncs.h>
+
+#include <asm/unaligned.h>
+
+/* Makefile pulls in drivers/block/dm/lvm for access to on-disk
+ * structures.
+ */
+#include <lvm2.h>
+#include <lvm-md.h>
+
+BSELFTEST_GLOBALS();
+
+#define PV_SIZE SZ_1M
+#define MDA_OFFSET (8 * SECTOR_SIZE)
+#define MDA_SIZE (SZ_64K - MDA_OFFSET)
+#define PE_START SZ_64K
+#define EXTENT_SIZE SZ_64K
+
+#define PE_START_SECTORS (PE_START / SECTOR_SIZE)
+#define EXTENT_SECTORS (EXTENT_SIZE / SECTOR_SIZE)
+#define PE_COUNT ((PV_SIZE - PE_START) / EXTENT_SIZE)
+
+#define VG_UUID "vgvgvg11112222333344445555vgvgvg"
+#define LV_UUID "lvlvlv11112222333344445555lvlvlv"
+
+struct test_pv {
+ struct ramdisk *rd;
+ struct block_device *blk;
+ const char *devname;
+ char uuid[LVM_UUID_LEN + 1];
+ u8 *img;
+};
+
+struct test_seg {
+ int pv;
+ u64 start_extent;
+ u64 extent_count;
+ u64 pv_extent;
+};
+
+/* How the image should deviate from a well formed one. */
+struct test_vg_xfrm {
+ struct {
+ struct {
+ int image; /* The sector in which to store the label */
+ int header; /* The recorded sector in the header */
+ } sector;
+
+ bool bad_crc;
+ } label;
+
+ struct {
+ int copies;
+
+ bool bad_text_crc;
+ bool bad_hdr_crc;
+ bool precommitted; /* Leave a second document behind */
+ bool wrap_text;
+ } md;
+};
+
+#define TEST_VG_XFRM_DEFAULT \
+ .label = { \
+ .sector = { \
+ .image = 1, \
+ .header = -1, \
+ }, \
+ }, \
+ .md = { \
+ .copies = 1, \
+ }
+
+#define TEST_MAX_PVS 2
+#define TEST_MAX_SEGS 3
+
+struct test_vg {
+ struct test_pv pvs[TEST_MAX_PVS];
+ int num_pvs;
+
+ struct test_seg segs[TEST_MAX_SEGS];
+ int num_segs;
+
+ u64 seqno;
+
+ struct test_vg_xfrm xfrm;
+};
+
+static char *generate_md(struct test_vg *tvg)
+{
+ char *text, *pvs = NULL, *segs = NULL;
+ struct test_seg *seg;
+ int i;
+
+ for (i = 0; i < tvg->num_pvs; i++)
+ pvs = xrasprintf(pvs,
+ " pv%u {\n"
+ " id = \"%s\"\n"
+ " device = \"/dev/%s\"\n"
+ " status = [\"ALLOCATABLE\"]\n"
+ " flags = []\n"
+ " dev_size = %u\n"
+ " pe_start = %u\n"
+ " pe_count = %u\n"
+ " }\n",
+ i, tvg->pvs[i].uuid, tvg->pvs[i].devname,
+ PV_SIZE / SECTOR_SIZE, PE_START_SECTORS,
+ PE_COUNT);
+
+ for (i = 0, seg = tvg->segs; i < tvg->num_segs; i++, seg++)
+ segs = xrasprintf(segs,
+ " segment%u {\n"
+ " start_extent = %llu\n"
+ " extent_count = %llu\n"
+ " type = \"striped\"\n"
+ " stripe_count = 1\n"
+ " stripes = [\n"
+ " \"pv%u\", %llu\n"
+ " ]\n"
+ " }\n",
+ i + 1, seg->start_extent, seg->extent_count,
+ seg->pv, seg->pv_extent);
+
+ text = xasprintf("vgtest {\n"
+ " id = \"%s\"\n"
+ " seqno = %llu\n"
+ " format = \"lvm2\"\n"
+ " status = [\"RESIZEABLE\", \"READ\", \"WRITE\"]\n"
+ " flags = []\n"
+ " extent_size = %u\n"
+ " max_lv = 0\n"
+ " max_pv = 0\n"
+ " metadata_copies = 0\n"
+ " physical_volumes {\n"
+ "%s"
+ " }\n"
+ " logical_volumes {\n"
+ " lv0 {\n"
+ " id = \"%s\"\n"
+ " status = [\"READ\", \"WRITE\", \"VISIBLE\"]\n"
+ " flags = []\n"
+ " segment_count = %u\n"
+ "%s"
+ " }\n"
+ " }\n"
+ "}\n"
+ "# Generated by the barebox self test\n",
+ VG_UUID, tvg->seqno, EXTENT_SECTORS, pvs, LV_UUID,
+ tvg->num_segs, segs);
+
+ free(pvs);
+ free(segs);
+ return text;
+}
+
+static void inject_md(struct test_pv *tpv, const char *text, int copy,
+ const struct test_vg_xfrm *xfrm)
+{
+ u64 start, head, offset = copy ? PV_SIZE - MDA_SIZE : MDA_OFFSET;
+ size_t len = strlen(text) + 1;
+ struct lvm2_md_header *hdr;
+ u32 crc;
+
+ hdr = (void *)(tpv->img + offset);
+
+ /* The document may start anywhere in the circular buffer that
+ * follows the header, and wraps back to its beginning.
+ */
+ start = xfrm->md.wrap_text ? MDA_SIZE - len / 2 : SECTOR_SIZE;
+ head = min_t(u64, len, MDA_SIZE - start);
+
+ memcpy(tpv->img + offset + start, text, head);
+ if (head < len)
+ memcpy(tpv->img + offset + SECTOR_SIZE, text + head,
+ len - head);
+
+ crc = lvm2_crc(text, len);
+ if (xfrm->md.bad_text_crc && !copy)
+ crc = ~crc;
+
+ *hdr = (struct lvm2_md_header) {
+ .version = cpu_to_le32(LVM2_MDA_VERSION),
+ .start = cpu_to_le64(offset),
+ .size = cpu_to_le64(MDA_SIZE),
+ };
+ memcpy(hdr->magic, LVM2_MDA_MAGIC, sizeof(hdr->magic));
+
+ hdr->area[0] = (struct lvm2_md_area) {
+ .offset = cpu_to_le64(start),
+ .size = cpu_to_le64(len),
+ .checksum = cpu_to_le32(crc),
+ };
+
+ if (xfrm->md.precommitted && !copy) {
+ /* LVM only leaves a second document behind if it was
+ * interrupted while committing new metadata.
+ */
+ start = SECTOR_SIZE + len;
+ memcpy(tpv->img + offset + start, text, len);
+
+ hdr->area[1] = (struct lvm2_md_area) {
+ .offset = cpu_to_le64(start),
+ .size = cpu_to_le64(len),
+ .checksum = cpu_to_le32(lvm2_crc(text, len)),
+ };
+ }
+
+ crc = lvm2_crc(&hdr->magic, SECTOR_SIZE - sizeof(hdr->checksum));
+ if (xfrm->md.bad_hdr_crc && !copy)
+ crc = ~crc;
+
+ hdr->checksum = cpu_to_le32(crc);
+}
+
+static void inject_label(struct test_pv *tpv, const struct test_vg_xfrm *xfrm)
+{
+ u8 *sector = tpv->img + xfrm->label.sector.image * SECTOR_SIZE;
+ struct lvm2_label *label = (void *)sector;
+ struct lvm2_pv_header *pvh;
+ struct lvm2_area *mda;
+ u32 crc;
+ int i;
+
+ *label = (struct lvm2_label) {
+ .sector = cpu_to_le64(xfrm->label.sector.header < 0 ?
+ xfrm->label.sector.image : xfrm->label.sector.header),
+ .pv_offset = cpu_to_le32(sizeof(*label)),
+ };
+ memcpy(label->id, LVM2_LABEL_ID, sizeof(label->id));
+ memcpy(label->type, LVM2_LABEL_TYPE, sizeof(label->type));
+
+ pvh = (void *)(sector + sizeof(*label));
+ *pvh = (struct lvm2_pv_header) {
+ .size = cpu_to_le64(PV_SIZE >> SECTOR_SHIFT),
+ };
+ memcpy(pvh->uuid, tpv->uuid, sizeof(pvh->uuid));
+
+ /* One data area, then the metadata areas, each list terminated
+ * by a zeroed entry.
+ */
+ mda = pvh->area;
+ mda->offset = cpu_to_le64(PE_START);
+ mda += 2;
+
+ for (i = 0; i < xfrm->md.copies; i++, mda++) {
+ *mda = (struct lvm2_area) {
+ .offset = cpu_to_le64(i ? PV_SIZE - MDA_SIZE : MDA_OFFSET),
+ .size = cpu_to_le64(MDA_SIZE),
+ };
+ }
+
+ crc = lvm2_crc(&label->pv_offset,
+ SECTOR_SIZE - offsetof(struct lvm2_label, pv_offset));
+ if (xfrm->label.bad_crc)
+ crc = ~crc;
+
+ label->crc = cpu_to_le32(crc);
+}
+
+static int tvg_init(struct test_vg *tvg)
+{
+ struct test_pv *tpv;
+ char *text;
+ int i, j;
+
+ for (i = 0, tpv = tvg->pvs; i < tvg->num_pvs; i++, tpv++) {
+ memset(tpv->uuid, 'a' + i, LVM_UUID_LEN);
+ tpv->uuid[LVM_UUID_LEN] = '\0';
+
+ tpv->rd = ramdisk_init(SECTOR_SIZE);
+ if (!tpv->rd) {
+ pr_err("Failed to create ramdisk\n");
+ return -ENODEV;
+ }
+
+ tpv->img = xzalloc(PV_SIZE);
+ ramdisk_setup_rw(tpv->rd, tpv->img, PV_SIZE);
+
+ tpv->blk = ramdisk_get_block_device(tpv->rd);
+ tpv->devname = cdev_name(&tpv->blk->cdev);
+ }
+
+ text = generate_md(tvg);
+
+ for (i = 0, tpv = tvg->pvs; i < tvg->num_pvs; i++, tpv++) {
+ for (j = 0; j < tvg->xfrm.md.copies; j++)
+ inject_md(tpv, text, j, &tvg->xfrm);
+
+ inject_label(tpv, &tvg->xfrm);
+ }
+
+ free(text);
+ return 0;
+}
+
+static int tvg_init_simple(struct test_vg *tvg, const struct test_vg_xfrm *xfrm)
+{
+ *tvg = (struct test_vg) {
+ .num_pvs = 1,
+ .num_segs = 1,
+ .segs = {
+ {
+ .pv = 0,
+ .pv_extent = 0,
+ .start_extent = 0,
+ .extent_count = 2,
+ },
+ },
+ .seqno = 7,
+
+ .xfrm = *xfrm,
+ };
+
+ return tvg_init(tvg);
+}
+
+static void tpv_free(struct test_pv *tpv)
+{
+ if (tpv->rd) {
+ ramdisk_setup_rw(tpv->rd, NULL, 0);
+ ramdisk_free(tpv->rd);
+ tpv->rd = NULL;
+ }
+
+ free(tpv->img);
+ tpv->img = NULL;
+}
+
+static void tvg_free(struct test_vg *tvg)
+{
+ struct test_pv *tpv;
+ int i;
+
+ for (i = 0, tpv = tvg->pvs; i < tvg->num_pvs; i++, tpv++)
+ tpv_free(tpv);
+}
+
+static void test_lvm_simple(void)
+{
+ struct test_vg_xfrm xfrm = { TEST_VG_XFRM_DEFAULT };
+ struct test_vg tvg;
+ struct lvm_vg *vg;
+ struct lvm_lv *lv;
+ char *table, *expect;
+
+ if (tvg_init_simple(&tvg, &xfrm))
+ return;
+
+ if (!assert_cond(!lvm_vg_alloc_by_cdev(&tvg.pvs[0].blk->cdev, &vg)))
+ goto out;
+
+ assert_streq(vg->name, "vgtest");
+ assert_streq(vg->uuid, VG_UUID);
+ assert_inteq(vg->seqno, 7);
+ assert_inteq(vg->pe_size, EXTENT_SECTORS);
+ assert_inteq(vg->num_pvs, 1);
+ assert_inteq(vg->num_lvs, 1);
+
+ lv = lvm_vg_lv_by_name(vg, "lv0");
+ if (!assert_cond(lv))
+ goto out_free;
+
+ assert_streq(lv->uuid, LV_UUID);
+ assert_inteq(lv->type, LVM_LV_LINEAR);
+ assert_inteq(lv->size, 2 * EXTENT_SECTORS);
+
+ table = lvm_lv_dm_ctable(lv);
+ if (assert_cond(!IS_ERR(table))) {
+ expect = xasprintf("0 %u linear /dev/%s %u\n",
+ 2 * EXTENT_SECTORS, tvg.pvs[0].devname,
+ PE_START_SECTORS);
+ assert_streq(table, expect);
+ free(expect);
+ free(table);
+ }
+
+out_free:
+ lvm_vg_free(vg);
+out:
+ tvg_free(&tvg);
+}
+bselftest(core, test_lvm_simple);
+
+
+/* Two PVs, holding an LV whose extents are neither contiguous nor in
+ * the order they appear on disk.
+ */
+static void test_lvm_scattered(void)
+{
+ struct test_vg tvg = {
+ .num_pvs = 2,
+ .num_segs = 3,
+ .segs = {
+ {
+ .pv = 0,
+ .pv_extent = 0,
+ .start_extent = 4,
+ .extent_count = 2,
+ },
+ {
+ .pv = 1,
+ .pv_extent = 1,
+ .start_extent = 2,
+ .extent_count = 2,
+ },
+ {
+ .pv = 0,
+ .start_extent = 0,
+ .extent_count = 2,
+ .pv_extent = 4,
+ }
+ },
+
+ .seqno = 3,
+
+ .xfrm = { TEST_VG_XFRM_DEFAULT },
+ };
+ struct lvm_vg *vg;
+ struct lvm_lv *lv;
+ char *table, *expect;
+
+ if (tvg_init(&tvg))
+ goto out;
+
+ if (!assert_cond(!lvm_vg_alloc_by_cdev(&tvg.pvs[0].blk->cdev, &vg)))
+ goto out;
+
+ assert_inteq(vg->num_pvs, 2);
+
+ lv = lvm_vg_lv_by_name(vg, "lv0");
+ if (!assert_cond(lv))
+ goto out_free;
+
+ assert_inteq(lv->size, 6 * EXTENT_SECTORS);
+
+ table = lvm_lv_dm_ctable(lv);
+ if (assert_cond(!IS_ERR(table))) {
+ /* Sorted by logical offset, with every segment mapped to
+ * the right offset of the right device.
+ */
+ expect = xasprintf("0 %u linear /dev/%s %u\n"
+ "%u %u linear /dev/%s %u\n"
+ "%u %u linear /dev/%s %u\n",
+ 2 * EXTENT_SECTORS, tvg.pvs[0].devname,
+ PE_START_SECTORS + 4 * EXTENT_SECTORS,
+
+ 2 * EXTENT_SECTORS, 2 * EXTENT_SECTORS,
+ tvg.pvs[1].devname,
+ PE_START_SECTORS + 1 * EXTENT_SECTORS,
+
+ 4 * EXTENT_SECTORS, 2 * EXTENT_SECTORS,
+ tvg.pvs[0].devname, PE_START_SECTORS);
+ assert_streq(table, expect);
+ free(expect);
+ free(table);
+ }
+
+out_free:
+ lvm_vg_free(vg);
+out:
+ tvg_free(&tvg);
+}
+bselftest(core, test_lvm_scattered);
+
+struct header_test_case {
+ const char *name;
+ bool valid;
+ struct test_vg_xfrm xfrm;
+};
+
+static const struct header_test_case header_cases[] = {
+ {
+ .name = "pristine",
+ .valid = true,
+ .xfrm = {
+ TEST_VG_XFRM_DEFAULT,
+ }
+ },
+ {
+ .name = "label in sector 0",
+ .valid = true,
+ .xfrm = {
+ TEST_VG_XFRM_DEFAULT,
+ .label.sector.image = 0,
+ }
+ },
+ {
+ .name = "label out of range",
+ .valid = false,
+ .xfrm = {
+ TEST_VG_XFRM_DEFAULT,
+ .label.sector.image = 4,
+ }
+ },
+ {
+ .name = "bad label crc",
+ .valid = false,
+ .xfrm = {
+ TEST_VG_XFRM_DEFAULT,
+ .label.bad_crc = true,
+ }
+ },
+ {
+ .name = "wrapped metadata",
+ .valid = true,
+ .xfrm = {
+ TEST_VG_XFRM_DEFAULT,
+ .md.wrap_text = true,
+ }
+ },
+ {
+ .name = "bad metadata crc",
+ .valid = false,
+ .xfrm = {
+ TEST_VG_XFRM_DEFAULT,
+ .md.bad_text_crc = true,
+ }
+ },
+ {
+ .name = "backup metadata area",
+ .valid = true,
+ .xfrm = {
+ TEST_VG_XFRM_DEFAULT,
+ .md = {
+ .bad_text_crc = true,
+ .copies = 2,
+ },
+ }
+ },
+ {
+ .name = "bad metadata header crc",
+ .valid = false,
+ .xfrm = {
+ TEST_VG_XFRM_DEFAULT,
+ .md.bad_hdr_crc = true,
+ }
+ },
+ {
+ .name = "backup after bad header",
+ .valid = true,
+ .xfrm = {
+ TEST_VG_XFRM_DEFAULT,
+ .md = {
+ .bad_hdr_crc = true,
+ .copies = 2,
+ },
+ }
+ },
+ {
+ .name = "relocated label",
+ .valid = false,
+ .xfrm = {
+ TEST_VG_XFRM_DEFAULT,
+ .label.sector.header = 2,
+ }
+ },
+ {
+ .name = "precommitted metadata",
+ .valid = false,
+ .xfrm = {
+ TEST_VG_XFRM_DEFAULT,
+ .md = {
+ .bad_text_crc = true,
+ .precommitted = true,
+ },
+ }
+ },
+};
+
+/* Images that must be parsed, and images that must be rejected. */
+static void test_lvm_headers(void)
+{
+ struct test_vg tvg;
+ struct lvm_vg *vg;
+ int err, i;
+
+ for (i = 0; i < ARRAY_SIZE(header_cases); i++) {
+ if (tvg_init_simple(&tvg, &header_cases[i].xfrm))
+ return;
+
+ err = lvm_vg_alloc_by_cdev(&tvg.pvs[0].blk->cdev, &vg);
+ if (!assert_inteq(!err, header_cases[i].valid))
+ pr_warn("%s: unexpectedly %s\n", header_cases[i].name,
+ err ? "rejected" : "accepted");
+
+ if (!err)
+ lvm_vg_free(vg);
+
+ tvg_free(&tvg);
+ }
+}
+bselftest(core, test_lvm_headers);
+
+/* When the PVs of a VG disagree, the most recently updated metadata
+ * wins.
+ */
+static void test_lvm_seqno(void)
+{
+ struct test_vg tvg = {
+ .num_pvs = 2,
+ .num_segs = 1,
+ .segs = {
+ {
+ .pv = 0,
+ .start_extent = 0,
+ .extent_count = 2
+ },
+ },
+ .seqno = 4,
+ .xfrm = { TEST_VG_XFRM_DEFAULT },
+ };
+ struct lvm_vg_iter *iter;
+ struct lvm_vg *vg;
+ int found = 0;
+ char *text;
+
+ if (tvg_init(&tvg))
+ goto out;
+
+ /* Rewrite the second PV with a newer document. */
+ tvg.seqno = 9;
+ text = generate_md(&tvg);
+ inject_md(&tvg.pvs[1], text, 0, &tvg.xfrm);
+ free(text);
+
+ iter = lvm_vg_iter_new();
+
+ /* The iterator hands over ownership of every VG it returns. */
+ while ((vg = lvm_vg_iter_next(iter))) {
+ if (!strcmp(vg->name, "vgtest")) {
+ found++;
+ assert_inteq(vg->seqno, 9);
+ }
+
+ lvm_vg_free(vg);
+ }
+
+ /* Both PVs describe the same VG, which must be reported once. */
+ assert_inteq(found, 1);
+ lvm_vg_iter_free(iter);
+out:
+ tvg_free(&tvg);
+}
+bselftest(core, test_lvm_seqno);
+
+/* An LV that references a PV which is not present must not be mapped. */
+static void test_lvm_missing_pv(void)
+{
+ struct test_vg tvg = {
+ .num_pvs = 2,
+ .num_segs = 2,
+ .segs = {
+ { .pv = 0, .start_extent = 0, .extent_count = 2 },
+ { .pv = 1, .start_extent = 2, .extent_count = 2 },
+ },
+ .seqno = 1,
+ .xfrm = { TEST_VG_XFRM_DEFAULT },
+ };
+ struct lvm_vg *vg;
+ struct lvm_lv *lv;
+ char *table;
+
+ if (tvg_init(&tvg))
+ goto out;
+
+ /* Take the second PV away again, leaving the metadata on the
+ * first one referring to a device that is not there.
+ */
+ tpv_free(&tvg.pvs[1]);
+
+ if (!assert_cond(!lvm_vg_alloc_by_cdev(&tvg.pvs[0].blk->cdev, &vg)))
+ goto out;
+
+ lv = lvm_vg_lv_by_name(vg, "lv0");
+ if (!assert_cond(lv))
+ goto out_free;
+
+ table = lvm_lv_dm_ctable(lv);
+ if (!assert_cond(IS_ERR(table))) {
+ pr_warn("mapped an LV with a missing PV: %s\n", table);
+ free(table);
+ }
+
+out_free:
+ lvm_vg_free(vg);
+out:
+ tvg_free(&tvg);
+}
+bselftest(core, test_lvm_missing_pv);
+
+/* Documents that the tokenizer must reject, and one that it must
+ * not.
+ */
+static void test_lvm_md_parse(void)
+{
+ static const struct {
+ const char *text;
+ bool valid;
+ } md_cases[] = {
+ { "", true },
+ { "vg {\n}\n", true },
+ { "vg {\nid = \"x\"\n}\n", true },
+ { "vg {\nflags = []\n}\n", true },
+ { "vg {\na = [1, 2, 3]\n}\n", true },
+ { "# just a comment\n", true },
+ { "vg {\nid = \"x\" # trailing\n}\n", true },
+ { "a = 1\0garbage", true },
+ { "vg {\n", false },
+ { "vg }\n", false },
+ { "vg {\nid = \n}\n", false },
+ { "vg {\nid = \"unterminated\n}\n", false },
+ { "vg {\na = [1, 2\n}\n", false },
+ { "= 1\n", false },
+ { "vg {\n}\n}\n", false },
+ { "\x01\x02\x03", false },
+ };
+ struct lvm_md *md;
+ int err, i;
+
+ for (i = 0; i < ARRAY_SIZE(md_cases); i++) {
+ err = lvm_md_parse_alloc(md_cases[i].text,
+ strlen(md_cases[i].text), &md);
+
+ if (!assert_inteq(!err, md_cases[i].valid))
+ pr_warn("\"%s\" unexpectedly %s\n", md_cases[i].text,
+ err ? "rejected" : "accepted");
+
+ if (!err)
+ lvm_md_free(md);
+ }
+}
+bselftest(parser, test_lvm_md_parse);
+
+/* The accessors on top of the tokenizer. */
+static void test_lvm_md_access(void)
+{
+ static const char text[] =
+ "vg {\n"
+ " id = \"abc\"\n"
+ " seqno = 42\n"
+ " nested {\n"
+ " deep = 7\n"
+ " }\n"
+ " arr = [\"a\", 2]\n"
+ " dangling\n"
+ "}\n";
+ const lvm_tok_t *vg, *key, *tok;
+ struct lvm_md *md;
+ char *str;
+ u64 v;
+
+ if (!assert_cond(!lvm_md_parse_alloc(text, strlen(text), &md)))
+ return;
+
+ vg = lvm_md_vgsect(md, &key);
+ if (!assert_cond(vg))
+ goto out;
+
+ str = lvm_md_tok_xstrdup(md, key);
+ assert_streq(str, "vg");
+ free(str);
+
+ str = lvm_md_strdup(md, vg, "id");
+ if (assert_cond(str)) {
+ assert_streq(str, "abc");
+ free(str);
+ }
+
+ assert_cond((!lvm_md_u64(md, vg, "seqno", &v) && v == 42));
+
+ /* A missing key, and a key without a value, both come back
+ * empty rather than as something one past the end.
+ */
+ assert_cond(!lvm_md_find(md, vg, "nosuchkey"));
+ assert_cond(!lvm_md_find(md, vg, "dangling"));
+ assert_cond(lvm_md_u64(md, vg, "nosuchkey", &v));
+
+ tok = lvm_md_find(md, vg, "nested");
+ if (assert_cond(tok)) {
+ assert_inteq(tok->type, LVM_TOK_SECTION);
+ assert_cond((!lvm_md_u64(md, tok, "deep", &v) && v == 7));
+ }
+
+ tok = lvm_md_find(md, vg, "arr");
+ if (assert_cond(tok)) {
+ assert_inteq(tok->type, LVM_TOK_ARRAY);
+ assert_inteq(tok->size, 2);
+
+ key = lvm_md_first(md, tok);
+ if (assert_cond(key)) {
+ str = lvm_md_tok_xstrdup(md, key);
+ assert_streq(str, "a");
+ free(str);
+
+ key = lvm_md_next(md, tok, key);
+ assert_cond((key &&
+ !lvm_md_tok_u64(md, key, &v) && v == 2));
+ }
+ }
+
+out:
+ lvm_md_free(md);
+}
+bselftest(parser, test_lvm_md_access);
--
2.43.0
^ permalink raw reply [flat|nested] 7+ messages in thread* [PATCH remotes/origin/next 4/5] commands: lvm: inspect VGs, activate LVs
2026-09-17 7:40 [PATCH remotes/origin/next 0/5] dm: lvm: Initial Logical Volume Management support Tobias Waldekranz
` (2 preceding siblings ...)
2026-09-17 7:40 ` [PATCH remotes/origin/next 3/5] test: self: lvm: Add tests " Tobias Waldekranz
@ 2026-09-17 7:40 ` Tobias Waldekranz
2026-09-17 7:40 ` [PATCH remotes/origin/next 5/5] test: py: lvm: Add basic LV activation test Tobias Waldekranz
4 siblings, 0 replies; 7+ messages in thread
From: Tobias Waldekranz @ 2026-09-17 7:40 UTC (permalink / raw)
To: barebox
Provide the most rudimentary commands needed to inspect and activate
volume groups (VGs) and logical volumes (LVs).
Signed-off-by: Tobias Waldekranz <tobias@waldekranz.com>
---
commands/Kconfig | 11 ++
commands/Makefile | 1 +
commands/lvm.c | 264 ++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 276 insertions(+)
create mode 100644 commands/lvm.c
diff --git a/commands/Kconfig b/commands/Kconfig
index c8804c8e17..1eb81f30b3 100644
--- a/commands/Kconfig
+++ b/commands/Kconfig
@@ -808,6 +808,17 @@ config CMD_VERITYSETUP
commands:
open <data-dev> <name> <hash-dev> <root-hash>
+config CMD_LVM
+ tristate
+ depends on DM_LVM
+ prompt "lvm"
+ help
+ lvm - inspect and activate LVM2 logical volumes
+
+ commands:
+ info
+ activate <vg> <lv> [<name>]
+
# end Partition commands
endmenu
diff --git a/commands/Makefile b/commands/Makefile
index 7f0c68a5e5..7dae582225 100644
--- a/commands/Makefile
+++ b/commands/Makefile
@@ -172,6 +172,7 @@ obj-$(CONFIG_CMD_EFI_HANDLE_DUMP) += efi_handle_dump.o
obj-$(CONFIG_CMD_HOST) += host.o
obj-$(CONFIG_CMD_DMSETUP) += dmsetup.o
obj-$(CONFIG_CMD_VERITYSETUP) += veritysetup.o
+obj-$(CONFIG_CMD_LVM) += lvm.o
obj-$(CONFIG_CMD_SCONFIG) += sconfig.o
obj-$(CONFIG_CMD_RKSECURE) += rksecure.o
UBSAN_SANITIZE_ubsan.o := y
diff --git a/commands/lvm.c b/commands/lvm.c
new file mode 100644
index 0000000000..9ae9e091c1
--- /dev/null
+++ b/commands/lvm.c
@@ -0,0 +1,264 @@
+// SPDX-License-Identifier: GPL-2.0-only
+// SPDX-FileCopyrightText: 2026 Tobias Waldekranz <tobias@waldekranz.com>
+
+#include <block.h>
+#include <command.h>
+#include <device-mapper.h>
+#include <driver.h>
+#include <fs.h>
+#include <stdio.h>
+#include <xfuncs.h>
+
+#include <linux/err.h>
+#include <linux/kernel.h>
+#include <linux/sprintf.h>
+
+#include <lvm.h>
+
+static int lvm_vg_by_devpath(const char *devpath, struct lvm_vg **vgp)
+{
+ struct cdev *cdev;
+
+ cdev = cdev_by_name(devpath_to_name(devpath));
+ if (!cdev)
+ return -ENODEV;
+
+ return lvm_vg_alloc_by_cdev(cdev, vgp);
+}
+
+static int lvm_vg_by_spec(const char *spec, struct lvm_vg **vgp)
+{
+ int err;
+
+ err = lvm_vg_by_devpath(spec, vgp);
+ switch (err) {
+ case 0:
+ return 0;
+ case -ENODEV:
+ err = lvm_vg_alloc_by_name(spec, vgp);
+ if (err)
+ printf("Found no volume group named \"%s\"\n", spec);
+
+ return err;
+ }
+
+ printf("No volume group found on \"%s\"\n", spec);
+ return err;
+}
+
+/* Total size of a VG, in sectors, i.e. the sum of the extents of all
+ * its physical volumes.
+ */
+static blkcnt_t lvm_vg_size(struct lvm_vg *vg)
+{
+ blkcnt_t sectors = 0;
+ size_t i;
+
+ for (i = 0; i < vg->num_pvs; i++)
+ sectors += vg->pvs[i]->pe_count * vg->pe_size;
+
+ return sectors;
+}
+
+/* LVM UUIDs are conventionally displayed in 6-4-4-4-4-4-6 groups. */
+#define LVM_UUID_STR_LEN (LVM_UUID_LEN + 6)
+
+static const char *lvm_uuid_str(char *buf, const char *uuid)
+{
+ static const int grp[] = { 6, 4, 4, 4, 4, 4, 6 };
+ char *out = buf;
+ size_t g;
+
+ for (g = 0; g < ARRAY_SIZE(grp); g++) {
+ if (g)
+ *out++ = '-';
+ memcpy(out, uuid, grp[g]);
+ out += grp[g];
+ uuid += grp[g];
+ }
+ *out = '\0';
+ return buf;
+}
+
+static void lvm_info_vg(struct lvm_vg *vg)
+{
+ char uuid[LVM_UUID_STR_LEN + 1];
+ char esz[32], vsz[32];
+ struct lvm_pv *pv;
+ struct lvm_lv *lv;
+ struct cdev *cdev;
+ size_t i;
+
+ strcpy(vsz, size_human_readable(lvm_vg_size(vg) << SECTOR_SHIFT));
+ strcpy(esz, size_human_readable(vg->pe_size << SECTOR_SHIFT));
+
+ printf("VG \"%s\":\n"
+ " UUID: %s\n"
+ " SeqNum: %llu\n"
+ " Size: %s (%s extents)\n"
+ " #PV: %zu\n"
+ " #LV: %zu\n",
+ vg->name, lvm_uuid_str(uuid, vg->uuid), vg->seqno, vsz, esz,
+ vg->num_pvs, vg->num_lvs);
+
+ for (i = 0; i < vg->num_pvs; i++) {
+ pv = vg->pvs[i];
+ cdev = lvm_pv_cdev(pv);
+
+ printf(" PV %s:\n"
+ " UUID: %s\n"
+ " Size: %s\n",
+ cdev ? cdev_name(cdev) : "[missing]",
+ lvm_uuid_str(uuid, pv->uuid),
+ size_human_readable((u64)pv->dev_size << SECTOR_SHIFT));
+ }
+
+ for (i = 0; i < vg->num_lvs; i++) {
+ lv = vg->lvs[i];
+
+ printf(" LV \"%s\":\n"
+ " UUID: %s\n"
+ " Type: %s\n"
+ " Size: %s\n",
+ lv->name, lvm_uuid_str(uuid, lv->uuid),
+ lv->type == LVM_LV_LINEAR ? "linear" : "unknown",
+ size_human_readable((u64)lv->size << SECTOR_SHIFT));
+ }
+}
+
+static void lvm_info_all(void)
+{
+ struct lvm_vg_iter *iter;
+ struct lvm_vg *vg;
+ int i = 0;
+
+ iter = lvm_vg_iter_new();
+ while ((vg = lvm_vg_iter_next(iter))) {
+ if (i++)
+ putchar('\n');
+
+ lvm_info_vg(vg);
+ lvm_vg_free(vg);
+ }
+
+ lvm_vg_iter_free(iter);
+}
+
+static int lvm_info(int argc, char *argv[])
+{
+ struct lvm_vg *vg = NULL;
+
+ if (argc > 1)
+ return COMMAND_ERROR_USAGE;
+
+ if (argc == 0) {
+ lvm_info_all();
+ return COMMAND_SUCCESS;
+ }
+
+ if (lvm_vg_by_spec(argv[0], &vg))
+ return COMMAND_ERROR;
+
+ lvm_info_vg(vg);
+ lvm_vg_free(vg);
+ return COMMAND_SUCCESS;
+}
+
+static int lvm_activate_lv(struct lvm_lv *lv, const char *name)
+{
+ char *defname, *table;
+ struct dm_device *dm;
+
+ table = lvm_lv_dm_ctable(lv);
+ if (IS_ERR(table)) {
+ printf("Cannot map %s/%s: %pe\n", lv->vg->name, lv->name, table);
+ return PTR_ERR(table);
+ }
+
+ defname = name ? NULL : xasprintf("%s-%s", lv->vg->name, lv->name);
+
+ dm = dm_create(name ? : defname, table);
+ free(table);
+ if (IS_ERR(dm))
+ printf("Failed to create %s: %pe\n", name ? : defname, dm);
+ else
+ printf("Created %s\n", name ? : defname);
+
+ free(defname);
+ return IS_ERR(dm) ? PTR_ERR(dm) : 0;
+}
+
+static int lvm_activate(int argc, char *argv[])
+{
+ struct lvm_vg *vg = NULL;
+ struct lvm_lv *lv;
+ int err;
+
+ if (argc < 2 || argc > 3)
+ return COMMAND_ERROR_USAGE;
+
+ if (lvm_vg_by_spec(argv[0], &vg))
+ return COMMAND_ERROR;
+
+ lv = lvm_vg_lv_by_name(vg, argv[1]);
+ if (!lv) {
+ printf("Logical volume \"%s/%s\" not found\n", vg->name, argv[1]);
+ err = -ENOENT;
+ goto out_free;
+ }
+
+ err = lvm_activate_lv(lv, (argc == 3) ? argv[2] : NULL);
+
+out_free:
+ lvm_vg_free(vg);
+ return err ? COMMAND_ERROR : COMMAND_SUCCESS;
+}
+
+static int do_lvm(int argc, char *argv[])
+{
+ const char *cmd;
+
+ switch (argc) {
+ case 1:
+ cmd = "info";
+ argc--;
+ argv++;
+ break;
+ default:
+ cmd = argv[1];
+ argc -= 2;
+ argv += 2;
+ break;
+ }
+
+ if (!strcmp(cmd, "info"))
+ return lvm_info(argc, argv);
+ else if (!strcmp(cmd, "activate"))
+ return lvm_activate(argc, argv);
+
+ printf("Unknown command: %s\n", cmd);
+ return COMMAND_ERROR_USAGE;
+}
+
+BAREBOX_CMD_HELP_START(lvm)
+BAREBOX_CMD_HELP_TEXT("lvm - inspect and activate LVM logical volumes")
+BAREBOX_CMD_HELP_TEXT("")
+BAREBOX_CMD_HELP_TEXT("Assembles the volume groups described by the LVM physical volumes")
+BAREBOX_CMD_HELP_TEXT("found on the available block devices, and activates logical volumes")
+BAREBOX_CMD_HELP_TEXT("as device mapper devices.")
+BAREBOX_CMD_HELP_TEXT("")
+BAREBOX_CMD_HELP_TEXT("A <vg> may be given either as a volume group name or as the path to")
+BAREBOX_CMD_HELP_TEXT("a block device holding one of its physical volumes.")
+BAREBOX_CMD_HELP_TEXT("")
+BAREBOX_CMD_HELP_TEXT("commands:")
+BAREBOX_CMD_HELP_OPT("info [<vg>]", "Show volume groups (all if <vg> is omitted)")
+BAREBOX_CMD_HELP_OPT("activate <vg> <lv> [<name>]", "Create a dm device for an LV")
+BAREBOX_CMD_HELP_END
+
+BAREBOX_CMD_START(lvm)
+ .cmd = do_lvm,
+ BAREBOX_CMD_DESC("inspect and activate LVM logical volumes")
+ BAREBOX_CMD_OPTS("<command> [args...]")
+ BAREBOX_CMD_GROUP(CMD_GRP_PART)
+ BAREBOX_CMD_HELP(cmd_lvm_help)
+BAREBOX_CMD_END
--
2.43.0
^ permalink raw reply [flat|nested] 7+ messages in thread* [PATCH remotes/origin/next 5/5] test: py: lvm: Add basic LV activation test
2026-09-17 7:40 [PATCH remotes/origin/next 0/5] dm: lvm: Initial Logical Volume Management support Tobias Waldekranz
` (3 preceding siblings ...)
2026-09-17 7:40 ` [PATCH remotes/origin/next 4/5] commands: lvm: inspect VGs, activate LVs Tobias Waldekranz
@ 2026-09-17 7:40 ` Tobias Waldekranz
2026-09-23 8:04 ` Sascha Hauer
4 siblings, 1 reply; 7+ messages in thread
From: Tobias Waldekranz @ 2026-09-17 7:40 UTC (permalink / raw)
To: barebox
Verify that Barebox can parse the metadata of a PV and activate an LV
discontinuously spread over two PVs.
Official LVM tooling only works on block devices, so guestfish(1) is
leveraged to create the test image. Since it is a bit finicky to run
on some platforms (e.g., Ubuntu installs kernels as 0600, which means
users can't run guestfish by default), we opt for checking in a
compressed version along with the script used to generate it.
Signed-off-by: Tobias Waldekranz <tobias@waldekranz.com>
---
.../boards/configs/enable_dm_testing.config | 2 +
scripts/lvm-pvs.guestfish | 53 ++++++++++++
test/py/test_lvm.py | 76 ++++++++++++++++++
test/testdata/lvm-pvs.disk.xz | Bin 0 -> 2956 bytes
4 files changed, 131 insertions(+)
create mode 100755 scripts/lvm-pvs.guestfish
create mode 100644 test/py/test_lvm.py
create mode 100644 test/testdata/lvm-pvs.disk.xz
diff --git a/common/boards/configs/enable_dm_testing.config b/common/boards/configs/enable_dm_testing.config
index 16c3f702c1..eda2fc41a3 100644
--- a/common/boards/configs/enable_dm_testing.config
+++ b/common/boards/configs/enable_dm_testing.config
@@ -3,7 +3,9 @@ CONFIG_DISK=y
CONFIG_DM_BLK=y
CONFIG_DM_BLK_LINEAR=y
CONFIG_DM_BLK_VERITY=y
+CONFIG_DM_LVM=y
CONFIG_CMD_DMSETUP=y
CONFIG_CMD_VERITYSETUP=y
+CONFIG_CMD_LVM=y
CONFIG_CMD_MD5SUM=y
CONFIG_CMD_READF=y
diff --git a/scripts/lvm-pvs.guestfish b/scripts/lvm-pvs.guestfish
new file mode 100755
index 0000000000..c8d31385f3
--- /dev/null
+++ b/scripts/lvm-pvs.guestfish
@@ -0,0 +1,53 @@
+#!/usr/bin/guestfish -f
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Create a single disk image that holds two LVM PVs. This is used by
+# the LVM labgrid tests.
+
+alloc lvm-pvs.disk 8M
+launch
+
+part-init /dev/sda gpt
+part-add /dev/sda p 34 8191
+part-add /dev/sda p 8192 -34
+
+# Keep sizes reasonably small
+debug sh "lvm pvcreate --metadatasize 4k /dev/sda1"
+debug sh "lvm pvcreate --metadatasize 4k --pvmetadatacopies 2 /dev/sda2"
+debug sh "lvm vgcreate -s 4k testvg /dev/sda1 /dev/sda2"
+
+# Create an LV with three segments that span both PVs
+debug sh "lvm lvcreate -y -n testlv -l 8 testvg /dev/sda1:8-15"
+debug sh "lvm lvextend -l +8 testvg/testlv /dev/sda2:0-7"
+debug sh "lvm lvextend -l +8 testvg/testlv /dev/sda1:0-7"
+
+debug sh "lvm lvs -o lv_name,seg_le_ranges --segments testvg"
+
+mkfs vfat /dev/testvg/testlv
+mount /dev/testvg/testlv /
+
+# Generate a test file that:
+# 1. Is big enough to be spread over all three segments
+# 2. Contains data that is sensitvie to reordering
+# 3. Compresses well, since we're checking it in
+debug sh "printf \'0%*s0\' 4094 >>/sysroot/bigfile"
+debug sh "printf \'1%*s1\' 4094 >>/sysroot/bigfile"
+debug sh "printf \'2%*s2\' 4094 >>/sysroot/bigfile"
+debug sh "printf \'3%*s3\' 4094 >>/sysroot/bigfile"
+debug sh "printf \'4%*s4\' 4094 >>/sysroot/bigfile"
+debug sh "printf \'5%*s5\' 4094 >>/sysroot/bigfile"
+debug sh "printf \'6%*s6\' 4094 >>/sysroot/bigfile"
+debug sh "printf \'7%*s7\' 4094 >>/sysroot/bigfile"
+debug sh "printf \'8%*s8\' 4094 >>/sysroot/bigfile"
+debug sh "printf \'9%*s9\' 4094 >>/sysroot/bigfile"
+debug sh "printf \'a%*sa\' 4094 >>/sysroot/bigfile"
+debug sh "printf \'b%*sb\' 4094 >>/sysroot/bigfile"
+debug sh "printf \'c%*sc\' 4094 >>/sysroot/bigfile"
+debug sh "printf \'d%*sd\' 4094 >>/sysroot/bigfile"
+debug sh "printf \'e%*se\' 4094 >>/sysroot/bigfile"
+debug sh "printf \'f%*sf\' 4094 >>/sysroot/bigfile"
+debug sh "printf \'g%*sg\' 4094 >>/sysroot/bigfile"
+debug sh "md5sum </sysroot/bigfile >/sysroot/bigfile.md5sum"
+
+umount /
+
diff --git a/test/py/test_lvm.py b/test/py/test_lvm.py
new file mode 100644
index 0000000000..c148571780
--- /dev/null
+++ b/test/py/test_lvm.py
@@ -0,0 +1,76 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import hashlib
+import lzma
+import os
+import pytest
+import shutil
+
+from .helper import skip_disabled
+
+
+@pytest.fixture(scope="module")
+def lvm_testdata(testfs):
+ """Extract checked in disk image containing two LVM PVs
+
+ The disk image was created using the standard LVM tooling, see
+ test/testdata/lvm-pvs.guestfish.
+ """
+ path = os.path.join(testfs, "lvm")
+ os.makedirs(path, exist_ok=True)
+
+ diskxz = os.path.join(os.path.dirname(__file__), os.pardir, "testdata",
+ "lvm-pvs.disk.xz")
+ disk = os.path.join(path, "lvm-pvs.disk")
+ dmtable = os.path.join(path, "lvm-pvs.disk.dm")
+
+ diskblks = 0
+ with lzma.open(diskxz) as src:
+ with open(disk, "wb") as dst:
+ dst.write(src.read())
+ diskblks = dst.tell() // 512
+
+ with open(dmtable, "w") as f:
+ f.write(f"0 {diskblks} linear lvm-pvs.disk 0\n")
+
+ yield {
+ "disk": disk,
+ "dmtable": dmtable,
+ }
+
+ shutil.rmtree(path)
+
+
+@pytest.fixture(autouse=True)
+def cleanup(barebox, barebox_config):
+ skip_disabled(barebox_config,
+ "CONFIG_CMD_LVM",
+ "CONFIG_CMD_DMSETUP")
+ yield
+ barebox.run("umount /mnt/testvg-testlv")
+ barebox.run("dmsetup remove testvg-testlv")
+ barebox.run("dmsetup remove pvs")
+ barebox.run("cd")
+
+
+def test_lvm(barebox, barebox_config, lvm_testdata):
+ barebox.run_check("cd /mnt/9p/testfs/lvm")
+
+ # LVM only operates on block devices. Create a linear mapping over
+ # the full disk image to accomplish the equivalent of `losetup`.
+ barebox.run_check("dmsetup create pvs lvm-pvs.disk.dm")
+
+ out = "\n".join(barebox.run_check("lvm info"))
+ for line in ("VG \"testvg\"",
+ "#PV: 2",
+ "#LV: 1",
+ "LV \"testlv\"",
+ "Type: linear"):
+ assert line in out, f"Expected \"{line}\" in output of 'lvm info'"
+
+ barebox.run_check("lvm activate testvg testlv")
+ barebox.run_check("mount testvg-testlv")
+
+ bigsum = barebox.run_check("md5sum /mnt/testvg-testlv/bigfile")[0].split()[0]
+ bigexp = barebox.run_check("cat /mnt/testvg-testlv/bigfile.md5sum")[0].split()[0]
+ assert bigsum == bigexp, "Expected md5sum of bigfile to match bigfile.md5sum"
diff --git a/test/testdata/lvm-pvs.disk.xz b/test/testdata/lvm-pvs.disk.xz
new file mode 100644
index 0000000000000000000000000000000000000000..589747a41ce43850e4bcf110a6e0aad95b31a3aa
GIT binary patch
literal 2956
zcmd5;S5y;-7EA(!4xxie6OblNkzznZFd!l=NC{nvgx&&(#1NYF4hEzsU8IN#7<!2!
zEf^^-U677KfJG7!vVOkv&iiuroPFQ-<Ic>TnRAcTBWw@=0R8IZYZwS14&(;_03k{%
zWi;9%qp$-2;D7uxg;|^-hNnw3)B)_aM4^iLF~n|VoYHz5ioN~{vozac*gdU4!dKr_
z43*E6xn!_gdN3#K#{;JJBl1p3F<pgk^Wp0x+gDqwgnGh|F^HSijm3}B$;brP@{DY+
z6Epvnj=611Jjb96A6pP&3d7prQDjc%ZuTV9z@eRG*CC~v;(g#RZJQMx;Pp8V5?eQj
zmK>%P#G7gha;E!Tu(R?2J|$DI59A&Nim4AjY7)I9V>j@DEPxUoQuZ&XFk@#FZ3>Hy
z+FTB*Ff8lYkTH4a3_;@)inq{r!ce!g?*u6dpi+0HByZsbuU;u#%jOOXRv~HLkGkj!
z`2+`gSI<`2(Kt0(W5-=jCownZ)QM}7O?#(*^we0HKwp6D0d}8MYqW^prh^+2btCCP
zdRfzS_2JP(#VvgFUH|XfwXoN3MzI{sDt0HL3*0*<{aQ+g^8q1dDL*9IN;7*|jc29=
z26MJza_~<3f4(t}IVdr_9y010VxQy@r`d&?z0JMn$sni7d^Dz9W|i>1(b{&bMVkde
zmSvr9tRknRc%@Y3L9yxckSTQwb|WIsL^Uzf(7|!UI4w1-_daC&o}V4TN_z4TFTB-i
ztm0$2?0g}xoV`V^+E0jUX5@!z$h}d67^#!lr6_HeN!>)R`SoX$Cf0Rov@WAb)qn>8
zuGTR2_z+pYwWf`-E>J8f{O(+(V;~Q58{SRo7D+GFOF8$mwzep{XF*SjRnlte2|RL3
zj7xfKh`+h)>!>J7PIH?eNr~o8Mv4|CWW8~WrGJWUeLc4a-Revd0g|Wlj-hm=J?tn7
z)m}k-vpS_{F!QH~!Uwg1cv*qb>av}64JCf0PLD*<cv}w7F1<S81j|Be-NT1=MF<=;
zCQ+7CLiEvs=f1$0D7dKz2CzYQg?}M+KT5;IK4g}OcS!0bboY-dWlK=v#o9z^k!46B
zu`fK+%AYwy2$e`MB=91J>qM9)Pka<zA|g8nWZoirBFNrrp<#2a*cF{Oo)hJ35$E+;
zVn?L&i@(me|CH}oa&ytXL_NPqSd_%z7}P={Jw9Lw8to1fnmk!T&HHP&$0aq;0L(ba
z=q%-Ma`+HvzVJs(Ve|3y0yo(mn8`RR&+H7H`>u!8NN?dVYF*Dkbh;jSw|FS8f69hA
zD|?+ajMV-9F{3@no+*$puaPFba|7CAPD~Nw*V0YcrBs_a5Rsh?uz?@uVF9j|6$#ic
zwmF~2diVSRPG715Tw+0%p=Td&KaBr0QO`AZ!nZ=WCWaT~vs}#ppI0R&1<)p{u6u-g
zn^r!*pn&B*^Bi*hm34FS?SXF0i?(-1sznB*#|8m{$&(pMl8L*7j6JQn!ppL$vk<v6
z=v5ZvP;ji5hx1j)lS&;ue(?IGJ4rloGaAwE#)(~R<(AW6JL`T}NG2WsoBL_GA2CI%
zZBo%98+T7RE24^&f5>fT<?~}T1^9Km*#fB~G^aTb<~c(bj3^}gQqJ5LRUGszRT>{d
zPwOie;IPwmKaVWrLq<gG#I8~2;Mx>fH)d$mkets#6O>HoP;{!{ymHgyS_TgX4U$a>
z47a7Qj?i*tV4Tl>M$R7<C@WfZr^oHbV7uK3QgF&arkV;v#fxqKo`72hY@|%CnmT9U
zR$`;HpuKJUhKGes=SFC)?edBp0~57>x^B|#&sJTFg!Wb!i`JW1bJ$BY%txcaTFaVA
zc&vIxbA|0|9(G+CYf#$CrZ6MVvc<ZO!pdpW8G=w}mE}_lVjnY>t8NLmw)?IWdB!BK
zp7WI|2#T}DgikoO`E4%jxa~dOJ3}cA0e|aJMs%b4V3rx+R3{L1O~ODj%CRXp{yRkJ
z7pZiXe$Wd(s<#;$6#evber==@7Rmq3FF)62(a+}X9!0fyP;1Q4seMKOXmcE7UaEgi
zHQBF4&CS$ANFg%0xAq%P5b^r6_eu|v13UVcG_gxAXFJUekQb#A@-K(Iw7=|32UlUX
zEjF}EGlDVN-+-eUGzj|d!$<l|{TaR*VR08;3zhV-*ckR;b5UeL;ssX3&ZCoy_p*l3
z%<C<r)j~S_!;xLY=ZEVpGtlO;Hns;FOjGq9;1wiJyEJL%ox;0YQ=ZEB(B>RI@Qi%M
zf`q<gP#F%)R`f#H#Md_hcR2+C-{1C8ziojF*qmM5nD`(mqWWR6<46j|SDcUWM5gAM
zQZXm@<>IQ0*(U0p<0BMuY=Q^Y5eXn?MlHsXBH26rK=f=;HyC#dcl<}gL<Z+?ocgz1
zroYCbQ9^?N<pE!V;Z?_-^^q#4N?KA?`~r%rB%Hp=48@UhaY+y4GWVoWheH1Xz<(wN
zzopIpTy4&&^Rk3S2TTRHRd#=rmuCnNZsFj6_%WqmbAvjfLBeuSq?)Wfr=1s6D)-io
zBYRzav!Crb-pCP?ZtWd81f`E}xy0XdR|$-mWBF&J{`WYZIyBo5Em*A$Eq-E_P{>5X
z$eojZMz#RO6Qwp8gP8JexO_3;^|5&6x;T8i#3@~BPk0oG!)I2cZ&k8lhVJRrc%X=l
zYv&}YDd6O(1I#4*X&rRf^B7)nKhi|R{uN&Mo&T(*zlZg|8u7A`rFO~5KJeQAqlbt0
z!cdrju~F?@)@Ha()u+0+hVO1o^5RxmM;3vwKqd*GFORjVsYuDc7QGU@d)CTf9dLPL
zn28!i#|t<OP%k%&=hc<5RR~OT400>^=KVfg;9}H}BI?!BufJNP51EI(v{ViNuJT7k
WMbRgAdCfKw?jC`D%>eYah`#`qRJ~LH
literal 0
HcmV?d00001
--
2.43.0
^ permalink raw reply [flat|nested] 7+ messages in thread* Re: [PATCH remotes/origin/next 5/5] test: py: lvm: Add basic LV activation test
2026-09-17 7:40 ` [PATCH remotes/origin/next 5/5] test: py: lvm: Add basic LV activation test Tobias Waldekranz
@ 2026-09-23 8:04 ` Sascha Hauer
0 siblings, 0 replies; 7+ messages in thread
From: Sascha Hauer @ 2026-09-23 8:04 UTC (permalink / raw)
To: Tobias Waldekranz; +Cc: barebox
Hi Tobias,
Only some nitpicking inside. Otherwise looks fine.
On 2026-09-17 07:40, Tobias Waldekranz wrote:
> diff --git a/scripts/lvm-pvs.guestfish b/scripts/lvm-pvs.guestfish
> new file mode 100755
> index 0000000000..c8d31385f3
> --- /dev/null
> +++ b/scripts/lvm-pvs.guestfish
> @@ -0,0 +1,53 @@
> +#!/usr/bin/guestfish -f
> +# SPDX-License-Identifier: GPL-2.0-only
> +#
> +# Create a single disk image that holds two LVM PVs. This is used by
> +# the LVM labgrid tests.
> +
> +alloc lvm-pvs.disk 8M
> +launch
> +
> +part-init /dev/sda gpt
> +part-add /dev/sda p 34 8191
> +part-add /dev/sda p 8192 -34
You could use part-set-name to avoid
WARNING: pvs: Failed to create link from pvs.primary to pvs.1
> +
> +# Keep sizes reasonably small
> +debug sh "lvm pvcreate --metadatasize 4k /dev/sda1"
> +debug sh "lvm pvcreate --metadatasize 4k --pvmetadatacopies 2 /dev/sda2"
> +debug sh "lvm vgcreate -s 4k testvg /dev/sda1 /dev/sda2"
> +
> +# Create an LV with three segments that span both PVs
> +debug sh "lvm lvcreate -y -n testlv -l 8 testvg /dev/sda1:8-15"
> +debug sh "lvm lvextend -l +8 testvg/testlv /dev/sda2:0-7"
> +debug sh "lvm lvextend -l +8 testvg/testlv /dev/sda1:0-7"
> +
> +debug sh "lvm lvs -o lv_name,seg_le_ranges --segments testvg"
> +
> +mkfs vfat /dev/testvg/testlv
> +mount /dev/testvg/testlv /
> +
> +# Generate a test file that:
> +# 1. Is big enough to be spread over all three segments
> +# 2. Contains data that is sensitvie to reordering
s/sensitvie/sensitive/
> +# 3. Compresses well, since we're checking it in
> +debug sh "printf \'0%*s0\' 4094 >>/sysroot/bigfile"
> +debug sh "printf \'1%*s1\' 4094 >>/sysroot/bigfile"
> +debug sh "printf \'2%*s2\' 4094 >>/sysroot/bigfile"
> +debug sh "printf \'3%*s3\' 4094 >>/sysroot/bigfile"
> +debug sh "printf \'4%*s4\' 4094 >>/sysroot/bigfile"
> +debug sh "printf \'5%*s5\' 4094 >>/sysroot/bigfile"
> +debug sh "printf \'6%*s6\' 4094 >>/sysroot/bigfile"
> +debug sh "printf \'7%*s7\' 4094 >>/sysroot/bigfile"
> +debug sh "printf \'8%*s8\' 4094 >>/sysroot/bigfile"
> +debug sh "printf \'9%*s9\' 4094 >>/sysroot/bigfile"
> +debug sh "printf \'a%*sa\' 4094 >>/sysroot/bigfile"
> +debug sh "printf \'b%*sb\' 4094 >>/sysroot/bigfile"
> +debug sh "printf \'c%*sc\' 4094 >>/sysroot/bigfile"
> +debug sh "printf \'d%*sd\' 4094 >>/sysroot/bigfile"
> +debug sh "printf \'e%*se\' 4094 >>/sysroot/bigfile"
> +debug sh "printf \'f%*sf\' 4094 >>/sysroot/bigfile"
> +debug sh "printf \'g%*sg\' 4094 >>/sysroot/bigfile"
> +debug sh "md5sum </sysroot/bigfile >/sysroot/bigfile.md5sum"
> +
> +umount /
> +
> diff --git a/test/py/test_lvm.py b/test/py/test_lvm.py
> new file mode 100644
> index 0000000000..c148571780
> --- /dev/null
> +++ b/test/py/test_lvm.py
> @@ -0,0 +1,76 @@
> +# SPDX-License-Identifier: GPL-2.0-or-later
> +
> +import hashlib
hashlib seems unused.
Sascha
--
Pengutronix e.K. | |
Steuerwalder Str. 21 | http://www.pengutronix.de/ |
31137 Hildesheim, Germany | Phone: +49-5121-206917-0 |
Amtsgericht Hildesheim, HRA 2686 | Fax: +49-5121-206917-5555 |
^ permalink raw reply [flat|nested] 7+ messages in thread