Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Mobile
DescriptionUse after free in Mobile
ComponentMobile
Bug ClassUAF
Tracker517548647
Fix commit1d80ab8d4829 (chromium/src) +14/-5
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
TEST_F
ios/chrome/browser/shared/ui/list_model/list_model_collapse_unittest.mm
modified

Files Changed

  • ios/chrome/browser/recent_tabs/ui/recent_tabs_table_view_controller.mm
  • ios/chrome/browser/shared/ui/list_model/list_model.mm
  • ios/chrome/browser/shared/ui/list_model/list_model_collapse_unittest.mm
From 1d80ab8d4829a9e5b9802cdda56de66a18130349 Mon Sep 17 00:00:00 2001
From: Huiting Yu <huitingyu@google.com>
Date: Wed, 22 Jul 2026 07:17:04 -0700
Subject: [PATCH] [iOS] Look up ListModel collapsed-section keys with dictionary subscripting

ListModelCollapsedMediator and RecentTabsTableViewController used
-[NSDictionary valueForKey:] and -[NSMutableDictionary setValue:forKey:]
to look up and set a section's collapsed flag.

When a collapsed key starts with "@" (derived from foreign sync session
tags), KVC strips the "@" prefix and forwards to NSObject KVC selector
dispatch instead of performing a literal dictionary lookup.

Use dictionary subscripting so keys are always treated as literal keys.

Fixed: 517548647
Change-Id: I6bab75593a44261acf01e8fe78ce3f516085b4a1
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8131683
Commit-Queue: Huiting Yu <huitingyu@google.com>
Reviewed-by: Gauthier Ambard <gambard@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1666267}
---

diff --git a/ios/chrome/browser/recent_tabs/ui/recent_tabs_table_view_controller.mm b/ios/chrome/browser/recent_tabs/ui/recent_tabs_table_view_controller.mm
index 5474a16..c637c84 100644
--- a/ios/chrome/browser/recent_tabs/ui/recent_tabs_table_view_controller.mm
+++ b/ios/chrome/browser/recent_tabs/ui/recent_tabs_table_view_controller.mm
@@ -1670,13 +1670,13 @@
   NSMutableDictionary* newCollapsedSection = [NSMutableDictionary
       dictionaryWithDictionary:newUserInfo[kListModelCollapsedKey]];
   newUserInfo[kListModelCollapsedKey] = newCollapsedSection;
-  newCollapsedSection[sectionKey] = [NSNumber numberWithBool:collapsed];
+  newCollapsedSection[sectionKey] = @(collapsed);
   _session.userInfo = newUserInfo;
 }
 
 - (BOOL)sectionKeyIsCollapsed:(NSString*)sectionKey {
   NSDictionary* collapsedSections = _session.userInfo[kListModelCollapsedKey];
-  NSNumber* value = (NSNumber*)[collapsedSections valueForKey:sectionKey];
+  NSNumber* value = (NSNumber*)collapsedSections[sectionKey];
   return [value boolValue];
 }
 
diff --git a/ios/chrome/browser/shared/ui/list_model/list_model.mm b/ios/chrome/browser/shared/ui/list_model/list_model.mm
index 77d999526..ec799c85 100644
--- a/ios/chrome/browser/shared/ui/list_model/list_model.mm
+++ b/ios/chrome/browser/shared/ui/list_model/list_model.mm
@@ -451,8 +451,7 @@
       [defaults dictionaryForKey:kListModelCollapsedKey];
   NSMutableDictionary* newCollapsedSection =
       [NSMutableDictionary dictionaryWithDictionary:collapsedSections];
-  NSNumber* value = [NSNumber numberWithBool:collapsed];
-  [newCollapsedSection setValue:value forKey:sectionKey];
+  newCollapsedSection[sectionKey] = @(collapsed);
   [defaults setObject:newCollapsedSection forKey:kListModelCollapsedKey];
 }
 
@@ -460,7 +459,7 @@
   NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
   NSDictionary* collapsedSections =
       [defaults dictionaryForKey:kListModelCollapsedKey];
-  NSNumber* value = (NSNumber*)[collapsedSections valueForKey:sectionKey];
+  NSNumber* value = (NSNumber*)collapsedSections[sectionKey];
   return [value boolValue];
 }
 
diff --git a/ios/chrome/browser/shared/ui/list_model/list_model_collapse_unittest.mm b/ios/chrome/browser/shared/ui/list_model/list_model_collapse_unittest.mm
index 61a02f0c..73e60f875 100644
--- a/ios/chrome/browser/shared/ui/list_model/list_model_collapse_unittest.mm
+++ b/ios/chrome/browser/shared/ui/list_model/list_model_collapse_unittest.mm
@@ -192,4 +192,14 @@
   EXPECT_FALSE([model sectionIsCollapsed:SectionIdentifierBar]);
 }
 
+// Tests that `@`-prefixed collapsed keys (e.g., `@count` or `@self`) round-trip
+// correctly without Key-Value Coding (KVC) dynamic selector dispatch.
+TEST_F(ListModelCollapseTest, CollapsedKeyWithAtPrefix) {
+  [model setSectionIdentifier:SectionIdentifierFoo collapsedKey:@"@count"];
+  [model setSection:SectionIdentifierFoo collapsed:YES];
+  EXPECT_TRUE([model sectionIsCollapsed:SectionIdentifierFoo]);
+  [model setSection:SectionIdentifierFoo collapsed:NO];
+  EXPECT_FALSE([model sectionIsCollapsed:SectionIdentifierFoo]);
+}
+
 }  // namespace
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/ios/chrome/browser/shared/ui/list_model/list_model_collapse_unittest.mm b/ios/chrome/browser/shared/ui/list_model/list_model_collapse_unittest.mm
index 61a02f0c..73e60f875 100644
--- a/ios/chrome/browser/shared/ui/list_model/list_model_collapse_unittest.mm
+++ b/ios/chrome/browser/shared/ui/list_model/list_model_collapse_unittest.mm
@@ -192,4 +192,14 @@
   EXPECT_FALSE([model sectionIsCollapsed:SectionIdentifierBar]);
 }
 
+// Tests that `@`-prefixed collapsed keys (e.g., `@count` or `@self`) round-trip
+// correctly without Key-Value Coding (KVC) dynamic selector dispatch.
+TEST_F(ListModelCollapseTest, CollapsedKeyWithAtPrefix) {
+  [model setSectionIdentifier:SectionIdentifierFoo collapsedKey:@"@count"];
+  [model setSection:SectionIdentifierFoo collapsed:YES];
+  EXPECT_TRUE([model sectionIsCollapsed:SectionIdentifierFoo]);
+  [model setSection:SectionIdentifierFoo collapsed:NO];
+  EXPECT_FALSE([model sectionIsCollapsed:SectionIdentifierFoo]);
+}
+
 }  // namespace
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential Use-after-free in iOS Recent Tabs via KVC over-release of collapsed sections dictionary

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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: The Recent Tabs UI on iOS utilizes valueForKey: instead of objectForKey: to query if a section is collapsed, using a sync-controlled session tag as the lookup key. Under Cocoa/Foundation Key-Value Coding (KVC), lookup keys prefixed with @ strip the prefix and dynamically invoke matching memory-management selectors (like -release or -dealloc) on the target dictionary. This can allow an attacker who controls a synced device to trigger an over-release or premature deallocation, leading to a potential Use-After-Free (UAF) in the browser process.

Affected files:

  • ios/chrome/browser/recent_tabs/ui/recent_tabs_table_view_controller.mm
  • ios/chrome/browser/shared/ui/list_model/list_model.mm

Estimated timestamp from git blame: 2018-03-15

Description

In iOS Chrome, the Recent Tabs UI manages the collapsed state of remote sessions using a dictionary. When querying whether a session section is collapsed, both ListModelCollapsedSceneSessionMediator and ListModelCollapsedMediator retrieve the collapsed state from a dictionary using valueForKey: instead of objectForKey:. The lookup key (sectionKey) is derived directly from the foreign session’s session_tag, which is sync-controlled.

In ios/chrome/browser/recent_tabs/ui/recent_tabs_table_view_controller.mm:

- (BOOL)sectionKeyIsCollapsed:(NSString*)sectionKey {
  NSDictionary* collapsedSections = _session.userInfo[kListModelCollapsedKey];
  NSNumber* value = (NSNumber*)[collapsedSections valueForKey:sectionKey];   // <-- sync-controlled key
  return [value boolValue];
}

In ios/chrome/browser/shared/ui/list_model/list_model.mm:

- (BOOL)sectionKeyIsCollapsed:(NSString*)sectionKey {
  NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
  NSDictionary* collapsedSections = [defaults dictionaryForKey:kListModelCollapsedKey];
  NSNumber* value = (NSNumber*)[collapsedSections valueForKey:sectionKey];   // <-- sync-controlled key
  return [value boolValue];
}

Under Cocoa/Foundation Key-Value Coding (KVC), if valueForKey: is called on an NSDictionary with an @-prefixed key (e.g., "@release" or "@dealloc"), the dictionary strips the @ prefix and forwards the call to [super valueForKey:]. The KVC engine then dynamically resolves and executes the matching selectors (like -release or -dealloc) on the receiver dictionary. This bypasses ARC ownership rules and can lead to immediate or delayed over-release, resulting in a potential Use-After-Free (UAF) or double-free condition when UIKit or ARC subsequently references the dictionary.

Potential Attack Scenario

Note: The following steps are potential/suggested vectors; our tooling does not currently have the capability to execute live proof-of-concept code on physical iOS devices.

  1. Ingest: An attacker with control over a synced device modifies a SESSIONS entity, setting the session_tag to a malicious string such as "@release" or "@dealloc".
  2. UI Loading: The victim opens the Recent Tabs UI, which parses the synced sessions and populates the table sections using addSessionSections.
  3. KVC Lookup: The UI queries sectionIsCollapsed: with the mapped session tag string. This triggers valueForKey: with the malicious key on the collapsed sections dictionary.
  4. Corruption: The dictionary receives a -release or -dealloc message directly via the KVC resolver. This decrements the dictionary’s reference count prematurely, resulting in a dangling reference/UAF or immediate crash when the dictionary is later accessed or serialized.

Suggested Fix

To remediate this issue, replace all instances of valueForKey: with objectForKey: (or dictionary subscripting) to ensure standard lookup semantics are enforced rather than dynamic KVC selector dispatch.

In ios/chrome/browser/recent_tabs/ui/recent_tabs_table_view_controller.mm:

- (BOOL)sectionKeyIsCollapsed:(NSString*)sectionKey {
  NSDictionary* collapsedSections = _session.userInfo[kListModelCollapsedKey];
  NSNumber* value = (NSNumber*)[collapsedSections objectForKey:sectionKey];
  return [value boolValue];
}

In ios/chrome/browser/shared/ui/list_model/list_model.mm:

- (BOOL)sectionKeyIsCollapsed:(NSString*)sectionKey {
  NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
  NSDictionary* collapsedSections = [defaults dictionaryForKey:kListModelCollapsedKey];
  NSNumber* value = (NSNumber*)[collapsedSections objectForKey:sectionKey];
  return [value boolValue];
}

Evaluated with Chrome root at commit: 5133b93d189b383c37805b1cf3a9d2dbfe8d7379


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

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.

View on issue tracker