0X0000009E

ERROR_NOT_LOCKED (0x9E): Segment Already Unlocked Fix

Windows Errors Intermediate 👁 2 views 📅 May 28, 2026

This error means code tried to unlock a segment that wasn't locked. Usually a stale handle in file-locking or memory-mapped file code. Fix is to rework the locking logic.

1. Stale Handle or Double Unlock in File Locking

I've seen this error more times than I can count. The culprit is almost always a thread or process that holds a handle to a locked segment, tries to unlock it, but the lock was already released — either by another thread, or the same code path got called twice. This happens often in custom file-locking libraries or when using LockFileEx / UnlockFileEx.

How it happens

Picture this: You've got a file region locked with LockFileEx. Your code calls UnlockFileEx on that region. But somewhere in the call stack, maybe an error handler or a cleanup routine also calls UnlockFileEx on the same region. The first call succeeds, the second one hits ERROR_NOT_LOCKED.

Fix it

  1. Track lock state explicitly. Use a boolean or enum in your lock object. Before unlocking, check if the lock is actually held.
  2. Make sure each LockFileEx is paired with exactly one UnlockFileEx. No more, no less.
  3. If you're using overlapped I/O, double-check the OVERLAPPED structure. A stale or reused OVERLAPPED can point to the wrong byte range.
// Bad: double unlock
BOOL UnlockRegion(HANDLE hFile, DWORD64 offset, DWORD64 length) {
    OVERLAPPED ov = {0};
    ov.Offset = (DWORD)(offset & 0xFFFFFFFF);
    ov.OffsetHigh = (DWORD)(offset >> 32);
    if (!UnlockFileEx(hFile, 0, (DWORD)length, 0, &ov)) {
        // May hit ERROR_NOT_LOCKED if already unlocked
        return FALSE;
    }
    return TRUE;
}

// Good: guard with state
BOOL UnlockRegionSafe(HANDLE hFile, BOOL* isLocked, DWORD64 offset, DWORD64 length) {
    if (!*isLocked) return TRUE; // already unlocked, no error
    OVERLAPPED ov = {0};
    ov.Offset = (DWORD)(offset & 0xFFFFFFFF);
    ov.OffsetHigh = (DWORD)(offset >> 32);
    BOOL result = UnlockFileEx(hFile, 0, (DWORD)length, 0, &ov);
    if (result) *isLocked = FALSE;
    return result;
}

2. Memory-Mapped File: Unmapping Without a Lock

This one's sneaky. When you use CreateFileMapping and MapViewOfFileEx, you're mapping a view of a file into memory. Some code mistakenly treats the unmapping operation (UnmapViewOfFile) as needing a lock — which is wrong. The OS doesn't lock segments for you. If your code tries to call UnlockFileEx on a memory-mapped region that wasn't locked with LockFileEx, you'll get error 0x9E.

Real-world trigger

I debugged a case where a developer mixed MapViewOfFile with LockFileEx. They'd lock a byte range, then map the same file, and later try to unlock the mapped view. The mapped view never had a lock — the lock was on the original handle's byte range. The unlock call failed with ERROR_NOT_LOCKED.

Fix it

  • Don't mix locking APIs. Use LockFileEx/UnlockFileEx on the file handle, not on the mapped view.
  • If you need to lock a region that's also mapped, lock the handle before mapping, unlock after unmapping.
  • Verify your code isn't trying to unlock a region that's already been unmapped. Unmapping a view releases any implicit locks on that view, but not explicit file locks.
// Wrong: trying to unlock a mapped view
HANDLE hMap = CreateFileMapping(hFile, NULL, PAGE_READWRITE, 0, 4096, NULL);
LPVOID pView = MapViewOfFileEx(hMap, FILE_MAP_WRITE, 0, 0, 4096, NULL);
// Some work...
UnmapViewOfFile(pView);
// This will fail with ERROR_NOT_LOCKED:
UnlockFileEx(hFile, 0, 4096, 0, &ov); // ov is stale or wrong

3. Reentrant or Recursive Unlock Calls in a Loop

Sometimes the error comes from a loop that processes multiple locked regions, but the loop logic has a bug. For example, you iterate over a list of lock ranges, and inside the loop you call unlock — but the loop also modifies the list, causing the same range to be unlocked twice.

How to spot it

Look for patterns like this in your code:

  • A for or while loop that calls UnlockFileEx or ReleaseMutex (if it's a mutex-based lock).
  • Any error handler that calls unlock inside the same function that already called unlock on success.
  • Use of try-finally or __try-__finally where the finally block calls unlock — but the try block might also call unlock.

Fix it

  1. Add a counter or flag. Only unlock if the lock count is > 0.
  2. Use a single exit point for unlock calls. Don't scatter them.
  3. If you're using C++ RAII, make sure your destructor checks if the lock was actually acquired before releasing.
// Example of a reentrant-unlock bug
for (int i = 0; i < lockCount; i++) {
    if (locks[i].isLocked) {
        UnlockFileEx(hFile, 0, locks[i].length, 0, &locks[i].ov);
        locks[i].isLocked = FALSE;
        // Bug: if the loop modifies 'locks' array, 'i' may repeat
    }
}

Quick-Reference Summary Table

CauseSymptomFix
Double unlock / stale handleUnlockFileEx called twice on same regionTrack lock state with a boolean
Mixing memory-mapped views with file locksUnlock called on unmapped viewKeep lock operations on the file handle only
Reentrant unlock in loops or error pathsSame region unlocked multiple timesUse a single exit point or RAII guard

Bottom line: ERROR_NOT_LOCKED is almost always a logic bug in your lock management. The OS is telling you you're trying to release something you don't own. Audit every unlock call — you'll find the culprit fast.

Was this solution helpful?