Windows/SQLite: Can supervised quiescence prove safe resource release when native failures are hidden?

Carlos Garcia 0 Reputation points
2026-11-10T17:31:55.93+00:00

I’m developing a paper/simulation-only Python research platform on Windows and would appreciate guidance from engineers experienced with Windows native resource lifetimes, SQLite, and process containment.

Environment

  • Windows 11 25H2, build 26200.9711
  • CPython 3.13.15
  • SQLite 3.50.4
  • Multiple SQLite connection owners, including a registry, retained journal, and budget ledger

Technical problem

Our original acceptance requirements call for independently verifiable disposal of native SQLite resources, including file handles, byte-range locks, mapped views, and pending I/O.

Some SQLite Windows VFS paths may suppress native release failures. We investigated ETW and Filter Manager minifilter capabilities, but have not established complete, independently correlated observability across all resource types.

We’re considering an alternative: supervised quiescence, with all database access fenced, disposable worker processes fully accounted for, and terminal storage postconditions independently verified before further work is permitted.

Questions

  1. Is supervised end-state quiescence technically defensible when individual native release outcomes cannot all be observed?
  2. Which supported Windows mechanisms could verify outstanding file handles, byte-range locks, mapped views, and pending I/O?
  3. What would be required to enforce an access fence across all SQLite owners, including failed initialization and inherited or duplicated handles?
  4. What limitations would prevent supervised quiescence from providing equivalent safety guarantees?

I’m seeking technical guidance and official documentation before deciding whether to propose a formal change to our acceptance requirements.

No production databases, live trading, or destructive process experiments are involved.

Thank you for your time and guidance. I’m developing a paper/simulation-only Python research platform on Windows and would appreciate guidance from engineers experienced with Windows native resource lifetimes, SQLite, and process containment.

Environment

  • Windows 11 25H2, build 26200.9711
  • CPython 3.13.15
  • SQLite 3.50.4
  • Multiple SQLite connection owners, including a registry, retained journal, and budget ledger

Technical problem

Our original acceptance requirements call for independently verifiable disposal of native SQLite resources, including file handles, byte-range locks, mapped views, and pending I/O.

Some SQLite Windows VFS paths may suppress native release failures. We investigated ETW and Filter Manager minifilter capabilities, but have not established complete, independently correlated observability across all resource types.

We’re considering an alternative: supervised quiescence, with all database access fenced, disposable worker processes fully accounted for, and terminal storage postconditions independently verified before further work is permitted.

Questions

  1. Is supervised end-state quiescence technically defensible when individual native release outcomes cannot all be observed?
  2. Which supported Windows mechanisms could verify outstanding file handles, byte-range locks, mapped views, and pending I/O?
  3. What would be required to enforce an access fence across all SQLite owners, including failed initialization and inherited or duplicated handles?
  4. What limitations would prevent supervised quiescence from providing equivalent safety guarantees?

I’m seeking technical guidance and official documentation before deciding whether to propose a formal change to our acceptance requirements.

No production databases, live trading, or destructive process experiments are involved.

Thank you for your time and guidance.

Windows development | Windows API - Win32
0 comments No comments

2 answers

Sort by: Most helpful
  1. Gatlin Le (WICLOUD CORPORATION) 1,390 Reputation points Microsoft External Staff Moderator
    2026-11-11T03:29:30.83+00:00

    Hi @Carlos Garcia ,

    Supervised quiescence can be defended if the claim is narrowed to three things: every authorized owner was started inside the containment boundary and has exited, no known owner can reopen the files, and the storage postconditions you defined passed afterwards. It cannot stand in for proof that each individual native release succeeded.

    To see how the mechanisms actually behave, I ran a small repro on Windows 11 Pro x64 build 26300 with CPython 3.13.12 and SQLite 3.50.4. The databases were disposable, in WAL and DELETE mode with synchronous=FULL. Each worker was created suspended, assigned to a job that only had JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE set, and then resumed. The test job was nested under an existing job. Since this is not your build, your Python version, or your application, the results describe mechanism behavior rather than a root cause in your platform.

    The TotalProcesses=2 and ActiveProcesses=1 pattern reproduced right after the worker exited. The remaining member was conhost.exe, which appeared because the console worker was launched with CREATE_NO_WINDOW. It did not appear with DETACHED_PROCESS. A venv worker added one more member, because .venv\Scripts\python.exe started the base python.exe as its child. On this build, the supervisor keeping the worker's process handle open did not hold ActiveProcesses above 0.

    CREATE_BREAKAWAY_FROM_JOB failed with ERROR_ACCESS_DENIED (5). A child created through WMI was outside the job and survived when the job was killed, and a process that had opened the database before the job existed kept it open as well. After a file handle was duplicated into a process outside the job, ActiveProcesses reached 0 while the database remained open, and RmGetList reported that outside process.

    While an owner had the files open, an exclusive open (CreateFileW with dwShareMode set to 0) on the database, -wal, -shm, and -journal failed with ERROR_SHARING_VIOLATION (32). After close or kill, it succeeded. A non-blocking LockFileEx on SQLite's lock bytes failed with ERROR_LOCK_VIOLATION (33) during an open transaction. The first successful probe came about 3 ms after the terminate call, a figure that includes the wait and the probe itself, so it is not the exact release time. With mmap_size enabled, SetEndOfFile failed with ERROR_USER_MAPPED_FILE (1224) on the database and on -shm while a view existed, and it succeeded once the worker exited. In a case where only the view was left, after its file and mapping handles had been closed, the exclusive open succeeded even though truncation still failed.

    The SQLite file handles in the worker were synchronous (FILE_SYNCHRONOUS_IO_NONALERT). During a write loop, the process handle became signaled about 77 ms after TerminateProcess was called. I did not capture a trace showing a request pending at that moment, so that part is still unverified. In both WAL and DELETE mode, clean close and forced kill both reopened with integrity_check returning ok. Committed rows were kept, and uncommitted changes were rolled back. The kill path left -wal and -shm, or -journal, on disk.

    So is end-state quiescence defensible? Yes, as long as the claim is about contained owners and verified postconditions, not about how each release ended. The Remarks section of TerminateProcess is the documented basis. Termination is asynchronous, a terminated process cannot exit until all pending I/O has been completed or canceled, and you confirm termination by waiting on the process handle. Once that handle is signaled, no pending I/O from the process remains, but the handle does not tell you how each request ended.

    No single supported mechanism verifies every resource type. An exclusive open catches open handles, but not a view that outlived its handles. SetEndOfFile catches a remaining view, and the Remarks section of SetEndOfFile explains why: all views must be unmapped and the mapping object closed first. Because it truncates the file, this probe is only suitable for disposable data. A non-blocking lock probe catches held byte ranges. The Remarks section of LockFile states two relevant points. First, the time the system takes to release locks after termination depends on available resources. Second, a byte-range lock does not prevent reads or writes through a mapped view. The lock probe therefore needs a deadline and cannot replace a mapping check. RmGetList only reports processes using resources registered with its session. For pending I/O, waiting on the process handle is the supported boundary. CancelIo does nothing on a handle opened without FILE_FLAG_OVERLAPPED.

    An access fence across every SQLite owner has to exist from launch time. A job covers only processes that are assigned to it or created by its members through CreateProcess. The Managing Processes in Jobs section of Job Objects also notes that children created through Win32_Process.Create are not associated with the job, which matches what I saw. In practice, the supervisor starts every opener inside the job, including openers that fail initialization. Database handles are created non-inheritable and are never passed with DuplicateHandle. Nothing starts an opener through WMI, a service, or a scheduler. When several owners open the same database, the fence only holds if every one of them is launched inside the same job, so the claim for each database has to cover all of its openers rather than one owner at a time. Any process that existed before the fence needs positive evidence that it does not hold the files. To pin down the termination domain before enabling kill on close, enumerate members with JobObjectBasicProcessIdList (JOBOBJECT_BASIC_PROCESS_ID_LIST), which for a nested job also includes processes in its child jobs. Then attribute each image and parent PID. The counters are not enough on their own, since TotalProcesses in JOBOBJECT_BASIC_ACCOUNTING_INFORMATION also counts processes that have already terminated.

    The claim should explicitly exclude whatever keeps quiescence from being equivalent. That covers processes outside the job (pre-existing, created through WMI, or holding a duplicated handle), views an exclusive open cannot see, the outcome of each I/O request at kill time, and lock release timing. It also covers durability beyond what SQLite and the flush calls guarantee, and any open after the final probe, since each probe only reflects one point in time. Sidecar files left after a kill do not by themselves mean an owner is still alive. A clean shutdown can only be told apart from a forced one by an acknowledgement the worker sends after closing the connection.

    Everything above is what I could observe and verify within the scope of this Microsoft Q&A forum, on the environment described and with documented APIs. I hope these results give you a firmer basis for deciding on the acceptance requirement change, with a clearer line between what the repro confirmed on this build, what rests on the documentation alone, and what is still unverified, such as the outcome of each I/O request at kill time.

    If you found my response helpful or informative, I would greatly appreciate it if you could follow this guidance so that others in the community facing a similar issue can find it more easily and help each other.

    Thank you.

    Was this answer helpful?


  2. AI answer

    2026-11-10T17:33:20.8166667+00:00

    Supervised quiescence is defensible only if the acceptance requirement is changed from “prove every native release succeeded” to “prove all authorized owners have terminated, prevent new owners, and verify defined storage postconditions”; it is not equivalent to complete observation of every handle, lock, mapping, or I/O release.

    1. Put every SQLite-capable process in one non-breakaway Job Object.
      • Create the job before launching workers.
      • Set JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE.
      • Do not set JOB_OBJECT_LIMIT_BREAKAWAY_OK or JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK.
      • Launch workers so they are associated with the job before they can initialize SQLite. Creating them suspended, assigning them with AssignProcessToJobObject, and only then resuming them closes the launch/assignment race.
      • Treat assignment failure as initialization failure and terminate the still-suspended process.
      Child processes normally inherit job membership. With neither breakaway flag enabled, the job can contain the process tree, although software that expects to create its own job may fail ^1^. JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE terminates associated processes when the final job handle closes ^2^.
    2. Prevent unintended handle propagation.
      • Create database and control handles as non-inheritable.
      • Use PROC_THREAD_ATTRIBUTE_HANDLE_LIST when a child requires a small, explicit handle set.
      • Do not use unrestricted bInheritHandles=TRUE.
      • Audit any use of DuplicateHandle, because duplicated handles can refer to the same file object from another process.
      Windows supports explicit handle allow-listing during process creation ^3^. Inherited and duplicated file handles can refer to one shared file object, so counting only independently opened files is insufficient ^4^.
    3. Close the logical access gate before shutdown.
      • Atomically change supervisor state to QUIESCING.
      • Reject new registry, journal, and ledger operations before connection creation.
      • Drain admitted operations and stop creating workers.
      • Require every owner, including partially initialized owners, to register before it can obtain a database path or connection.
      • Keep one supervisor outside the disposable job; otherwise closing the last job handle cannot be controlled reliably.
    4. Add an OS-level fence if protection from non-cooperating local processes is required.
      • An application mutex or Python lock fences only cooperating code.
      • A restrictive CreateFile share mode can block conflicting future opens, but cannot revoke handles already open. Share settings remain effective until each open handle closes ^5^.
      • Therefore, acquire any exclusive sentinel only after existing owners have drained, and cover the database plus applicable journal/WAL/SHM files. Whether this works with SQLite must be tested against the exact access/share flags used by your VFS; the supplied documentation does not establish those flags for SQLite 3.50.4.
    5. Perform graceful SQLite shutdown, then enforce a deadline.
      • End active statements and transactions.
      • Close every SQLite connection.
      • In WAL mode, the last clean sqlite3_close() can checkpoint and remove -wal and -shm; abnormal termination can leave those artifacts ^6^.
      • If workers miss the deadline, terminate the job and classify the run as forced quiescence rather than clean disposal.
    6. Verify zero live owners before reopening storage.
      • Query job state/accounting and retain process handles until every process has terminated.
      • An I/O-completion-port JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO is useful, but ordinary job completion messages are not universally guaranteed; corroborate the notification by querying job state and waiting on the tracked process handles ^1^ ^7^.
      • Only after that boundary should a designated verifier reopen the database and evaluate the formally specified SQLite/storage postconditions.

    What Windows can and cannot verify

    Resource Supported evidence Limitation
    File handles Restart Manager can register filenames and use RmGetList to identify applications and services using them ^8^. Useful corroboration, not a complete kernel-wide negative proof. It covers registered resources and identified applications/services.
    Byte-range locks A nonblocking LockFileEx probe can test a precisely known range. Process termination or closing the file releases outstanding locks ^9^. Every relevant SQLite lock range and mode must be known. Release after process termination can be delayed by system resources. Locks also do not prevent access through mapped views.
    Mapped views Owners can explicitly call UnmapViewOfFile; complete mapping closure requires all views to be unmapped and mapping handles closed ^10^. QueryWorkingSetEx answers questions about specified virtual addresses, not “does any process map this file?” ^11^. No cited public interface supplies complete system-wide negative proof by pathname.
    Pending I/O An owner can track each OVERLAPPED request and wait with GetOverlappedResult; CancelIoEx can request cancellation for a handle ^12^. Cancellation is process-local, does not wait, and races with normal completion. Every request’s final completion status must still be collected. Job I/O accounting is cumulative, not a list of outstanding requests.
    Durable writes For application-owned mapped writes, use FlushViewOfFile followed by FlushFileBuffers; FlushViewOfFile alone does not flush metadata or guarantee physical completion ^13^. A supervisor cannot retroactively perform this sequence without the required mapping base address and writable file handle. It also does not prove SQLite’s logical transaction state.

    Conditions that prevent equivalence

    Quiescence cannot equal per-operation disposal proof if any of these remain possible:

    • A database-capable process can escape the job, pre-exist outside it, or be launched through an uncontained path.
    • A handle can be inherited or duplicated into an untracked process.
    • Another account, service, or process can open the database after the logical fence closes.
    • The acceptance requirement includes proof that each CloseHandle, unmap, unlock, cancellation, or flush completed successfully—not merely that no authorized owner remains.
    • Lock ranges, mapping addresses, or individual asynchronous requests are unknown.
    • Forced termination is treated as equivalent to clean SQLite shutdown.
    • Storage durability requires evidence through the physical device/cache stack beyond the documented flush contract.

    Accordingly, the formal claim should be narrowly stated: containment plus zero-owner verification and explicit storage postconditions establish a controlled quiescent state, but do not reconstruct or prove every hidden native release outcome.


    References

    1. Job Objects
    2. JOBOBJECT_BASIC_LIMIT_INFORMATION structure (winnt.h)
    3. Inheritance
    4. File Handles
    5. CreateFileW function (fileapi.h)
    6. WAL-mode File Format - SQLite
    7. JOBOBJECT_ASSOCIATE_COMPLETION_PORT structure (winnt.h)
    8. Using Restart Manager with a Primary Installer
    9. LockFileEx function (fileapi.h)
    10. Closing a File Mapping Object
    11. QueryWorkingSetEx function (psapi.h)
    12. CancelIoEx function (ioapiset.h)
    13. FlushViewOfFile function (memoryapi.h)
    AI-generated content may be incorrect. Read our transparency notes for more information.

    Was this answer helpful?

Your answer

Answers can be marked as 'Accepted' by the question author and 'Recommended' by moderators, which helps users know the answer solved the author's problem.