]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
repart: Optionally write minimal an El Torito boot catalog for EFI
authorValentin David <me@valentindavid.com>
Sat, 21 Mar 2026 14:42:13 +0000 (15:42 +0100)
committerDaan De Meyer <daan.j.demeyer@gmail.com>
Sun, 29 Mar 2026 20:46:54 +0000 (22:46 +0200)
This only points the firmware to the ESP. The ISO9660 is empty.
The initramfs should create a loop device to change block size
and enable GPT partitions.

This was tested using OVMF on qemu, with:
`-drive if=pflash,file=OVMF_CODE.fd,readonly=on,format=raw -drive if=pflash,file=OVMF_VARS.fd,format=raw -drive if=none,id=live-disk,file=dick.iso,media=cdrom,format=raw,readonly=on -device virtio-scsi-pci,id=scsi -device scsi-cd,drive=live-disk`

And a simple definition:
```
[Partition]
Type=esp
Format=vfat
CopyFiles=/usr/lib/systemd/boot/efi/systemd-bootx64.efi:/EFI/BOOT/BOOTX64.EFI
```

man/systemd-repart.xml
src/repart/iso9660.c [new file with mode: 0644]
src/repart/iso9660.h [new file with mode: 0644]
src/repart/meson.build
src/repart/repart.c

index 18e127be4648d09dd5b4dbe6515eb410fecf8f5d..0cb14b6991392d3c453ec8e4e916d4711c0bcf83 100644 (file)
         <xi:include href="version-info.xml" xpointer="v257"/></listitem>
       </varlistentry>
 
+      <varlistentry>
+        <term><option>--el-torito=<replaceable>BOOL</replaceable></option></term>
+
+        <listitem><para>Write a minimal ISO9660 header with El Torito boot catalog. That will
+        boot the ESP on EFI firmware.</para>
+
+        <para>The ISO9660 filesystem created by it will be empty. The initramfs is expected to create a
+        partitionable loop device on top of the device to change the block size and enable GPT
+        partitions.</para>
+
+        <para>The disk requires at least one partition with <varname>Type=esp</varname>. The first one will
+        be the one referenced in the boot catalog.</para>
+
+        <para>This option is available only when creating a new partition table, that is when
+        <option>--empty=</option> has value <literal>require</literal>, <literal>force</literal> or
+        <literal>create</literal>.</para>
+
+        <xi:include href="version-info.xml" xpointer="v261"/></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><option>--el-torito-system=<replaceable>STRING</replaceable></option></term>
+
+        <listitem><para>When creating an ISO9660 header, this value will be used as the system identifier.
+        This is useful for the media to be matched against osinfo db.</para>
+
+        <para>The value is limited to 32 characters.</para>
+
+        <xi:include href="version-info.xml" xpointer="v261"/></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><option>--el-torito-volume=<replaceable>STRING</replaceable></option></term>
+
+        <listitem><para>When creating an ISO9660 header, this value will be used as volume identifier.
+        This is useful for the media to be matched against osinfo db.</para>
+
+        <para>The value is limited to 32 characters.</para>
+
+        <xi:include href="version-info.xml" xpointer="v261"/></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><option>--el-torito-publisher=<replaceable>STRING</replaceable></option></term>
+
+        <listitem><para>When creating an ISO9660 header, this value will be used as publisher identifier.
+        This is useful for the media to be matched against osinfo db.</para>
+
+        <para>The value is limited to 128 characters.</para>
+
+        <xi:include href="version-info.xml" xpointer="v261"/></listitem>
+      </varlistentry>
+
       <xi:include href="standard-options.xml" xpointer="help" />
       <xi:include href="standard-options.xml" xpointer="version" />
       <xi:include href="standard-options.xml" xpointer="no-pager" />
diff --git a/src/repart/iso9660.c b/src/repart/iso9660.c
new file mode 100644 (file)
index 0000000..5bc9588
--- /dev/null
@@ -0,0 +1,116 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <time.h>
+
+#include "iso9660.h"
+#include "log.h"
+#include "stdio-util.h"
+#include "string-util.h"
+#include "time-util.h"
+
+void no_iso9660_datetime(struct iso9660_datetime *ret) {
+        assert(ret);
+
+        memcpy(ret->year, "0000", 4);
+        memcpy(ret->month, "00", 2);
+        memcpy(ret->day, "00", 2);
+        memcpy(ret->hour, "00", 2);
+        memcpy(ret->minute, "00", 2);
+        memcpy(ret->second, "00", 2);
+        memcpy(ret->deci, "00", 2);
+        ret->zone = 0;
+}
+
+int time_to_iso9660_datetime(usec_t usec, bool utc, struct iso9660_datetime *ret) {
+        struct tm t;
+        int r;
+
+        assert(ret);
+
+        r = localtime_or_gmtime_usec(usec, utc, &t);
+        if (r < 0)
+                return r;
+
+        if (t.tm_year >= 10000 - 1900)
+                return log_error_errno(SYNTHETIC_ERRNO(ERANGE), "Year has more than 4 digits and is incompatible with ISO9660.");
+        if (t.tm_year + 1900 < 0)
+                return log_error_errno(SYNTHETIC_ERRNO(ERANGE), "Year is negative and is incompatible with ISO9660.");
+
+        char buf[17];
+        /* Ignore leap seconds, no real hope for hardware. Deci-seconds always zero. */
+        xsprintf(buf, "%04d%02d%02d%02d%02d%02d00",
+                 t.tm_year + 1900, t.tm_mon + 1, t.tm_mday,
+                 t.tm_hour, t.tm_min, MIN(t.tm_sec, 59));
+        memcpy(ret, buf, sizeof(buf)-1);
+
+        /* The time zone is encoded by 15 minutes increments */
+        ret->zone = t.tm_gmtoff / (15*60);
+
+        return 0;
+}
+
+int time_to_iso9660_dir_datetime(usec_t usec, bool utc, struct iso9660_dir_time *ret) {
+        struct tm t;
+        int r;
+
+        assert(ret);
+
+        r = localtime_or_gmtime_usec(usec, utc, &t);
+        if (r < 0)
+                return r;
+
+        if (t.tm_year < 0 || t.tm_year > UINT8_MAX)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Year is incompatible with ISO9660.");
+
+        *ret = (struct iso9660_dir_time) {
+                .year = t.tm_year,
+                .month = t.tm_mon + 1,
+                .day = t.tm_mday,
+                .hour = t.tm_hour,
+                .minute = t.tm_min,
+                .second = MIN(t.tm_sec, 59),
+                /* The time zone is encoded by 15 minutes increments */
+                .offset = t.tm_gmtoff / (15*60),
+        };
+
+        return 0;
+}
+
+static bool valid_iso9660_string(const char *str, bool allow_a_chars) {
+        /* note that a-chars are not supposed to accept lower case letters, but it looks like common practice
+         * to use them
+         */
+        return in_charset(str, allow_a_chars ? UPPERCASE_LETTERS LOWERCASE_LETTERS DIGITS " _!\"%&'()*+,-./:;<=>?" : UPPERCASE_LETTERS DIGITS "_");
+}
+
+int set_iso9660_string(char target[], size_t len, const char *source, bool allow_a_chars) {
+        if (source && !valid_iso9660_string(source, allow_a_chars))
+                return -EINVAL;
+
+        if (source) {
+                size_t slen = strlen(source);
+                if (slen > len)
+                        return -EINVAL;
+                void *p = mempcpy(target, source, slen);
+                memset(p, ' ', len - slen);
+        } else
+                memset(target, ' ', len);
+
+        return 0;
+}
+
+bool iso9660_volume_name_valid(const char *name) {
+        /* In theory the volume identifier should be d-chars, but in practice, a-chars are allowed */
+        return valid_iso9660_string(name, /* allow_a_chars= */ true) &&
+                strlen(name) <= 32;
+}
+
+bool iso9660_system_name_valid(const char *name) {
+        return valid_iso9660_string(name, /* allow_a_chars= */ true) &&
+                strlen(name) <= 32;
+}
+
+bool iso9660_publisher_name_valid(const char *name) {
+        return valid_iso9660_string(name, /* allow_a_chars= */ true) &&
+                strlen(name) <= 128;
+}
diff --git a/src/repart/iso9660.h b/src/repart/iso9660.h
new file mode 100644 (file)
index 0000000..7a51928
--- /dev/null
@@ -0,0 +1,172 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "shared-forward.h"
+#include "sparse-endian.h"
+
+/* ISO9660 is 5 blocks:
+ * - Primary descriptor
+ * - El torito descriptor
+ * - Terminal descriptor
+ * - El Torito boot catalog
+ * - Root directory
+ */
+#define ISO9660_BLOCK_SIZE 2048U
+#define ISO9660_START 16U
+#define ISO9660_PRIMARY_DESCRIPTOR (ISO9660_START+0U)
+#define ISO9660_ELTORITO_DESCRIPTOR (ISO9660_START+1U)
+#define ISO9660_TERMINAL_DESCRIPTOR (ISO9660_START+2U)
+#define ISO9660_BOOT_CATALOG (ISO9660_START+3U)
+#define ISO9660_ROOT_DIRECTORY (ISO9660_START+4U)
+#define ISO9660_SIZE 5U
+
+struct _packed_ iso9660_volume_descriptor_header {
+        uint8_t type;
+        char identifier[5];
+        uint8_t version;
+};
+
+struct _packed_ iso9660_terminal_descriptor {
+        struct iso9660_volume_descriptor_header header;
+        uint8_t data[2041];
+};
+assert_cc(sizeof(struct iso9660_terminal_descriptor) == 2048);
+
+struct _packed_ iso9660_datetime {
+        char year[4];
+        char month[2];
+        char day[2];
+        char hour[2];
+        char minute[2];
+        char second[2];
+        char deci[2];
+        int8_t zone;
+};
+
+struct _packed_ iso9660_eltorito_descriptor {
+        struct iso9660_volume_descriptor_header header;
+
+        char boot_system_identifier[32];
+        uint8_t unused_1[32];
+        le32_t boot_catalog_sector;
+        uint8_t unused_2[1973];
+};
+
+assert_cc(sizeof(struct iso9660_eltorito_descriptor) == 2048);
+
+struct _packed_ iso9660_dir_time {
+        uint8_t year;
+        uint8_t month;
+        uint8_t day;
+        uint8_t hour;
+        uint8_t minute;
+        uint8_t second;
+        int8_t offset;
+};
+
+struct _packed_ iso9660_directory_entry {
+        uint8_t len;
+        uint8_t xattr_len;
+        le32_t extent_loc_little;
+        be32_t extent_loc_big;
+        le32_t data_len_little;
+        be32_t data_len_big;
+        struct iso9660_dir_time time;
+        uint8_t flags;
+        uint8_t unit_size;
+        uint8_t gap_size;
+        le16_t volume_seq_num_little;
+        be16_t volume_seq_num_big;
+        uint8_t ident_len;
+        char ident[1]; /* variable */
+};
+
+struct _packed_ iso9660_primary_volume_descriptor {
+        struct iso9660_volume_descriptor_header header;
+
+        uint8_t unused_1;
+        char system_identifier[32];
+        char volume_identifier[32];
+        uint8_t unused_2[8];
+        le32_t volume_space_size_little;
+        be32_t volume_space_size_big;
+        uint8_t unused_3[32];
+
+        le16_t volume_set_size_little;
+        be16_t volume_set_size_big;
+        le16_t volume_sequence_number_little;
+        be16_t volume_sequence_number_big;
+        le16_t logical_block_size_little;
+        be16_t logical_block_size_big;
+
+        le32_t path_table_size_little;
+        be32_t path_table_size_big;
+
+        le32_t path_table_little;
+        le32_t opt_path_table_little;
+
+        be32_t path_table_big;
+        be32_t opt_path_table_big;
+
+        struct iso9660_directory_entry root_directory_entry;
+
+        char volume_set_identifier[128];
+        char publisher_identifier[128];
+        char data_preparer_identifier[128];
+        char application_identifier[128];
+
+        char copyright_file_identifier[37];
+        char abstract_file_identifier[37];
+        char bibliographic_file_identifier[37];
+
+        struct iso9660_datetime volume_creation_date;
+        struct iso9660_datetime volume_modification_date;
+        struct iso9660_datetime volume_expiration_date;
+        struct iso9660_datetime volume_effective_date;
+
+        uint8_t file_structure_version; /* 1 */
+        uint8_t unused_5;
+        char application_used[512];
+        char reserved[653];
+};
+assert_cc(sizeof(struct iso9660_primary_volume_descriptor) == 2048);
+
+struct _packed_ el_torito_validation_entry {
+        uint8_t header_indicator;
+        uint8_t platform;
+        char reserved[2];
+        char id_string[24];
+        le16_t checksum;
+        uint8_t key_bytes[2];
+};
+
+struct _packed_ el_torito_initial_entry {
+        uint8_t boot_indicator;
+        uint8_t boot_media_type;
+        le16_t load_segment;
+        uint8_t system_type;
+        uint8_t unused_1[1];
+        le16_t sector_count;
+        le32_t load_rba;
+        uint8_t unused_2[20];
+};
+
+struct _packed_ el_torito_section_header {
+        uint8_t header_indicator;
+        uint8_t platform;
+        le16_t nentries;
+        char id_string[28];
+};
+
+void no_iso9660_datetime(struct iso9660_datetime *ret);
+int time_to_iso9660_datetime(usec_t usec, bool utc, struct iso9660_datetime *ret);
+int time_to_iso9660_dir_datetime(usec_t usec, bool utc, struct iso9660_dir_time *ret);
+int set_iso9660_string(char target[], size_t len, const char *source, bool allow_a_chars);
+
+static inline void set_iso9660_const_string(char target[], size_t len, const char *source, bool allow_a_chars) {
+        assert_se(set_iso9660_string(target, len, source, allow_a_chars) == 0);
+}
+
+bool iso9660_volume_name_valid(const char *name);
+bool iso9660_system_name_valid(const char *name);
+bool iso9660_publisher_name_valid(const char *name);
index e6e32f54c7a25abbb8b5b3638e993c5a0e257ecd..92c7d37da5af8bd0f0dd6e247c69812ec873e747 100644 (file)
@@ -8,7 +8,10 @@ executables += [
         executable_template + {
                 'name' : 'systemd-repart',
                 'public' : true,
-                'extract' : files('repart.c'),
+                'extract' : files(
+                        'repart.c',
+                        'iso9660.c',
+                ),
                 'link_with' : [
                         libshared,
                         libshared_fdisk,
index bae376fc0ee392ebbeccf0dd36936983cf58339e..5c36bad0758f2d6e2ba76f6e05569464f03c8eae 100644 (file)
@@ -49,6 +49,7 @@
 #include "initrd-util.h"
 #include "install-file.h"
 #include "io-util.h"
+#include "iso9660.h"
 #include "json-util.h"
 #include "libmount-util.h"
 #include "list.h"
@@ -213,6 +214,10 @@ static char *arg_generate_crypttab = NULL;
 static Set *arg_verity_settings = NULL;
 static bool arg_relax_copy_block_security = false;
 static bool arg_varlink = false;
+static bool arg_eltorito = false;
+static char *arg_eltorito_system = NULL;
+static char *arg_eltorito_volume = NULL;
+static char *arg_eltorito_publisher = NULL;
 
 STATIC_DESTRUCTOR_REGISTER(arg_node, freep);
 STATIC_DESTRUCTOR_REGISTER(arg_root, freep);
@@ -237,6 +242,9 @@ STATIC_DESTRUCTOR_REGISTER(arg_make_ddi, freep);
 STATIC_DESTRUCTOR_REGISTER(arg_generate_fstab, freep);
 STATIC_DESTRUCTOR_REGISTER(arg_generate_crypttab, freep);
 STATIC_DESTRUCTOR_REGISTER(arg_verity_settings, set_freep);
+STATIC_DESTRUCTOR_REGISTER(arg_eltorito_system, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_eltorito_volume, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_eltorito_publisher, freep);
 
 typedef enum ProgressPhase {
         PROGRESS_LOADING_DEFINITIONS,
@@ -7651,6 +7659,321 @@ static int context_split(Context *context) {
         return 0;
 }
 
+static int write_primary_descriptor(
+                int fd,
+                uint32_t root_sector,
+                usec_t usec,
+                bool utc,
+                const char *system_id,
+                const char *volume_id,
+                const char *publisher_id) {
+        int r;
+
+        struct iso9660_primary_volume_descriptor desc = {
+                .header = {
+                        .type = 1,
+                        .version = 1,
+                },
+                .volume_space_size_little = htole32(ISO9660_START + ISO9660_SIZE),
+                .volume_space_size_big = htobe32(ISO9660_START + ISO9660_SIZE),
+                .volume_set_size_little = htole16(1),
+                .volume_set_size_big = htobe16(1),
+                .volume_sequence_number_little = htole16(1),
+                .volume_sequence_number_big = htobe16(1),
+                .logical_block_size_little = htole16(ISO9660_BLOCK_SIZE),
+                .logical_block_size_big = htobe16(ISO9660_BLOCK_SIZE),
+                .file_structure_version = 1,
+                .root_directory_entry = {
+                        .len = sizeof(struct iso9660_directory_entry),
+                        .extent_loc_little = htole32(root_sector),
+                        .extent_loc_big = htobe32(root_sector),
+                        .data_len_little = htole32(2*sizeof(struct iso9660_directory_entry)), /* 2 entries with ident size 1: . and .. */
+                        .data_len_big = htobe32(2*sizeof(struct iso9660_directory_entry)), /* 2 entries with ident size 1: . and .. */
+                        .flags = 2, /* directory */
+                        .volume_seq_num_little = htole16(1),
+                        .volume_seq_num_big = htobe16(1),
+                        .ident_len = 1,
+                        .ident[0] = 0, /* special value for root */
+                }
+        };
+
+        set_iso9660_const_string(desc.header.identifier, sizeof(desc.header.identifier), "CD001", /* allow_a_chars= */ true);
+
+        r = time_to_iso9660_dir_datetime(usec, utc, &desc.root_directory_entry.time);
+        if (r < 0)
+                return r;
+
+        r = set_iso9660_string(desc.system_identifier, sizeof(desc.system_identifier), system_id, /* allow_a_chars= */ true);
+        if (r < 0)
+                return r;
+
+        /* In theory the volume identifier should be d-chars, but in practice, a-chars are allowed */
+        r = set_iso9660_string(desc.volume_identifier, sizeof(desc.volume_identifier), volume_id, /* allow_a_chars= */ true);
+        if (r < 0)
+                return r;
+
+        set_iso9660_const_string(desc.volume_set_identifier, sizeof(desc.volume_set_identifier), NULL, /* allow_a_chars= */ false);
+
+        r = set_iso9660_string(desc.publisher_identifier, sizeof(desc.publisher_identifier), publisher_id, /* allow_a_chars= */ true);
+        if (r < 0)
+                return r;
+
+        set_iso9660_const_string(desc.data_preparer_identifier, sizeof(desc.data_preparer_identifier), NULL, /* allow_a_chars= */ true);
+        set_iso9660_const_string(desc.application_identifier, sizeof(desc.application_identifier), "SYSTEMD-REPART", /* allow_a_chars= */ true);
+        set_iso9660_const_string(desc.copyright_file_identifier, sizeof(desc.copyright_file_identifier), NULL, /* allow_a_chars= */ false);
+        set_iso9660_const_string(desc.abstract_file_identifier, sizeof(desc.abstract_file_identifier), NULL, /* allow_a_chars= */ false);
+        set_iso9660_const_string(desc.bibliographic_file_identifier, sizeof(desc.bibliographic_file_identifier), NULL, /* allow_a_chars= */ false);
+
+        r = time_to_iso9660_datetime(usec, utc, &desc.volume_creation_date);
+        if (r < 0)
+                return r;
+
+        r = time_to_iso9660_datetime(usec, utc, &desc.volume_modification_date);
+        if (r < 0)
+                return r;
+
+        no_iso9660_datetime(&desc.volume_expiration_date);
+        no_iso9660_datetime(&desc.volume_effective_date);
+
+        ssize_t s = pwrite(fd, &desc, sizeof(desc), ISO9660_PRIMARY_DESCRIPTOR*ISO9660_BLOCK_SIZE);
+        if (s < 0)
+                return log_error_errno(errno, "Failed to write ISO9660 primary descriptor: %m");
+        if (s != sizeof(desc))
+                return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to fully write ISO9660 primary descriptor");
+
+        return 0;
+}
+
+static int write_eltorito_descriptor(int fd, uint32_t catalog_sector) {
+        struct iso9660_eltorito_descriptor desc = {
+                .header = {
+                        .type = 0,
+                        .version = 1,
+                },
+                .boot_catalog_sector = htole32(catalog_sector),
+        };
+
+        set_iso9660_const_string(desc.header.identifier, sizeof(desc.header.identifier), "CD001", /* allow_a_chars= */ true);
+
+        strncpy(desc.boot_system_identifier, "EL TORITO SPECIFICATION", sizeof(desc.boot_system_identifier));
+
+        ssize_t s = pwrite(fd, &desc, sizeof(desc), ISO9660_ELTORITO_DESCRIPTOR*ISO9660_BLOCK_SIZE);
+        if (s < 0)
+                return log_error_errno(errno, "Failed to write ISO9660 El-Torito descriptor: %m");
+        if (s != sizeof(desc))
+                return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to fully write ISO9660 El-Torito descriptor");
+
+        return 0;
+}
+
+static int write_terminal_descriptor(int fd) {
+        struct iso9660_terminal_descriptor desc = {
+                .header = {
+                        .type = 255,
+                        .version = 1,
+                },
+        };
+
+        set_iso9660_const_string(desc.header.identifier, sizeof(desc.header.identifier), "CD001", /* allow_a_chars= */ true);
+
+        ssize_t s = pwrite(fd, &desc, sizeof(desc), ISO9660_TERMINAL_DESCRIPTOR*ISO9660_BLOCK_SIZE);
+        if (s < 0)
+                return log_error_errno(errno, "Failed to write ISO9660 terminal descriptor: %m");
+        if (s != sizeof(desc))
+                return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to fully write ISO9660 terminal descriptor");
+
+        return 0;
+}
+
+static uint16_t calculate_validation_entry_checksum(const void *p, size_t size) {
+        assert(size % 2 == 0);
+
+        uint16_t checksum = 0;
+
+        for (size_t i = 0; i < (size/2); i++)
+                checksum -= le16toh(((const le16_t*)p)[i]);
+
+        return checksum;
+}
+
+static int write_boot_catalog(int fd, uint32_t load_block) {
+        struct el_torito_validation_entry ve = {
+                .header_indicator = 1,
+                .platform = 0xef, /* EFI */
+                .key_bytes = {0x55, 0xaa},
+        };
+
+        ve.checksum = htole16(calculate_validation_entry_checksum(&ve, sizeof(ve)));
+
+        struct el_torito_initial_entry ie = {
+                .boot_indicator = 0x88, /* bootable */
+                .boot_media_type = 0, /* no emul */
+                /* From UEFI specification:
+                 * > If the value of Sector Count is set to 0 or 1, EFI will assume the system partition
+                 * > consumes the space from the beginning of the “no emulation” image to the end of the
+                 * > CD-ROM.
+                 */
+                .sector_count = htole16(0),
+                .load_rba = htole32(load_block),
+
+        };
+
+        struct el_torito_section_header sh = {
+                .header_indicator = 0x91, /* final header */
+                .nentries = htole16(0), /* no more entries */
+        };
+
+        uint8_t sector[ISO9660_BLOCK_SIZE] = {};
+        uint8_t *p = sector;
+        p = mempcpy(p, &ve, sizeof(ve));
+        p = mempcpy(p, &ie, sizeof(ie));
+        p = mempcpy(p, &sh, sizeof(sh));
+        assert((size_t) (p - sector) <= sizeof(sector));
+
+        ssize_t s = pwrite(fd, &sector, sizeof(sector), ISO9660_BOOT_CATALOG*ISO9660_BLOCK_SIZE);
+        if (s < 0)
+                return log_error_errno(errno, "Failed to write El-Torito boot catalog: %m");
+        if (s != sizeof(sector))
+                return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to fully write El-Torito boot catalog");
+
+        return 0;
+}
+
+static int write_directories(int fd, usec_t usec, bool utc, uint32_t root_sector) {
+        int r;
+
+        uint32_t dir_size = 2*sizeof(struct iso9660_directory_entry); /* 2 entries with ident size 1: . and .. */
+
+        struct iso9660_directory_entry self = {
+                .len = sizeof(struct iso9660_directory_entry),
+                .extent_loc_little = htole32(root_sector),
+                .extent_loc_big = htobe32(root_sector),
+                .data_len_little = htole32(dir_size),
+                .data_len_big = htobe32(dir_size),
+                .flags = 2, /* directory */
+                .volume_seq_num_little = htole16(1),
+                .volume_seq_num_big = htobe16(1),
+                .ident_len = 1,
+                .ident[0] = 0, /* special value for self */
+        };
+
+        r = time_to_iso9660_dir_datetime(usec, utc, &self.time);
+        if (r < 0)
+                return r;
+
+        struct iso9660_directory_entry parent = {
+                .len = sizeof(struct iso9660_directory_entry),
+                .extent_loc_little = htole32(root_sector),
+                .extent_loc_big = htobe32(root_sector),
+                .data_len_little = htole32(dir_size),
+                .data_len_big = htobe32(dir_size),
+                .flags = 2, /* directory */
+                .volume_seq_num_little = htole16(1),
+                .volume_seq_num_big = htobe16(1),
+                .ident_len = 1,
+                .ident[0] = 1, /* special value for parent */
+        };
+
+        // TODO: we should probably add some text file explaining there is no content through ISO9660
+
+        r = time_to_iso9660_dir_datetime(usec, utc, &parent.time);
+        if (r < 0)
+                return r;
+
+        uint8_t sector[ISO9660_BLOCK_SIZE] = {};
+        uint8_t *p = sector;
+        p = mempcpy(p, &self, sizeof(self));
+        p = mempcpy(p, &parent, sizeof(parent));
+        assert((size_t) (p - sector) <= sizeof(sector));
+
+        ssize_t s = pwrite(fd, &sector, sizeof(sector), ISO9660_ROOT_DIRECTORY*ISO9660_BLOCK_SIZE);
+        if (s < 0)
+                return log_error_errno(errno, "Failed to write ISO9660 root directory: %m");
+        if (s != sizeof(sector))
+                return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to fully write ISO9660 root directory");
+
+        return 0;
+}
+
+static int write_eltorito(int fd, usec_t usec, bool utc, uint32_t load_block, const char *system_id, const char *volume_id, const char *publisher_id) {
+        int r;
+
+        r = write_primary_descriptor(fd, ISO9660_ROOT_DIRECTORY, usec, utc, system_id, volume_id, publisher_id);
+        if (r < 0)
+                return r;
+
+        r = write_eltorito_descriptor(fd, ISO9660_BOOT_CATALOG);
+        if (r < 0)
+                return r;
+
+        r = write_terminal_descriptor(fd);
+        if (r < 0)
+                return r;
+
+        r = write_boot_catalog(fd, load_block);
+        if (r < 0)
+                return r;
+
+        r = write_directories(fd, usec, utc, ISO9660_ROOT_DIRECTORY);
+        if (r < 0)
+                return r;
+
+        return 0;
+}
+
+static int context_verify_eltorito_overlap(Context *context) {
+        /* before writing the partition table, we check if we have collision with ISO9660 */
+        assert(context);
+
+        if (!arg_eltorito)
+                return 0;
+
+        /* Check how many GPT partition entries can be stored. */
+        size_t nents = fdisk_get_npartitions(context->fdisk_context);
+        /* The GPT contains
+         *  - 1 unused block (protective MBR)
+         *  - GPT header
+         *  - N entries of 128 bytes each.
+         */
+        size_t first_free_offset = 2*context->sector_size + round_up_size(nents*128, context->sector_size);
+
+        if (first_free_offset > ISO9660_START*ISO9660_BLOCK_SIZE)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The partition table is overlapping with the El Torito boot catalog.");
+
+        /* The first lba is the first block where a partition could exist. Even if there is no partition
+         * there, we should still not overlap with it since a partition could be added later.
+         * It is unexpected for tools to change the first lba in the GPT header. So this should be safe.
+         */
+        if (fdisk_get_first_lba(context->fdisk_context) * context->sector_size < (ISO9660_START+ISO9660_SIZE)*ISO9660_BLOCK_SIZE)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "El Torito is overlapping with the first partition block.");
+
+        return 0;
+}
+
+static int context_find_esp_offset(Context *context, uint64_t *ret) {
+        assert(ret);
+
+        uint64_t esp_offset = UINT64_MAX;
+        LIST_FOREACH(partitions, p, context->partitions) {
+                if (p->dropped || PARTITION_IS_FOREIGN(p))
+                        continue;
+                if (p->type.designator == PARTITION_ESP) {
+                        esp_offset = p->offset;
+                        break;
+                }
+        }
+
+        if (esp_offset == UINT64_MAX)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "El Torito boot catalog requires an ESP.");
+        if (esp_offset / ISO9660_BLOCK_SIZE > UINT32_MAX)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "ESP offset is farther than El Torito boot catalog can support.");
+        if (esp_offset % ISO9660_BLOCK_SIZE != 0)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "ESP offset not aligned on 2K blocks.");
+
+        *ret = esp_offset;
+        return 0;
+}
+
 static int context_write_partition_table(Context *context) {
         _cleanup_(fdisk_unref_tablep) struct fdisk_table *original_table = NULL;
         int capable, r;
@@ -7717,6 +8040,10 @@ static int context_write_partition_table(Context *context) {
 
         (void) context_notify(context, PROGRESS_WRITING_TABLE, /* object= */ NULL, UINT_MAX);
 
+        r = context_verify_eltorito_overlap(context);
+        if (r < 0)
+                return r;
+
         r = fdisk_write_disklabel(context->fdisk_context);
         if (r < 0)
                 return log_error_errno(r, "Failed to write partition table: %m");
@@ -7736,6 +8063,24 @@ static int context_write_partition_table(Context *context) {
         } else
                 log_notice("Not telling kernel to reread partition table, because selected image does not support kernel partition block devices.");
 
+        if (arg_eltorito) {
+                bool utc = true;
+                usec_t usec = parse_source_date_epoch();
+                if (usec == USEC_INFINITY) {
+                        usec = now(CLOCK_REALTIME);
+                        utc = false;
+                }
+
+                uint64_t esp_offset;
+                r = context_find_esp_offset(context, &esp_offset);
+                if (r < 0)
+                        return r;
+
+                r = write_eltorito(fdisk_get_devfd(context->fdisk_context), usec, utc, esp_offset / ISO9660_BLOCK_SIZE, arg_eltorito_system, arg_eltorito_volume, arg_eltorito_publisher);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to write El Torito boot catalog: %m");
+        }
+
         log_info("All done.");
 
         return 0;
@@ -9205,6 +9550,14 @@ static int help(void) {
                "                          Write fstab configuration to the given path\n"
                "     --generate-crypttab=PATH\n"
                "                          Write crypttab configuration to the given path\n"
+               "\n%3$sEl Torito boot catalog:%4$s\n"
+               "     --el-torito=BOOL     Whether to add a boot catalog to boot the ESP\n"
+               "     --el-torito-system=STRING\n"
+               "                          Set the system identifier in the ISO9660 descriptor\n"
+               "     --el-torito-volume=STRING\n"
+               "                          Set the volume identifier in the ISO9660 descriptor\n"
+               "     --el-torito-publisher=STRING\n"
+               "                          Set the publisher identifier in the ISO9660 descriptor\n"
                "\nSee the %2$s for details.\n",
                program_invocation_short_name,
                link,
@@ -9264,6 +9617,10 @@ static int parse_argv(int argc, char *argv[]) {
                 ARG_GENERATE_CRYPTTAB,
                 ARG_LIST_DEVICES,
                 ARG_JOIN_SIGNATURE,
+                ARG_ELTORITO,
+                ARG_ELTORITO_SYSTEM,
+                ARG_ELTORITO_VOLUME,
+                ARG_ELTORITO_PUBLISHER,
         };
 
         static const struct option options[] = {
@@ -9314,6 +9671,10 @@ static int parse_argv(int argc, char *argv[]) {
                 { "generate-crypttab",              required_argument, NULL, ARG_GENERATE_CRYPTTAB              },
                 { "list-devices",                   no_argument,       NULL, ARG_LIST_DEVICES                   },
                 { "join-signature",                 required_argument, NULL, ARG_JOIN_SIGNATURE                 },
+                { "el-torito",                      required_argument, NULL, ARG_ELTORITO                       },
+                { "el-torito-system",               required_argument, NULL, ARG_ELTORITO_SYSTEM                },
+                { "el-torito-volume",               required_argument, NULL, ARG_ELTORITO_VOLUME                },
+                { "el-torito-publisher",            required_argument, NULL, ARG_ELTORITO_PUBLISHER             },
                 {}
         };
 
@@ -9738,6 +10099,43 @@ static int parse_argv(int argc, char *argv[]) {
                                 return r;
                         break;
 
+                case ARG_ELTORITO:
+                        r = parse_boolean_argument("--el-torito=", optarg, &arg_eltorito);
+                        if (r < 0)
+                                return r;
+
+                        break;
+
+                case ARG_ELTORITO_SYSTEM:
+                        if (!iso9660_system_name_valid(optarg))
+                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid value '%s' for --el-torito-system=.", optarg);
+
+                        r = free_and_strdup_warn(&arg_eltorito_system, optarg);
+                        if (r < 0)
+                                return r;
+
+                        break;
+
+                case ARG_ELTORITO_VOLUME:
+                        if (!iso9660_volume_name_valid(optarg))
+                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid value '%s' for --el-torito-volume=.", optarg);
+
+                        r = free_and_strdup_warn(&arg_eltorito_volume, optarg);
+                        if (r < 0)
+                                return r;
+
+                        break;
+
+                case ARG_ELTORITO_PUBLISHER:
+                        if (!iso9660_publisher_name_valid(optarg))
+                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid value '%s' for --el-torito-publisher=.", optarg);
+
+                        r = free_and_strdup_warn(&arg_eltorito_publisher, optarg);
+                        if (r < 0)
+                                return r;
+
+                        break;
+
                 case '?':
                         return -EINVAL;
 
@@ -9882,6 +10280,10 @@ static int parse_argv(int argc, char *argv[]) {
                 arg_pager_flags |= PAGER_DISABLE;
         }
 
+        if (arg_eltorito && !IN_SET(arg_empty, EMPTY_REQUIRE, EMPTY_FORCE, EMPTY_CREATE))
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+                                       "--el-torito=yes requires --empty= to be either require, force or create.");
+
         return 1;
 }