Borys Tsyrulnikov

Two bugs in Intel's TDX guest crate

INTEL-SA-01462CVE-2026-20763, CVE-2026-20765

Intel's tdx-guest Rust crate before version 0.3.1 contained two logic errors in wrappers for TDX 1.5 L2 VM operations. One wrapper discarded a correctly packed RCX value. Another reversed a bitmap validation check. Intel assigned CVE-2026-20763 and CVE-2026-20765 and rated both issues 4.6 Medium.

At a glance

  • Affected: Intel TDX Guest software before version 0.3.1.
  • Fixed: in upstream commit 311d33b and released in version 0.3.1.
  • Prerequisites: local access and high privileges. No user interaction is required.
  • Vendor assessment: escalation of privilege, CVSS 4.0 score 4.6 Medium.

What the crate does

Intel TDX is designed to isolate a Trust Domain from the host virtual machine monitor. Software inside the Trust Domain calls the TDX module through TDCALL. The leaf number goes in rax, and operands go in rcx, rdx, and other registers. A wrong register value changes the operation.

tdx-guest is Intel's Rust crate for these guest APIs. It lets guest software use Rust functions instead of assembling each register value. A caller relies on the wrapper to translate function arguments into the correct TDCALL state.

Both bugs affected TDX 1.5 L2 VM operations. These operations support TD partitioning, where an L1 VMM inside a Trust Domain manages nested L2 VMs.

Review method

I found both bugs during source review. I compared each Rust wrapper with the Intel TDX module ABI, followed computed values into TdcallArgs, and evaluated validation conditions with small inputs. This was enough to demonstrate the wrapper errors without TDX hardware. It was not an end-to-end platform exploit.

CVE-2026-20763: the correct RCX value was discarded

enter_l2_vcpu invokes TDG.VP.ENTER. RCX contains the L2 VM index in bits 53:52 and an invalidation control value in bits 1:0. The InvdTranslations enum uses values from 0 through 3. It is not a Boolean flag.

let rcx = (l2_vm_idx << 52) | (invd_translations as u64);
let mut args = TdcallArgs {
    rax: TdcallNum::VpEnter as u64,
-   rcx: l2_vm_idx,
+   rcx,
    rdx: guest_state_gpa,
    ..Default::default()
};

The function computes the packed value correctly, but it then puts the raw VM index in RCX. The computed value is never used.

A concrete value shows the difference:

Input:
  l2_vm_idx = 1
  invd_translations = InvdTlb (2)

Expected RCX:
  (1 << 52) | 2 = 0x0010_0000_0000_0002

Actual RCX:
  1               = 0x0000_0000_0000_0001

The expected value selects L2 VM 1 and invalidation mode 2. The actual value encodes VM index 0 and invalidation mode 1. The TDX module receives a different operand from the one the caller requested.

The second part of the fix is in lib.rs:

 #![cfg_attr(not(test), no_std)]
 #![allow(dead_code)]
-#![allow(unused_variables)]

The crate-wide allow(unused_variables) suppressed the warning that identified rcx as unused. Without the suppression, the compiler reports:

warning: unused variable: `rcx`
  --> src/tdcall.rs:495:9

This does not prove that nobody noticed the code. It shows that ordinary builds hid a useful warning. The same commit prefixes three intentionally unused names with an underscore: _interrupt_vector, _metadata, and _operand.

CVE-2026-20765: the validation check was inverted

invalidate_l2_cached_ept takes a bitmap that selects nested VMs. Bits 1 through 3 select L2 VMs 1 through 3. The fixed Rust wrapper rejects an empty bitmap and any value containing bits outside 0b1110. The ABI marks bit 0 and bits 4 through 63 as reserved, but does not explicitly require a nonzero bitmap.

-if l2_vm_idx_bitmap & !0b1110 == 0 {
+if l2_vm_idx_bitmap == 0 || (l2_vm_idx_bitmap & !0b1110u64) != 0 {
     return Err(TdCallError::TdxOperandInvalid);
 }

bitmap & !0b1110 isolates bits that are not allowed. A result of zero means that the bitmap has no invalid bits. The original function returns an error in this case, so it rejects valid input.

Input Status with three L2 VMs configured Original result
0b0010 Valid, VM 1 true, reject
0b1110 Valid, VMs 1 through 3 true, reject
0b0001 Invalid, bit 0 false, accept
0b1_0000 Invalid, bit 4 false, accept

The corrected condition rejects an empty bitmap and rejects any bit outside 0b1110. This truth table was also the simplest reproduction. It shows the error without TDX hardware.

Security impact and limits

Intel classified both issues as escalation of privilege and gave each one a CVSS 4.0 score of 4.6 Medium. The assessment requires local access and high privileges. It requires no user interaction. Intel rated the confidentiality, integrity, and availability impact as low.

The examples prove incorrect wrapper behavior, not a platform exploit. In Intel's public TDX Module 1.5.09 source, TDG.VP.ENTER rejects VM index 0, while TDG.VP.INVEPT rejects bit 0 and bits above the configured L2 VM count. I did not demonstrate bypassing these module-side checks or breaking TDX isolation.

The bugs are interesting because they occur at an abstraction boundary. One wrapper sends a register value that the caller did not request. Another accepts values that its Rust-side validation is supposed to reject. Wrapper errors can be security-relevant because callers rely on the wrapper instead of checking the ABI themselves.

Fix and prevention

The patch passes the computed rcx, corrects the bitmap condition, and removes the crate-wide unused-variable suppression. Two controls follow from these bugs: use narrow lint exceptions, and test bitmask validation with explicit valid and invalid values.

Timeline

2026-02-17
Both reported to Intel through its bug bounty program.
2026-03-10
Fixed upstream and released in tdx-guest 0.3.1.
2026-08-11
Published as INTEL-SA-01462, with CVE-2026-20763 and CVE-2026-20765.

References