@@ -183,6 +183,24 @@ impl AttestServicesOp {
183183 }
184184}
185185
186+ // Compile-time ABI layout guard: any change to field order or padding that
187+ // breaks the wire format with the guest will be caught here.
188+ const _: ( ) = assert ! (
189+ core:: mem:: offset_of!( AttestServicesOp , report_gpa) == 0x00
190+ && core:: mem:: offset_of!( AttestServicesOp , report_size) == 0x08
191+ && core:: mem:: offset_of!( AttestServicesOp , reserved_1) == 0x0c
192+ && core:: mem:: offset_of!( AttestServicesOp , nonce_gpa) == 0x10
193+ && core:: mem:: offset_of!( AttestServicesOp , nonce_size) == 0x18
194+ && core:: mem:: offset_of!( AttestServicesOp , reserved_2) == 0x1a
195+ && core:: mem:: offset_of!( AttestServicesOp , manifest_gpa) == 0x20
196+ && core:: mem:: offset_of!( AttestServicesOp , manifest_size) == 0x28
197+ && core:: mem:: offset_of!( AttestServicesOp , reserved_3) == 0x2c
198+ && core:: mem:: offset_of!( AttestServicesOp , certificate_gpa) == 0x30
199+ && core:: mem:: offset_of!( AttestServicesOp , certificate_size) == 0x38
200+ && core:: mem:: offset_of!( AttestServicesOp , reserved_4) == 0x3c
201+ && core:: mem:: size_of:: <AttestServicesOp >( ) == 0x40
202+ ) ;
203+
186204#[ derive( Clone ) ]
187205struct GuidTableEntry {
188206 guid : uuid:: Uuid ,
@@ -314,6 +332,16 @@ impl AttestSingleServiceOp {
314332 }
315333}
316334
335+ // Compile-time ABI layout guard for the extended single-service structure
336+ // that wraps AttestServicesOp.
337+ const _: ( ) = assert ! (
338+ core:: mem:: offset_of!( AttestSingleServiceOp , op) == 0x00
339+ && core:: mem:: offset_of!( AttestSingleServiceOp , guid) == 0x40
340+ && core:: mem:: offset_of!( AttestSingleServiceOp , manifest_ver) == 0x50
341+ && core:: mem:: offset_of!( AttestSingleServiceOp , reserved_5) == 0x54
342+ && core:: mem:: size_of:: <AttestSingleServiceOp >( ) == 0x58
343+ ) ;
344+
317345fn get_attestation_report_standard ( nonce : & [ u8 ] ) -> Result < Box < SnpReportResponse > , SvsmReqError > {
318346 let mut resp = SnpReportResponse :: new_box_zeroed ( )
319347 . map_err ( |_| SvsmReqError :: FatalError ( SvsmError :: Mem ) ) ?;
@@ -584,3 +612,204 @@ pub fn attest_protocol_request(
584612 _ => Err ( SvsmReqError :: unsupported_protocol ( ) ) ,
585613 }
586614}
615+
616+ #[ cfg( test) ]
617+ mod tests {
618+ use super :: * ;
619+ use crate :: protocols:: errors:: SvsmResultCode ;
620+ use alloc:: vec;
621+ use core:: mem:: offset_of;
622+ use zerocopy:: FromZeros ;
623+
624+ fn is_invalid_parameter ( err : & SvsmReqError ) -> bool {
625+ matches ! (
626+ err,
627+ SvsmReqError :: RequestError ( SvsmResultCode :: INVALID_PARAMETER )
628+ )
629+ }
630+
631+ fn is_unsupported_protocol ( err : & SvsmReqError ) -> bool {
632+ matches ! (
633+ err,
634+ SvsmReqError :: RequestError ( SvsmResultCode :: UNSUPPORTED_PROTOCOL )
635+ )
636+ }
637+
638+ mod attest_services {
639+ use super :: * ;
640+
641+ fn base_op ( ) -> AttestServicesOp {
642+ AttestServicesOp :: new_zeroed ( )
643+ }
644+
645+ /// The struct has four separate reserved regions. Iterate over
646+ /// all of them to ensure is_reserved_clear() inspects every one,
647+ /// not just the first.
648+ #[ test]
649+ fn reserved_clear_rejects_each_field ( ) {
650+ for offset in [
651+ offset_of ! ( AttestServicesOp , reserved_1) ,
652+ offset_of ! ( AttestServicesOp , reserved_2) ,
653+ offset_of ! ( AttestServicesOp , reserved_3) ,
654+ offset_of ! ( AttestServicesOp , reserved_4) ,
655+ ] {
656+ let mut bytes = base_op ( ) . as_bytes ( ) . to_vec ( ) ;
657+ bytes[ offset] = 0xff ;
658+ let op = AttestServicesOp :: ref_from_bytes ( & bytes) . unwrap ( ) ;
659+ assert ! (
660+ !op. is_reserved_clear( ) ,
661+ "reserved check should fail for byte at offset {offset:#x}"
662+ ) ;
663+ }
664+ }
665+
666+ /// Parsing must reject requests with non-zero reserved fields
667+ /// to enforce forward-compatibility with future spec revisions.
668+ #[ test]
669+ fn try_from_as_ref_rejects_nonzero_reserved ( ) {
670+ for offset in [
671+ offset_of ! ( AttestServicesOp , reserved_1) ,
672+ offset_of ! ( AttestServicesOp , reserved_2) ,
673+ offset_of ! ( AttestServicesOp , reserved_3) ,
674+ offset_of ! ( AttestServicesOp , reserved_4) ,
675+ ] {
676+ let mut bytes = base_op ( ) . as_bytes ( ) . to_vec ( ) ;
677+ bytes[ offset] = 1 ;
678+ let err = AttestServicesOp :: try_from_as_ref ( & bytes) . unwrap_err ( ) ;
679+ assert ! (
680+ is_invalid_parameter( & err) ,
681+ "should reject nonzero reserved at offset {offset:#x}"
682+ ) ;
683+ }
684+ }
685+
686+ /// The certificate region is optional (size == 0 means absent).
687+ /// When present, it must enforce MAX_CERTIFICATE_SIZE as an upper
688+ /// bound to prevent guests from requesting unbounded allocations.
689+ #[ test]
690+ fn certificate_region_boundary ( ) {
691+ let mut op = base_op ( ) ;
692+ assert ! ( op. get_certificate_region( ) . unwrap( ) . is_none( ) ) ;
693+
694+ op. certificate_gpa = 0x4000 ;
695+ op. certificate_size = MAX_CERTIFICATE_SIZE as u32 ;
696+ assert ! ( op. get_certificate_region( ) . unwrap( ) . is_some( ) ) ;
697+
698+ op. certificate_size = ( MAX_CERTIFICATE_SIZE + 1 ) as u32 ;
699+ let err = op. get_certificate_region ( ) . unwrap_err ( ) ;
700+ assert ! ( is_invalid_parameter( & err) ) ;
701+ }
702+ }
703+
704+ mod attest_single_service {
705+ use super :: * ;
706+
707+ fn base_op ( ) -> AttestSingleServiceOp {
708+ AttestSingleServiceOp :: new_zeroed ( )
709+ }
710+
711+ /// Only manifest version 0 is currently defined by the protocol.
712+ /// Parsing must reject any other version to prevent silent
713+ /// misinterpretation of the manifest payload.
714+ #[ test]
715+ fn try_from_as_ref_rejects_manifest_version ( ) {
716+ let mut bytes = base_op ( ) . as_bytes ( ) . to_vec ( ) ;
717+ bytes[ offset_of ! ( AttestSingleServiceOp , manifest_ver) ] = 1 ;
718+ let err = AttestSingleServiceOp :: try_from_as_ref ( & bytes) . unwrap_err ( ) ;
719+ assert ! ( is_invalid_parameter( & err) ) ;
720+ }
721+
722+ /// AttestSingleServiceOp embeds an AttestServicesOp. Parsing must
723+ /// validate the reserved fields of the inner struct too, not just
724+ /// its own reserved_5.
725+ #[ test]
726+ fn try_from_as_ref_rejects_inner_reserved ( ) {
727+ let mut bytes = base_op ( ) . as_bytes ( ) . to_vec ( ) ;
728+ bytes[ offset_of ! ( AttestServicesOp , reserved_1) ] = 1 ;
729+ let err = AttestSingleServiceOp :: try_from_as_ref ( & bytes) . unwrap_err ( ) ;
730+ assert ! ( is_invalid_parameter( & err) ) ;
731+ }
732+
733+ /// GUID bytes are stored in little-endian in the wire format;
734+ /// verify get_guid() reconstructs the original UUID correctly.
735+ #[ test]
736+ fn get_guid_round_trip ( ) {
737+ let expected = uuid ! ( "c476f1eb-0123-45a5-9641-b4e7dde5bfe3" ) ;
738+ let mut op = base_op ( ) ;
739+ op. guid = expected. to_bytes_le ( ) ;
740+ assert_eq ! ( op. get_guid( ) , expected) ;
741+ }
742+ }
743+
744+ mod guid_table {
745+ use super :: * ;
746+
747+ /// Verify the serialized wire format for a single-entry GuidTable:
748+ /// header GUID, total length, entry count, per-entry GUID, offset,
749+ /// size, and trailing payload must all appear at the correct byte
750+ /// positions.
751+ #[ test]
752+ fn single_entry_wire_format ( ) {
753+ let guid = uuid ! ( "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" ) ;
754+ let payload = vec ! [ 0x01 , 0x02 , 0x03 , 0x04 ] ;
755+
756+ let mut table = GuidTable :: new ( ) ;
757+ table. push ( guid, payload. clone ( ) ) ;
758+
759+ let data = table. to_vec ( ) . unwrap ( ) ;
760+ assert_eq ! ( data. len( ) , 48 + payload. len( ) ) ;
761+
762+ let entry_count = u32:: from_le_bytes ( data[ 20 ..24 ] . try_into ( ) . unwrap ( ) ) ;
763+ assert_eq ! ( entry_count, 1 ) ;
764+
765+ assert_eq ! ( & data[ 24 ..40 ] , & guid. to_bytes_le( ) ) ;
766+ let entry_offset = u32:: from_le_bytes ( data[ 40 ..44 ] . try_into ( ) . unwrap ( ) ) ;
767+ assert_eq ! ( entry_offset, 48 ) ;
768+ let entry_size = u32:: from_le_bytes ( data[ 44 ..48 ] . try_into ( ) . unwrap ( ) ) ;
769+ assert_eq ! ( entry_size as usize , payload. len( ) ) ;
770+
771+ assert_eq ! ( & data[ 48 ..] , & payload) ;
772+ }
773+
774+ /// With two entries the payload offsets must account for the larger
775+ /// header. This catches off-by-one errors in the offset arithmetic.
776+ #[ test]
777+ fn multiple_entries_offset_arithmetic ( ) {
778+ let guid1 = uuid ! ( "11111111-1111-1111-1111-111111111111" ) ;
779+ let guid2 = uuid ! ( "22222222-2222-2222-2222-222222222222" ) ;
780+ let payload1 = vec ! [ 0xaa ; 10 ] ;
781+ let payload2 = vec ! [ 0xbb ; 20 ] ;
782+
783+ let mut table = GuidTable :: new ( ) ;
784+ table. push ( guid1, payload1. clone ( ) ) ;
785+ table. push ( guid2, payload2. clone ( ) ) ;
786+
787+ let data = table. to_vec ( ) . unwrap ( ) ;
788+ assert_eq ! ( data. len( ) , 72 + 30 ) ;
789+
790+ let entry_count = u32:: from_le_bytes ( data[ 20 ..24 ] . try_into ( ) . unwrap ( ) ) ;
791+ assert_eq ! ( entry_count, 2 ) ;
792+
793+ let offset1 = u32:: from_le_bytes ( data[ 40 ..44 ] . try_into ( ) . unwrap ( ) ) ;
794+ assert_eq ! ( offset1, 72 ) ;
795+ let offset2 = u32:: from_le_bytes ( data[ 64 ..68 ] . try_into ( ) . unwrap ( ) ) ;
796+ assert_eq ! ( offset2, 82 ) ;
797+
798+ assert_eq ! ( & data[ 72 ..82 ] , & payload1) ;
799+ assert_eq ! ( & data[ 82 ..102 ] , & payload2) ;
800+ }
801+ }
802+
803+ mod protocol_routing {
804+ use super :: * ;
805+
806+ /// The protocol handler must reject unknown request codes with
807+ /// UNSUPPORTED_PROTOCOL to prevent silent misrouting.
808+ #[ test]
809+ fn rejects_unknown_request ( ) {
810+ let mut params = RequestParams :: default ( ) ;
811+ let err = attest_protocol_request ( 99 , & mut params) . unwrap_err ( ) ;
812+ assert ! ( is_unsupported_protocol( & err) ) ;
813+ }
814+ }
815+ }
0 commit comments