Skip to content

fix: guard against OOB read in FlutterStandardCodecHelper string decode path (iOS/macOS)#183996

Open
artahir-dev wants to merge 1 commit intoflutter:masterfrom
artahir-dev:fix/standard-codec-oob-read-ios-macos
Open

fix: guard against OOB read in FlutterStandardCodecHelper string decode path (iOS/macOS)#183996
artahir-dev wants to merge 1 commit intoflutter:masterfrom
artahir-dev:fix/standard-codec-oob-read-ios-macos

Conversation

@artahir-dev
Copy link

@artahir-dev artahir-dev commented Mar 22, 2026

Fixes a crash in FlutterStandardCodecHelper on iOS and macOS where a crafted platform channel message could cause the codec to read memory beyond the end of the buffer.

ReadDataNoCopy was calling CFDataCreateWithBytesNoCopy using the length field directly from the wire without first checking that enough bytes remain. That function doesn't validate the pointer arithmetic, so a message with an inflated size — for example a kString type byte followed by 0xFF 0xFF 0xFF 0xFF 0xFF encoding a claimed length of 4 GiB — would wrap a huge range of unowned heap memory in a CFData. When CFStringCreateFromExternalRepresentation then tried to scan that "string" it would read far past the actual buffer and crash.

The typed-data decode path already had this issue documented and worked around: there's a comment in FlutterStandardCodec.mm (around ReadTypedDataOfType) that says CFDataCreateBytesNoCopy crashes and uses subdataWithRange: instead. That fix was never applied to the string path (ReadDataNoCopy / FlutterStandardCodecHelperReadUTF8), which is what this PR addresses.

The change is small — a bounds check at the top of ReadDataNoCopy and a nil guard in FlutterStandardCodecHelperReadUTF8. Three regression tests are added covering the inflated-size case, a normally-encoded string truncated by one byte, and the FlutterStandardMessageCodec layer decode.

Pre-launch Checklist

  • I read the [Contributor Guide] and followed the process outlined there for submitting PRs.
  • I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools.
  • I read the [Tree Hygiene] wiki page, which explains my responsibilities.
  • I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement].
  • I signed the [CLA].
  • I listed at least one issue that this PR fixes in the description above.
  • I updated/added relevant documentation (doc comments with ///).
  • I added new tests to check the change I am making, or this PR is [test-exempt].
  • I followed the [breaking change policy] and added [Data Driven Fixes] where supported.
  • All existing and new tests are passing.

…wrapping CFData


ReadDataNoCopy was calling CFDataCreateWithBytesNoCopy with the raw
wire-claimed length without validating that enough bytes remain in the
buffer. A crafted codec message with an inflated string size field (e.g.
0xFFFFFFFF) would produce a CFData "view" into unowned memory, causing
CFStringCreateFromExternalRepresentation to read far beyond the actual
buffer — OOB read and crash.

The fix adds a remaining-bytes check in ReadDataNoCopy and makes
FlutterStandardCodecHelperReadUTF8 treat a nil return as a decode
failure rather than passing it to CFStringCreateFromExternalRepresentation.

Three regression tests are added to flutter_standard_codec_unittest.mm:
inflated size field, normally-encoded string truncated by one byte, and
the message codec layer decode path.
@artahir-dev artahir-dev requested a review from a team as a code owner March 22, 2026 14:31
@github-actions github-actions bot added platform-ios iOS applications specifically engine flutter/engine related. See also e: labels. a: desktop Running on desktop team-ios Owned by iOS platform team team-macos Owned by the macOS platform team labels Mar 22, 2026
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request addresses a critical out-of-bounds read vulnerability in FlutterStandardCodecHelper by adding necessary bounds checking. The changes are logical and well-supported by new regression tests. My feedback includes a suggestion to improve the readability and robustness of the bounds check and to strengthen the assertions in the new tests for better precision.

Comment on lines +68 to +73
CFIndex data_length = CFDataGetLength(data);
if ((CFIndex)*location > data_length ||
length > (unsigned long)(data_length - (CFIndex)*location)) {
*location += length;
return nil;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The bounds check logic is correct, but it involves mixed signed/unsigned arithmetic and multiple casts which can be hard to reason about. Consider simplifying this by performing the check using unsigned integers. This improves readability and makes the logic less prone to subtle errors related to integer promotion and conversion rules.

  const unsigned long data_length = CFDataGetLength(data);
  if (*location >= data_length || length > data_length - *location) {
    *location += length;
    return nil;
  }
References
  1. Code should be optimized for readability, as it is read more often than it is written. The suggested change simplifies the conditional logic, making it easier to understand. (link)

FlutterStandardReader* reader =
[[FlutterStandardReader alloc] initWithData:data];
id result = [reader readValue];
ASSERT_TRUE(result == nil || [result isKindOfClass:[NSString class]]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The assertion is correct but could be more specific. For a malformed input that causes a decoding error, the expected outcome is nil. Strengthening the assertion to check specifically for nil makes the test's intent clearer and more robust against future changes that might incorrectly return a different value (like an empty string) on error.

  ASSERT_TRUE(result == nil);

NSData* message = [NSData dataWithBytes:bytes length:sizeof(bytes)];
FlutterStandardMessageCodec* codec = [FlutterStandardMessageCodec sharedInstance];
id decoded = [codec decode:message];
ASSERT_TRUE(decoded == nil || [decoded isKindOfClass:[NSString class]]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The assertion is correct but could be more specific. For a malformed input that causes a decoding error, the expected outcome is nil. Strengthening the assertion to check specifically for nil makes the test's intent clearer and more robust against future changes that might incorrectly return a different value on error.

  ASSERT_TRUE(decoded == nil);

NSData* full = [codec encode:@"hello"];
NSData* truncated = [full subdataWithRange:NSMakeRange(0, full.length - 1)];
id decoded = [codec decode:truncated];
ASSERT_TRUE(decoded == nil || [decoded isKindOfClass:[NSString class]]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The assertion is correct but could be more specific. For a malformed input that causes a decoding error, the expected outcome is nil. Strengthening the assertion to check specifically for nil makes the test's intent clearer and more robust against future changes that might incorrectly return a different value on error.

  ASSERT_TRUE(decoded == nil);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

a: desktop Running on desktop engine flutter/engine related. See also e: labels. platform-ios iOS applications specifically team-ios Owned by iOS platform team team-macos Owned by the macOS platform team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant