Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Device Trust
DescriptionUse after free in Device Trust
ComponentDevice Trust
Bug ClassUAF
Tracker501360342
Fix commita89b032b2bad (chromium/src) +58/-13
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
if
components/device_signals/core/browser/mac/plist_settings_client.mm
modified
for
components/device_signals/core/browser/mac/plist_settings_client.mm
modified

Files Changed

  • components/device_signals/core/browser/mac/plist_settings_client.mm
  • components/device_signals/core/browser/mac/plist_settings_client_unittest.mm
From a89b032b2bad17eafeb4e1c2c0b2d793ac64f7bf Mon Sep 17 00:00:00 2001
From: hamda mare <[email protected]>
Date: Wed, 29 Apr 2026 12:45:34 -0700
Subject: [PATCH] Fix potential UAF in PlistSettingsClient on macOS

This CL refactors ParsePlist to merge loops and add safety checks using
ObjCCast. It also replaces unsafe Key-Value Coding (KVC) method calls
with explicit dictionary lookups using objectForKey:. This prevents
untrusted input from being executed as Objective-C selectors, addressing
a potential Use-After-Free vulnerability.

Fixed: 501360342
Change-Id: I8058e32b8e1748888dd0fafca344bd77b6d1eea6
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7796282
Reviewed-by: Sebastien Lalancette <[email protected]>
Commit-Queue: Hamda Mare <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1622657}
---

diff --git a/components/device_signals/core/browser/mac/plist_settings_client.mm b/components/device_signals/core/browser/mac/plist_settings_client.mm
index e93ae0f..4eea4624 100644
--- a/components/device_signals/core/browser/mac/plist_settings_client.mm
+++ b/components/device_signals/core/browser/mac/plist_settings_client.mm
@@ -65,7 +65,7 @@
     }
 
     NSUInteger index = base::checked_cast<NSUInteger>(index_str.integerValue);
-    if (index > data_array.count) {
+    if (index >= data_array.count) {
       return nil;
     }
 
@@ -78,30 +78,39 @@
 // Parses the loaded plist `dict` for the setting item at `key_path`. Returns
 // the setting object if it is found or nil otherwise.
 id ParsePlist(NSDictionary* dict, NSString* key_path) {
-  // Check if an array exists in the path, If not, the plist can be parsed
-  // directly.
-  NSRange test_range = [key_path rangeOfString:@"["];
-  if (test_range.location == NSNotFound)
-    return [dict valueForKeyPath:key_path];
-
-  NSDictionary* current_obj = dict;
+  id current_obj = dict;
+  bool has_brackets = false;
   for (NSString* sub_path in [key_path componentsSeparatedByString:@"."]) {
+    NSDictionary* current_dict =
+        base::apple::ObjCCast<NSDictionary>(current_obj);
+    if (!current_dict) {
+      return nil;
+    }
+
     NSRange range = [sub_path rangeOfString:@"["];
     if (range.location == NSNotFound) {
-      current_obj = [current_obj valueForKey:sub_path];
+      current_obj = [current_dict objectForKey:sub_path];
     } else {
-      current_obj =
-          [current_obj valueForKey:[sub_path substringToIndex:range.location]];
+      has_brackets = true;
+      current_obj = [current_dict
+          objectForKey:[sub_path substringToIndex:range.location]];
       current_obj = ParseArrays(current_obj,
                                 [sub_path substringFromIndex:range.location]);
     }
+
+    if (!current_obj) {
+      return nil;
+    }
   }
 
   // This will occur if the key path is incorrect and does not actually point to
   // a setting item. At the end of a parse, the only remaining object should be
   // the single setting item.
-  if ([current_obj isKindOfClass:[NSArray class]] && current_obj.count != 1) {
-    return nil;
+  if (has_brackets) {
+    NSArray* final_array = base::apple::ObjCCast<NSArray>(current_obj);
+    if (final_array && final_array.count != 1) {
+      return nil;
+    }
   }
   return current_obj;
 }
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 5172a51d..581ef96 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
@@ -299,4 +299,40 @@
   EXPECT_EQ(items, future_.Get());
 }
 
+// Tests a request to GetSettings with an array index that is exactly equal to
+// the array size. This should be handled as "Not Found".
+TEST_F(PlistSettingsClientTest,
+       GetSettings_Plist_MixOfArrayDictItems_ArrayOutOfBounds) {
+  test_file_path_ = test::GetMixArrayDictionaryPlistPath();
+
+  std::string key_path = "Key1.Array[2]";
+
+  std::vector<GetSettingsOptions> options;
+  options.push_back(CreateOption(key_path, true));
+
+  std::vector<SettingsItem> items;
+  items.push_back(CreateSettingItem(key_path, PresenceValue::kNotFound, ""));
+
+  client_.GetSettings(options, future_.GetCallback());
+  EXPECT_EQ(items, future_.Get());
+}
+
+// Tests that a key path containing a KVC operator is not executed and is
+// instead treated as a literal key (which should not be found).
+TEST_F(PlistSettingsClientTest,
+       GetSettings_Plist_OnlyDictionaryItems_KVCVulnerability) {
+  test_file_path_ = test::GetOnlyDictionaryPlistPath();
+
+  std::string key_path = "@count";
+
+  std::vector<GetSettingsOptions> options;
+  options.push_back(CreateOption(key_path, true));
+
+  std::vector<SettingsItem> items;
+  items.push_back(CreateSettingItem(key_path, PresenceValue::kNotFound, ""));
+
+  client_.GetSettings(options, future_.GetCallback());
+  EXPECT_EQ(items, future_.Get());
+}
+
 }  // namespace device_signals
Loading diff…

Regression Test / PoC

shipped with the fix
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 5172a51d..581ef96 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
@@ -299,4 +299,40 @@
   EXPECT_EQ(items, future_.Get());
 }
 
+// Tests a request to GetSettings with an array index that is exactly equal to
+// the array size. This should be handled as "Not Found".
+TEST_F(PlistSettingsClientTest,
+       GetSettings_Plist_MixOfArrayDictItems_ArrayOutOfBounds) {
+  test_file_path_ = test::GetMixArrayDictionaryPlistPath();
+
+  std::string key_path = "Key1.Array[2]";
+
+  std::vector<GetSettingsOptions> options;
+  options.push_back(CreateOption(key_path, true));
+
+  std::vector<SettingsItem> items;
+  items.push_back(CreateSettingItem(key_path, PresenceValue::kNotFound, ""));
+
+  client_.GetSettings(options, future_.GetCallback());
+  EXPECT_EQ(items, future_.Get());
+}
+
+// Tests that a key path containing a KVC operator is not executed and is
+// instead treated as a literal key (which should not be found).
+TEST_F(PlistSettingsClientTest,
+       GetSettings_Plist_OnlyDictionaryItems_KVCVulnerability) {
+  test_file_path_ = test::GetOnlyDictionaryPlistPath();
+
+  std::string key_path = "@count";
+
+  std::vector<GetSettingsOptions> options;
+  options.push_back(CreateOption(key_path, true));
+
+  std::vector<SettingsItem> items;
+  items.push_back(CreateSettingItem(key_path, PresenceValue::kNotFound, ""));
+
+  client_.GetSettings(options, future_.GetCallback());
+  EXPECT_EQ(items, future_.Get());
+}
+
 }  // namespace device_signals
Loading diff…

Original Bug Report

reported by [email protected]

Potential browser process UAF via unvalidated KVC key path in PlistSettingsClient

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team.

Overview: A potential Use-After-Free vulnerability exists in the macOS implementation of the enterprise.reportingPrivate API. By passing a specially crafted Key-Value Coding (KVC) key path to valueForKey:, a compromised allowlisted extension can over-release an Objective-C dictionary. This could potentially allow an attacker to escape the renderer sandbox and execute arbitrary code in the browser process.

Affected files:

  • components/device_signals/core/browser/mac/plist_settings_client.mm
  • chrome/browser/extensions/api/enterprise_reporting_private/enterprise_reporting_private_api.cc
  • chrome/browser/extensions/api/enterprise_reporting_private/conversion_utils.cc
  • components/device_signals/core/common/posix/platform_utils_posix.cc

Estimated timestamp from git blame: 2022-10-12

Summary

A potential Use-After-Free (UAF) vulnerability exists in PlistSettingsClient on macOS. The issue stems from passing unsanitized, renderer-supplied strings directly to Objective-C Key-Value Coding (KVC) methods (valueForKey: and valueForKeyPath:). By supplying a crafted key path such as @autorelease, an attacker can invoke arbitrary zero-argument methods on an NSDictionary. This can be abused to over-release the dictionary, creating dangling pointers in the thread’s autorelease pool, which may lead to arbitrary code execution in the browser process.

Technical Details

The chrome.enterpriseReportingPrivate.getSettings API allows specific allowlisted extensions to retrieve configuration settings. On macOS, the request is routed to the browser process and handled by PlistSettingsClient::GetSettings, which schedules GetSettingItems to run on a base::ThreadPool worker thread.

In components/device_signals/core/browser/mac/plist_settings_client.mm, the GetSettingItems function parses a plist file into an ARC-managed NSDictionary* (plist_dict). It then attempts to extract the requested setting by calling ParsePlist:

id ParsePlist(NSDictionary* dict, NSString* key_path) {
  NSRange test_range = [key_path rangeOfString:@"["];
  if (test_range.location == NSNotFound)
    return [dict valueForKeyPath:key_path];

  NSDictionary* current_obj = dict;
  for (NSString* sub_path in [key_path componentsSeparatedByString:@"."]) {
    // ...
    current_obj = [current_obj valueForKey:sub_path];
    // ...
  }
  return current_obj;
}

The key_path originates directly from the extension and is not sanitized. In Objective-C, if a key passed to valueForKey: or valueForKeyPath: begins with an @ character, NSDictionary strips the @ and invokes [super valueForKey:]. The default NSObject implementation of KVC will then dynamically invoke a method matching the remaining string.

If an attacker provides a key path like @autorelease.@autorelease.@autorelease, the autorelease method is invoked multiple times on the dictionary. This registers the object multiple times in the worker thread’s apple::ScopedNSAutoreleasePool. At the end of the loop iteration in GetSettingItems, ARC automatically releases the dictionary, and it is deallocated. However, the autorelease pool still holds dangling pointers to the freed memory. When the pool drains at the end of the task, it sends release messages to the reclaimed memory, causing a UAF.

Potential Attack Steps

Note: These are suggested steps; our tooling agent does not yet have the ability to run code to produce a working Proof-of-Concept.

  1. An attacker compromises the renderer process of an extension allowlisted for the enterprise.reportingPrivate permission (e.g., SecureConnect).
  2. The attacker calls chrome.enterpriseReportingPrivate.getSettings and provides an options array.
  3. The first option is crafted with a valid plist path and a malicious key set to "@autorelease.@autorelease.@autorelease".
  4. Subsequent options in the array are crafted with specific path lengths to trigger heap allocations of specific sizes. This is used to groom the heap and reclaim the memory previously occupied by the freed dictionary with a fake Objective-C object (a “fake-isa” payload).
  5. The browser process executes the request on a worker thread. The dictionary is allocated, over-autoreleased via KVC, and then freed by ARC.
  6. The subsequent requests reclaim the freed memory with the attacker’s payload.
  7. When the thread pool task completes, the ScopedNSAutoreleasePool drains, sending a release message to the dangling pointer, which now points to the attacker’s fake object. This vectors execution to an attacker-controlled address, achieving a sandbox escape.

Suggested Fix

Untrusted input should never be passed to valueForKey: or valueForKeyPath:.

In ParsePlist, replace the KVC method calls with explicit dictionary lookups. Instead of using valueForKeyPath:, iteratively split the string by . and use [dict objectForKey:key].

For example, change [current_obj valueForKey:sub_path] to [current_obj objectForKey:sub_path], and manually handle the key path traversal without relying on KVC.

Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

Raised in root component due to access or custom field issues on 1163683

View on issue tracker