mirror of
https://github.com/Momoko-Ayase/Senbei.git
synced 2026-11-04 10:31:29 -05:00
274 lines
8.9 KiB
Rust
274 lines
8.9 KiB
Rust
//! 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
|
|
})
|
|
));
|
|
}
|
|
}
|