Skip to content

Commit b2d3ecf

Browse files
esaurezCopilot
andcommitted
Let a guest runtime own workload identity across restore
A runtime that runs several independent workloads has no single workload process or per-workload machine ID to publish. It may now create the regular file /run/nvx/runtime-workload-identity before capture. The native helper then requires neither single-workload identity on a workload-start restore and leaves every workload hostname to the runtime hook, which remains mandatory. The guest's own identity is still refreshed. Capture and restore reject a marker that is not a regular file, and the helper is unchanged when the marker is absent. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 4920be0 commit b2d3ecf

3 files changed

Lines changed: 96 additions & 7 deletions

File tree

‎doc/design/snapshot-and-restore.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,16 @@ identities to be absent only for this explicit image format; the runtime hook
6464
remains mandatory. This does not make workload snapshots safe to share outside
6565
their trust domain.
6666

67+
A guest runtime that runs several independent workloads may instead create the
68+
regular file `/run/nvx/runtime-workload-identity` before capture. The marker
69+
declares that the runtime's post-restore hook refreshes or deliberately
70+
preserves each workload's identity. The native helper then requires neither a
71+
single workload process nor a per-workload machine ID on a `workload-start`
72+
restore, and it does not rewrite any workload hostname. It still refreshes the
73+
guest's own identity, and the runtime hook remains mandatory. Capture and
74+
restore reject a marker that is not a regular file. Without the marker, the
75+
helper's behavior is unchanged.
76+
6777
1. gate host input and defer completion of the snapshot-port write;
6878
2. stop the vCPU at the I/O boundary while completing the write, so saved state
6979
starts at the instruction immediately after `out`;

‎guest/common/nvx-snapshot‎

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,23 @@ cmdline_value() {
8787
return 1
8888
}
8989

90+
workload_identity_marker=$runtime/runtime-workload-identity
91+
92+
# A guest runtime that runs several independent workloads creates this regular
93+
# file before capture. Its post-restore hook then refreshes every workload's
94+
# identity, so no single workload process or machine ID is published.
95+
workload_identity_owner() {
96+
if [ ! -e "$workload_identity_marker" ] && [ ! -L "$workload_identity_marker" ]; then
97+
printf 'single\n'
98+
elif [ -f "$workload_identity_marker" ] && [ ! -L "$workload_identity_marker" ]; then
99+
printf 'runtime\n'
100+
else
101+
return 1
102+
fi
103+
}
104+
90105
workload_identity_required() {
91-
[ "$image_format" != flat-ext4 ]
106+
[ "$image_format" != flat-ext4 ] && [ "$identity_owner" != runtime ]
92107
}
93108

94109
read_port_byte() {
@@ -435,6 +450,8 @@ post_restore() {
435450

436451
if [ "$snapshot_tier" != instance-checkpoint ]; then
437452
console_status "NVX-POST-RESTORE-STAGE: identity"
453+
identity_owner=$(workload_identity_owner) ||
454+
fail_closed "runtime workload identity marker is invalid"
438455
machine_id=$(sha256sum "$entropy" | cut -c1-32)
439456
printf '%s\n' "$machine_id" >/etc/machine-id ||
440457
fail_closed "failed to refresh machine identity"
@@ -450,7 +467,10 @@ post_restore() {
450467
hostname_value=$(cmdline_value nvx_hostname || true)
451468
[ -n "$hostname_value" ] || hostname_value=nvx-sandbox
452469
hostname "$hostname_value" || fail_closed "failed to refresh hostname"
453-
if [ -s "$workload_pid_file" ]; then
470+
if [ "$identity_owner" = runtime ]; then
471+
# Each workload keeps its own hostname; the runtime hook owns refresh.
472+
:
473+
elif [ -s "$workload_pid_file" ]; then
454474
IFS= read -r workload_pid <"$workload_pid_file"
455475
case "$workload_pid" in
456476
'' | *[!0-9]*) fail_closed "workload PID is invalid" ;;
@@ -498,6 +518,11 @@ if [ "$snapshot_tier" = workload-start ] &&
498518
exit 1
499519
fi
500520

521+
if ! workload_identity_owner >/dev/null; then
522+
echo "nvx-snapshot: runtime workload identity marker is invalid" >&2
523+
exit 1
524+
fi
525+
501526
if [ "$has_scratch" = true ]; then
502527
if [ "$scratch_policy" = paired ] && [ "$scratch_mounted" = false ]; then
503528
echo "nvx-snapshot: paired capture requires mounted scratch" >&2

‎scripts/test_microvm_tests.py‎

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1469,15 +1469,16 @@ def test_filesystem_only_clone_does_not_require_a_workload_process(self):
14691469
snapshot.count("workload_identity_required; then"),
14701470
2,
14711471
)
1472-
for image_format, outcome in (
1473-
("", "required"),
1474-
("flat-ext4", "optional"),
1472+
for image_format, identity_owner, outcome in (
1473+
("", "single", "required"),
1474+
("flat-ext4", "single", "optional"),
1475+
("", "runtime", "optional"),
14751476
):
14761477
result = subprocess.run(
14771478
[shell, "-c",
1478-
"set -eu\nimage_format=$1\n" + required
1479+
"set -eu\nimage_format=$1\nidentity_owner=$2\n" + required
14791480
+ "\nif workload_identity_required; then echo required; else echo optional; fi",
1480-
"_", image_format],
1481+
"_", image_format, identity_owner],
14811482
text=True,
14821483
capture_output=True,
14831484
timeout=5,
@@ -1486,6 +1487,59 @@ def test_filesystem_only_clone_does_not_require_a_workload_process(self):
14861487
self.assertEqual(result.returncode, 0, result.stderr)
14871488
self.assertEqual(result.stdout.strip(), outcome)
14881489

1490+
def test_runtime_owned_workload_identity_marker_is_a_regular_file(self):
1491+
if sys.platform == "win32":
1492+
self.skipTest("native POSIX guest helper test requires Linux")
1493+
shell = _posix_shell()
1494+
if shell is None:
1495+
self.skipTest("POSIX shell is unavailable")
1496+
snapshot = (
1497+
Path(__file__).parents[1] / "guest" / "common" / "nvx-snapshot"
1498+
).read_text(encoding="utf-8")
1499+
begin = snapshot.index("workload_identity_owner() {")
1500+
end = snapshot.index("\n}\n", begin) + 2
1501+
owner = snapshot[begin:end]
1502+
capture_check = snapshot.index("if ! workload_identity_owner >/dev/null; then")
1503+
freeze = snapshot.index('echo 1 >"$container_cgroup/cgroup.freeze"')
1504+
self.assertLess(capture_check, freeze)
1505+
restore_check = snapshot.index(
1506+
'fail_closed "runtime workload identity marker is invalid"'
1507+
)
1508+
hostname_reset = snapshot.index('nsenter -t "$workload_pid" -u hostname')
1509+
self.assertLess(restore_check, hostname_reset)
1510+
self.assertIn('if [ "$identity_owner" = runtime ]; then', snapshot)
1511+
1512+
with tempfile.TemporaryDirectory() as directory:
1513+
marker = Path(directory) / "runtime-workload-identity"
1514+
1515+
def resolve() -> subprocess.CompletedProcess[str]:
1516+
return subprocess.run(
1517+
[shell, "-c",
1518+
"set -eu\nworkload_identity_marker=$1\n" + owner
1519+
+ "\nworkload_identity_owner",
1520+
"_", str(marker)],
1521+
text=True,
1522+
capture_output=True,
1523+
timeout=5,
1524+
check=False,
1525+
)
1526+
1527+
result = resolve()
1528+
self.assertEqual(result.returncode, 0, result.stderr)
1529+
self.assertEqual(result.stdout.strip(), "single")
1530+
marker.write_bytes(b"")
1531+
result = resolve()
1532+
self.assertEqual(result.returncode, 0, result.stderr)
1533+
self.assertEqual(result.stdout.strip(), "runtime")
1534+
marker.unlink()
1535+
marker.mkdir()
1536+
self.assertNotEqual(resolve().returncode, 0)
1537+
marker.rmdir()
1538+
target = Path(directory) / "target"
1539+
target.write_bytes(b"")
1540+
marker.symlink_to(target)
1541+
self.assertNotEqual(resolve().returncode, 0)
1542+
14891543
def test_snapshot_console_diagnostics_are_nonfatal_and_ordered(self):
14901544
shell = _posix_shell()
14911545
if shell is None:

0 commit comments

Comments
 (0)