diff --git a/Cabal.cabal b/Cabal.cabal
--- a/Cabal.cabal
+++ b/Cabal.cabal
@@ -1,6 +1,6 @@
 cabal-version: 3.8
 name:          Cabal
-version:       3.18.1.0
+version:       3.18.2.0
 copyright:     2003-2026, Cabal Development Team (see AUTHORS file)
 license:       BSD-3-Clause
 license-file:  LICENSE
diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,3 +1,6 @@
+# 3.18.2.0 [Francesco Ariis](mailto:fa-ml@ariis.it) September 2026
+* See https://github.com/haskell/cabal/blob/master/release-notes/Cabal-3.18.2.0.md
+
 # 3.18.1.0 [Artem Pelenitsyn](mailto:a@pelenitsyn.top) July 2026
 * See https://github.com/haskell/cabal/blob/master/release-notes/Cabal-3.18.1.0.md
 
diff --git a/src/Distribution/Simple/Utils.hs b/src/Distribution/Simple/Utils.hs
--- a/src/Distribution/Simple/Utils.hs
+++ b/src/Distribution/Simple/Utils.hs
@@ -209,6 +209,9 @@
   , isAbsoluteOnAnyPlatform
   , isRelativeOnAnyPlatform
   , exceptionWithCallStackPrefix
+
+    -- * Cross-process locking
+  , withFileLock
   ) where
 
 import Distribution.Compat.Async (waitCatch, withAsyncNF)
@@ -254,6 +257,7 @@
 import Distribution.Compat.Process (proc)
 import Foreign.C.Error (Errno (..), ePIPE)
 import qualified GHC.IO.Exception as GHC
+import GHC.IO.Handle.Lock (LockMode (..), hLock, hTryLock, hUnlock)
 import GHC.Stack (HasCallStack)
 import Numeric (showFFloat)
 import System.Directory
@@ -286,6 +290,7 @@
 import System.IO
   ( BufferMode (..)
   , Handle
+  , IOMode (..)
   , hClose
   , hFlush
   , hGetContents
@@ -293,6 +298,7 @@
   , hPutStrLn
   , hSetBinaryMode
   , hSetBuffering
+  , openFile
   , stderr
   , stdin
   , stdout
@@ -1604,6 +1610,24 @@
           if isDirectory
             then collect files (dirEntry : dirs') entries
             else collect (dirEntry : files) dirs' entries
+
+-- | Hold an exclusive cross-process lock on @lockPath@ for the duration of
+-- @action@, creating the lock file if it does not exist.
+withFileLock :: Verbosity -> FilePath -> String -> IO a -> IO a
+withFileLock verbosity lockPath waitMsg action =
+  Exception.bracket takeLock releaseLock (const action)
+  where
+    takeLock = do
+      h <- openFile lockPath ReadWriteMode
+      -- First try non-blocking, but if we would have to wait then
+      -- log an explanation and do it again in blocking mode.
+      gotlock <- hTryLock h ExclusiveLock
+      unless gotlock $ do
+        info verbosity waitMsg
+        hLock h ExclusiveLock
+      return h
+
+    releaseLock h = hUnlock h `Exception.finally` hClose h
 
 ------------------------
 -- Environment variables
