[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[PATCH 1/2] Support qcow disks in GRUB
From: |
Vladimir Serbinenko |
Subject: |
[PATCH 1/2] Support qcow disks in GRUB |
Date: |
Thu, 16 May 2024 21:57:47 +0300 |
Signed-off-by: Vladimir Serbinenko <phcoder@gmail.com>
---
Makefile.util.def | 1 +
grub-core/Makefile.core.def | 6 +
grub-core/commands/nativedisk.c | 1 +
grub-core/disk/qcow.c | 464 ++++++++++++++++++++++++++++++++
include/grub/disk.h | 1 +
5 files changed, 473 insertions(+)
create mode 100644 grub-core/disk/qcow.c
diff --git a/Makefile.util.def b/Makefile.util.def
index 9432365a9..d8b556afd 100644
--- a/Makefile.util.def
+++ b/Makefile.util.def
@@ -71,6 +71,7 @@ library = {
common = grub-core/commands/ls.c;
common = grub-core/disk/dmraid_nvidia.c;
common = grub-core/disk/loopback.c;
+ common = grub-core/disk/qcow.c;
common = grub-core/disk/lvm.c;
common = grub-core/disk/mdraid_linux.c;
common = grub-core/disk/mdraid_linux_be.c;
diff --git a/grub-core/Makefile.core.def b/grub-core/Makefile.core.def
index 8e1b1d9f3..da65ba68c 100644
--- a/grub-core/Makefile.core.def
+++ b/grub-core/Makefile.core.def
@@ -1204,6 +1204,12 @@ module = {
common = disk/loopback.c;
};
+module = {
+ name = qcow;
+ common = disk/qcow.c;
+ cppflags = '-I$(srcdir)/lib/posix_wrap -I$(srcdir)/lib/zstd';
+};
+
module = {
name = cryptodisk;
common = disk/cryptodisk.c;
diff --git a/grub-core/commands/nativedisk.c b/grub-core/commands/nativedisk.c
index 580c8d3b0..3e9bafac7 100644
--- a/grub-core/commands/nativedisk.c
+++ b/grub-core/commands/nativedisk.c
@@ -98,6 +98,7 @@ get_uuid (const char *name, char **uuid, int getnative)
/* FIXME: those probably need special handling. */
case GRUB_DISK_DEVICE_LOOPBACK_ID:
+ case GRUB_DISK_DEVICE_QCOW_ID:
case GRUB_DISK_DEVICE_DISKFILTER_ID:
case GRUB_DISK_DEVICE_CRYPTODISK_ID:
break;
diff --git a/grub-core/disk/qcow.c b/grub-core/disk/qcow.c
new file mode 100644
index 000000000..ed99b4c78
--- /dev/null
+++ b/grub-core/disk/qcow.c
@@ -0,0 +1,464 @@
+/* qcow.c - command to add loopback qcow devices. */
+/*
+ * GRUB -- GRand Unified Bootloader
+ * Copyright (C) 2024 Free Software Foundation, Inc.
+ *
+ * GRUB is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * GRUB is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with GRUB. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <grub/dl.h>
+#include <grub/misc.h>
+#include <grub/file.h>
+#include <grub/disk.h>
+#include <grub/mm.h>
+#include <grub/extcmd.h>
+#include <grub/i18n.h>
+#include <grub/deflate.h>
+#include <zstd.h>
+
+GRUB_MOD_LICENSE ("GPLv3+");
+
+#define QCOW_MAGIC 0x514649fb
+#define LX_OFFSET_MASK 0xfffffffffffe00LL
+struct qcow_header
+{
+ grub_uint32_t magic;
+ grub_uint32_t version;
+ grub_uint64_t backing_file_offset;
+ grub_uint32_t backing_file_size;
+ grub_uint32_t cluster_bits;
+ grub_uint64_t size;
+ grub_uint32_t crypt_method;
+ grub_uint32_t l1_size;
+ grub_uint64_t l1_table_offset;
+ grub_uint64_t refcount_table_offset;
+ grub_uint32_t refcount_table_clusters;
+ grub_uint32_t nb_snapshots;
+ grub_uint64_t snapshots_offset;
+
+ /* v3 only */
+ grub_uint64_t feat_incompat;
+ grub_uint64_t feat_compat;
+ grub_uint64_t feat_autoclear;
+ grub_uint32_t refcount_order;
+ grub_uint32_t header_length;
+
+ /* Only if v3 and header_length allows it. */
+ grub_uint8_t compression_type;
+};
+
+struct qcow_header_extension
+{
+ grub_uint32_t type;
+ grub_uint32_t length;
+};
+
+struct grub_qcow
+{
+ struct grub_qcow *next;
+ char *devname;
+ grub_file_t file;
+ unsigned long id;
+ struct qcow_header head;
+ grub_uint64_t *l1;
+ grub_uint64_t *l2_0;
+ grub_uint64_t *l2_cache;
+ grub_uint64_t l2_cache_current;
+ grub_uint8_t compression_type;
+};
+
+static struct grub_qcow *qcow_list;
+static unsigned long last_id = 0;
+
+static const struct grub_arg_option options[] =
+ {
+ /* TRANSLATORS: The disk is simply removed from the list of available ones,
+ not wiped, avoid to scare user. */
+ {"delete", 'd', 0, N_("Delete the specified qcow drive."), 0, 0},
+ {0, 0, 0, 0, 0, 0}
+ };
+
+static grub_err_t
+open_qcow (struct grub_qcow *qcow)
+{
+ grub_file_read (qcow->file, &qcow->head, sizeof(qcow->head));
+ if (grub_errno)
+ return grub_errno;
+ if (qcow->head.magic != grub_cpu_to_be32_compile_time(QCOW_MAGIC))
+ return grub_error(GRUB_ERR_BAD_ARGUMENT, "invalid qcow magic");
+ if (qcow->head.version != grub_cpu_to_be32_compile_time(2)
+ && qcow->head.version != grub_cpu_to_be32_compile_time(3))
+ return grub_error(GRUB_ERR_NOT_IMPLEMENTED_YET, "unsupported qcow
version");
+ if (qcow->head.backing_file_offset || qcow->head.backing_file_size)
+ return grub_error(GRUB_ERR_NOT_IMPLEMENTED_YET, "qcow backing file
unsupported");
+ if (qcow->head.crypt_method)
+ return grub_error(GRUB_ERR_NOT_IMPLEMENTED_YET, "encrypted qcow is not
supported");
+
+ if (grub_be_to_cpu32(qcow->head.l1_size) >= (1 << 28))
+ return grub_error(GRUB_ERR_BAD_ARGUMENT, "qcow l1 table is too large");
+ if (!qcow->head.l1_size)
+ return grub_error(GRUB_ERR_BAD_ARGUMENT, "L1 table is missing");
+ if (grub_be_to_cpu32(qcow->head.cluster_bits) >= 26)
+ return grub_error(GRUB_ERR_BAD_ARGUMENT, "qcow cluster size is too large");
+
+ grub_size_t l1_bytes = grub_be_to_cpu32(qcow->head.l1_size) << 3;
+ qcow->l1 = grub_malloc(l1_bytes);
+ if (!qcow->l1)
+ return grub_errno;
+
+ grub_file_seek (qcow->file, grub_be_to_cpu64(qcow->head.l1_table_offset));
+ grub_file_read (qcow->file, qcow->l1, l1_bytes);
+ if (grub_errno)
+ return grub_errno;
+
+ grub_size_t l2_bytes = 1 << grub_be_to_cpu32(qcow->head.cluster_bits);
+ qcow->l2_0 = grub_zalloc(l2_bytes);
+ qcow->l2_cache = grub_zalloc(l2_bytes);
+ if (!qcow->l2_0 || !qcow->l2_cache)
+ {
+ grub_free(qcow->l2_0);
+ grub_free(qcow->l2_cache);
+ return grub_errno;
+ }
+
+ if (qcow->l1[0])
+ {
+ grub_file_seek (qcow->file, grub_be_to_cpu64(qcow->l1[0]) &
LX_OFFSET_MASK);
+ grub_file_read (qcow->file, qcow->l2_0, l2_bytes);
+ }
+ if (grub_errno)
+ return grub_errno;
+
+ grub_uint32_t header_length = qcow->head.version ==
grub_cpu_to_be32_compile_time(3) ? grub_be_to_cpu32(qcow->head.header_length) :
72;
+ qcow->compression_type = (header_length >= 105) ?
qcow->head.compression_type : 0;
+
+ return GRUB_ERR_NONE;
+}
+
+/* Delete the qcow device NAME. */
+static grub_err_t
+delete_qcow (const char *name)
+{
+ struct grub_qcow *dev;
+ struct grub_qcow **prev;
+
+ /* Search for the device. */
+ for (dev = qcow_list, prev = &qcow_list;
+ dev;
+ prev = &dev->next, dev = dev->next)
+ if (grub_strcmp (dev->devname, name) == 0)
+ break;
+
+ if (! dev)
+ return grub_error (GRUB_ERR_BAD_DEVICE, "device not found");
+
+ /* Remove the device from the list. */
+ *prev = dev->next;
+
+ grub_free (dev->devname);
+ grub_file_close (dev->file);
+ grub_free (dev);
+
+ return 0;
+}
+
+/* The command to add and remove qcow devices. */
+static grub_err_t
+grub_cmd_qcow (grub_extcmd_context_t ctxt, int argc, char **args)
+{
+ struct grub_arg_list *state = ctxt->state;
+ grub_file_t file;
+ enum grub_file_type type = GRUB_FILE_TYPE_LOOPBACK;
+ struct grub_qcow *newdev;
+ grub_err_t ret;
+
+ if (argc < 1)
+ return grub_error (GRUB_ERR_BAD_ARGUMENT, "device name required");
+
+ /* Check if `-d' was used. */
+ if (state[0].set)
+ return delete_qcow (args[0]);
+
+ type |= GRUB_FILE_TYPE_NO_DECOMPRESS;
+
+ if (argc < 2)
+ return grub_error (GRUB_ERR_BAD_ARGUMENT, N_("filename expected"));
+
+ /* Check that a device with requested name does not already exist. */
+ for (newdev = qcow_list; newdev; newdev = newdev->next)
+ if (grub_strcmp (newdev->devname, args[0]) == 0)
+ return grub_error (GRUB_ERR_BAD_ARGUMENT, "device name already exists");
+
+ file = grub_file_open (args[1], type);
+ if (! file)
+ return grub_errno;
+
+ /* Unable to replace it, make a new entry. */
+ newdev = grub_zalloc (sizeof (struct grub_qcow));
+ if (! newdev)
+ goto fail;
+
+ newdev->devname = grub_strdup (args[0]);
+ if (! newdev->devname)
+ {
+ grub_free (newdev);
+ goto fail;
+ }
+
+ newdev->file = file;
+ newdev->id = last_id++;
+
+ ret = open_qcow(newdev);
+ if (ret)
+ {
+ grub_free(newdev->devname);
+ grub_free(newdev);
+ goto fail;
+ }
+
+ /* Add the new entry to the list. */
+ newdev->next = qcow_list;
+ qcow_list = newdev;
+
+ return 0;
+
+fail:
+ ret = grub_errno;
+ grub_file_close (file);
+ return ret;
+}
+
+
+static int
+grub_qcow_iterate (grub_disk_dev_iterate_hook_t hook, void *hook_data,
+ grub_disk_pull_t pull)
+{
+ struct grub_qcow *d;
+ if (pull != GRUB_DISK_PULL_NONE)
+ return 0;
+ for (d = qcow_list; d; d = d->next)
+ {
+ if (hook (d->devname, hook_data))
+ return 1;
+ }
+ return 0;
+}
+
+static grub_err_t
+grub_qcow_open (const char *name, grub_disk_t disk)
+{
+ struct grub_qcow *dev;
+
+ for (dev = qcow_list; dev; dev = dev->next)
+ if (grub_strcmp (dev->devname, name) == 0)
+ break;
+
+ if (! dev)
+ return grub_error (GRUB_ERR_UNKNOWN_DEVICE, "can't open device");
+
+ /* Use the filesize for the disk size, round up to a complete sector. */
+ disk->total_sectors = grub_be_to_cpu64(dev->head.size) >>
GRUB_DISK_SECTOR_BITS;
+ /* Avoid reading more than 512M. */
+ disk->max_agglomerate = 1 << (29 - GRUB_DISK_SECTOR_BITS
+ - GRUB_DISK_CACHE_BITS);
+
+ disk->id = dev->id;
+
+ disk->data = dev;
+
+ return 0;
+}
+
+static grub_err_t
+get_l2_entry(struct grub_qcow *qcow, grub_uint64_t cluster, grub_uint64_t *l2e)
+{
+ grub_size_t l2_bytes = 1 << grub_be_to_cpu32(qcow->head.cluster_bits);
+ grub_uint64_t l2_table_bits = grub_be_to_cpu32(qcow->head.cluster_bits) - 3;
+ grub_uint64_t l2n = cluster & ((1 << l2_table_bits) - 1);
+ grub_uint64_t l1n = cluster >> l2_table_bits;
+ if (qcow->l1[l1n] == 0)
+ {
+ *l2e = 0;
+ return GRUB_ERR_NONE;
+ }
+
+ if (l1n >= grub_be_to_cpu32(qcow->head.l1_size))
+ return grub_error(GRUB_ERR_IO, "seeking outside of L1 table");
+ if (l1n == 0)
+ {
+ *l2e = grub_be_to_cpu64(qcow->l2_0[l2n]);
+ return GRUB_ERR_NONE;
+ }
+ if (l1n == qcow->l2_cache_current)
+ {
+ *l2e = grub_be_to_cpu64(qcow->l2_cache[l2n]);
+ return GRUB_ERR_NONE;
+ }
+
+ qcow->l2_cache_current = 0;
+ grub_file_seek (qcow->file, grub_be_to_cpu64(qcow->l1[l1n]) &
LX_OFFSET_MASK);
+ grub_file_read (qcow->file, qcow->l2_cache, l2_bytes);
+ if (grub_errno)
+ return grub_errno;
+ qcow->l2_cache_current = l1n;
+ *l2e = grub_be_to_cpu64(qcow->l2_cache[l2n]);
+ return GRUB_ERR_NONE;
+}
+
+static grub_err_t
+grub_qcow_read (grub_disk_t disk, grub_disk_addr_t sector,
+ grub_size_t size, char *buf)
+{
+ struct grub_qcow *qcow = (struct grub_qcow *) disk->data;
+ unsigned cluster_sec_bits = grub_be_to_cpu32(qcow->head.cluster_bits) -
GRUB_DISK_SECTOR_BITS;
+ grub_uint64_t cluster_sec_size = 1 << cluster_sec_bits;
+ grub_uint64_t cluster = sector >> cluster_sec_bits;
+ grub_size_t cluster_sec_offset = sector & ((1 << cluster_sec_bits) - 1);
+ grub_file_t file = qcow->file;
+ char *decompress_buf = NULL;
+ grub_size_t decompress_buf_size = 0;
+
+ while (size)
+ {
+ grub_size_t max_read = cluster_sec_size - cluster_sec_offset;
+ grub_uint64_t l2e = 0;
+ if (max_read > size)
+ max_read = size;
+
+ grub_err_t err = get_l2_entry(qcow, cluster, &l2e);
+ if (err)
+ {
+ grub_free (decompress_buf);
+ return err;
+ }
+
+ /* Empty. */
+ if (l2e == 0)
+ {
+ grub_memset (buf, 0, max_read << GRUB_DISK_SECTOR_BITS);
+ }
+ /* Uncompressed. */
+ else if (!(l2e & (1LL << 62)))
+ {
+ grub_file_seek (file, (l2e & LX_OFFSET_MASK) + (cluster_sec_offset <<
GRUB_DISK_SECTOR_BITS));
+ grub_file_read (file, buf, max_read << GRUB_DISK_SECTOR_BITS);
+ if (grub_errno)
+ {
+ grub_free (decompress_buf);
+ return grub_errno;
+ }
+ }
+ /* Compressed. */
+ else
+ {
+ int offset_bits = 62 - (grub_be_to_cpu32(qcow->head.cluster_bits) -
8);
+ grub_uint64_t off = l2e & ((1LL << offset_bits) - 1);
+ grub_uint32_t compressed_size = (((l2e & 0x3fffffffffffffffLL) >>
offset_bits) << 9) + 0x200 - (off & 0x1ff);
+ if (qcow->compression_type > 1)
+ {
+ grub_free (decompress_buf);
+ return grub_error(GRUB_ERR_NOT_IMPLEMENTED_YET, "compression type
%d not supported yet", qcow->compression_type);
+ }
+ grub_file_seek (file, off);
+ if (compressed_size > decompress_buf_size)
+ {
+ grub_free(decompress_buf);
+ decompress_buf_size = compressed_size * 2;
+ decompress_buf = grub_malloc (decompress_buf_size);
+ if (!decompress_buf)
+ return grub_errno;
+ }
+ grub_file_read (file, decompress_buf, compressed_size);
+
+ grub_size_t decompressed_size = cluster_sec_size << 9;
+
+ switch (qcow->compression_type)
+ {
+ case 0:
+ if (grub_deflate_decompress(decompress_buf, compressed_size,
(cluster_sec_offset << GRUB_DISK_SECTOR_BITS), buf, max_read <<
GRUB_DISK_SECTOR_BITS) < 0)
+ {
+ grub_free (decompress_buf);
+ return grub_errno;
+ }
+ break;
+ case 1:
+ {
+ char *target_buf = NULL, *target;
+ if (max_read == cluster_sec_size && cluster_sec_offset == 0)
+ target = buf;
+ else
+ {
+ target = target_buf = grub_malloc(decompressed_size);
+ if (!target)
+ {
+ grub_free (decompress_buf);
+ return grub_errno;
+ }
+ }
+ ZSTD_decompress (target, decompressed_size, decompress_buf,
compressed_size);
+ if (target != buf)
+ grub_memcpy(buf, target + (cluster_sec_offset <<
GRUB_DISK_SECTOR_BITS), max_read << GRUB_DISK_SECTOR_BITS);
+ grub_free (target_buf);
+ }
+ break;
+ }
+ }
+ buf += max_read << GRUB_DISK_SECTOR_BITS;
+ size -= max_read;
+ cluster_sec_offset = 0;
+ cluster++;
+ }
+
+ grub_free (decompress_buf);
+ return 0;
+}
+
+static grub_err_t
+grub_qcow_write (grub_disk_t disk __attribute ((unused)),
+ grub_disk_addr_t sector __attribute ((unused)),
+ grub_size_t size __attribute ((unused)),
+ const char *buf __attribute ((unused)))
+{
+ return grub_error (GRUB_ERR_NOT_IMPLEMENTED_YET,
+ "qcow write is not supported");
+}
+
+static struct grub_disk_dev grub_qcow_dev =
+ {
+ .name = "qcow",
+ .id = GRUB_DISK_DEVICE_QCOW_ID,
+ .disk_iterate = grub_qcow_iterate,
+ .disk_open = grub_qcow_open,
+ .disk_read = grub_qcow_read,
+ .disk_write = grub_qcow_write,
+ .next = 0
+ };
+
+static grub_extcmd_t cmd;
+
+GRUB_MOD_INIT(qcow)
+{
+ cmd = grub_register_extcmd ("qcow", grub_cmd_qcow, 0,
+ N_("[-d] [-D] DEVICENAME FILE."),
+ /* TRANSLATORS: The file itself is not destroyed
+ or transformed into drive. */
+ N_("Make a virtual drive from a file."), options);
+ grub_disk_dev_register (&grub_qcow_dev);
+}
+
+GRUB_MOD_FINI(qcow)
+{
+ grub_unregister_extcmd (cmd);
+ grub_disk_dev_unregister (&grub_qcow_dev);
+}
diff --git a/include/grub/disk.h b/include/grub/disk.h
index fbf23df7f..60bcd92be 100644
--- a/include/grub/disk.h
+++ b/include/grub/disk.h
@@ -52,6 +52,7 @@ enum grub_disk_dev_id
GRUB_DISK_DEVICE_UBOOTDISK_ID,
GRUB_DISK_DEVICE_XEN,
GRUB_DISK_DEVICE_OBDISK_ID,
+ GRUB_DISK_DEVICE_QCOW_ID,
};
struct grub_disk;
--
2.39.2
- [PATCH 1/2] Support qcow disks in GRUB,
Vladimir Serbinenko <=