mirror of
https://github.com/Momoko-Ayase/Senbei.git
synced 2026-11-04 10:31:29 -05:00
fix(android): bound archive extraction and decoder resources
This commit is contained in:
1 parent
22dd92982e
commit
a380031567
21 files changed
+2714
-181
No files matched your search
@@ -0,0 +1,273 @@
|
||||
//! Shared Android resource ceilings for extraction, restoration, and archives.
|
||||
//!
|
||||
//! These bounds reject an attacker-controlled size before the corresponding
|
||||
//! allocation or file growth. They are per-allocation and per-task accounting
|
||||
//! caps, not a measurement of peak resident memory: decoder scratch, Huffman
|
||||
//! tables, and mapped inputs can exist at the same time.
|
||||
|
||||
/// Default ceiling for one ZIP entry, one decoder output, one writer source or
|
||||
/// destination, one artifact read, and the restored output image.
|
||||
pub const DEFAULT_MAX_ENTRY_BYTES: u64 = 2 << 30;
|
||||
/// Default ceiling for the sum of retained or temporary bytes charged by one
|
||||
/// extraction, restoration, or archive task.
|
||||
pub const DEFAULT_MAX_TOTAL_BYTES: u64 = 8 << 30;
|
||||
/// Default ceiling for ZIP entries and decoded stage-2 records in one task.
|
||||
pub const DEFAULT_MAX_ENTRIES: usize = 100_000;
|
||||
/// Default nesting ceiling. Depth `0` is the outermost container or stream, and
|
||||
/// a depth equal to this value is still allowed.
|
||||
pub const DEFAULT_MAX_DEPTH: usize = 32;
|
||||
|
||||
/// Overridable ceilings shared by Android extraction, restoration, and I/O.
|
||||
///
|
||||
/// Archive code uses the same type: `max_entry_bytes` bounds one streamed ZIP
|
||||
/// entry, `max_total_bytes` bounds the sum of those entries, `max_entries`
|
||||
/// bounds the entry count including nested archives, and `max_depth` bounds
|
||||
/// archive nesting.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct ResourceLimits {
|
||||
pub max_entry_bytes: u64,
|
||||
pub max_total_bytes: u64,
|
||||
pub max_entries: usize,
|
||||
pub max_depth: usize,
|
||||
}
|
||||
|
||||
impl Default for ResourceLimits {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_entry_bytes: DEFAULT_MAX_ENTRY_BYTES,
|
||||
max_total_bytes: DEFAULT_MAX_TOTAL_BYTES,
|
||||
max_entries: DEFAULT_MAX_ENTRIES,
|
||||
max_depth: DEFAULT_MAX_DEPTH,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ResourceLimits {
|
||||
/// Numeric output ceiling passed into the crypto crate.
|
||||
#[must_use]
|
||||
pub fn output_limit(self) -> senbei_crypto::android::OutputLimit {
|
||||
senbei_crypto::android::OutputLimit {
|
||||
max_output_bytes: self.max_entry_bytes,
|
||||
}
|
||||
}
|
||||
|
||||
/// Reject one allocation, entry, or output before it is reserved.
|
||||
pub fn check_entry_bytes(self, bytes: u64) -> std::result::Result<(), LimitExceeded> {
|
||||
if bytes > self.max_entry_bytes {
|
||||
return Err(LimitExceeded::Entry {
|
||||
requested: bytes,
|
||||
limit: self.max_entry_bytes,
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reject a declared entry or record count before those records are stored.
|
||||
pub fn check_entries(self, count: usize) -> std::result::Result<(), LimitExceeded> {
|
||||
if count > self.max_entries {
|
||||
return Err(LimitExceeded::Entries {
|
||||
requested: count,
|
||||
limit: self.max_entries,
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reject a nested container or stream whose depth exceeds the ceiling.
|
||||
///
|
||||
/// `depth` is zero for the outermost item. `max_depth` itself is accepted.
|
||||
pub fn check_depth(self, depth: usize) -> std::result::Result<(), LimitExceeded> {
|
||||
if depth > self.max_depth {
|
||||
return Err(LimitExceeded::Depth {
|
||||
requested: depth,
|
||||
limit: self.max_depth,
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// A size or count that would exceed a [`ResourceLimits`] ceiling.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
|
||||
pub enum LimitExceeded {
|
||||
/// One entry or allocation is larger than `max_entry_bytes`.
|
||||
#[error("resource limit: entry of {requested} bytes exceeds the {limit}-byte ceiling")]
|
||||
Entry { requested: u64, limit: u64 },
|
||||
/// Charging more bytes would pass `max_total_bytes`.
|
||||
#[error(
|
||||
"resource limit: task total of {requested} bytes exceeds the {limit}-byte ceiling (adding {adding} bytes)"
|
||||
)]
|
||||
Total {
|
||||
requested: u64,
|
||||
adding: u64,
|
||||
limit: u64,
|
||||
},
|
||||
/// An entry or record count passed `max_entries`.
|
||||
#[error("resource limit: {requested} entries exceed the ceiling of {limit}")]
|
||||
Entries { requested: usize, limit: usize },
|
||||
/// Nesting passed `max_depth`.
|
||||
#[error("resource limit: nesting depth {requested} exceeds the ceiling of {limit}")]
|
||||
Depth { requested: usize, limit: usize },
|
||||
}
|
||||
|
||||
/// Running sum of retained or temporary bytes for one task.
|
||||
///
|
||||
/// Each call starts its own budget at zero. Charging checks the single-entry
|
||||
/// ceiling and the task total, but it does not account for buffers that were
|
||||
/// never charged, so it is not a peak-RSS budget.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ByteBudget {
|
||||
limits: ResourceLimits,
|
||||
used: u64,
|
||||
}
|
||||
|
||||
impl ByteBudget {
|
||||
#[must_use]
|
||||
pub fn new(limits: &ResourceLimits) -> Self {
|
||||
Self {
|
||||
limits: *limits,
|
||||
used: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn used(&self) -> u64 {
|
||||
self.used
|
||||
}
|
||||
|
||||
/// Reject `bytes` against the single-entry ceiling without charging it.
|
||||
pub fn check_entry(&self, bytes: u64) -> std::result::Result<(), LimitExceeded> {
|
||||
self.limits.check_entry_bytes(bytes)
|
||||
}
|
||||
|
||||
/// Charge `bytes` against both the single-entry ceiling and the task total.
|
||||
pub fn charge(&mut self, bytes: u64) -> std::result::Result<(), LimitExceeded> {
|
||||
self.check_entry(bytes)?;
|
||||
self.charge_total(bytes)
|
||||
}
|
||||
|
||||
/// Charge `bytes` against the task total only.
|
||||
///
|
||||
/// Use this for a sum of pieces that were each already checked against the
|
||||
/// single-entry ceiling, such as several decoded records in one stream.
|
||||
pub fn charge_total(&mut self, bytes: u64) -> std::result::Result<(), LimitExceeded> {
|
||||
let requested = self
|
||||
.used
|
||||
.checked_add(bytes)
|
||||
.filter(|total| *total <= self.limits.max_total_bytes);
|
||||
match requested {
|
||||
Some(total) => {
|
||||
self.used = total;
|
||||
Ok(())
|
||||
}
|
||||
None => Err(LimitExceeded::Total {
|
||||
requested: self.used.saturating_add(bytes),
|
||||
adding: bytes,
|
||||
limit: self.limits.max_total_bytes,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Running count of entries or decoded records for one task.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EntryBudget {
|
||||
limits: ResourceLimits,
|
||||
used: usize,
|
||||
}
|
||||
|
||||
impl EntryBudget {
|
||||
#[must_use]
|
||||
pub fn new(limits: &ResourceLimits) -> Self {
|
||||
Self {
|
||||
limits: *limits,
|
||||
used: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn used(&self) -> usize {
|
||||
self.used
|
||||
}
|
||||
|
||||
/// Charge `count` additional entries against `max_entries`.
|
||||
pub fn charge(&mut self, count: usize) -> std::result::Result<(), LimitExceeded> {
|
||||
let requested = self
|
||||
.used
|
||||
.checked_add(count)
|
||||
.filter(|total| *total <= self.limits.max_entries);
|
||||
match requested {
|
||||
Some(total) => {
|
||||
self.used = total;
|
||||
Ok(())
|
||||
}
|
||||
None => Err(LimitExceeded::Entries {
|
||||
requested: self.used.saturating_add(count),
|
||||
limit: self.limits.max_entries,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn defaults_match_the_documented_ceilings() {
|
||||
let limits = ResourceLimits::default();
|
||||
assert_eq!(limits.max_entry_bytes, 2 << 30);
|
||||
assert_eq!(limits.max_total_bytes, 8 << 30);
|
||||
assert_eq!(limits.max_entries, 100_000);
|
||||
assert_eq!(limits.max_depth, 32);
|
||||
assert!(limits.check_entry_bytes(limits.max_entry_bytes).is_ok());
|
||||
assert!(limits.check_entries(limits.max_entries).is_ok());
|
||||
assert!(limits.check_depth(limits.max_depth).is_ok());
|
||||
assert!(
|
||||
limits
|
||||
.check_entry_bytes(limits.max_entry_bytes + 1)
|
||||
.is_err()
|
||||
);
|
||||
assert!(limits.check_depth(limits.max_depth + 1).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn byte_budget_accepts_the_boundary_and_rejects_the_next_byte() {
|
||||
let limits = ResourceLimits {
|
||||
max_entry_bytes: 4,
|
||||
max_total_bytes: 8,
|
||||
max_entries: 2,
|
||||
max_depth: 1,
|
||||
};
|
||||
let mut budget = ByteBudget::new(&limits);
|
||||
budget.charge(4).unwrap();
|
||||
budget.charge_total(4).unwrap();
|
||||
assert_eq!(budget.used(), 8);
|
||||
assert!(matches!(
|
||||
budget.charge(5),
|
||||
Err(LimitExceeded::Entry { requested: 5, .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
budget.charge_total(1),
|
||||
Err(LimitExceeded::Total { .. })
|
||||
));
|
||||
assert_eq!(budget.used(), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entry_budget_rejects_the_count_past_the_ceiling() {
|
||||
let limits = ResourceLimits {
|
||||
max_entries: 2,
|
||||
..ResourceLimits::default()
|
||||
};
|
||||
let mut budget = EntryBudget::new(&limits);
|
||||
budget.charge(2).unwrap();
|
||||
assert!(matches!(
|
||||
budget.charge(1),
|
||||
Err(LimitExceeded::Entries {
|
||||
requested: 3,
|
||||
limit: 2
|
||||
})
|
||||
));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user