# Two bugs in Intel's TDX guest crate

> A discarded RCX value and an inverted bitmap check in Intel's `tdx-guest` Rust crate.

Canonical page: [Two bugs in Intel's TDX guest crate](https://borystsyrulnikov.dev/posts/tdx-guest-l2-vm-bugs/)

- Published: 2026-08-17
- Author: [Borys Tsyrulnikov](https://borystsyrulnikov.dev/about/)
- Advisory: [INTEL-SA-01462](https://www.intel.com/content/www/us/en/security-center/advisory/intel-sa-01462.html)
- CVEs: [CVE-2026-20763](https://nvd.nist.gov/vuln/detail/CVE-2026-20763) and [CVE-2026-20765](https://nvd.nist.gov/vuln/detail/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](https://docs.rs/crate/tdx-guest/0.3.1/source/src/tdcall.rs#495).
- **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.

~~~diff
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:

~~~text
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`:

~~~diff
 #![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:

~~~text
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.

~~~diff
-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`](https://github.com/intel/confidential-computing.tdx.tdx-module/blob/TDX_MODULE_1.5.09/src/td_transitions/tdg_vp_enter.c#L100-L105) rejects VM index 0, while [`TDG.VP.INVEPT`](https://github.com/intel/confidential-computing.tdx.tdx-module/blob/TDX_MODULE_1.5.09/src/td_dispatcher/vm_exits/tdg_vp_invept.c#L39-L64) 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

- [INTEL-SA-01462](https://www.intel.com/content/www/us/en/security-center/advisory/intel-sa-01462.html), Intel TDX Guest Software Advisory
- [CVE-2026-20763](https://nvd.nist.gov/vuln/detail/CVE-2026-20763), incorrect calculation
- [CVE-2026-20765](https://nvd.nist.gov/vuln/detail/CVE-2026-20765), incorrect comparison
- [Fixed source](https://docs.rs/crate/tdx-guest/0.3.1/source/src/tdcall.rs#495), `tdx-guest` 0.3.1, upstream commit `311d33b18a`
- [Vulnerable source](https://docs.rs/crate/tdx-guest/0.3.0/source/src/tdcall.rs#495), `tdx-guest` 0.3.0
- [Intel TDX Module v1.5 ABI Specification](https://www.intel.com/content/www/us/en/content-details/795475/intel-tdx-module-v1-5-abi-specification.html)
