Skip to content

fix: serialise BuildInplaceOnly tarball extraction to prevent TOCTOU race#269

Open
angerman wants to merge 1 commit intowip/andrea/add-executable-extensionfrom
fix/unpack-toctou-race
Open

fix: serialise BuildInplaceOnly tarball extraction to prevent TOCTOU race#269
angerman wants to merge 1 commit intowip/andrea/add-executable-extensionfrom
fix/unpack-toctou-race

Conversation

@angerman
Copy link

Summary

  • Adds an unpackLock to serialise the doesDirectoryExist check and unpackPackageTarball call in the BuildInplaceOnly path of withTarballLocalDirectory
  • Prevents a TOCTOU race when two stages of the same package (e.g. build: and host: in cross-compilation) are scheduled concurrently
  • Uses the existing Lock/criticalSection pattern from JobControl, matching registerLock and cacheLock

Root Cause

In cross-compilation builds, cabal-install creates two independent graph nodes for each package: WithStage Build <unitid> and WithStage Host <unitid>. Both are scheduled concurrently by InstallPlan.execute. When both stages have BuildInplaceOnly build style, they call withTarballLocalDirectory which:

  1. Checks if distUnpackedSrcDirectory pkgid exists (keyed only by PackageId, not stage)
  2. Both threads see it as non-existent
  3. Both race to extract the tarball to the same directory
  4. The second extraction overwrites the first mid-flight, corrupting the result

This manifests as intermittent "No cabal file found" errors (observed on FreeBSD CI for os-string-2.0.8).

Fix

Wrap the check-and-extract block in criticalSection unpackLock so only one thread performs the extraction while the other waits and finds the directory already present.

Test plan

  • Verify the fix compiles (single file change, no new dependencies)
  • Run cross-compilation build that previously triggered the race
  • Verify normal (non-cross) builds are unaffected

…race

When two stages of the same package (e.g. build: and host: in cross-
compilation) are scheduled concurrently, both threads can observe the
unpacked source directory as non-existent and race to extract the
tarball. The second extraction overwrites the first mid-flight,
corrupting the result and causing intermittent "No cabal file found"
errors.

Add an unpackLock (using the existing Lock/criticalSection from
JobControl) to serialise the doesDirectoryExist check and tarball
extraction in the BuildInplaceOnly path of withTarballLocalDirectory.
This is the same pattern already used for registerLock and cacheLock.
@angerman angerman force-pushed the fix/unpack-toctou-race branch from 39c39f7 to 254388a Compare February 28, 2026 07:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant