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,8 @@ systemSupportsMemoryTagging()) Primary.Options.set(OptionBit::UseMemoryTagging); Primary.Options.set(OptionBit::UseOddEvenTags); + if (RssChecker.getHardRssLimit() || RssChecker.getSoftRssLimit()) + Primary.Options.set(OptionBit::CheckRssLimit); QuarantineMaxChunkSize = static_cast(getFlags()->quarantine_max_chunk_size); @@ -346,6 +348,26 @@ } DCHECK_LE(Size, NeededSize); + if (Options.get(OptionBit::CheckRssLimit)) { + auto RssExceeded = RssChecker.GetRssLimitExceeded(); + + if (UNLIKELY(RssExceeded == RssLimitChecker::Soft)) { + if (Options.get(OptionBit::MayReturnNull)) + return nullptr; + + const u64 CurrentRssMb = GetRSS() >> 20; + u64 SoftRssLimitMb = RssChecker.getSoftRssLimit(); + reportRSSCheck("Soft RSS limit exhausted (Limit x Current): ", + SoftRssLimitMb, CurrentRssMb); + } else if (UNLIKELY(RssExceeded == RssLimitChecker::Hard)) { + const u64 CurrentRssMb = GetRSS() >> 20; + u64 HardRssLimitMb = RssChecker.getHardRssLimit(); + + reportRSSCheck("Hard RSS limit exhausted (Limit x Current): ", + HardRssLimitMb, CurrentRssMb); + } + } + void *Block = nullptr; uptr ClassId = 0; uptr SecondaryBlockEnd = 0; @@ -994,6 +1016,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,8 @@ #ifndef SCUDO_COMMON_H_ #define SCUDO_COMMON_H_ +#include "atomic_helpers.h" +#include "flags.h" #include "internal_defs.h" #include "fuchsia.h" @@ -208,6 +210,50 @@ // zero-initialized already. }; +class RssLimitChecker { +public: + enum RssLimitExceeded { + Neither, + Soft, + Hard, + }; + +#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. + RssLimitExceeded GetRssLimitExceeded() { + u64 NextCheck = atomic_load_relaxed(&RssNextCheckAtNS); + u64 Now = getMonotonicTime(); + + if (UNLIKELY(Now >= NextCheck)) { + Check(NextCheck); + } + + return static_cast(atomic_load_relaxed(&RssLimitStatus)); + } +#else + RssLimitExceeded IsExceeded() { return RssLimitExceeded::Neither; } +#endif + + u64 getSoftRssLimit() { return SoftRssLimit; } + u64 getHardRssLimit() { return HardRssLimitMb; } + +private: + void Check(u64 NextCheck); + + u64 SoftRssLimit{static_cast(scudo::getFlags()->soft_rss_limit_mb)}; + + u64 HardRssLimitMb{static_cast(scudo::getFlags()->hard_rss_limit_mb)}; + +#if SCUDO_LINUX + // The interval for the checks is 250ms. + static constexpr u64 CheckInterval = 250 * 1000000; + + atomic_u64 RssNextCheckAtNS = {}; + atomic_u8 RssLimitStatus = {}; +#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,7 @@ // //===----------------------------------------------------------------------===// +#include "atomic_helpers.h" #include "platform.h" #if SCUDO_LINUX @@ -211,6 +212,36 @@ return GetRSSFromBuffer(Buf); } +void RssLimitChecker::Check(u64 NextCheck) { + // Early return in case another thread already did the calculation. + if (!atomic_compare_exchange_strong(&RssNextCheckAtNS, &NextCheck, + getMonotonicTime() + CheckInterval, + memory_order_relaxed)) { + return; + } + + u64 HardRssLimitMb = getHardRssLimit(); + u64 SoftRssLimitMb = getSoftRssLimit(); + + if (!HardRssLimitMb && !SoftRssLimitMb) + return; + + // 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)) + atomic_store_relaxed(&RssLimitStatus, + static_cast(RssLimitExceeded::Hard)); + else if (SoftRssLimitMb && UNLIKELY(SoftRssLimitMb < CurrentRssMb)) + atomic_store_relaxed(&RssLimitStatus, + static_cast(RssLimitExceeded::Soft)); + else + atomic_store_relaxed(&RssLimitStatus, + static_cast(RssLimitExceeded::Neither)); +} + 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,6 +6,7 @@ // //===----------------------------------------------------------------------===// +#include "flags.h" #include "memtag.h" #include "tests/scudo_unit_test.h" @@ -14,6 +15,7 @@ #include "combined.h" #include +#include #include #include #include @@ -86,18 +88,23 @@ void operator delete(void *ptr) { free(ptr); } }; -template struct ScudoCombinedTest : public Test { - ScudoCombinedTest() { +template struct ScudoCombinedBaseTest : public Test { + ScudoCombinedBaseTest() { UseQuarantine = std::is_same::value; - Allocator = std::make_unique(); - } - ~ScudoCombinedTest() { - Allocator->releaseToOS(); - UseQuarantine = true; } void RunTest(); + ~ScudoCombinedBaseTest() { UseQuarantine = true; } +}; + +template +struct ScudoCombinedTest : public ScudoCombinedBaseTest { + ScudoCombinedTest() { Allocator = std::make_unique(); } + ~ScudoCombinedTest() { Allocator->releaseToOS(); } + + void RunTest(); + void BasicTest(scudo::uptr SizeLog); using AllocatorT = TestAllocator; @@ -117,10 +124,38 @@ SCUDO_TYPED_TEST_TYPE(FIXTURE, NAME, AndroidConfig) #endif +#if SCUDO_FUCHSIA +#define SCUDO_TYPED_TEST_ALL_TYPES_WITH_CONFIG(FIXTURE, NAME, TEST_CONFIG) \ + SCUDO_TYPED_TEST_TYPE_WITH_CONFIG(FIXTURE, NAME, AndroidSvelteConfig, \ + TEST_CONFIG) \ + SCUDO_TYPED_TEST_TYPE_WITH_CONFIG(FIXTURE, NAME, FuchsiaConfig, TEST_CONFIG) +#else +#define SCUDO_TYPED_TEST_ALL_TYPES_WITH_CONFIG(FIXTURE, NAME, TEST_CONFIG) \ + SCUDO_TYPED_TEST_TYPE_WITH_CONFIG(FIXTURE, NAME, AndroidSvelteConfig, \ + TEST_CONFIG) \ + SCUDO_TYPED_TEST_TYPE_WITH_CONFIG(FIXTURE, NAME, DefaultConfig, TEST_CONFIG) \ + SCUDO_TYPED_TEST_TYPE_WITH_CONFIG(FIXTURE, NAME, AndroidConfig, TEST_CONFIG) +#endif + #define SCUDO_TYPED_TEST_TYPE(FIXTURE, NAME, TYPE) \ using FIXTURE##NAME##_##TYPE = FIXTURE##NAME; \ TEST_F(FIXTURE##NAME##_##TYPE, NAME) { FIXTURE##NAME::Run(); } +#define SCUDO_TYPED_TEST_TYPE_WITH_CONFIG(FIXTURE, NAME, TYPE, TEST_CONFIG) \ + using FIXTURE##NAME##_##TYPE = FIXTURE##NAME; \ + TEST_F(FIXTURE##NAME##_##TYPE, NAME) { \ + FIXTURE##NAME::Run(); \ + } + +#define SCUDO_TYPED_TEST_WITH_CONFIG(FIXTURE, NAME, TEST_CONFIG) \ + template \ + struct FIXTURE##NAME : public FIXTURE { \ + void Run(); \ + }; \ + SCUDO_TYPED_TEST_ALL_TYPES_WITH_CONFIG(FIXTURE, NAME, TEST_CONFIG) \ + template \ + void FIXTURE##NAME::Run() + #define SCUDO_TYPED_TEST(FIXTURE, NAME) \ template \ struct FIXTURE##NAME : public FIXTURE { \ @@ -731,3 +766,83 @@ #endif #endif + +#if SCUDO_LINUX + +template +struct ScudoCombinedRssTest : public ScudoCombinedBaseTest { + ScudoCombinedRssTest() { + OldHardRssLimit = scudo::getFlags()->hard_rss_limit_mb; + OldSoftRssLimit = scudo::getFlags()->soft_rss_limit_mb; + OldMayReturnNull = scudo::getFlags()->may_return_null; + + scudo::getFlags()->hard_rss_limit_mb = TestConfig::HardRssLimit; + scudo::getFlags()->soft_rss_limit_mb = TestConfig::SoftRssLimit; + scudo::getFlags()->may_return_null = TestConfig::MayReturnNull; + + Allocator = std::make_unique(); + } + ~ScudoCombinedRssTest() { + Allocator->releaseToOS(); + + scudo::getFlags()->hard_rss_limit_mb = OldHardRssLimit; + scudo::getFlags()->soft_rss_limit_mb = OldSoftRssLimit; + scudo::getFlags()->may_return_null = OldMayReturnNull; + } + + void RunTest(); + + int64_t OldHardRssLimit; + int64_t OldSoftRssLimit; + bool OldMayReturnNull; + + using AllocatorT = TestAllocator; + std::unique_ptr Allocator; +}; + +struct SoftRssConfig { + static const int64_t HardRssLimit = 0; + static const int64_t SoftRssLimit = 1; + static const bool MayReturnNull = true; +}; + +SCUDO_TYPED_TEST_WITH_CONFIG(ScudoCombinedRssTest, SoftRssLimit, + SoftRssConfig) { + size_t Megabyte = 1024 * 1024; + size_t ChunkSize = 16; + size_t Error = 256; + + auto *Allocator = this->Allocator.get(); + + 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); +} + +struct HardRssConfig { + static const int64_t HardRssLimit = 1; + static const int64_t SoftRssLimit = 0; + static const bool MayReturnNull = false; +}; + +SCUDO_TYPED_TEST_WITH_CONFIG(ScudoCombinedRssTest, HardRssLimit, + HardRssConfig) { + size_t Megabyte = 1024 * 1024; + + auto *Allocator = this->Allocator.get(); + + EXPECT_DEATH( + { + disableDebuggerdMaybe(); + Allocator->allocate(Megabyte, Origin); + }, + ""); +} +#endif