Skip to content

Commit 9c31b7a

Browse files
JacobSzwejbkafacebook-github-bot
authored andcommitted
Add manifest extension AoT (#14128)
Summary: Add some infra for us to optionally add some key structured data to the end of a pte. This diff is around enabling users to easily tag their model with a cryptographic signature. Has room to expand later. A key design motivation is it would be ideal if this is transparent to the rest of the extensions we have today. Im claiming the prime footer real estate for this which is unused today by anything in tree. This should let it be composable with other formats like bundledProgram too. Differential Revision: D82052721
1 parent 66639e4 commit 9c31b7a

File tree

5 files changed

+513
-0
lines changed

5 files changed

+513
-0
lines changed

extension/manifest/TARGETS

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
load("@fbcode_macros//build_defs:python_library.bzl", "python_library")
2+
3+
oncall("executorch")
4+
5+
python_library(
6+
name = "_manifest",
7+
srcs = [
8+
"_manifest.py",
9+
],
10+
deps = [
11+
"//executorch/exir/_serialize:lib",
12+
"//executorch/exir:_warnings",
13+
],
14+
visibility = ["PUBLIC"],
15+
)

extension/manifest/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
from executorch.extension.manifest._manifest import Manifest, append_manifest
8+
9+
__all__ = [
10+
"Manifest",
11+
"append_manifest",
12+
]

extension/manifest/_manifest.py

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
from dataclasses import dataclass
2+
from typing import ClassVar, Literal
3+
4+
from executorch.exir._serialize.padding import padding_required
5+
from executorch.exir._warnings import experimental
6+
7+
# Byte order of numbers written to the manifest. Always little-endian
8+
# regardless of the host system, since all commonly-used modern CPUs are little
9+
# endian.
10+
_MANIFEST_BYTEORDER: Literal["little"] = "little"
11+
12+
@dataclass
13+
class _ManifestLayout:
14+
"""Python class mirroring the binary layout of the manifest.
15+
separate from the Manifest class, which is the user facing
16+
representation.
17+
"""
18+
EXPECTED_MAGIC: ClassVar[bytes] = b"em00"
19+
20+
MAX_SIGNATURE_SIZE: ClassVar[int] = 512
21+
22+
EXPECTED_MIN_LENGTH: ClassVar[int] = (
23+
# Header magic
24+
4
25+
# Header length
26+
+ 4
27+
# program offset
28+
+ 8
29+
# Padding
30+
+ 4
31+
# signature size
32+
+ 4
33+
)
34+
35+
EXPECTED_MAX_LENGTH: ClassVar[int] = (
36+
EXPECTED_MIN_LENGTH + MAX_SIGNATURE_SIZE
37+
)
38+
39+
signature: bytes
40+
41+
# The actual size of the signature
42+
signature_size: int = 0
43+
44+
# The size of any padding required
45+
padding_size: int = 0
46+
47+
# Size in bytes between the top of the manifest and the start of the data it was appended to.
48+
program_offset: int = 0
49+
50+
# The manifest length, in bytes, read from or to be written to the binary
51+
# footer.
52+
length: int = 0
53+
54+
# The magic bytes read from or to be written to the binary footer.
55+
magic: bytes = EXPECTED_MAGIC
56+
57+
def __post_init__(self):
58+
"""Post init hook to validate the manifest."""
59+
if self.signature_size == 0:
60+
self.signature_size = len(self.signature)
61+
if self.length == 0:
62+
self.length = _ManifestLayout.EXPECTED_MIN_LENGTH + self.signature_size
63+
64+
# Not using self.is_valid() here to deliver better error messages.
65+
if len(self.signature) > _ManifestLayout.MAX_SIGNATURE_SIZE:
66+
raise ValueError(f"Signature is too large. {self.signature_size}. Manifest only supports signatures up to {_ManifestLayout.MAX_SIGNATURE_SIZE} bytes.")
67+
if self.magic != _ManifestLayout.EXPECTED_MAGIC:
68+
raise ValueError(f"Invalid magic. Expected {_ManifestLayout.EXPECTED_MAGIC}. Got {self.magic}")
69+
if self.length < _ManifestLayout.EXPECTED_MIN_LENGTH:
70+
raise ValueError(f"Invalid length. Expected at least {_ManifestLayout.EXPECTED_MIN_LENGTH}. Got {self.length}")
71+
if self.length > _ManifestLayout.EXPECTED_MAX_LENGTH:
72+
raise ValueError(f"Invalid length. Expected at most {_ManifestLayout.EXPECTED_MAX_LENGTH}. Got {self.length}")
73+
if self.signature_size != len(self.signature):
74+
raise ValueError(f"Invalid signature size must match len(self.signature). Expected {len(self.signature)}. Got {self.signature_size}")
75+
76+
def is_valid(self) -> bool:
77+
"""Returns true if the manifest appears to be well-formed."""
78+
return (
79+
self.magic == _ManifestLayout.EXPECTED_MAGIC
80+
and self.length >= _ManifestLayout.EXPECTED_MIN_LENGTH and self.length <= _ManifestLayout.EXPECTED_MAX_LENGTH
81+
and self.signature_size >= 0 and self.signature_size <= _ManifestLayout.MAX_SIGNATURE_SIZE
82+
and self.program_offset >= 0
83+
and len(self.signature) == self.signature_size
84+
)
85+
86+
def to_bytes(self) -> bytes:
87+
"""Returns the binary representation of the Manifest. Written
88+
bottom up.
89+
90+
Note that this will ignore self.magic and self.length and will always
91+
write the proper magic/length.
92+
"""
93+
if not self.is_valid():
94+
raise ValueError("Cannot serialize an invalid manifest")
95+
96+
data: bytes = (
97+
# bytes: Signature unique ID for the data the manifest was appended to.
98+
self.signature
99+
# actual size of the signature
100+
+self.signature_size.to_bytes(4, byteorder=_MANIFEST_BYTEORDER)
101+
# uint32_t: Any padding required to align the manifest.
102+
+self.padding_size.to_bytes(4, byteorder=_MANIFEST_BYTEORDER)
103+
# uint64_t: Size in bytes between the manifest and the data it was appended to.
104+
+self.program_offset.to_bytes(8, byteorder=_MANIFEST_BYTEORDER)
105+
# uint32_t: Actual size of this manifest.
106+
+self.length.to_bytes(4, byteorder=_MANIFEST_BYTEORDER)
107+
# Manifest magic. This lets consumers detect whether the
108+
# manifest was inserted or not. Always use the proper magic value
109+
# (i.e., ignore self.magic) since there's no reason to create an
110+
# invalid manifest.
111+
+ self.EXPECTED_MAGIC
112+
)
113+
return data
114+
115+
@staticmethod
116+
def from_bytes(data: bytes) -> "_ManifestLayout":
117+
"""Tries to read a manifest from the provided data.
118+
119+
Does not validate that the header is well-formed. Callers should
120+
use is_valid().
121+
122+
Args:
123+
data: The data to read from.
124+
Returns:
125+
The contents of the serialized manifest.
126+
Raises:
127+
ValueError: If not enough data is provided.
128+
"""
129+
if len(data) <= _ManifestLayout.EXPECTED_MIN_LENGTH:
130+
raise ValueError(
131+
f"Not enough data for the manifest: {len(data)} "
132+
+ f"< {_ManifestLayout.EXPECTED_MIN_LENGTH}"
133+
)
134+
magic = data[-4:]
135+
length = int.from_bytes(data[-8:-4], byteorder=_MANIFEST_BYTEORDER)
136+
program_offset = int.from_bytes(data[-16:-8], byteorder=_MANIFEST_BYTEORDER)
137+
padding_size = int.from_bytes(data[-20:-16], byteorder=_MANIFEST_BYTEORDER)
138+
signature_size = int.from_bytes(data[-24:-20], byteorder=_MANIFEST_BYTEORDER)
139+
signature = data[-(signature_size + 24): -24]
140+
return _ManifestLayout(
141+
signature=signature,
142+
signature_size=signature_size,
143+
padding_size=padding_size,
144+
program_offset=program_offset,
145+
length=length,
146+
magic=magic,
147+
)
148+
149+
@staticmethod
150+
def from_manifest(manifest: "Manifest") -> "_ManifestLayout":
151+
return _ManifestLayout(
152+
signature = manifest.signature,
153+
signature_size=len(manifest.signature),
154+
length=_ManifestLayout.EXPECTED_MIN_LENGTH + len(manifest.signature),
155+
# program_offset and padding_size are set at append time.
156+
)
157+
158+
@experimental("This API is experimental and subject to change without notice.")
159+
@dataclass
160+
class Manifest:
161+
"""A manifest that can be appended to a binary blob. The manifest contains
162+
meta information about the binary blob. You must know who created the manifest
163+
to be able to interpret the data in the manifest."""
164+
165+
# Unique ID for the data the manifest was appended to. Often this might contain
166+
# a crytographic signature for the data.
167+
signature: bytes
168+
169+
@staticmethod
170+
def _from_manifest_layout(layout: _ManifestLayout) -> "Manifest":
171+
return Manifest(
172+
signature=layout.signature,
173+
)
174+
175+
@staticmethod
176+
def from_bytes(data: bytes) -> "Manifest":
177+
"""Tries to read a manifest from the provided data."""
178+
layout = _ManifestLayout.from_bytes(data)
179+
if not layout.is_valid():
180+
raise ValueError("Cannot parse manifest from bytes")
181+
return Manifest._from_manifest_layout(layout)
182+
183+
@experimental("This API is experimental and subject to change without notice.")
184+
def append_manifest(pte_data: bytes, manifest: Manifest, alignment:int=16):
185+
"""Appends a manifest to the provided data."""
186+
padding = padding_required(len(pte_data), alignment)
187+
188+
manifest_layout = _ManifestLayout.from_manifest(manifest)
189+
manifest_layout.program_offset = len(pte_data) + manifest_layout.padding_size
190+
manifest_layout.padding_size = padding
191+
192+
return pte_data + (b'\x00' * padding) + manifest_layout.to_bytes()
193+

extension/manifest/test/TARGETS

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
load("@fbcode_macros//build_defs:python_unittest.bzl", "python_unittest")
2+
3+
oncall("executorch")
4+
5+
python_unittest(
6+
name = "test_manifest",
7+
srcs = [
8+
"test_manifest.py",
9+
],
10+
deps = [
11+
"//executorch/extension/manifest:_manifest",
12+
"//executorch/extension/pybindings:portable_lib",
13+
"//executorch/exir:lib",
14+
"//caffe2:torch",
15+
],
16+
)

0 commit comments

Comments
 (0)