Skip to content

Support suspending and resuming Linux VMs - #1297

Open
randvoorhies wants to merge 1 commit into
openai:mainfrom
randvoorhies:linux-suspend-resume
Open

Support suspending and resuming Linux VMs#1297
randvoorhies wants to merge 1 commit into
openai:mainfrom
randvoorhies:linux-suspend-resume

Conversation

@randvoorhies

@randvoorhies randvoorhies commented Jul 28, 2026

Copy link
Copy Markdown

Fixes #1177

Summary

Previous investigation in #796 and #1177 implied that restoring suspended Linux VMs wasn't supported by Virtualization.framework. Some testing on current macOS shows that Linux restoration does actually work properly, and that Tart's existing suspend/restore mechanisms were very close to working.

The Darwin platform implementation persists the VM's ECID and assigns it to VZMacPlatformConfiguration.machineIdentifier. However, the Linux platform didn't specify a machineIdentifier when constructing its VZGenericPlatformConfiguration, which caused a new identifier to be generated each time the platform was instantiated.

Unfortunately, Virtualization.framework returns a very opaque error if saved machine state is restored with a mismatched identifier:

Error Domain=VZErrorDomain Code=12 "The virtual machine failed to restore with error “invalid argument”."

This PR generates and persists a VZGenericMachineIdentifier in new Linux VM config files, then reuses it whenever the platform is constructed. The goal is to get the Linux behavior as close as possible to the existing Darwin behavior (with the exception of handling legacy config files with no machineIdentifier). Future work could probably align these more closely, but I'd like to keep this PR as minimal as I can.

Now, existing Linux configs without a machineIdentifier are loadable for normal use but can't be suspended. When cloning a stopped legacy Linux VM, the missing machineIdentifier is initialized in the new clone. Existing identifiers are preserved, matching Tart's current behavior for Darwin ECIDs, and suspended clones keep their original identity and saved state.

The Linux platform now conforms to PlatformSuspendable. Suspendable Linux VMs retain USB keyboard support but omit VZUSBScreenCoordinatePointingDeviceConfiguration, which passes validateSaveRestoreSupport() but causes restoration to fail.

Testing

  • The full Swift test suite passes.
  • Two unit tests cover identifier serialization and platform reconstruction, plus legacy configuration compatibility.
  • A current Ubuntu OCI image without an identifier was cloned and verified to receive one before its first run.
  • The resulting Ubuntu VM was started, suspended, and restored while streaming an in-memory counter over UDP to the host:
boot=d055e850-7951-4744-8790-380df1fa628c pid=1031 count=109
boot=d055e850-7951-4744-8790-380df1fa628c pid=1031 count=110
boot=d055e850-7951-4744-8790-380df1fa628c pid=1031 count=111
# VM suspended
# VM restored
boot=d055e850-7951-4744-8790-380df1fa628c pid=1031 count=112
boot=d055e850-7951-4744-8790-380df1fa628c pid=1031 count=113

The matching boot ID and PID with a continuously incrementing counter confirm that saved machine state was restored rather than cold-booted.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0d2a3fcca1

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread Sources/tart/VMDirectory.swift Outdated
}
}

private func regenerateMachineIdentifier() throws {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Regenerate Linux machine IDs when importing archives

When a Linux .tvm archive is imported more than once, or imported on a host that still has the exported VM, Import.swift only fixes MAC collisions; the new persisted VZGenericMachineIdentifier is copied unchanged from config.json. Since this helper is private and only the clone path can call it, those imported local Linux VMs remain suspendable with the same supposedly unique platform identifier, defeating the uniqueness invariant this change adds for save/restore. Please make this available to import and regenerate it when a local Linux identifier collision exists.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. I updated Import to regenerate the VZGenericMachineIdentifier when it collides with an existing local Linux VM. Regeneration also removes state.vzvmsave if a new identifier is minted, since that saved state is tied to the old identifier and cannot be restored using the new one.

I added an import test using distinct MAC addresses to isolate the machine-identifier collision. It verifies that the identifier changes, the MAC address remains unchanged, and the incompatible saved state is
removed.

By the way, I think the same collision risk may exist for macOS VMs. Darwin's ecid is a VZMacMachineIdentifier, and the clone and import paths appear to preserve it. I kept that preexisting behavior out of this PR, but it may be worth unifying the identity handling in a follow-up.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@codex review

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I took a step back to think about why this would be any different for the Linux vs Darwin platforms, and ended up simplifying the whole PR to make the new behavior more consistent with how Darwin currently handles its ecid. Existing identifiers are now preserved just like Darwin, and the only Linux-specific clone behavior is initializing the missing identifier for a stopped legacy config.

I agree that Import should probably do the machineIdentity/ecid collision check, but it should be done consistently for Darwin and Linux and that felt out of scope for this PR.

@randvoorhies
randvoorhies force-pushed the linux-suspend-resume branch from 0d2a3fc to 769f6a2 Compare July 28, 2026 23:52
@randvoorhies

Copy link
Copy Markdown
Author

I'm very excited about getting this merged in. My org has a suite of playwright tests that run against a containerized stack of services running on a Linux host. Bringing that stack up and down to reset state for each test is expensive at around 30s per respin. Some initial experiments with code behind this new PR show that we can take a snapshot of a VM running our system, and then resume it in ~2s before each test.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 769f6a21be

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread Sources/tart/Commands/Push.swift Outdated
if populateCache {
let expectedPushedVMDir = try ociStorage.create(pushedRemoteName)
try localVMDir.clone(to: expectedPushedVMDir, generateMAC: false)
try localVMDir.clone(to: expectedPushedVMDir, generateMAC: false, generateMachineIdentifier: false)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Strip saved state from pushed-image cache

When --populate-cache is used after pushing a suspended Linux VM, this cache clone still copies state.vzvmsave, even though pushToRegistry only uploads config, disk, and NVRAM. A later tart clone <that ref> can then open the populated OCI cache, see the cached VM as .Suspended, preserve the same Linux machine identifier, and produce a local clone that restores a snapshot that was never part of the pushed image; strip the saved state from the pushed-image cache (or clone without it) before linking the ref.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, but that's really an issue that pre-dates this PR and also applies to Darwin. I removed all of my changes to Push and Clone (other than the new machineIdentifier generation for legacy configs) so this PR doesn't change that behavior at all.

Similar to the other comment, this is probably a good idea to fix consistently for both platforms but I think it's out of scope for the current PR.

Persist a VZGenericMachineIdentifier in Linux VM configurations and
reuse it when building VZGenericPlatformConfiguration. Saved machine
state is tied to this identifier, so generating a new one prevents
restoration.

Legacy configurations remain loadable but cannot be suspended.
When cloning a stopped legacy Linux VM, initialize the missing identifier
in the clone. Existing identifiers and suspended state remain unchanged.

Linux now conforms to PlatformSuspendable and retains USB keyboard
support in suspendable mode, but omits
VZUSBScreenCoordinatePointingDeviceConfiguration because it passes
save/restore validation but causes restoration to fail.
@randvoorhies
randvoorhies force-pushed the linux-suspend-resume branch from 769f6a2 to 01d9359 Compare July 31, 2026 20:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feature request: suspend/resume linux vm via --suspendable flag

1 participant