Repository navigation
Description
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):
-
Silent wrong tree-shaking when recordings differ (e.g., different
-D/--defineor--enable-assertsflags):- Flags like
-D(--define) and--enable-assertsaffect constant evaluation before TFA and can produce differentrecorded_usages.jsonfiles while sharing the sameRecordUseConfig(entryPointsandcompiler: '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
R1to<buildDirUri>/recorded_uses.jsonand enters_runHookForPackageCached(acquiring the lock and compilinghook/link.dart). - Before Process A records
lastModifiedCutoffTime = DateTime.now()and runs its hook, Process B reacheslink()and overwrites<buildDirUri>/recorded_uses.jsonwithR2(since that write is not guarded byrunUnderDirectoriesLock). - Process A then runs its link hook against
R2, hashesR2intodependencies.dependencies_hash_file.json, and exits0with no warnings — silently bundling Process B's tree-shaken assets intoout1.
- Process A writes
- Flags like
-
Mid-hook modification / truncation even with identical flags:
- If Process B reaches
resourcesFile.writeAsString(...)while Process A's link hook is actively running (or readingrecorded_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
SEVEREerror ("File modified during build. Build must be rerun.") whendependenciesHashes.updateHashescheckslastModifiedCutoffTime.
- If Process B reaches
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.
Activity