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,10 @@ 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); + atomic_store_relaxed(&RssLastCheckedAtNS, getMonotonicTime()); + } QuarantineMaxChunkSize = static_cast(getFlags()->quarantine_max_chunk_size); @@ -346,6 +350,16 @@ } DCHECK_LE(Size, NeededSize); + if ((Options.get(OptionBit::CheckRssLimit)) && + UNLIKELY(isRssLimitExceeded())) { + if (Options.get(OptionBit::MayReturnNull)) + return nullptr; + const u64 CurrentRssMb = GetRSS() >> 20; + u64 SoftRssLimitMb = (u64)getFlags()->soft_rss_limit_mb; + reportRSSCheck("Soft RSS limit exhausted (Limit x Current): ", + SoftRssLimitMb, CurrentRssMb); + } + void *Block = nullptr; uptr ClassId = 0; uptr SecondaryBlockEnd = 0; @@ -671,6 +685,52 @@ return NewPtr; } +// Opportunistic RSS limit check. This will update the RSS limit status, if +// it can, every 250ms, otherwise it will just return the current one. +#if SCUDO_LINUX + NOINLINE bool isRssLimitExceeded() { + u64 LastCheck = atomic_load_relaxed(&RssLastCheckedAtNS); + const u64 CurrentCheck = getMonotonicTime(); + u64 HardRssLimitMb = (u64)getFlags()->hard_rss_limit_mb; + u64 SoftRssLimitMb = (u64)getFlags()->soft_rss_limit_mb; + + if (LIKELY(CurrentCheck < LastCheck + (250ULL * 1000000ULL))) + return atomic_load_relaxed(&RssLimitExceeded); + + if (!atomic_compare_exchange_weak(&RssLastCheckedAtNS, &LastCheck, + CurrentCheck, memory_order_relaxed)) + return atomic_load_relaxed(&RssLimitExceeded); + + // 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) { + if (atomic_load_relaxed(&RssLimitExceeded)) { + if (CurrentRssMb <= SoftRssLimitMb) + atomic_store_relaxed(&RssLimitExceeded, false); + } else { + if (CurrentRssMb > SoftRssLimitMb) { + atomic_store_relaxed(&RssLimitExceeded, true); + } + } + } + + return atomic_load_relaxed(&RssLimitExceeded); + } +#else + // We redefine as ALWAYS_INLINE for platforms that are not supported + // The compiler optimizes out the calling of the function and the entire + // check from the caller, so no symbol lookup problems for GetRSS either + ALWAYS_INLINE bool isRssLimitExceeded() { return 0; } +#endif + // TODO(kostyak): disable() is currently best-effort. There are some small // windows of time when an allocation could still succeed after // this function finishes. We will revisit that later. @@ -994,6 +1054,8 @@ QuarantineT Quarantine; TSDRegistryT TSDRegistry; pthread_once_t PostInitNonce = PTHREAD_ONCE_INIT; + atomic_u64 RssLastCheckedAtNS = {}; + atomic_u8 RssLimitExceeded = {}; #ifdef GWP_ASAN_HOOKS gwp_asan::GuardedPoolAllocator GuardedAlloc; 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/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) {