Skip to content

Dart VM leaks memory on unregistered command line flags #133824

@dkwingsmt

Description

@dkwingsmt

Dart SDK revision: ac3bc9f6351ac0a9c7f6b2dc3db735bd5b9fa2c5
Flutter engine base revision: df619c6a5237c2f82fd48806be2cfaf621024759
Environment: gLinux

Details
[!] Flutter (Channel main, 3.14.0-13.0.pre.60, on Debian GNU/Linux rodete 6.3.11-1rodete1-amd64, locale en_US.utf8)
    • Flutter version 3.14.0-13.0.pre.60 on channel main at /[redacted]/dev/flutter
    ! Warning: `dart` on your path resolves to /usr/lib/google-dartlang/bin/dart, which is not inside your current Flutter SDK checkout at /[redacted]/dev/flutter. Consider adding
      /[redacted]/dev/flutter/bin to the front of your path.
    • Upstream repository [email protected]:flutter/flutter.git
    • Framework revision 956999a4b9 (57 minutes ago), 2023-08-31 16:23:44 -0700
    • Engine revision 10e2df60b0
    • Dart version 3.2.0 (build 3.2.0-126.0.dev)
    • DevTools version 2.27.0
    • If those were intentional, you can disregard the above warnings; however it is recommended to use "git" directly to perform update checks and upgrades.

[✓] Android toolchain - develop for Android devices (Android SDK version 30.0.3)
• Android SDK at /[redacted]/Android/Sdk
• Platform android-31, build-tools 30.0.3
• Java binary at: /opt/android-studio-with-blaze-2020.3/jre/bin/java
• Java version OpenJDK Runtime Environment (build 11.0.10+0-b96-7249189)
• All Android licenses accepted.

[✓] Chrome - develop for the web
• Chrome at google-chrome

[✓] Linux toolchain - develop for Linux desktop
• Debian clang version 14.0.6
• cmake version 3.25.1
• ninja version 1.11.1
• pkg-config version 1.8.1

[✓] Android Studio (version 3.5)
• Android Studio at /[redacted]/src/android-studio
• Flutter plugin version 43.0.1
• Dart plugin version 191.8593
• Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b49-5587405)

[✓] Android Studio (version 2020.3)
• Android Studio at /opt/android-studio-with-blaze-2020.3
• Flutter plugin version 65.2.1
• Dart plugin version 203.8452
• Java version OpenJDK Runtime Environment (build 11.0.10+0-b96-7249189)

[✓] VS Code (version 1.78.0)
• VS Code at /usr/share/code
• Flutter extension version 3.70.0

[✓] Connected device (2 available)
• Linux (desktop) • linux • linux-x64 • Debian GNU/Linux rodete 6.3.11-1rodete1-amd64
• Chrome (web) • chrome • web-javascript • Google Chrome 116.0.5845.140

[✓] Network resources
• All expected network resources are available.

! Doctor found issues in 1 category.

When the Dart VM parses a flag, if the flag is not registered, then the name is captured by Flag but is never disposed, and eventually leaked.

Following is the leakage reported by ASAN:

=================================================================
==132854==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 27 byte(s) in 1 object(s) allocated from:
    #0 0x557aef9e2fdd in operator new[](unsigned long) ../llvm_build/tools/clang/stage2-bins/runtimes/runtimes-x86_64-unknown-linux-gnu-bins/../llvm_build/tools/clang/stage2-bins/runtimes/runtimes-x86_64-unknown-linux-gnu-bins/compiler-rt/lib/asan/asan_new_delete.cpp:98:3
    #1 0x557af52f0573 in dart::Flags::Parse(char const*) /[redacted]/dev/engine/src/out/host_debug_unopt/../../third_party/dart/runtime/vm/flags.cc:400:22
    #2 0x557af52f09d8 in dart::Flags::ProcessCommandLineFlags(int, char const**) /[redacted]/dev/engine/src/out/host_debug_unopt/../../third_party/dart/runtime/vm/flags.cc:447:5
    #3 0x557af5e2a301 in flutter::DartVM::DartVM(std::_LIBCPP_ABI_NAMESPACE::shared_ptr<flutter::DartVMData const> const&, std::_LIBCPP_ABI_NAMESPACE::shared_ptr<flutter::IsolateNameServer>) /[redacted]/dev/engine/src/out/host_debug_unopt/../../flutter/runtime/dart_vm.cc:430:23
    #4 0x557af5e28bb0 in flutter::DartVM::Create(flutter::Settings const&, fml::RefPtr<flutter::DartSnapshot const>, fml::RefPtr<flutter::DartSnapshot const>, std::_LIBCPP_ABI_NAMESPACE::shared_ptr<flutter::IsolateNameServer>) /[redacted]/dev/engine/src/out/host_debug_unopt/../../flutter/runtime/dart_vm.cc:274:11
    #5 0x557af5e387e4 in flutter::DartVMRef::Create(flutter::Settings const&, fml::RefPtr<flutter::DartSnapshot const>, fml::RefPtr<flutter::DartSnapshot const>) /[redacted]/dev/engine/src/out/host_debug_unopt/../../flutter/runtime/dart_vm_lifecycle.cc:78:13
    #6 0x557af5f1d29f in flutter::Shell::InferVmInitDataFromSettings(flutter::Settings&) /[redacted]/dev/engine/src/out/host_debug_unopt/../../flutter/shell/common/shell.cc:154:13
    #7 0x557aefc3ec04 in flutter::(anonymous namespace)::RuntimeControllerContext::Create(flutter::Settings, flutter::TaskRunners const&, flutter::RuntimeDelegate&) /[redacted]/dev/engine/src/out/host_debug_unopt/../../flutter/lib/ui/window/platform_configuration_unittests.cc:109:35
    #8 0x557aefc3fe48 in flutter::testing::PlatformConfigurationTest_DuplicateRenderCallsAreIgnored_Test::TestBody() /[redacted]/dev/engine/src/out/host_debug_unopt/../../flutter/lib/ui/window/platform_configuration_unittests.cc:544:7
    #9 0x557af829386f in void testing::internal::HandleSehExceptionsInMethodIfSupported<testing::Test, void>(testing::Test*, void (testing::Test::*)(), char const*) /[redacted]/dev/engine/src/out/host_debug_unopt/../../third_party/googletest/googletest/src/gtest.cc:2631:10
    #10 0x557af8270920 in void testing::internal::HandleExceptionsInMethodIfSupported<testing::Test, void>(testing::Test*, void (testing::Test::*)(), char const*) /[redacted]/dev/engine/src/out/host_debug_unopt/../../third_party/googletest/googletest/src/gtest.cc:2686:12
    #11 0x557af8242dcd in testing::Test::Run() /[redacted]/dev/engine/src/out/host_debug_unopt/../../third_party/googletest/googletest/src/gtest.cc:2706:5
    #12 0x557af8244090 in testing::TestInfo::Run() /[redacted]/dev/engine/src/out/host_debug_unopt/../../third_party/googletest/googletest/src/gtest.cc:2885:11
    #13 0x557af8245200 in testing::TestSuite::Run() /[redacted]/dev/engine/src/out/host_debug_unopt/../../third_party/googletest/googletest/src/gtest.cc:3044:30
    #14 0x557af825d6cc in testing::internal::UnitTestImpl::RunAllTests() /[redacted]/dev/engine/src/out/host_debug_unopt/../../third_party/googletest/googletest/src/gtest.cc:5913:44
    #15 0x557af829c5af in bool testing::internal::HandleSehExceptionsInMethodIfSupported<testing::internal::UnitTestImpl, bool>(testing::internal::UnitTestImpl*, bool (testing::internal::UnitTestImpl::*)(), char const*) /[redacted]/dev/engine/src/out/host_debug_unopt/../../third_party/googletest/googletest/src/gtest.cc:2631:10
    #16 0x557af8274e6d in bool testing::internal::HandleExceptionsInMethodIfSupported<testing::internal::UnitTestImpl, bool>(testing::internal::UnitTestImpl*, bool (testing::internal::UnitTestImpl::*)(), char const*) /[redacted]/dev/engine/src/out/host_debug_unopt/../../third_party/googletest/googletest/src/gtest.cc:2686:12
    #17 0x557af825cb34 in testing::UnitTest::Run() /[redacted]/dev/engine/src/out/host_debug_unopt/../../third_party/googletest/googletest/src/gtest.cc:5482:10
    #18 0x557af01821f0 in RUN_ALL_TESTS() /[redacted]/dev/engine/src/out/host_debug_unopt/../../third_party/googletest/googletest/include/gtest/gtest.h:2497:46
    #19 0x557af0181ded in main /[redacted]/dev/engine/src/out/host_debug_unopt/../../flutter/testing/run_all_unittests.cc:71:17
    #20 0x7fd90fbc36c9 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16

The leaked string is disable-service-auth-codes\0, which Flutter has been assigning but I suspect Dart has recently removed. Removing this flag from Flutter's code also eliminates leakage warnings.

Analysis

The leakage message leads us to the following code in flags.cc:

void Flags::Parse(const char* option) {
  // ...
  Flag* flag = Flags::Lookup(name);
  if (flag == nullptr) {
    // Collect unrecognized flags.
    char* new_flag = new char[name_len + 1];     // <-- HERE
    strncpy(new_flag, option, name_len);
    new_flag[name_len] = '\0';
    Flags::Register_bool(nullptr, new_flag, true, nullptr);
  } else {
    // Only set values for recognized flags, skip collected
    // unrecognized flags.
    if (!flag->IsUnrecognized()) {
      if (!SetFlagFromString(flag, argument)) {
        OS::PrintErr("Ignoring flag: %s is an invalid value for flag %s\n",
                     argument, name);
      }
    }
  }

The new_flag is allocated here, passed into Register_bool, and never deleted. Register_bool stores the string in a Flag, but Flag doesn't deallocate this string either, probably because normally Flag receives a constant string.

FYI, we can't immediately delete new_flag after Register_bool, apparently since it's used in Flag later.

Reproduction

The leakage can be reproduced by running the unit tests introduced in my PR when compiled with --asan. (I understand that having to compile the entire Flutter engine for it might be too much, but honestly I think the analysis above should be convincing enough that a repro is not needed. Let me know if a more isolated repro is wanted.)

  • Set up the Flutter engine development environment
  • Check out my PR:
git checkout -b dkwingsmt-enforce-rendering-rule main
git pull [email protected]:dkwingsmt/engine.git enforce-rendering-rule
git checkout 151ed777ca244f187588945f9b2f29012ca8869d
  • Compile.
gn --asan --unoptimized
ninja -C $ENGINE_SRC/out/host_debug_unopt
  • Run test:
../out/host_debug_unopt/exe.unstripped/ui_unittests --gtest_filter="*RenderCalls*"

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions