Skip to content

Commit 88a5591

Browse files
committed
ctap2: take &mut Response in trait + outline call_ctap2 arms to reduce stack by 40kB
1 parent f6db2f7 commit 88a5591

3 files changed

Lines changed: 183 additions & 55 deletions

File tree

src/ctap2.rs

Lines changed: 146 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -498,14 +498,55 @@ pub enum Error {
498498
/// [`Response`].
499499
pub trait Authenticator {
500500
fn get_info(&mut self) -> get_info::Response;
501+
502+
/// Build a `MakeCredential` response into the caller-supplied buffer.
503+
///
504+
/// Returns by-out-parameter instead of by-value because the response
505+
/// is large (~6 KB with `mldsa44` enabled) and the CTAP dispatch chain
506+
/// is several layers deep — every layer that carries a
507+
/// `Result<Response>` reserves a Response-sized slot in its frame even
508+
/// when NRVO would elide the final move. Writing into the caller's
509+
/// slot keeps the chain flat (~6 KB total instead of ~6 KB per layer).
510+
///
511+
/// Same pattern as `std::io::Read::read_to_end(&mut self, buf: &mut Vec<u8>)`
512+
/// / `BufRead::read_line(buf: &mut String)`. For callers that don't
513+
/// care about the stack copy, [`Authenticator::make_credential`] is a
514+
/// convenience by-value wrapper.
515+
fn make_credential_into(
516+
&mut self,
517+
request: &make_credential::Request,
518+
response: &mut make_credential::Response,
519+
) -> Result<()>;
520+
521+
/// Convenience by-value wrapper around [`make_credential_into`] for
522+
/// callers that don't need to avoid the stack copy.
501523
fn make_credential(
502524
&mut self,
503525
request: &make_credential::Request,
504-
) -> Result<make_credential::Response>;
526+
) -> Result<make_credential::Response> {
527+
let mut response = make_credential::Response::empty();
528+
self.make_credential_into(request, &mut response)?;
529+
Ok(response)
530+
}
531+
532+
/// Build a `GetAssertion` response into the caller-supplied buffer.
533+
/// See [`make_credential_into`] for the rationale.
534+
fn get_assertion_into(
535+
&mut self,
536+
request: &get_assertion::Request,
537+
response: &mut get_assertion::Response,
538+
) -> Result<()>;
539+
540+
/// Convenience by-value wrapper around [`get_assertion_into`].
505541
fn get_assertion(
506542
&mut self,
507543
request: &get_assertion::Request,
508-
) -> Result<get_assertion::Response>;
544+
) -> Result<get_assertion::Response> {
545+
let mut response = get_assertion::Response::empty();
546+
self.get_assertion_into(request, &mut response)?;
547+
Ok(response)
548+
}
549+
509550
fn get_next_assertion(&mut self) -> Result<get_assertion::Response>;
510551
fn reset(&mut self) -> Result<()>;
511552
fn client_pin(&mut self, request: &client_pin::Request) -> Result<client_pin::Response>;
@@ -527,119 +568,170 @@ pub trait Authenticator {
527568
Err(Error::InvalidCommand)
528569
}
529570

530-
/// Dispatches the enum of possible requests into the appropriate trait method.
571+
/// Dispatches the enum of possible requests into the appropriate trait
572+
/// method. Writes the result into `*response` in place, so the caller
573+
/// owns the (~6 KB with `mldsa44`) Response storage and intermediate
574+
/// frames don't carry duplicate copies. Saves ~6 KB per chain layer
575+
/// vs the previous by-value-return shape.
576+
///
577+
/// Each match arm that constructs a non-trivial Response variant is
578+
/// outlined into its own `#[inline(never)]` helper below. Without that,
579+
/// LLVM reserves a separate stack region for *every* variant's temporary
580+
/// in `call_ctap2`'s frame (measured ~29 KB before outlining); with it,
581+
/// only the currently-active arm's helper frame is live, shrinking the
582+
/// shared dispatch frame to a small jump table.
531583
#[inline(never)]
532-
fn call_ctap2(&mut self, request: &Request) -> Result<Response> {
584+
fn call_ctap2(&mut self, request: &Request, response: &mut Response) -> Result<()> {
533585
match request {
534586
// 0x4
535587
Request::GetInfo => {
536588
debug_now!("CTAP2.GI");
537-
Ok(Response::GetInfo(self.get_info()))
589+
self.dispatch_get_info(response)
538590
}
539-
540591
// 0x2
541592
Request::MakeCredential(request) => {
542593
debug_now!("CTAP2.MC");
543-
Ok(Response::MakeCredential(
544-
self.make_credential(request).inspect_err(|_e| {
545-
debug!("error: {:?}", _e);
546-
})?,
547-
))
594+
self.dispatch_make_credential(request, response)
548595
}
549-
550596
// 0x1
551597
Request::GetAssertion(request) => {
552598
debug_now!("CTAP2.GA");
553-
Ok(Response::GetAssertion(
554-
self.get_assertion(request).inspect_err(|_e| {
555-
debug!("error: {:?}", _e);
556-
})?,
557-
))
599+
self.dispatch_get_assertion(request, response)
558600
}
559-
560601
// 0x8
561602
Request::GetNextAssertion => {
562603
debug_now!("CTAP2.GNA");
563-
Ok(Response::GetNextAssertion(
564-
self.get_next_assertion().inspect_err(|_e| {
565-
debug!("error: {:?}", _e);
566-
})?,
567-
))
604+
self.dispatch_get_next_assertion(response)
568605
}
569-
570606
// 0x7
571607
Request::Reset => {
572608
debug_now!("CTAP2.RST");
573609
self.reset().inspect_err(|_e| {
574610
debug!("error: {:?}", _e);
575611
})?;
576-
Ok(Response::Reset)
612+
*response = Response::Reset;
613+
Ok(())
577614
}
578-
579615
// 0x6
580616
Request::ClientPin(request) => {
581617
debug_now!("CTAP2.PIN");
582-
Ok(Response::ClientPin(self.client_pin(request).inspect_err(
583-
|_e| {
584-
debug!("error: {:?}", _e);
585-
},
586-
)?))
618+
self.dispatch_client_pin(request, response)
587619
}
588-
589620
// 0xA
590621
Request::CredentialManagement(request) => {
591622
debug_now!("CTAP2.CM");
592-
Ok(Response::CredentialManagement(
593-
self.credential_management(request).inspect_err(|_e| {
594-
debug!("error: {:?}", _e);
595-
})?,
596-
))
623+
self.dispatch_credential_management(request, response)
597624
}
598-
599625
// 0xB
600626
Request::Selection => {
601627
debug_now!("CTAP2.SEL");
602628
self.selection().inspect_err(|_e| {
603629
debug!("error: {:?}", _e);
604630
})?;
605-
Ok(Response::Selection)
631+
*response = Response::Selection;
632+
Ok(())
606633
}
607-
608634
// 0xC
609635
Request::LargeBlobs(request) => {
610636
debug_now!("CTAP2.LB");
611-
Ok(Response::LargeBlobs(
612-
self.large_blobs(request).inspect_err(|_e| {
613-
debug!("error: {:?}", _e);
614-
})?,
615-
))
637+
self.dispatch_large_blobs(request, response)
616638
}
617-
618639
// 0xD
619640
Request::AuthenticatorConfig(request) => {
620641
debug_now!("CTAP2.CFG");
621642
self.authenticator_config(request).inspect_err(|_e| {
622643
debug!("error: {:?}", _e);
623644
})?;
624-
Ok(Response::AuthenticatorConfig)
645+
*response = Response::AuthenticatorConfig;
646+
Ok(())
625647
}
626-
627648
// Not stable
628649
Request::Vendor(op) => {
629650
debug_now!("CTAP2.V");
630651
self.vendor(*op).inspect_err(|_e| {
631652
debug!("error: {:?}", _e);
632653
})?;
633-
Ok(Response::Vendor)
654+
*response = Response::Vendor;
655+
Ok(())
634656
}
635657
}
636658
}
637-
}
638659

639-
impl<A: Authenticator> crate::Rpc<Error, Request<'_>, Response> for A {
640-
/// Dispatches the enum of possible requests into the appropriate trait method.
641660
#[inline(never)]
642-
fn call(&mut self, request: &Request) -> Result<Response> {
643-
self.call_ctap2(request)
661+
fn dispatch_get_info(&mut self, response: &mut Response) -> Result<()> {
662+
*response = Response::GetInfo(self.get_info());
663+
Ok(())
664+
}
665+
666+
#[inline(never)]
667+
fn dispatch_make_credential(
668+
&mut self,
669+
request: &make_credential::Request,
670+
response: &mut Response,
671+
) -> Result<()> {
672+
*response = Response::MakeCredential(make_credential::Response::empty());
673+
let Response::MakeCredential(inner) = response else { unreachable!() };
674+
self.make_credential_into(request, inner).inspect_err(|_e| {
675+
debug!("error: {:?}", _e);
676+
})
677+
}
678+
679+
#[inline(never)]
680+
fn dispatch_get_assertion(
681+
&mut self,
682+
request: &get_assertion::Request,
683+
response: &mut Response,
684+
) -> Result<()> {
685+
*response = Response::GetAssertion(get_assertion::Response::empty());
686+
let Response::GetAssertion(inner) = response else { unreachable!() };
687+
self.get_assertion_into(request, inner).inspect_err(|_e| {
688+
debug!("error: {:?}", _e);
689+
})
690+
}
691+
692+
#[inline(never)]
693+
fn dispatch_get_next_assertion(&mut self, response: &mut Response) -> Result<()> {
694+
*response = Response::GetNextAssertion(self.get_next_assertion().inspect_err(|_e| {
695+
debug!("error: {:?}", _e);
696+
})?);
697+
Ok(())
698+
}
699+
700+
#[inline(never)]
701+
fn dispatch_client_pin(
702+
&mut self,
703+
request: &client_pin::Request,
704+
response: &mut Response,
705+
) -> Result<()> {
706+
*response = Response::ClientPin(self.client_pin(request).inspect_err(|_e| {
707+
debug!("error: {:?}", _e);
708+
})?);
709+
Ok(())
710+
}
711+
712+
#[inline(never)]
713+
fn dispatch_credential_management(
714+
&mut self,
715+
request: &credential_management::Request,
716+
response: &mut Response,
717+
) -> Result<()> {
718+
*response = Response::CredentialManagement(
719+
self.credential_management(request).inspect_err(|_e| {
720+
debug!("error: {:?}", _e);
721+
})?,
722+
);
723+
Ok(())
724+
}
725+
726+
#[inline(never)]
727+
fn dispatch_large_blobs(
728+
&mut self,
729+
request: &large_blobs::Request,
730+
response: &mut Response,
731+
) -> Result<()> {
732+
*response = Response::LargeBlobs(self.large_blobs(request).inspect_err(|_e| {
733+
debug!("error: {:?}", _e);
734+
})?);
735+
Ok(())
644736
}
645737
}

src/ctap2/get_assertion.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::{Bytes, Vec};
1+
use crate::{Bytes, String, Vec};
22
use cosey::EcdhEsHkdf256PublicKey;
33
use serde::{Deserialize, Serialize};
44
use serde_bytes::ByteArray;
@@ -179,6 +179,24 @@ impl ResponseBuilder {
179179
}
180180
}
181181

182+
impl Response {
183+
/// Empty `Response` with default fields. Used by `Authenticator::call_ctap2`
184+
/// to preallocate the `Response::GetAssertion` variant slot so the inner
185+
/// `get_assertion` impl can write via `&mut` — same shape as
186+
/// `make_credential::Response::empty()`.
187+
pub fn empty() -> Self {
188+
ResponseBuilder {
189+
credential: PublicKeyCredentialDescriptor {
190+
id: Bytes::new(),
191+
key_type: String::new(),
192+
},
193+
auth_data: Bytes::new(),
194+
signature: Bytes::new(),
195+
}
196+
.build()
197+
}
198+
}
199+
182200
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
183201
#[non_exhaustive]
184202
pub struct UnsignedExtensionOutputs {}

src/ctap2/make_credential.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,24 @@ impl ResponseBuilder {
206206
}
207207
}
208208

209+
impl Response {
210+
/// Empty `Response` with default fields. Used by `Authenticator::call_ctap2`
211+
/// to preallocate the `Response::MakeCredential` variant slot in the
212+
/// outer `ctap2::Response` so the inner `make_credential` impl can write
213+
/// via `&mut` — saves the ~6 KB return-by-value copy through the
214+
/// dispatch chain. Inner is sized to the type's full capacity
215+
/// (`auth_data` is `Bytes<AUTHENTICATOR_DATA_LENGTH>` ≈ 2 KB with
216+
/// `mldsa44`); the slot is allocated either way, the win is that it
217+
/// lives in ONE place instead of three.
218+
pub fn empty() -> Self {
219+
ResponseBuilder {
220+
fmt: AttestationStatementFormat::None,
221+
auth_data: super::SerializedAuthenticatorData::new(),
222+
}
223+
.build()
224+
}
225+
}
226+
209227
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
210228
#[cfg_attr(feature = "test-client", derive(Deserialize))]
211229
#[non_exhaustive]

0 commit comments

Comments
 (0)