diff --git a/compiler-rt/lib/scudo/standalone/combined.h b/compiler-rt/lib/scudo/standalone/combined.h --- a/compiler-rt/lib/scudo/standalone/combined.h +++ b/compiler-rt/lib/scudo/standalone/combined.h @@ -162,6 +162,9 @@ systemSupportsMemoryTagging()) Primary.Options.set(OptionBit::UseMemoryTagging); Primary.Options.set(OptionBit::UseOddEvenTags); + if (getFlags()->hard_rss_limit_mb || getFlags()->soft_rss_limit_mb) { + Primary.Options.set(OptionBit::CheckRssLimit); + } QuarantineMaxChunkSize = static_cast(getFlags()->quarantine_max_chunk_size); @@ -346,6 +349,16 @@ } DCHECK_LE(Size, NeededSize); + if ((Options.get(OptionBit::CheckRssLimit)) && + UNLIKELY(RssChecker.IsExceeded())) { + if (Options.get(OptionBit::MayReturnNull)) + return nullptr; + const u64 CurrentRssMb = GetRSS() >> 20; + u64 SoftRssLimitMb = static_cast(getFlags()->soft_rss_limit_mb); + reportRSSCheck("Soft RSS limit exhausted (Limit x Current): ", + SoftRssLimitMb, CurrentRssMb); + } + void *Block = nullptr; uptr ClassId = 0; uptr SecondaryBlockEnd = 0; @@ -956,6 +969,8 @@ MemorySize, 2, 16); } + AtomicOptions &getOptionsForTesting() { return Primary.Options; } + private: using SecondaryT = MapAllocator; typedef typename PrimaryT::SizeClassMap SizeClassMap; @@ -994,6 +1009,7 @@ QuarantineT Quarantine; TSDRegistryT TSDRegistry; pthread_once_t PostInitNonce = PTHREAD_ONCE_INIT; + RssLimitChecker RssChecker; #ifdef GWP_ASAN_HOOKS gwp_asan::GuardedPoolAllocator GuardedAlloc; diff --git a/compiler-rt/lib/scudo/standalone/common.h b/compiler-rt/lib/scudo/standalone/common.h --- a/compiler-rt/lib/scudo/standalone/common.h +++ b/compiler-rt/lib/scudo/standalone/common.h @@ -9,6 +9,7 @@ #ifndef SCUDO_COMMON_H_ #define SCUDO_COMMON_H_ +#include "atomic_helpers.h" #include "internal_defs.h" #include "fuchsia.h" @@ -208,6 +209,41 @@ // zero-initialized already. }; +class RssLimitChecker { +public: +#if SCUDO_LINUX + // Opportunistic RSS limit check. This will update the RSS limit status, if + // it can, every 250ms, otherwise it will just return the current one. + bool IsExceeded() { + u64 NextCheck = atomic_load_relaxed(&RssNextCheckAtNS); + u64 Now = getMonotonicTime(); + + if (UNLIKELY(Now >= NextCheck)) { + if (atomic_compare_exchange_strong(&RssNextCheckAtNS, &NextCheck, + Now + CheckInterval, + memory_order_relaxed)) { + + Check(); + } + } + return atomic_load_relaxed(&RssLimitExceeded); + } +#else + bool IsExceeded() { return false; } +#endif + +private: + void Check(); + +#if SCUDO_LINUX + // The interval for the checks is 250ms. + static constexpr u64 CheckInterval = (250ULL * 1000000ULL); + + atomic_u64 RssNextCheckAtNS = {}; + atomic_u8 RssLimitExceeded = {}; +#endif +}; + } // namespace scudo #endif // SCUDO_COMMON_H_ diff --git a/compiler-rt/lib/scudo/standalone/common.cpp b/compiler-rt/lib/scudo/standalone/common.cpp --- a/compiler-rt/lib/scudo/standalone/common.cpp +++ b/compiler-rt/lib/scudo/standalone/common.cpp @@ -37,6 +37,8 @@ #if !SCUDO_LINUX u64 GetRSS() { return 0; } + +void RssLimitChecker::Check() {} #endif } // namespace scudo diff --git a/compiler-rt/lib/scudo/standalone/flags.inc b/compiler-rt/lib/scudo/standalone/flags.inc --- a/compiler-rt/lib/scudo/standalone/flags.inc +++ b/compiler-rt/lib/scudo/standalone/flags.inc @@ -45,3 +45,13 @@ SCUDO_FLAG(int, release_to_os_interval_ms, SCUDO_ANDROID ? INT32_MIN : 5000, "Interval (in milliseconds) at which to attempt release of unused " "memory to the OS. Negative values disable the feature.") + +SCUDO_FLAG(int, hard_rss_limit_mb, 0, + "LINUX-ONLY: Hard RSS Limit in Mb" + "If non-zero, once the limit is achieved, abort the process") + +SCUDO_FLAG(int, soft_rss_limit_mb, 0, + "LINUX_ONLY: Soft RSS Limit in Mb" + "If non-zero, once the limit is reached, all subsequent calls will " + "fail or return NULL" + "until the RSS goes below the soft limit") diff --git a/compiler-rt/lib/scudo/standalone/internal_defs.h b/compiler-rt/lib/scudo/standalone/internal_defs.h --- a/compiler-rt/lib/scudo/standalone/internal_defs.h +++ b/compiler-rt/lib/scudo/standalone/internal_defs.h @@ -103,6 +103,8 @@ #define RAW_CHECK(Expr) RAW_CHECK_MSG(Expr, #Expr) +void NORETURN reportRSSCheck(const char *Message, u64 RssLimit, u64 RssCurrent); + void NORETURN reportCheckFailed(const char *File, int Line, const char *Condition, u64 Value1, u64 Value2); #define CHECK_IMPL(C1, Op, C2) \ diff --git a/compiler-rt/lib/scudo/standalone/linux.cpp b/compiler-rt/lib/scudo/standalone/linux.cpp --- a/compiler-rt/lib/scudo/standalone/linux.cpp +++ b/compiler-rt/lib/scudo/standalone/linux.cpp @@ -6,6 +6,8 @@ // //===----------------------------------------------------------------------===// +#include "atomic_helpers.h" +#include "flags.h" #include "platform.h" #if SCUDO_LINUX @@ -211,6 +213,25 @@ return GetRSSFromBuffer(Buf); } +void RssLimitChecker::Check() { + u64 HardRssLimitMb = static_cast(getFlags()->hard_rss_limit_mb); + u64 SoftRssLimitMb = static_cast(getFlags()->soft_rss_limit_mb); + + // TODO: We currently use sanitizer_common's GetRSS which reads the + // RSS from /proc/self/statm by default. We might want to + // call getrusage directly, even if it's less accurate. + const u64 CurrentRssMb = GetRSS() >> 20; + + if (HardRssLimitMb && UNLIKELY(HardRssLimitMb < CurrentRssMb)) { + reportRSSCheck("Hard RSS limit exhausted (Limit x Current): ", + HardRssLimitMb, CurrentRssMb); + } + + if (SoftRssLimitMb) { + atomic_store_relaxed(&RssLimitExceeded, CurrentRssMb > SoftRssLimitMb); + } +} + void outputRaw(const char *Buffer) { if (&async_safe_write_log) { constexpr s32 AndroidLogInfo = 4; diff --git a/compiler-rt/lib/scudo/standalone/options.h b/compiler-rt/lib/scudo/standalone/options.h --- a/compiler-rt/lib/scudo/standalone/options.h +++ b/compiler-rt/lib/scudo/standalone/options.h @@ -25,6 +25,7 @@ UseOddEvenTags, UseMemoryTagging, AddLargeAllocationSlack, + CheckRssLimit, }; struct Options { diff --git a/compiler-rt/lib/scudo/standalone/report.cpp b/compiler-rt/lib/scudo/standalone/report.cpp --- a/compiler-rt/lib/scudo/standalone/report.cpp +++ b/compiler-rt/lib/scudo/standalone/report.cpp @@ -36,6 +36,12 @@ inline void NORETURN trap() { __builtin_trap(); } +void NORETURN reportRSSCheck(const char *Message, u64 RssLimit, + u64 RssCurrent) { + ScopedErrorReport Report; + Report.append("%s %d x %d\n", Message, RssLimit, RssCurrent); +} + // This could potentially be called recursively if a CHECK fails in the reports. void NORETURN reportCheckFailed(const char *File, int Line, const char *Condition, u64 Value1, u64 Value2) { diff --git a/compiler-rt/lib/scudo/standalone/tests/combined_test.cpp b/compiler-rt/lib/scudo/standalone/tests/combined_test.cpp --- a/compiler-rt/lib/scudo/standalone/tests/combined_test.cpp +++ b/compiler-rt/lib/scudo/standalone/tests/combined_test.cpp @@ -6,7 +6,10 @@ // //===----------------------------------------------------------------------===// +#include "common.h" +#include "flags.h" #include "memtag.h" +#include "options.h" #include "tests/scudo_unit_test.h" #include "allocator_config.h" @@ -731,3 +734,55 @@ #endif #endif + +#if SCUDO_LINUX +SCUDO_TYPED_TEST(ScudoCombinedTest, SoftRssLimit) { + size_t Megabyte = 1024 * 1024; + size_t ChunkSize = 16; + size_t Error = 256; + + // Safe current flag state to restore it later. + auto old_soft_limit = scudo::getFlags()->soft_rss_limit_mb; + auto old_may_return_null = scudo::getFlags()->may_return_null; + + scudo::getFlags()->soft_rss_limit_mb = 1; + scudo::getFlags()->may_return_null = true; + + auto *Allocator = this->Allocator.get(); + Allocator->getOptionsForTesting().set(scudo::OptionBit::CheckRssLimit); + Allocator->getOptionsForTesting().set(scudo::OptionBit::MayReturnNull); + + std::vector Ptrs; + for (size_t index = 0; index < Megabyte + Error; index += ChunkSize) { + void *Ptr = Allocator->allocate(ChunkSize, Origin); + Ptrs.push_back(Ptr); + } + + EXPECT_EQ(Allocator->allocate(ChunkSize, Origin), nullptr); + + for (void *Ptr : Ptrs) + Allocator->deallocate(Ptr, Origin); + + scudo::getFlags()->soft_rss_limit_mb = old_soft_limit; + scudo::getFlags()->may_return_null = old_may_return_null; +} + +SCUDO_TYPED_TEST(ScudoCombinedTest, HardRssLimit) { + size_t Megabyte = 1024 * 1024; + + auto old_hard_limit = scudo::getFlags()->hard_rss_limit_mb; + scudo::getFlags()->hard_rss_limit_mb = 1; + + auto *Allocator = this->Allocator.get(); + Allocator->getOptionsForTesting().set(scudo::OptionBit::CheckRssLimit); + + EXPECT_DEATH( + { + disableDebuggerdMaybe(); + Allocator->allocate(Megabyte, Origin); + }, + ""); + + scudo::getFlags()->hard_rss_limit_mb = old_hard_limit; +} +#endif