Skip to content

feat: add memory limit for RPC value deserialization - #7440

Open
francesco-stacks wants to merge 2 commits into
stacks-network:mainfrom
francesco-stacks:feat/read-only-parse-mem-limit
Open

feat: add memory limit for RPC value deserialization#7440
francesco-stacks wants to merge 2 commits into
stacks-network:mainfrom
francesco-stacks:feat/read-only-parse-mem-limit

Conversation

@francesco-stacks

Copy link
Copy Markdown
Contributor

Description

This extend our existing memory heap allocator guards to also cover clarity value deserialization in read-only RPC functions

Applicable issues

  • fixes #

Additional info (benefits, drawbacks, caveats)

Checklist

  • Test coverage for new or modified code paths
  • For new Clarity features or consensus changes, add property tests (see docs/property-testing.md)
  • Changelog fragment(s) or "no changelog" label added (see changelog.d/README.md)
  • Required documentation changes (e.g., rpc/openapi.yaml for RPC endpoints, event-dispatcher.md for new events)
  • New clarity functions have corresponding PR in clarity-benchmarking repo

@francesco-stacks francesco-stacks self-assigned this Jul 27, 2026
@francesco-stacks
francesco-stacks requested review from aaronb-stacks and benjamin-stacks and removed request for aaronb-stacks July 27, 2026 09:34
@coveralls

coveralls commented Jul 27, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 30926002638

Coverage decreased (-0.02%) to 86.598%

Details

  • Coverage decreased (-0.02%) from the base build.
  • Patch coverage: 18 uncovered changes across 3 files (182 of 200 lines covered, 91.0%).
  • 163 coverage regressions across 27 files.

Uncovered Changes

File Changed Covered %
stackslib/src/net/api/read_only_parse.rs 138 127 92.03%
stackslib/src/config/mod.rs 5 0 0.0%
stacks-node/src/main.rs 2 0 0.0%
Total (9 files) 200 182 91.0%

Coverage Regressions

163 previously-covered lines in 27 files lost coverage.

Top 10 Files by Coverage Loss Lines Losing Coverage Coverage
stackslib/src/net/p2p.rs 51 74.14%
clarity/src/vm/functions/bitcoin_madhouse.rs 25 77.06%
stackslib/src/net/download/epoch2x.rs 15 59.69%
stackslib/src/net/inv/epoch2x.rs 12 79.44%
stacks-signer/src/v0/signer_state.rs 9 92.61%
stackslib/src/burnchains/burnchain.rs 7 71.39%
stackslib/src/net/relay.rs 5 74.67%
stackslib/src/net/stackerdb/sync.rs 5 76.57%
stackslib/src/net/unsolicited.rs 5 82.4%
stacks-node/src/neon_node.rs 4 83.2%

Coverage Stats

Coverage Status
Relevant Lines: 232892
Covered Lines: 201679
Line Coverage: 86.6%
Coverage Strength: 19340243.56 hits per line

💛 - Coveralls

aaronb-stacks
aaronb-stacks previously approved these changes Jul 27, 2026

@aaronb-stacks aaronb-stacks left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM!

@francesco-stacks
francesco-stacks force-pushed the feat/read-only-parse-mem-limit branch from a874438 to 478b5b2 Compare August 5, 2026 08:48

@benjamin-stacks benjamin-stacks left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM, couple nits. Didn't pick "approve" because you've enabled automerge, and I'd like you to consider some of my notes at least. Happy to approve as-is though.

"TrackingAllocator is not installed as the global allocator; any miner or signer configured memory limits will never trigger"
);
}
tracking_allocator_installed();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Not sure I agree with this change. tracking_allocator_installed() only outputs a warning the first time it's called (which is hopefully in a sport where it prevents node startup with a panic).

I think it's appropriate for this to log an error every time this assumption is incorrectly relied on.

read_only_call_limit: ExecutionCost,
read_only_max_execution_time: Duration,
read_only_call_max_mem_bytes: u64,
pub read_only_call_max_mem_bytes: u64,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: I don't have much context on how CallReadOnly and FastCallReadOnly relate to each other, but this feels like a little bit of a smell. (Pre-existing smell, to be clear -- I know this didn't start here in this PR, and in fact you've improved the situation a little)

@@ -0,0 +1,279 @@
// Copyright (C) 2026 Stacks Open Internet Foundation

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: Can we move this file to a subfolder? I've always liked the fact that net/api/ basically has a 1:1 correspondence between files and endpoints (with mod.rs being the reasonable exception).

use crate::chainstate::stacks::MAX_TRANSACTION_LEN;
use crate::net::http::Error;

/// Shortest Clarity parameter declaration, `(a int)`, plus a separating space.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: Technically only a very limited number of parameters can be single characters (they type checker seems to let it slide if you have duplicates, but execution fails). So the true number would be less.

That said a) it doesn't matter in practice, and b), I think it would be totally okay to set the cap at a more reasonable upper limit. If somebody has really deployed a function with 200k parameters, they deserver their fate 😅 (at least for a non-consensus-critical API call).

/// Parse a JSON body, charging its wire size up front and re-measuring
/// retention afterwards.
fn parse_json_body<T: DeserializeOwned>(body: &[u8], budget: &ParseBudget) -> Result<T, Error> {
budget.preflight(body.len() as u64)?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If the purpose of this is to charge the caller for the memory usage of the serialized arguments in the request, and make up for the fact that that memory has already been allocated before taking the baseline measurement? If so, shouldn't this modify the baseline instead?

tracker: MemoryTracker,
}

impl ParseBudget {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Some nits on this. Nothing to block this PR over, so feel free to go ahead with it, just take it as me saying "I might go refactor this" and speak out if you disagree 😁

First, this is conflating the terminology I deliberately chose in #7432: A budget only defines the upper bounds, while a limiter is responsible for measuring the baseline and enforcing the budget. In that terminology, ParseBudget is a limiter.

Second, one of the purposes of #7432 was to encapsulate all this in dedicated structures for a general "resource" concept, so we have one way of achieving this instead of the previous multitudes. I still think that architectural choice was correct, and I don't think opening it up again here achieves much that would make up for the disadvantage.

The preflight thing sounds like a good enhancement for the ResourceLimiter, and it looks like that's the only thing that makes the pub-lication of the MemoryTracker necessary (and maybe not even that, given that it's just a thin wrapper around Aaron's thread_allocated() API).

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