Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 46 additions & 3 deletions src/CacheDigest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -267,12 +267,55 @@ cacheDigestReport(CacheDigest * cd, const SBuf &label, StoreEntry * e)
);
}

/// CacheDigest::CalcMaskSize() helper to compute digest mask size without
/// accounting for any limits or restrictions other than those imposed by
/// uint64_t type/math itself.
static uint64_t
UnsafeMaskSize(const uint64_t cap, const uint8_t bpe)
{
Assure(bpe);

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.

Squid already rejects zero bpe in received digests. We must also reject digest_bits_per_entry 0 configuration directives, but perhaps that should be done in a dedicated PR.


// This limit is paranoid because no instance can store enough objects to
// exceed this maximum.
const auto maxMaskSize = std::numeric_limits<uint64_t>::max() / 8;
Comment on lines +278 to +280

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 my math is correct, at 255 bits per entry, it would take about 2.3 million years to reach this limit while storing/adding 1000 new objects every second. Still, it is probably better to have this specific "we can count all bits using uint64_t math" limit than to use std::numeric_limits<uint64_t>::max().


// Same as ((cap*bpe + 7)/8 > maxMaskSize) but without overflowing multiplication or sum
if (cap > (maxMaskSize*8 - 7)/bpe)
return maxMaskSize;

return (cap*bpe + 7)/8;
}

uint32_t
CacheDigest::CalcMaskSize(uint64_t cap, uint8_t bpe)
{
uint64_t bitCount = (cap * bpe) + 7;
assert(bitCount < INT_MAX); // do not 31-bit overflow later
return static_cast<uint32_t>(bitCount / 8);
// Our mask_size data member is uint32_t. That type is hard-coded in several
// places. TODO: Use a unique type name while revising related types. We
// cannot simply cap calculations at the maximum uint32_t value because we
// must also satisfy the following requirements to protect mask_size users:
//
// R1. Avoid overflows in code that does `mask_size * 8` (e.g., to compute bit positions).
// R2. Avoid overflows in code that stores `mask_size * 8` as `int`.
// R3. Avoid overflows in legacy callers that cast `mask_size` to `ssize_t`.
// R4. Avoid unreasonably large memory allocations for mask storage.
// Bug 4534 fix defined 256MB allocations as "reasonable".
// R5. Ensure that the digest capacity derived from this mask size can be
// sent to legacy installations that assert that the corresponding mask
// bit count is less than INT_MAX.
//
// Some of the current limits below are mathematically redundant (e.g., R5
// satisfies R2), but are explicitly listed to assist with safe refactoring.
//
// For a typical 32-bit `int`, this maxMaskSize is 268'435'454 bytes.
const auto maxMaskSize = std::min({
static_cast<uint64_t>(std::numeric_limits<uint32_t>::max()) / 8, // R1
static_cast<uint64_t>(std::numeric_limits<int>::max()) / 8, // R2
static_cast<uint64_t>(std::numeric_limits<ssize_t>::max()), // R3
static_cast<uint64_t>(256)*1024*1024, // R4
static_cast<uint64_t>(INT_MAX - 8) / 8}); // R5

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.

I used INT_MAX here instead of the usually preferred std::numeric_limits<int>::max() because I wanted to tie R5 to the problematic assertion in legacy code. That assertion is using INT_MAX.


const auto rawMaskSize = ::UnsafeMaskSize(cap, bpe);
return static_cast<uint32_t>(std::min(rawMaskSize, maxMaskSize));
}

static void
Expand Down
12 changes: 6 additions & 6 deletions src/peer_digest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -780,11 +780,11 @@ peerDigestSetCBlock(PeerDigest * pd, const char *buf)
}

/* check consistency further */
if ((size_t)cblock.mask_size != CacheDigest::CalcMaskSize(cblock.capacity, cblock.bits_per_entry)) {
debugs(72, DBG_CRITICAL, host << " digest cblock is corrupted " <<
"(mask size mismatch: " << cblock.mask_size << " ? " <<
CacheDigest::CalcMaskSize(cblock.capacity, cblock.bits_per_entry)
<< ").");
const auto calculatedMaskSize = CacheDigest::CalcMaskSize(cblock.capacity, cblock.bits_per_entry);
if (size_t(cblock.mask_size) != calculatedMaskSize) {
debugs(72, DBG_CRITICAL, "ERROR: " << host << " digest cblock is corrupted or unsupported " <<

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.

We can undo all changes in this file, but I think it is best to use a new ERROR message for these cases so that we can tell whether these errors are printed by problematic/legacy code or upgraded one. In a peering hierarchy, it may be tricky to be sure that every instance is running the intended Squid version...

"(unexpected mask size: " << cblock.mask_size << " for " << cblock.capacity << '*' << cblock.bits_per_entry <<
"; expected: " << calculatedMaskSize << ")");
return 0;
}

Expand All @@ -799,7 +799,7 @@ peerDigestSetCBlock(PeerDigest * pd, const char *buf)
* no cblock bugs below this point
*/
/* check size changes */
if (pd->cd && cblock.mask_size != (ssize_t)pd->cd->mask_size) {
if (pd->cd && size_t(cblock.mask_size) != pd->cd->mask_size) {
debugs(72, 2, host << " digest changed size: " << cblock.mask_size <<
" -> " << pd->cd->mask_size);
freed_size = pd->cd->mask_size;
Expand Down
18 changes: 12 additions & 6 deletions src/store_digest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
#include "util.h"

#include <cmath>
#include <limits>

/*
* local types
Expand Down Expand Up @@ -104,21 +105,26 @@ storeDigestCalcCap()
* cap = hi_cap;
*/

// Bug 4534: we still have to set an upper-limit at some reasonable value though.
// this matches cacheDigestCalcMaskSize doing (cap*bpe)+7 < INT_MAX
const uint64_t absolute_max = (INT_MAX -8) / Config.digest.bits_per_entry;
if (cap > absolute_max) {
Comment on lines -107 to -110

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.

This restriction is now R4 inside CacheDigest::CalcMaskSize().

const auto bpe = Config.digest.bits_per_entry;
Assure(bpe);

// Digest recipients recalculate mask size using received capacity and bpe
// values. Limit sent capacity value to keep legacy digest recipients safe.
const auto safeMaskSizeMax = CacheDigest::CalcMaskSize(std::numeric_limits<uint64_t>::max(), bpe); // absolute maximum
const auto safeCapMax = uint64_t(safeMaskSizeMax) * 8 / bpe;
const auto safeCap = std::min(cap, safeCapMax);
if (cap > safeCap) {
const auto absolute_max = safeCap; // diff reducer

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.

We can reduce the diff further by using absolute_max instead of safeCap, but I think we should use safeCap instead because we have two sets of variables here, one set for the mask size (safeMaskSizeMax) and one for the digest capacity (safeCapMax and safeCap). absolute_max does not tell the reader which set/object that maximum applies to.

static time_t last_loud = 0;
if (last_loud < squid_curtime - 86400) {
debugs(71, DBG_IMPORTANT, "WARNING: Cache Digest cannot store " << cap << " entries. Limiting to " << absolute_max);
last_loud = squid_curtime;
} else {
debugs(71, 3, "WARNING: Cache Digest cannot store " << cap << " entries. Limiting to " << absolute_max);
}
cap = absolute_max;
}

return cap;
return safeCap;
}
#endif /* USE_CACHE_DIGESTS */

Expand Down
Loading