-
Notifications
You must be signed in to change notification settings - Fork 107
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Safe Preallocation for (most) Vec<T>
#1880
Comments
Hi @preston-evans98, thanks for this suggestion, and working through these calculations. In general, we've been working on the assumption that parsed data structures are slightly larger than their serialized equivalents. But I don't think we realised that malicious blocks could take up 6 times as much memory when deserialized. Here's some further analysis of this issue: Rust
|
Overall, I think this is a good change, but we should think about our test strategy. If any of these new limits are too low, they might end up rejecting large blocks that other nodes accept. (Rejecting other large messages isn't as much of an issue, because most large messages aren't consensus-critical.) So let's think about a test strategy that:
We can't really run proptests to find the largest possible blocks, because large proptests are very slow. |
* Implement SafePreallocate. Resolves #1880 * Add proptests for SafePreallocate * Apply suggestions from code review Comments which did not include replacement code will be addressed in a follow-up commit. Co-authored-by: teor <[email protected]> * Rename [Safe-> Trusted]Allocate. Add doc and tests Add tests to show that the largest allowed vec under TrustedPreallocate is small enough to fit in a Zcash block/message (depending on type). Add doc comments to all TrustedPreallocate test cases. Tighten bounds on max_trusted_alloc for some types. Note - this commit does NOT include TrustedPreallocate impls for JoinSplitData, String, and Script. These impls will be added in a follow up commit * Implement SafePreallocate. Resolves #1880 * Add proptests for SafePreallocate * Apply suggestions from code review Comments which did not include replacement code will be addressed in a follow-up commit. Co-authored-by: teor <[email protected]> * Rename [Safe-> Trusted]Allocate. Add doc and tests Add tests to show that the largest allowed vec under TrustedPreallocate is small enough to fit in a Zcash block/message (depending on type). Add doc comments to all TrustedPreallocate test cases. Tighten bounds on max_trusted_alloc for some types. Note - this commit does NOT include TrustedPreallocate impls for JoinSplitData, String, and Script. These impls will be added in a follow up commit * Impl TrustedPreallocate for Joinsplit * Impl ZcashDeserialize for Vec<u8> * Arbitrary, TrustedPreallocate, Serialize, and tests for Spend<SharedAnchor> Co-authored-by: teor <[email protected]>
Overview - Safe Preallocation
Currently, Zebra doesn't preallocate Vectors when deserializing, since blind preallocations present a DOS vector. Instead, it relies on maximum block and transaction size to limit allocations. This is inefficient.
We can mitigate the DOS potential and allow for preallocation as follows:
SafeAllocate
, defining the maximum length a Vec<T: SafeAllocate> can sensibly reach for each implementing typeZcashDeserialize
implementation forSafeAllocate
rs.This allows us to pre-allocate for certain types without presenting a DOS vector, while retaining the flexibilty to use the current (lazy) allocation strategy for types that defy safe blind allocation.
Note that deserialization using this method is guaranteed to fail during the deserialization of the first malicious vector. However, Block messages contain nested
Vec<T>
types, and it is possible for a Byzantine message to cause improper allocations at each level of nesting before being discovered. This is why the potential spike for a Block message is much higher than for other message types.Potential SafeAllocation Implementors
Arc<Transaction>
- Suggested allocation limit: 49_000. A Zcash block is capped at 2MB. Since transparent Txs are the smallest allowable transactions, and each transparent Tx requires at least one Input with a minimum serialized size of 41 bytes, an upper bound the length of aVec<Arc<Transaction>>
is 2MB / 41 Bytes < 49_000. Note thatstd::mem::size_of::<Arc<T>>()
is 8, so the maximum memory wasted by a ByzantineVec<Arc<Transaction>>
is 49_000 * 8 Bytes < 400KB.transparent::Input
- Suggested allocation limit: 49_000. A Zcash block is capped at 2MB. Each Input has a minimum serialized size of 41 bytes, so an upper bound the length of aVec<transparent::Input>
is 2MB / 41 Bytes < 49_000. With Inputs taking less than 80 bytes on the stack (36 for the Outpoint, 26 for the Script, and 4 for the sequence), the maximum memory wasted by a ByzantineVec<transparent::Input>
is 49_000 * 80 Bytes < 4MB.transparent::Output
- Suggested allocation limit: 225_000. A Zcash block is capped at 2MB. Outputs have a minimum serialized size of 9 bytes, so an upper bound the length of aVec<Arc<transparent::Output>>
is 2MB / 9 Bytes < 225_000. With Outputs taking less than 40 bytes on the stack (call it 10 for the value, 26 for the Script, for a total of 36 ), the maximum memory wasted by a ByzantineVec<Arc<transparent::Output>>
is 225_000 * 40 Bytes = 9MB.MetaAddr
- Suggested allocation limit: 1000. No fancy math required, since this limit is in the Addr message specification. Estimate a MetaAddr at a generous 100 bytes of stack space and you get max memory waste = 1000 * 100B = 100KB.block::Hash
- Suggested allocation limit: 65536. We derive this limit as MAX_MESSAGE_SIZE / 32 = 2 * 1024 _ 1024 / 32. Since a block hash takes 32 bytes on the stack, the max waste here is MAX_MESSAGE_SIZE.InventoryHash
- Suggested allocation limit: 50,000. This limit is the listed in the Inv message spec. Maximum waste: 50,000 * 32 B = 1.6 MB.block::CountedHeader
- Suggested allocation limit: 2000. This limit is in the Headers message spec. Per the most recent spec, each Zcash header is less than 2kb. Maximum waste: 2000 * 2000 Bytes < 4MBu8
- Suggested allocation limit: MAX_MESSAGE_SIZE. Since a u8 takes 1 byte on the stack, the maximum waste here is MAX_MESSAGE_SIZE = 2MB.Example attacks
Using all of these numbers, we calculate the total improper allocation caused by the worst-case instance of each Zcash Message as follows:
Vec<MetaAddr>
=> 100KB (see above)Vec<block::Hash>
=> MAX_MESSAGE_SIZE (2MB)Vec<InventoryHash>
=> 1.6 MB (see above)Vec<block::Hash>
=> MAX_MESSAGE_SIZE (2MB)Vec<block::CountedHeader>
=> 4MBVec<InventoryHash>
=> 1.6 MB (see above)Vec<Arc<Transaction>
, 1Vec<transparent::Output>
, and 1 maliciousScript(Vec<u8>)
=> 400KB + 9MB + 2MB = 11.4 MB.Vec<transparent::Input>
would be discovered before the maliciousVec<transparent::Output>
was allocated for. Since Outputs can waste more memory than Inputs, a smart attacker will choose to make only hisVec<transparent::Output>
malicious.Vec<InventoryHash>
=> 1.6 MB (see above)Filter(Vec<u8>)
=> 2MBVec<u8>
=> 2MBAlternatives
Do nothing. This is a fine option, since Zebra's networking stack is already fast!
Summary and Recommendations
With the
SafeAllocate
trait, we can allow preallocation for many Vector types without risking DOS attacks.In the worst case, a malicious message can cause a short-lived spike in memory usage. The size of this spike depends on the max_allocation defined in
SafeAllocate
and the depth of nestedVec<T: SafeAllocate>
types. Calculations of the maximum spike caused by each message type are included above. Based on these calculations, I recommend implementing SafeAllocate for all types listed in the "Potential SafeAllocate Implementors" section, with the possible exception oftransparent::Input
andtransparent::Output
.If this recommendation is adopted , the worst case memory spike that a malicious peer can cause will be roughly 4MB, or roughly double that peer connection's usual memory consumption.
If
transparent::Input
s andtransparent::Output
s are included, the worst case spike rises to 11.5MB, or about six times a peer connection's usual memory consumption.The text was updated successfully, but these errors were encountered: