Skip to content

Commit b1ccf44

Browse files
committed
Add OCI snapshot persistence
Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com>
1 parent 0076a8e commit b1ccf44

12 files changed

Lines changed: 1815 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
44

55
## [Prerelease] - Unreleased
66

7+
### Added
8+
* `Snapshot::to_oci`, `Snapshot::from_oci`, and `Snapshot::from_oci_unchecked` for persisting and loading sandbox snapshots as OCI Image Layout directories by @ludfjig in https://github.com/hyperlight-dev/hyperlight/pull/1465
9+
710
## [v0.15.0] - 2026-05-06
811

912
### Added

Cargo.lock

Lines changed: 186 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/snapshot-oci-format.md

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# Hyperlight snapshot on-disk format
2+
3+
Hyperlight serialises a `Snapshot` to disk as an [OCI Image Layout]
4+
directory. `Snapshot::to_oci` writes one. `Snapshot::from_oci` and
5+
`Snapshot::from_oci_unchecked` read one back.
6+
7+
[OCI Image Layout]: https://github.com/opencontainers/image-spec/blob/main/image-layout.md
8+
9+
## Directory layout
10+
11+
```text
12+
path/
13+
oci-layout {"imageLayoutVersion":"1.0.0"}
14+
index.json one manifest descriptor per tag,
15+
tagged via the OCI standard
16+
`org.opencontainers.image.ref.name`
17+
annotation
18+
blobs/sha256/
19+
<manifest-digest> OCI image manifest JSON
20+
<config-digest> Hyperlight config JSON
21+
<snapshot-digest> raw memory bytes
22+
(`memory_size` bytes)
23+
```
24+
25+
Three blob kinds per tag:
26+
27+
* **manifest** (`application/vnd.oci.image.manifest.v1+json`). Tiny JSON
28+
pointer record selected via `index.json`. References one config and
29+
one layer by digest.
30+
* **config** (`application/vnd.hyperlight.snapshot.config.v1+json`). The
31+
snapshot descriptor: arch, ABI version, entrypoint sregs, memory
32+
layout, registered host functions, snapshot generation counter.
33+
Loaded eagerly and fully parsed.
34+
* **layer / memory** (`application/vnd.hyperlight.snapshot.memory.v1`).
35+
The raw guest memory image, exactly `memory_size` bytes. mmap'd on
36+
restore.
37+
38+
Blob filenames are the sha256 of the blob bytes, so identical blobs
39+
across tags are stored once.
40+
41+
## What is one snapshot
42+
43+
A single saved `Snapshot` consists of exactly:
44+
45+
* one entry in `index.json`, carrying the `tag` as
46+
`org.opencontainers.image.ref.name`,
47+
* one **manifest** blob (referenced by that index entry),
48+
* one **config** blob (referenced by the manifest's `config` field),
49+
* one **layer** blob (the only entry in the manifest's `layers`
50+
array, holding the raw memory image).
51+
52+
Saving two snapshots under different tags into the same `path`
53+
produces two index entries and two manifests. Configs and layers are
54+
deduplicated by content, so identical bytes are stored once and
55+
referenced by both manifests.
56+
57+
Saving the same tag a second time replaces that tag's index entry
58+
and writes a fresh manifest. The previous manifest, and any of its
59+
config or layer blobs that no other tag references, become orphans
60+
in `blobs/sha256/`.
61+
62+
## Write semantics
63+
64+
`Snapshot::to_oci(path, tag)` opens or creates the OCI layout at
65+
`path` and writes one snapshot under `tag`. The parent directory of
66+
`path` must already exist. `path` itself is created if absent. An
67+
existing layout at `path` is preserved: other tags are kept, and a
68+
tag equal to `tag` is replaced.
69+
70+
`index.json` is rewritten via a tmp file plus `rename`, the commit
71+
point for the whole operation. A crash before that rename leaves the
72+
prior layout intact. A crash after it leaves the new layout intact.
73+
74+
Replaced tags leave orphan blobs behind. To compact, remove the
75+
directory and re-save. Concurrent writers to the same `path` are
76+
unsupported.
77+
78+
This mirrors the merge behaviour of `containers/image` (skopeo,
79+
podman), `go-containerregistry` (crane), and `regclient`.
80+
81+
## Read semantics
82+
83+
`Snapshot::from_oci(path, tag)` verifies sha256 for manifest, config,
84+
and snapshot blobs. `Snapshot::from_oci_unchecked` skips the digest
85+
verification, trading integrity for performance, and keeps every
86+
other check (OCI structure, descriptor sizes, schema versions, arch /
87+
hypervisor / ABI tags, layout bounds, entrypoint bounds).
88+
89+
A missing tag or duplicate tag in `index.json` is rejected.
90+
91+
## Portability
92+
93+
Snapshot images are bound to a specific CPU architecture and
94+
hypervisor. Both are recorded in the config blob and checked at load
95+
time, with mismatches rejected with a clear error. The hypervisor
96+
tag (`kvm`, `mshv`, `whp`) constrains the host OS.

0 commit comments

Comments
 (0)