Skip to content

Conversation

@spacebear21
Copy link
Collaborator

This was prompted by the discussion in #710 (comment).

Using a reader still left callers susceptible to writing memory exhaustion vulnerable code, and just introduced extra complexity by having to buffer twice. We can recognize this and leave the buffering entirely up to the caller. Also, the V2 sender and receiver response processing functions already take &[u8], so this makes the function signatures more consistent across versions.

@spacebear21 spacebear21 force-pushed the u8-responses branch 2 times, most recently from 228c39b to fd0e04a Compare June 5, 2025 18:32
@spacebear21
Copy link
Collaborator Author

Added one more commit which removes the now unused Io internal error variants.

Comment on lines 24 to +26
impl UncheckedProposal {
pub fn from_request(
body: impl std::io::Read,
body: &[u8],
Copy link
Collaborator

@benalleng benalleng Jun 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be helpful! I was having a really tough time figuring out how to get a Read across the ffi layer in the v2_to_v1 python integration test

@coveralls
Copy link
Collaborator

coveralls commented Jun 5, 2025

Pull Request Test Coverage Report for Build 15543598153

Details

  • 31 of 34 (91.18%) changed or added relevant lines in 6 files are covered.
  • 2 unchanged lines in 2 files lost coverage.
  • Overall coverage increased (+0.01%) to 85.474%

Changes Missing Coverage Covered Lines Changed/Added Lines %
payjoin-cli/src/app/v2/mod.rs 0 1 0.0%
payjoin-cli/src/app/v1.rs 4 6 66.67%
Files with Coverage Reduction New Missed Lines %
payjoin/src/receive/v1/exclusive/mod.rs 1 96.15%
payjoin/src/send/v1.rs 1 87.56%
Totals Coverage Status
Change from base Build 15542505619: 0.01%
Covered Lines: 6973
Relevant Lines: 8158

💛 - Coveralls

Copy link
Collaborator

@nothingmuch nothingmuch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cACK, utACK apart from one correctness suggestion (body.len() != content_length)

but i also have some suggestions for how to simplify a further which also apply to the sender side of things but i only commented on the receiver side, if you think they are appropriate then it's probably a bit cleaner to do it in this PR rather than a followup

let mut buf = vec![0; content_length];
body.read_exact(&mut buf).map_err(InternalRequestError::Io)?;
Ok(buf)
Ok(body.to_vec())
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what do you think about returning just Ok(body), with the following signature:

fn validate_body<'b>(headers: impl Headers, body: &'b [u8]) -> Result<&'b [u8]>, RequestError> {

that would avoid allocation, and there's no real need to copy it just to return it back to the caller, since the caller already has a means of giving access to the body as a &[u8]

alternatively if we forego the content length consistency check, even simpler would be to rename this method validate_header, drop the body argument since it doesn't really do anything with the it, and return Result<usize, RequestError> where the usize is the expected length (or a newtype ExpectedContentLength(usize) but maybe that's overkill since this is all private)

.parse::<usize>()
.map_err(InternalRequestError::InvalidContentLength)?;
if content_length > MAX_CONTENT_LENGTH {
if content_length > MAX_CONTENT_LENGTH || body.len() > MAX_CONTENT_LENGTH {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably be:

Suggested change
if content_length > MAX_CONTENT_LENGTH || body.len() > MAX_CONTENT_LENGTH {
if content_length > MAX_CONTENT_LENGTH || body.len() != content_length {

but a different error (which should still map to bad request error).

however, since body is already allocated, and presumably does not exceed the app's limits by the time this code is run, maybe it's better to just remove MAX_CONTENT_LENGTH altogether?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of introducing this check, this function now returns body[..content_length] which preserves the prior behavior of capping the body length without throwing an error.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think i prefer breaking compatibility here, because a truncated request should also error, but the error will be less intelligible (PSBT parsing presumably will fail)

let validated_body = validate_body(headers, body).map_err(ReplyableError::V1)?;

let base64 = String::from_utf8(parsed_body).map_err(InternalPayloadError::Utf8)?;
let base64 = String::from_utf8(validated_body).map_err(InternalPayloadError::Utf8)?;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

taken with the suggestion to have a validate_header method below, if a newtype ExpectedContentLength(usize), it could simply do

fn parse_body<'a>(&self, body: &'a [u8]) Result<&'a str, ReplyableError> 
    if body.len() != self.0 {
        return Err(todo!());
    }

    str::from_utf8(body).map_err(InternalPayloadError::Utf8)
}

it could handle this conversion from &[u8] using &str::from_utf8, to again avoid copying the data, since we assume we already have contiguous random access to it as a &[u8].

Simplify the function signature to take a bytes slice instead of a
generic reader. `parse_body` is renamed to better reflect its role.
Simplify the function signature to take a bytes slice instead of a
generic reader.
They are no longer constructed after the changes from the previous two
commits.
@spacebear21
Copy link
Collaborator Author

I elected to keep the MAX_CONTENT_LENGTH check as-is to avoid changing behavior in this PR, and applied your suggestion to not allocate in a new commit (because it cascaded to touch many files so I kept it separate).

There is no real need to copy it just to return it back to the caller.
@spacebear21 spacebear21 merged commit 8933188 into payjoin:master Jun 9, 2025
13 checks passed
benalleng added a commit to benalleng/rust-payjoin that referenced this pull request Jun 12, 2025
After payjoin#745 I realized we can go further and completely remove the
FinalizeResponseError in favor of using FinalizedError which already has
access to DirectoryResponse errors.
spacebear21 added a commit that referenced this pull request Jun 12, 2025
After #745 I realized we can go further and completely remove the
`FinalizeResponseError` in favor of using `FinalizedError` which already
has access to `DirectoryResponse` errors.
benalleng added a commit to benalleng/rust-payjoin that referenced this pull request Jun 19, 2025
After payjoin#745 I realized we can go further and completely remove the
FinalizeResponseError in favor of using FinalizedError which already has
access to DirectoryResponse errors.
shinghim pushed a commit to shinghim/rust-payjoin that referenced this pull request Jul 2, 2025
After payjoin#745 I realized we can go further and completely remove the
FinalizeResponseError in favor of using FinalizedError which already has
access to DirectoryResponse errors.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants