Skip to content

Commit

Permalink
rouge: add Hybrid MBR support for GPT
Browse files Browse the repository at this point in the history
Hybrid MBR is kind of accepted hack, that defines (subset) of
partitions both in MBR and in GPT. It is primarily used in cases when
we want to use GPT, but some part of the system (like bootloader)
supports only MBR. This thing is not standardized so different OSes
can react differently to this it. Thus, it should be used only if
there are no other options.

This patch was made with intention to be able to create Raspberry Pi
Zero boot images.

Signed-off-by: Volodymyr Babchuk <[email protected]>
  • Loading branch information
lorc committed Sep 3, 2024
1 parent b5124fa commit 1ee0ddb
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 10 deletions.
16 changes: 16 additions & 0 deletions docs/rouge.rst
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,17 @@ includes Raw Image block.
:code:`partitions:` section is mandatory. It defines list of
partitions, where key is a partition label.

:code:`hybrid_mbr` forces rouge to create a Hybrid MBR instead of
default Protective MBR. Take note that this is experimental feature,
as different OSes handle Hybrid MBR differently, in other words - this
type of MBR is not standardized and is not guaranteed to work on your
setup. It is added mostly to enable support of older Raspberry PI
models, whose bootloader can't parse GPT. When Hybrid MBR is enabled,
first three partition entries should contain :code:`mbr_type` property
with MBR Partitions type code. In most cases you will need
:code:`0x0C` for FAT32 partitions and :code:`0x83` for Linux file
systems.

Each partition contains definition of other block type plus optional keys:

:code:`gpt_type:` (which we strongly suggest to provide) key holds GPT Partition
Expand All @@ -339,6 +350,11 @@ new Unique Partition GUID in each GPT Partition Entry.
(e.g. fancy flash storage) might have a sector of different size.
This key allows tuning for such cases.

:code:`mbr_type` is used only when :code:`hybrid_mbr` is set for the
GPT block entry. It corresponds to MBR partition type byte. List of
partition types can be found on `Wikipedia
<https://en.wikipedia.org/wiki/Partition_type>`_.

`rouge` will place partitions one after another, aligning partition
start to 1 MiB (as per standard recommendation) and partition size to
sector size, which defaults to 512 bytes.
Expand Down
21 changes: 17 additions & 4 deletions moulin/rouge/block_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class GPTPartition(NamedTuple):
gpt_guid: str
start: int
size: int
protective_mbr_type: int
entry: BlockEntry


Expand All @@ -53,6 +54,8 @@ def __init__(self, node: YamlValue, **kwargs):
self._sector_size: int = 512
self._requested_image_size: Optional[int] = None

self._hybrid_mbr: bool = node.get("hybrid_mbr", False).as_bool

_requested_image_size_node = node.get("image_size", None)
if _requested_image_size_node:
self._requested_image_size = _parse_size(_requested_image_size_node)
Expand All @@ -61,8 +64,16 @@ def __init__(self, node: YamlValue, **kwargs):

for part_id, part in node["partitions"].items():
label = part_id
entry_obj, gpt_type, gpt_guid = self._process_entry(part, sector_size=self._sector_size)
self._partitions.append(GPTPartition(label, gpt_type, gpt_guid, start=0, size=0, entry=entry_obj))
entry_obj, gpt_type, gpt_guid, mbr_type = self._process_entry(
part, sector_size=self._sector_size)
self._partitions.append(
GPTPartition(label,
gpt_type,
gpt_guid,
start=0,
size=0,
entry=entry_obj,
protective_mbr_type=mbr_type))

def size(self) -> int:
"Returns size of image in bytes. Requested in yaml or actually calculated."
Expand All @@ -88,7 +99,9 @@ def _process_entry(node: YamlValue, **kwargs):

gpt_guid = node.get("gpt_guid", "").as_str

return (entry_obj, gpt_type, gpt_guid)
mbr_type = node.get("mbr_type", 0x100).as_int

return (entry_obj, gpt_type, gpt_guid, mbr_type)

def _complete_init(self):
partitions = [x._replace(size=x.entry.size()) for x in self._partitions]
Expand All @@ -98,7 +111,7 @@ def write(self, fp, offset):
if not self._size:
self._complete_init()

gpti.write(fp, self._partitions, offset, self._size, self._sector_size)
gpti.write(fp, self._partitions, offset, self._size, self._sector_size, self._hybrid_mbr)

for part in self._partitions:
part.entry.write(fp, part.start + offset)
Expand Down
59 changes: 53 additions & 6 deletions moulin/rouge/gpti.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from gpt_image.geometry import Geometry
from gpt_image.table import Table
from gpt_image.partition import Partition
import struct
import logging

log = logging.getLogger(__name__)
Expand Down Expand Up @@ -68,17 +69,63 @@ def fixup_partition_table(partitions: List[Any], sector_size=512) -> Tuple[List[
return ret, end + 16 * 1024 * 1024


def write(fp: BinaryIO, partitions: List[Any], offset: int, size: int, sector_size=512):
def create_mbr(partitions: List[Any]):
partition_format = struct.Struct("<B3xB3xII")

# Bootstrap code area. Hope, no on use it today
ret = bytes(446)

# Count partitions
part_count = len(partitions)
if part_count > 3:
log.warn(
f"There are {part_count} partitions in the partition table, but Hybrid MBR will contain only first 3"
)

# Write info about real partitions
for i in range(min(3, part_count)):
if partitions[i].protective_mbr_type > 0xFF:
raise Exception(
f"You must provide mbr_type for all partitions when Hybrd MBR is enabled")
ret += partition_format.pack(0x80, partitions[i].protective_mbr_type,
partitions[i].start // 512, partitions[i].size // 512)

# Write protective partition
ret += partition_format.pack(0x80, 0xEE, 1, (partitions[0].start - 1) // 512)

# Pad till full sector size
if part_count < 3:
ret += bytes(16) * (3 - part_count)

# Add magic bytes aka boot signature
ret += b"\x55\xAA"

return ret


def write(fp: BinaryIO,
partitions: List[Any],
offset: int,
size: int,
sector_size=512,
hybrid_mbr=False):
geometry = Geometry(size, sector_size)
table = Table(geometry)
for part in partitions:
table.partitions.add(Partition(
part.label, part.size, part.gpt_type,
part.gpt_guid, DEFAULT_ALIGNMENT // sector_size)
)
table.partitions.add(
Partition(part.label, part.size, part.gpt_type, part.gpt_guid,
DEFAULT_ALIGNMENT // sector_size))
table.update()
fp.seek(offset)
fp.write(table.protective_mbr.marshal())

# Create Protective or Hybryd MBR
if not hybrid_mbr:
fp.write(table.protective_mbr.marshal())
else:
if sector_size != 512:
raise Exception(f"It is not possible to use sector size {sector_size} with hybrid MBR")
fp.write(create_mbr(partitions))

# write primary header
fp.seek(offset + geometry.primary_header_byte)
fp.write(table.primary_header.marshal())
Expand Down

0 comments on commit 1ee0ddb

Please sign in to comment.