David Bebawy 99e01e108a
fix(windows): use wcsnlen for defensive programming (CWE-126) (#180419)
## 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
2026-01-16 18:21:15 +00:00

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();
}