mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
## Description This PR replaces `wcslen` with `wcsnlen` in the Windows runner template and all example/dev/integration test files to address CWE-126 (Buffer Over-read) flagged by static analysis tools (Semgrep/GitLab SAST). ## Changes The `Utf8FromUtf16` function now uses `wcsnlen` with the `UNICODE_STRING_MAX_CHARS` constant (32767) as the maximum length, providing defensive programming against potential buffer over-reads. **Key improvements:** 1. Calculate `input_length` **first** using `wcsnlen(utf16_string, UNICODE_STRING_MAX_CHARS)` 2. Use that bounded length for **both** `WideCharToMultiByte` calls (eliminates the `-1` unbounded read) 3. Remove the `-1` adjustment since explicit length excludes null terminator 4. Use `static_cast` instead of C-style casts per Google C++ Style Guide ## Test Coverage Added comprehensive edge case tests for `Utf8FromUtf16` in `windows_startup_test`: - **nullptr input**: Verifies function returns empty string - **Empty string input**: Verifies function returns empty string - **Invalid UTF-16 (unpaired surrogate)**: Verifies function handles malformed input gracefully These tests address reviewer feedback from @loic-sharma requesting coverage for corner cases. ## Files Updated **Template (source of truth):** - `packages/flutter_tools/templates/app/windows.tmpl/runner/utils.cpp` **Integration tests (4 files):** - `dev/integration_tests/flutter_gallery/windows/runner/utils.cpp` - `dev/integration_tests/ui/windows/runner/utils.cpp` - `dev/integration_tests/windowing_test/windows/runner/utils.cpp` - `dev/integration_tests/windows_startup_test/windows/runner/utils.cpp` **Examples and dev apps (10 files):** - `examples/hello_world/windows/runner/utils.cpp` - `examples/layers/windows/runner/utils.cpp` - `examples/platform_view/windows/runner/utils.cpp` - `examples/flutter_view/windows/runner/utils.cpp` - `examples/platform_channel/windows/runner/utils.cpp` - `examples/api/windows/runner/utils.cpp` - `examples/multiple_windows/windows/runner/utils.cpp` - `dev/manual_tests/windows/runner/utils.cpp` - `dev/benchmarks/complex_layout/windows/runner/utils.cpp` - `dev/a11y_assessments/windows/runner/utils.cpp` **Test files (4 files):** - `dev/integration_tests/windows_startup_test/windows/runner/flutter_window.cpp` - `dev/integration_tests/windows_startup_test/lib/main.dart` - `dev/integration_tests/windows_startup_test/lib/windows.dart` - `dev/integration_tests/windows_startup_test/test_driver/main_test.dart` ## Rationale While the Windows API guarantees null-termination for strings returned by `CommandLineToArgvW`, using `wcsnlen` with an explicit bound is a defensive programming best practice that: - Satisfies static analysis tools - Provides an extra safety layer - Follows the principle of defense in depth The limit of 32767 (`UNICODE_STRING_MAX_CHARS`) is the maximum length of a `UNICODE_STRING` structure and is far beyond any realistic command-line argument length. ## Related Issues Fixes https://github.com/flutter/flutter/issues/180418 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and labeled this PR with `severe: API break` if it contains a breaking change. - [x] All existing and new tests are passing. [Contributor Guide]: https://github.com/flutter/flutter/blob/master/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/master/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/master/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/master/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/master/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [breaking change policy]: https://github.com/flutter/flutter/blob/master/docs/contributing/Tree-hygiene.md#breaking-changes
149 lines
5.9 KiB
Dart
149 lines
5.9 KiB
Dart
// Copyright 2014 The Flutter Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
import 'dart:async';
|
|
import 'dart:typed_data';
|
|
import 'dart:ui' as ui;
|
|
|
|
import 'package:flutter_driver/driver_extension.dart';
|
|
|
|
import 'windows.dart';
|
|
|
|
void drawHelloWorld(ui.FlutterView view) {
|
|
final style = ui.ParagraphStyle();
|
|
final paragraphBuilder = ui.ParagraphBuilder(style)..addText('Hello world');
|
|
final ui.Paragraph paragraph = paragraphBuilder.build();
|
|
|
|
paragraph.layout(const ui.ParagraphConstraints(width: 100.0));
|
|
|
|
final recorder = ui.PictureRecorder();
|
|
final canvas = ui.Canvas(recorder);
|
|
|
|
canvas.drawParagraph(paragraph, ui.Offset.zero);
|
|
|
|
final ui.Picture picture = recorder.endRecording();
|
|
final sceneBuilder = ui.SceneBuilder()
|
|
..addPicture(ui.Offset.zero, picture)
|
|
..pop();
|
|
|
|
view.render(sceneBuilder.build());
|
|
}
|
|
|
|
Future<void> _waitUntilWindowVisible() async {
|
|
while (!await isWindowVisible()) {
|
|
await Future<void>.delayed(const Duration(milliseconds: 100));
|
|
}
|
|
}
|
|
|
|
void _expectVisible(bool current, bool expect, Completer<String> completer, int frameCount) {
|
|
if (current != expect) {
|
|
try {
|
|
throw 'Window should be ${expect ? 'visible' : 'hidden'} on frame $frameCount';
|
|
} catch (e) {
|
|
if (!completer.isCompleted) {
|
|
completer.completeError(e);
|
|
}
|
|
rethrow;
|
|
}
|
|
}
|
|
}
|
|
|
|
void main() async {
|
|
// TODO(goderbauer): Create a window if embedder doesn't provide an implicit view to draw into.
|
|
assert(ui.PlatformDispatcher.instance.implicitView != null);
|
|
final ui.FlutterView view = ui.PlatformDispatcher.instance.implicitView!;
|
|
|
|
// Create a completer to send the window visibility result back to the
|
|
// integration test.
|
|
final visibilityCompleter = Completer<String>();
|
|
enableFlutterDriverExtension(
|
|
handler: (String? message) async {
|
|
if (message == 'verifyWindowVisibility') {
|
|
return visibilityCompleter.future;
|
|
} else if (message == 'verifyTheme') {
|
|
final bool app = await isAppDarkModeEnabled();
|
|
final bool system = await isSystemDarkModeEnabled();
|
|
|
|
return (app == system)
|
|
? 'success'
|
|
: 'error: app dark mode ($app) does not match system dark mode ($system)';
|
|
} else if (message == 'verifyStringConversion') {
|
|
// Use a test string that contains code points that fit in both 8 and 16 bits.
|
|
// The code points are passed a list of integers through the method channel,
|
|
// which will use the UTF16 to UTF8 utility function to convert them to a
|
|
// std::string, which should equate to the original expected string.
|
|
const expected = 'ABCℵ';
|
|
final codePoints = Int32List.fromList(expected.codeUnits);
|
|
final String converted = await testStringConversion(codePoints);
|
|
return (converted == expected)
|
|
? 'success'
|
|
: 'error: conversion of UTF16 string to UTF8 failed, expected "${expected.codeUnits}" but got "${converted.codeUnits}"';
|
|
} else if (message == 'verifyNullStringConversion') {
|
|
// Test that Utf8FromUtf16 handles nullptr gracefully by returning empty string.
|
|
final String converted = await testNullStringConversion();
|
|
return converted.isEmpty
|
|
? 'success'
|
|
: 'error: nullptr conversion should return empty string, got "${converted.codeUnits}"';
|
|
} else if (message == 'verifyEmptyStringConversion') {
|
|
// Test that Utf8FromUtf16 handles empty string gracefully.
|
|
final String converted = await testEmptyStringConversion();
|
|
return converted.isEmpty
|
|
? 'success'
|
|
: 'error: empty string conversion should return empty string, got "${converted.codeUnits}"';
|
|
} else if (message == 'verifyInvalidUtf16Conversion') {
|
|
// Test that Utf8FromUtf16 handles invalid UTF-16 (unpaired surrogate) gracefully.
|
|
// With WC_ERR_INVALID_CHARS flag, WideCharToMultiByte returns 0 for invalid input,
|
|
// so Utf8FromUtf16 should return an empty string.
|
|
final String converted = await testInvalidUtf16Conversion();
|
|
return converted.isEmpty
|
|
? 'success'
|
|
: 'error: invalid UTF-16 conversion should return empty string, got "${converted.codeUnits}"';
|
|
}
|
|
|
|
throw 'Unrecognized message: $message';
|
|
},
|
|
);
|
|
|
|
try {
|
|
if (await isWindowVisible()) {
|
|
throw 'Window should be hidden at startup';
|
|
}
|
|
|
|
var frameCount = 0;
|
|
ui.PlatformDispatcher.instance.onBeginFrame = (Duration duration) {
|
|
// Our goal is to verify that it's `drawHelloWorld` that makes the window
|
|
// appear, not anything else. This requires checking the visibility right
|
|
// before drawing, but since `isWindowVisible` has to be async, and
|
|
// `FlutterView.render` (in `drawHelloWorld`) forbids async before it,
|
|
// this can not be done during a single onBeginFrame. However, we can
|
|
// verify in separate frames to indirectly prove it, by ensuring that
|
|
// no other mechanism can affect isWindowVisible in the first frame at all.
|
|
frameCount += 1;
|
|
switch (frameCount) {
|
|
// The 1st frame: render nothing, just verify that the window is hidden.
|
|
case 1:
|
|
isWindowVisible().then((bool visible) {
|
|
_expectVisible(visible, false, visibilityCompleter, frameCount);
|
|
ui.PlatformDispatcher.instance.scheduleFrame();
|
|
});
|
|
// The 2nd frame: render, which makes the window appear.
|
|
case 2:
|
|
drawHelloWorld(view);
|
|
_waitUntilWindowVisible().then((_) {
|
|
if (!visibilityCompleter.isCompleted) {
|
|
visibilityCompleter.complete('success');
|
|
}
|
|
});
|
|
// Others, in case requested to render.
|
|
default:
|
|
drawHelloWorld(view);
|
|
}
|
|
};
|
|
} catch (e) {
|
|
visibilityCompleter.completeError(e);
|
|
rethrow;
|
|
}
|
|
ui.PlatformDispatcher.instance.scheduleFrame();
|
|
}
|