stride3d/stride · error · IOException

Couldn't unlock file.

Error message

Couldn't unlock file.

What it means

TryUnlockFile releases a byte-range lock previously taken on a file. On Windows it calls UnlockFileEx; if the OS reports the unlock failed (e.g. the range was not locked by this handle), an IOException is thrown.

Solutions

  1. Ensure every unlock uses the exact offset and count used when locking, on the same FileStream instance
  2. Guard against double-unlock with a flag or try/finally pairing lock/unlock
  3. Verify the handle still owns the lock (no intermediate close/reopen)
  4. Check the underlying Win32 error via the inner details; treat EINVAL-style 'not locked' as a logic bug in the caller

Example fix

// before
lockFile.TryLockFile(0, len);
// ... exception path skips unlock ...
lockFile.TryUnlockFile(0, len); // may throw if never locked
// after
lockFile.TryLockFile(0, len);
try { /* work */ }
finally { if (isLocked) lockFile.TryUnlockFile(0, len); }
Defensive patterns

Strategy: try-catch

Validate before calling

// track locked ranges in a HashSet<(long offset,long count)> owned by the same FileStream
bool owned = lockedRanges.Contains((offset, count));

Try / catch

try { lockFile.TryUnlockFile(offset, count); }
catch (IOException) { /* range not locked by this handle — log and continue */ }
finally { lockedRanges.Remove((offset, count)); }

Prevention

When it happens

Trigger: Calling TryUnlockFile on a range (offset/count) that was never locked via the matching TryLockFile on the same FileStream/SafeFileHandle, or with an offset/count that doesn't exactly match the locked region.

Common situations: Double-unlock after an exception, unlocking after the stream was reopened (locks are per-handle), mismatched offset/count values between lock and unlock calls.

Understand the failure class

Background: "open() failed", "failed to open file", "cannot create file" — what a file open error means and how to fix it — this error's family across 42 libraries.

Related errors


AI-assisted analysis of stride3d/stride@96fad776d2 (2026-09-14). Data as JSON: /api/errors/5257068ece643a94. Report an issue: GitHub.

Appendix: source

Thrown at sources/core/Stride.Core.IO/NativeLockFile.cs:154

        if (Platform.Type == PlatformType.Android)
        {
            count = (count + offset > int.MaxValue) ? int.MaxValue - offset : count;
        }

        if (OperatingSystem.IsWindows())
        {
            var countLow = (uint)count;
            var countHigh = (uint)(count >> 32);

            var overlapped = new NativeOverlapped()
            {
                OffsetLow = (int)(offset & uint.MaxValue),
                OffsetHigh = (int)(offset >> 32),
            };

            if (!UnlockFileEx(fileStream.SafeFileHandle, 0, countLow, countHigh, ref overlapped))
            {
                throw new IOException("Couldn't unlock file.");
            }
        }
        else
        {
            UnixUnlock(fileStream, offset, count);
        }
    }

    /// <summary>
    ///   Lock on Unix: on Linux/Android, tries OFD locks first (per-handle, like Windows LockFileEx),
    ///   falls back to standard fcntl (per-process) if OFD is not supported.
    ///   On macOS/iOS, uses standard fcntl directly (OFD not available on XNU).
    /// </summary>
    private static FileLockResult UnixLock(FileStream fileStream, long offset, long count, bool exclusive)
    {
        int fd = fileStream.SafeFileHandle.DangerousGetHandle().ToInt32();

        var lockInfo = new Flock

View on GitHub (pinned to 96fad776d2)