Skip to content

readlink error handling bugs in GetExecutablePath and GetExecutableDirectory on Linux #184476

Description

@randomizedcoder

Steps to reproduce

This is a code-level bug found via static analysis (flawfinder CWE-362/CWE-20, CERT POS30-C). Two readlink("/proc/self/exe") call sites have correctness issues.

Bug 1 — Undersized buffer in engine/src/flutter/fml/platform/linux/paths_linux.cc:12-19:

  1. Deploy a Flutter application to a Linux system where the executable path exceeds 255 bytes (e.g. a deeply nested directory or long directory names).
  2. The application calls fml::paths::GetExecutablePath().
  3. The returned path is silently truncated to 255 bytes because the buffer is hardcoded to 255 instead of PATH_MAX (4096).

Bug 2 — Missing error check in engine/src/flutter/shell/platform/common/path_utils.cc:24-31:

  1. Run a Flutter desktop application on a Linux system where /proc is not mounted or /proc/self/exe is otherwise unavailable (e.g. certain container configurations, restricted sandboxes).
  2. readlink("/proc/self/exe") returns -1.
  3. The code checks length > PATH_MAX which does not catch -1.
  4. std::string(buffer, (size_t)-1) is called — this is undefined behavior ((size_t)-1 = 18446744073709551615 on 64-bit), causing a crash or massive allocation attempt.

Both bugs have been present since the engine monorepo merge (7e0bed752f3, 2023-04-25). path_utils.cc dates to 2015 (ad9b1352171).

Existing issue search: We searched for related issues before filing. The following are related but do not identify these specific bugs:

Expected results

GetExecutablePath() should use a PATH_MAX-sized buffer and return {false, ""} on any readlink error.

GetExecutableDirectory() should return an empty path on any readlink error, without triggering undefined behavior.

Both should match Chromium's ReadSymbolicLink() pattern in base/files/file_util_posix.cc:

char buf[PATH_MAX];
ssize_t count = ::readlink(symlink_path.value().c_str(), buf, std::size(buf));
bool error = count <= 0;
if (error) {
  target_path->clear();
  return false;
}
*target_path = FilePath(FilePath::StringType(buf, static_cast<size_t>(count)));

Actual results

Bug 1 (paths_linux.cc): Executable paths longer than 255 bytes are silently truncated. The application proceeds with an incorrect path, potentially leading to failures resolving assets, ICU data, or the Dart VM snapshot.

Bug 2 (path_utils.cc): When readlink fails (returns -1), the code constructs std::string(buffer, (size_t)-1) which is undefined behavior. In practice this causes either:

  • An std::bad_alloc exception (attempting to allocate ~18 exabytes)
  • A segfault
  • Memory corruption

The current code:

// paths_linux.cc — buffer too small
const int path_size = 255;          // should be PATH_MAX (4096)
char path[path_size] = {0};
auto read_size = ::readlink("/proc/self/exe", path, path_size);

// path_utils.cc — missing error check
ssize_t length = readlink("/proc/self/exe", buffer, sizeof(buffer));
if (length > PATH_MAX) {            // does not catch length == -1
  return std::filesystem::path();
}
std::filesystem::path executable_path(std::string(buffer, length)); // UB when length == -1

Code sample

Code sample

This is a C++ engine bug, not reproducible from Dart. The affected code:

engine/src/flutter/fml/platform/linux/paths_linux.cc (buffer too small):

std::pair<bool, std::string> GetExecutablePath() {
  const int path_size = 255;          // BUG: should be PATH_MAX (4096)
  char path[path_size] = {0};
  auto read_size = ::readlink("/proc/self/exe", path, path_size);
  if (read_size == -1) {
    return {false, ""};
  }
  return {true, std::string{path, static_cast<size_t>(read_size)}};
}

engine/src/flutter/shell/platform/common/path_utils.cc (missing error check):

std::filesystem::path GetExecutableDirectory() {
  char buffer[PATH_MAX + 1];
  ssize_t length = readlink("/proc/self/exe", buffer, sizeof(buffer));
  if (length > PATH_MAX) {            // BUG: does not catch length == -1
    return std::filesystem::path();
  }
  std::filesystem::path executable_path(std::string(buffer, length));  // UB when length is -1
  return executable_path.remove_filename();
}

Chromium's correct implementation (base/files/file_util_posix.cc:695-719):

bool ReadSymbolicLink(const FilePath& symlink_path, FilePath* target_path) {
  DCHECK(!symlink_path.empty());
  DCHECK(target_path);
  char buf[PATH_MAX];
  ssize_t count = ::readlink(symlink_path.value().c_str(), buf, std::size(buf));
  bool error = count <= 0;
  if (error) {
    target_path->clear();
    return false;
  }
  *target_path = FilePath(FilePath::StringType(buf, static_cast<size_t>(count)));
  return true;
}

Screenshots or Video

Screenshots / Video demonstration

Not applicable — this is a C++ engine code bug found by static analysis, not a visual issue.

Logs

Logs

Static analysis output that identified the bugs:

$ flawfinder engine/src/flutter/fml/platform/linux/paths_linux.cc
engine/src/flutter/fml/platform/linux/paths_linux.cc:15:22:  [5] (race) readlink:
  This accepts filename arguments; if an attacker can move those files or
  change the link content, a race condition results. Also, it does not
  terminate with ASCII NUL. (CWE-362, CWE-20). Reconsider approach.

$ flawfinder engine/src/flutter/shell/platform/common/path_utils.cc
engine/src/flutter/shell/platform/common/path_utils.cc:26:20:  [5] (race) readlink:
  This accepts filename arguments; if an attacker can move those files or
  change the link content, a race condition results. Also, it does not
  terminate with ASCII NUL. (CWE-362, CWE-20). Reconsider approach.

Manual review against CERT POS30-C ("Use the readlink() function properly") confirmed:

  • paths_linux.cc: 255-byte buffer violates the standard (should be PATH_MAX)
  • path_utils.cc: Missing -1 return check leads to undefined behavior

Note: The CWE-362 (TOCTOU race) flagged by flawfinder is a false positive for /proc/self/exe specifically — it is a kernel-managed symlink that cannot be redirected by userspace. The real bugs are the undersized buffer and the missing error check.

Flutter Doctor output

Doctor output
[!] Flutter (Channel [user-branch], 3.43.0-1.0.pre-391, on NixOS 26.05 (Yarara) 6.19.9, locale en_US.UTF-8) [51ms]
    ! Flutter version 3.43.0-1.0.pre-391 on channel [user-branch] at /home/das/Downloads/flutter
      Currently on an unknown channel. Run `flutter channel` to switch to an official channel.
      If that doesn't fix the issue, reinstall Flutter by following instructions at https://flutter.dev/setup.
    • Framework revision c589dfffda (16 hours ago), 2026-03-31 22:16:58 -0400
    • Engine revision be1e70f0a8
    • Dart version 3.12.0 (build 3.12.0-304.0.dev)
    • DevTools version 2.57.0-dev.0

[!] Android toolchain - develop for Android devices (Android SDK version 31.0.0)
    • Android SDK at /home/das/Android/Sdk

[✓] Chrome - develop for the web
    • CHROME_EXECUTABLE = /nix/store/5fyi5df2lfhmvjgvlkdm5b46rbwxibsy-google-chrome-146.0.7680.153/bin/google-chrome-stable

[✗] Linux toolchain - develop for Linux desktop
    ✗ clang++ is required for Linux development.
    ✗ CMake is required for Linux development.
    ✗ ninja is required for Linux development.

[✓] Connected device (2 available)
    • Linux (desktop) • linux  • linux-x64      • NixOS 26.05 (Yarara) 6.19.9
    • Chrome (web)    • chrome • web-javascript • Google Chrome 146.0.7680.153

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

! Doctor found issues in 3 categories.

Metadata

Metadata

Assignees

Labels

P2Important issues not at the top of the work listplatform-linuxBuilding on or for Linux specificallyteam-linuxOwned by the Linux platform teamtriaged-linuxTriaged by the Linux platform team

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions