Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Two places on the host→guest boundary where a check exists but the untrusted host decides whether it applies.
1. An SMBIOS string picks the attestation variant, and one variant measures nothing
detect_tee_variant()(dstack/dstack-attest/src/attestation.rs) resolves the platform fromPlatform::detect_or_dstack(), which reads/sys/class/dmi/id/product_nameandsys_vendor. QEMU takes those strings from host-side configuration —configure_smbiosindstack/vmm/src/app/qemu.rs:1083, fed by[cvm.product]invmm.toml— and no TDX or SEV-SNP measurement covers SMBIOS. It is a host claim, not evidence.Platform::NitroEnclavewas the one arm that asked for nothing else. Naming it selectsTeeVariant::DstackNitroEnclave, and that variant turns off two guest-side checks:TeeVariant::has_tdx()is false andtpm_event_pcr_and_bank()isNone, soemit_runtime_eventextends no measurement register and still returnsOk—app-id,compose-hash,instance-id,init-script-hash,key-providerand the rest are appended to the userspace log and measured nowhere;verify_mr_config_id_for_modereturnsOk(())for it without reading MR_CONFIG_ID at all (dstack-util/src/system_setup/config_id_verifier.rs:96-99), so the whole compose-hash/app-id/key-provider-id binding is skipped.Reproduced on
origin/nexton a machine with no TDX, no SEV-SNP and no/dev/nsm, with only the SMBIOS product name replaced:The code already knows DMI is not authoritative here:
Platform::detectchecks/dev/nsmbefore DMI, with the comment "/dev/nsmis the authoritative Nitro Enclave ABI". The DMI fallback then admits the claim without it.2. The host-shared size limit is a
stat, and the thing it stats need not be a fileHostShared::copy(dstack/dstack-util/src/system_setup.rs) tooksrc_path.metadata()?.len(), compared it tomax_size, and then ran an unboundedstd::io::copyfrom a path openedO_NOFOLLOW. The source is the host share;cvm.host_share_modedefaults to9p(dstack/vmm/vmm.toml:106), so the host serves every read live and decides what kind of file each name is.A FIFO stats as zero bytes and passes the limit. Opening it without
O_NONBLOCKblocks until a writer appears, and once one does,io::copyreads it with no limit into the guest's tmpfs.dstack-prepare.servicecarriesFailureAction=reboot, so either outcome is a reboot loop the host triggers on demand rather than an error message.Pre-fix, with the
origin/nextcopy body and the new tests:Fix
resolve_tee_variant(platform, devices)replaces the inline match. The platform hint may only select among variants the hardware corroborates:NitroEnclavenow requires/dev/nsm, and is refused outright when a TDX or SEV-SNP device is present, because the variant it selects measures no runtime event. The other arms keep the device requirements they already had (Gcpneeds TDX,AwsEc2needs a TPM), now expressed against oneTeeDevicesstruct so the function is testable without a real platform.copy_host_shared_file(src, dst, max_size)opens withO_NOFOLLOW | O_NONBLOCK, checks the file type on the open descriptor rather than the path, and applies the limit to the copy viaRead::take(max_size + 1)— reading one byte past the limit is what distinguishes "fits" from "was truncated", so an over-long file is refused rather than silently arriving as its firstmax_sizebytes. The destination now getstruncate(true).Compatibility
Neither change touches a wire format, a derived key, or a measurement. No SDK is affected.
resolve_tee_variant: rejects input that previously worked only in the case a guest claims Nitro Enclave without exposing/dev/nsm. A real Nitro Enclave exposes it, andAttestation::quotealready fails without it — so every configuration that could previously produce a quote still can; what changes is that the ones that could not now fail at detection with a clear message instead of silently skipping two checks first.dstack-tee-simulatoris unaffected: its Nitro Enclave mode creates/dev/nsmininit_donebefore it notifies systemdREADY, and the unit isType=notifyBefore=dstack-prepare.service.copy_host_shared_file: rejects a host-shared entry that is not a regular file, and a file larger than the documented per-file limit. Both were already contract violations; the second was already supposed to be rejected and was rejected wheneverstattold the truth.Verification
No simulator and no
DSTACK_SIMULATOR_ENDPOINTare needed for any of the above. The DMI reproduction needsunshare -Urm(unprivileged user namespaces); the committed test passes trivially without it and is documented with the command that makes it bite. Nothing underos/is touched, soos/tests/acceptance.shwas not run. No guest image was built: theemit_runtime_event-measures-nothing andverify_mr_config_id-returns-Ok consequences are established by readingTeeVariant::has_tdx/tpm_event_pcr_and_bankandverify_mr_config_id_for_mode, not by booting a CVM.Test-merge against open PRs
fix/guest-secret-file-modesdstack-util/src/system_setup.rsfix/guest-boot-and-envfix/guest-exposure-surfacesfix/guest-boot-chainfix/guest-agent-residualsfix/guest-setup-residualsfix/verifier-result-contractfix/guest-initramfs-hardeningfix/verifier-input-canonicalizationfix/config-parser-bounds#1233 resolution: both branches extract the same function from the same closure, which is why they collide. #1233's signature is the better one — it takes
ignore_missingtoo, so the whole closure body moves out. Take #1233's signature and call site, and this branch's body: keep #1233'sexists()/ignore_missingguard and itstruncate(true), replace themetadata()-then-io::copypair with theO_NOFOLLOW | O_NONBLOCKopen, the file-type check on the descriptor, andtake(max_size + 1). The four tests inhost_shared_copy_teststhen needignore_missing: falseadded at each call. No semantic overlap otherwise.Not in this PR, because #1254 already does it:
mount_host_sharedmounts the host share-o rowith nonosuid,nodev,noexec, so on 9p a device node or a setuid bit on the share is live in the guest. #1254 adds exactlyHOST_SHARED_MOUNT_OPTIONS = "ro,nosuid,nodev,noexec"on both the disk and 9p paths, with tests. This branch had the same change and it was dropped rather than raced.