@@ -1047,10 +1047,153 @@ async fn do_sys_setup(stage0: Stage0<'_>) -> Result<()> {
10471047 } else {
10481048 info ! ( "System time will be synchronized by chronyd in background" ) ;
10491049 }
1050+ stage0
1051+ . setup_gpu ( )
1052+ . await
1053+ . context ( "Failed to verify GPU TEE attestation" ) ?;
10501054 let stage1 = stage0. setup_fs ( ) . await ?;
10511055 stage1. setup ( ) . await
10521056}
10531057
1058+ /// GPU TEE attestation gate (`requirements.verify_gpu`, defaults to true).
1059+ ///
1060+ /// Runs before key provisioning so a CVM whose GPU cannot prove it is a
1061+ /// genuine, CC-enabled NVIDIA TEE never gets its app keys. The GPU
1062+ /// "ready" state (`nvidia-smi conf-compute -srs 1`) is only set from here —
1063+ /// nvidia-persistenced deliberately does not set it — so CUDA work cannot be
1064+ /// submitted to an unverified GPU either.
1065+ mod gpu {
1066+ use super :: * ;
1067+
1068+ const NVATTEST : & str = "/usr/bin/nvattest" ;
1069+ const ATTESTATION_OUTPUT : & str = "/run/nvidia-gpu-attestation/attestation.out" ;
1070+ const ATTESTATION_TIMEOUT : Duration = Duration :: from_secs ( 300 ) ;
1071+
1072+ /// True if a passed-through NVIDIA GPU is present, detected via sysfs PCI
1073+ /// (vendor 0x10de, class VGA 0x0300xx or 3D controller 0x0302xx) so it
1074+ /// works even before the nvidia driver is loaded.
1075+ pub ( super ) fn nvidia_gpu_present ( ) -> bool {
1076+ let Ok ( entries) = fs:: read_dir ( "/sys/bus/pci/devices" ) else {
1077+ return false ;
1078+ } ;
1079+ entries. filter_map ( |e| e. ok ( ) ) . any ( |dev| {
1080+ let read = |name : & str | {
1081+ fs:: read_to_string ( dev. path ( ) . join ( name) )
1082+ . unwrap_or_default ( )
1083+ . trim ( )
1084+ . to_string ( )
1085+ } ;
1086+ read ( "vendor" ) == "0x10de"
1087+ && matches ! ( read( "class" ) . get( ..6 ) , Some ( "0x0300" ) | Some ( "0x0302" ) )
1088+ } )
1089+ }
1090+
1091+ /// Mark the GPU as ready to accept work. Only meaningful (and only
1092+ /// succeeds) when the GPU runs in CC mode.
1093+ pub ( super ) fn set_gpu_ready_state ( ) -> Result < ( ) > {
1094+ let output = Command :: new ( "nvidia-smi" )
1095+ . args ( [ "conf-compute" , "-srs" , "1" ] )
1096+ . output ( )
1097+ . context ( "failed to run nvidia-smi conf-compute" ) ?;
1098+ if !output. status . success ( ) {
1099+ bail ! (
1100+ "nvidia-smi conf-compute -srs 1 failed ({}): {}" ,
1101+ output. status,
1102+ truncated_lossy( & output. stderr, 512 ) ,
1103+ ) ;
1104+ }
1105+ info ! ( "GPU ready state set" ) ;
1106+ Ok ( ( ) )
1107+ }
1108+
1109+ /// Run local GPU attestation via nvattest with a fresh nonce, keeping the
1110+ /// verifier output in /run for debugging. Fails on any non-zero exit —
1111+ /// including a GPU that cannot produce an attestation report at all (a
1112+ /// non-CC GPU, or CC mode left off by the host).
1113+ pub ( super ) async fn attest_gpu ( ) -> Result < ( ) > {
1114+ if !Path :: new ( NVATTEST ) . exists ( ) {
1115+ bail ! ( "nvattest is not available in this image" ) ;
1116+ }
1117+ // Certificate/OCSP validation needs a sane clock even when
1118+ // secure_time is off; best-effort step chrony before attesting.
1119+ if let Err ( err) = cmd ! ( chronyc makestep) {
1120+ warn ! ( "failed to step system clock: {err:?}" ) ;
1121+ }
1122+ let nonce = hex:: encode ( rand:: thread_rng ( ) . gen :: < [ u8 ; 32 ] > ( ) ) ;
1123+ let output = tokio:: time:: timeout (
1124+ ATTESTATION_TIMEOUT ,
1125+ tokio:: process:: Command :: new ( NVATTEST )
1126+ . args ( [ "attest" , "--device" , "gpu" , "--verifier" , "local" ] )
1127+ . args ( [ "--nonce" , & nonce] )
1128+ . output ( ) ,
1129+ )
1130+ . await
1131+ . context ( "GPU attestation timed out" ) ?
1132+ . context ( "failed to run nvattest" ) ?;
1133+ if !output. stderr . is_empty ( ) {
1134+ info ! ( "nvattest: {}" , truncated_lossy( & output. stderr, 2048 ) ) ;
1135+ }
1136+ if !output. status . success ( ) {
1137+ bail ! (
1138+ "nvattest exited with {}: {}" ,
1139+ output. status,
1140+ truncated_lossy( & output. stderr, 512 ) ,
1141+ ) ;
1142+ }
1143+ if let Err ( err) = save_attestation_output ( & output. stdout ) {
1144+ warn ! ( "failed to save GPU attestation output: {err:?}" ) ;
1145+ }
1146+ Ok ( ( ) )
1147+ }
1148+
1149+ fn save_attestation_output ( stdout : & [ u8 ] ) -> Result < ( ) > {
1150+ let output_path = Path :: new ( ATTESTATION_OUTPUT ) ;
1151+ if let Some ( parent) = output_path. parent ( ) {
1152+ fs:: create_dir_all ( parent) ?;
1153+ }
1154+ safe_write ( output_path, stdout) ?;
1155+ fs:: set_permissions (
1156+ output_path,
1157+ std:: os:: unix:: fs:: PermissionsExt :: from_mode ( 0o600 ) ,
1158+ ) ?;
1159+ Ok ( ( ) )
1160+ }
1161+
1162+ fn truncated_lossy ( bytes : & [ u8 ] , limit : usize ) -> String {
1163+ let text = String :: from_utf8_lossy ( bytes) ;
1164+ let text = text. trim ( ) ;
1165+ match text. char_indices ( ) . nth ( limit) {
1166+ Some ( ( idx, _) ) => format ! ( "{}..." , & text[ ..idx] ) ,
1167+ None => text. to_string ( ) ,
1168+ }
1169+ }
1170+ }
1171+
1172+ impl Stage0 < ' _ > {
1173+ /// Enforce `requirements.verify_gpu` (default true): attest an attached
1174+ /// NVIDIA GPU before continuing to key provisioning, or — when explicitly
1175+ /// disabled — set the GPU ready state without verification.
1176+ async fn setup_gpu ( & self ) -> Result < ( ) > {
1177+ if !gpu:: nvidia_gpu_present ( ) {
1178+ return Ok ( ( ) ) ;
1179+ }
1180+ if !self . shared . app_compose . verify_gpu ( ) {
1181+ warn ! ( "requirements.verify_gpu is false; setting GPU ready state without attestation" ) ;
1182+ // Best-effort: a GPU with CC mode off has no ready state to set.
1183+ if let Err ( err) = gpu:: set_gpu_ready_state ( ) {
1184+ warn ! ( "failed to set GPU ready state: {err:?}" ) ;
1185+ }
1186+ return Ok ( ( ) ) ;
1187+ }
1188+ self . vmm . notify_q ( "boot.progress" , "attesting GPU" ) . await ;
1189+ info ! ( "verifying GPU TEE attestation" ) ;
1190+ gpu:: attest_gpu ( ) . await ?;
1191+ gpu:: set_gpu_ready_state ( ) ?;
1192+ info ! ( "GPU TEE attestation succeeded" ) ;
1193+ Ok ( ( ) )
1194+ }
1195+ }
1196+
10541197pub async fn cmd_gateway_refresh ( args : GatewayRefreshArgs ) -> Result < ( ) > {
10551198 let host_shared_dir = args. work_dir . join ( HOST_SHARED_DIR_NAME ) ;
10561199 let shared = HostShared :: load ( host_shared_dir. as_path ( ) ) . with_context ( || {
0 commit comments