ERROR_NOT_LOCKED (0x9E): Segment Already Unlocked Fix
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
- Track lock state explicitly. Use a boolean or enum in your lock object. Before unlocking, check if the lock is actually held.
- Make sure each
LockFileExis paired with exactly oneUnlockFileEx. No more, no less. - 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/UnlockFileExon 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
fororwhileloop that callsUnlockFileExorReleaseMutex(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-finallyor__try-__finallywhere the finally block calls unlock — but the try block might also call unlock.
Fix it
- Add a counter or flag. Only unlock if the lock count is > 0.
- Use a single exit point for unlock calls. Don't scatter them.
- 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
| Cause | Symptom | Fix |
|---|---|---|
| Double unlock / stale handle | UnlockFileEx called twice on same region | Track lock state with a boolean |
| Mixing memory-mapped views with file locks | Unlock called on unmapped view | Keep lock operations on the file handle only |
| Reentrant unlock in loops or error paths | Same region unlocked multiple times | Use 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?