|
| 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