Skip to content

[hooks_runner] Race condition in NativeAssetsBuildRunner.link: recorded_uses.json is written outside runUnderDirectoriesLock #3769

Closed

Description

@goderbauer

Gemini found a potential race condition in hooks_runner:

Description

In NativeAssetsBuildRunner.link (pkgs/hooks_runner/lib/src/build_runner/build_runner.dart), the per-package recorded_uses.json file in the hook cache directory (.dart_tool/hooks_runner/<package>/<checksum>/recorded_uses.json) is created and overwritten before acquiring the directory lock in _runHookForPackageCached:

      final (buildDirUri, outDirUri, outDirSharedUri) = await _setupDirectories(
        Hook.link,
        inputBuilder,
        package,
      );

      File? resourcesFile;
      if (packageRecordings != null) {
        resourcesFile = _fileSystem.file(
          buildDirUri.resolve('recorded_uses.json'),
        );
        await resourcesFile.create();
        final filteredRecordings = packageRecordings.filter(
          definitionPackageName: package.name,
        );
        await resourcesFile.writeAsString(
          _jsonEncoder.convert(filteredRecordings.toJson()),
        );
      }

runUnderDirectoriesLock is only acquired later inside _runHookForPackageCached.

Impact

When two dart build cli (or NativeAssetsBuildRunner.link) invocations run concurrently on the same package and map to the same <checksum> directory (i.e. same target OS/arch/sanitizer/entrypoint):

  1. Silent wrong tree-shaking when recordings differ (e.g., different -D / --define or --enable-asserts flags):

    • Flags like -D (--define) and --enable-asserts affect constant evaluation before TFA and can produce different recorded_usages.json files while sharing the same RecordUseConfig (entryPoints and compiler: 'vm_aot_<os>') and therefore the same <checksum> directory.
    • If Process A (-Dfoo=1, -o out1) and Process B (-Dfoo=2, -o out2) run concurrently:
      • Process A writes R1 to <buildDirUri>/recorded_uses.json and enters _runHookForPackageCached (acquiring the lock and compiling hook/link.dart).
      • Before Process A records lastModifiedCutoffTime = DateTime.now() and runs its hook, Process B reaches link() and overwrites <buildDirUri>/recorded_uses.json with R2 (since that write is not guarded by runUnderDirectoriesLock).
      • Process A then runs its link hook against R2, hashes R2 into dependencies.dependencies_hash_file.json, and exits 0 with no warnings — silently bundling Process B's tree-shaken assets into out1.
  2. Mid-hook modification / truncation even with identical flags:

    • If Process B reaches resourcesFile.writeAsString(...) while Process A's link hook is actively running (or reading recorded_uses.json), Process B truncates and rewrites the file mid-execution.
    • Process A may either read truncated JSON in the link hook or log a SEVERE error ("File modified during build. Build must be rerun.") when dependenciesHashes.updateHashes checks lastModifiedCutoffTime.

Suggested Fix

Move the writing of buildDirUri.resolve('recorded_uses.json') inside the runUnderDirectoriesLock critical section in _runHookForPackageCached (before lastModifiedCutoffTime = DateTime.now() and before dependenciesHashes.findOutdatedDependency is checked), so that concurrent link() calls serialize all reads and writes to <buildDirUri>/recorded_uses.json.

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

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