← 输入框|github.com|刷新
Skip to content

Restore legacy Windows support (XP, Win9x, WinCE) in the Windows VFS - #55

Open
guilt wants to merge 7 commits into
sqlite:masterfrom
guilt:master
Open

guilt wants to merge 7 commits into
sqlite:masterfrom
guilt:master

Conversation

@guilt

@guilt guilt commented Oct 12, 2026

Copy link
Copy Markdown

Restore legacy Windows support (XP, Win9x, WinCE) in the Windows VFS

Reverses the removal of Windows XP, Windows 9x/ME and Windows CE support
from the VFS layer in 3.54.0, without discarding anything that landed in
the meantime.

5 files changed, 2206 insertions(+), 375 deletions(-) across 7 commits,
based on 6e35c19f17.

What is restored

src/os_win.c, src/os_win.h and src/mutex_w32.c are put back to a
state where they build and run on Windows NT4/2000/XP, Windows 9x/ME and
Windows CE:

  • The aSyscall[] override table is back to its full 87 entries, with the
    wide-character and CE-only entries guarded so they compile to NULL
    thunks rather than vanishing.
  • SRWLOCK mutexes are replaced by CRITICAL_SECTION again, since SRWLOCK
    is Vista-and-later.
  • ANSI (A-suffixed) code paths are reinstated behind osIsNT().
  • SQLITE_WIN32_VOLATILE and SQLITE_OS_WINCE support return.

Nothing upstream added since the removal was reverted — the restore was
checked feature-by-feature against the current tree, and
SQLITE_WIN32_HAS_GETTICKCOUNT64 was added so GetTickCount64 is guarded
rather than assumed.

Bugs found in the restored code

Five real defects turned up while getting this working. The first two were
latent in the restored code; the next three only became visible once
binaries were inspected at the import-table level.

1. Calling-convention mismatch in the shims (crash).
winGetFileSizeEx and winSetFilePointerEx were declared __cdecl, but
the thunk table casts every entry through WINAPI (__stdcall). The
callee popped 4 bytes where the caller expected 8, so the caller's
&x was not the address the callee wrote to. The bogus file size then
caused a NULL write later in sqlite3_open. Declared WINAPI instead.

2. LPWSTR/LPCSTR mismatch in winDelete().
The converted filename was declared LPWSTR, but
winConvertFromUtf8Filename() only returns a wide string when
osIsNT() is true. On the ANSI path it returns a narrow char* that was
passed to the A-suffixed thunks uncast. Cast at all four use sites,
matching what winOpen() already does.

3. GetNativeSystemInfo broke every Win9x load.
The entry was the only one in the table with no availability guard, and
the table takes each API's address at static init — so the linker adds
the symbol to the import table whether or not any code calls it. The
loader then refuses to start the executable on Win9x before any runtime
guard could run. It was also dead: osGetNativeSystemInfo has no call
sites anywhere in the tree. Added SQLITE_WIN32_HAS_GETNATIVESYSTEMINFO
alongside the existing SQLITE_WIN32_HAS_GETTICKCOUNT64.

4. Same class of bug: GetSystemTimeAsFileTime and
TryEnterCriticalSection.

Both are absent on Windows 95 and both were statically imported.
GetSystemTimeAsFileTime is now behind
SQLITE_WIN32_HAS_GETSYSTEMTIMEASFILETIME, which widens the existing
Windows CE fallback (GetSystemTime + SystemTimeToFileTime) to cover
Windows 95 as well. That fallback was also missing an error check in
winRandomness() — SystemTimeToFileTime() can fail, and the old code
would have XORed uninitialized stack bytes into the entropy pool.
TryEnterCriticalSection is now behind SQLITE_MUTEX_HAVE_TRYENTER,
which folds SQLITE_OS_WINNT together with the version check and also
guards winMutex_isNt and the sqlite3_win32_is_nt() prototype.

Note that Windows 95 and Windows NT 4.0 both report _WIN32_WINNT as
0x0400, so SQLITE_OS_WINNT is what separates them. Every existing
configuration compiles to exactly the same code as before; only an
explicit SQLITE_OS_WINNT=0 build changes.

5. tool/win95gs.c — the MinGW CRT's own Win98 import.
With SQLite's own code clean, a MinGW-linked binary still would not load
on Windows 95. crt2.o calls __security_init_cookie() on every start,
and the only definition in the toolchain lives in an archive member that
also imports GetSystemTimeAsFileTime. No compiler flag avoids it — the
reference is from crt2.o, not from user code. This file defines the
four symbols that member provides so the linker never extracts it.

It is not part of the amalgamation and no makefile references it; it is
linked in by hand for a deliberate Windows 95 build. Two details are
load-bearing and fail silently by falling back to the archive member: the
C identifiers need two underscores so cdecl decoration yields the three
crt2.o asks for, and they must be cdecl rather than WINAPI.

Verification

test/testopen.c is a standalone smoke test for the Windows VFS with a
structured exception filter that reports exception code, faulting address
and read/write. That is what makes the calling-convention bug diagnosable
at all — without it an access violation just kills the process silently.
Its --ansi option sets sqlite3_os_type = 1 before the first database
access, which is the mechanism os_win.c documents for emulating Win98
on a modern host.

MSVC x86, nmake /f Makefile.msc:

testopen.exe --wal <db>          ALL PASSED
testopen.exe --ansi --wal <db>   ALL PASSED

The second is the first runtime exercise the restored ANSI paths have had.

MinGW-W64 i686-w64-mingw32-gcc 8.1.0 — 0 errors on all five targets,
only a pre-existing sigterm_handler warning from shell.c:

target flags post-Win95 imports
nt4 _WIN32_WINNT=0x0400 none
win95 +SQLITE_OS_WINNT=0 +OMIT_WAL +MAX_MMAP_SIZE=0 + win95gs.c none
win98 +SQLITE_OS_WINNT=0 +OMIT_WAL +MAX_MMAP_SIZE=0 none
xp _WIN32_WINNT=0x0501 GetNativeSystemInfo (correct)
vista _WIN32_WINNT=0x0600 + GetTickCount64 (correct)

The Win95 import table now contains only KERNEL32, msvcrt.dll and USER32,
and its PE header still declares OS version 4.0. MinGW is the toolchain
that matters here because it links msvcrt.dll rather than the
Vista-and-later ucrtbase.

Also clean: tclsh tool/renumber-win-thunk.tcl src/os_win.c, and
0 errors out of 13,780 testfixture tests across
attach, corrupt, io, lock, wal, wal2, wal3.

Limitations

  • Windows CE is compile-only. The CE branches are restored and build,
    but there is no CE toolchain or SDK here, so they have never executed.
  • A SQLITE_OS_WINNT=0 build must pass SQLITE_OMIT_WAL=1 and
    SQLITE_MAX_MMAP_SIZE=0. That is an existing upstream requirement —
    WAL and mmap rely on the NT kernel — and SQLite raises an #error if
    they are left on.
  • Untested on real legacy hardware; the Win9x behaviour is exercised by
    building against the legacy API surface and by the Win98 emulation mode
    above, not by running on an actual Windows 9x machine.

Commits

  1. 6c81103580 Restore legacy Windows support (XP, Win9x, WinCE) in the VFS layer
  2. 9b025c5122 Fix calling-convention mismatch in the GetFileSizeEx/SetFilePointerEx shims
  3. 9fdf6d1ae3 Fix LPWSTR/LPCSTR type mismatch in winDelete()'s ANSI branch
  4. 9b5d0aac39 Add a standalone smoke test for the Windows VFS, including the Win9x paths
  5. 422344d1cd Stop GetNativeSystemInfo from breaking Win9x loads
  6. 7b3b18041d Stop GetSystemTimeAsFileTime and TryEnterCriticalSection from breaking Win9x loads
  7. 8d5d67378e Add a Windows 95 build helper that defeats the MinGW CRT's Win98 import

guilt added 7 commits October 11, 2026 22:53
Revert the 3.54.0 removal of legacy Windows support so that the engine
again builds and runs on Windows XP, Windows 9x/ME and Windows CE.

* src/os_win.h: restore SQLITE_OS_WINNT, SQLITE_OS_WINCE and
  SQLITE_WIN32_VOLATILE definitions that os_win.c and mutex_w32.c
  depend on.

* src/os_win.c: restore the 87-entry aSyscall[] thunk table (up from
  the 58 entries left after the removal), including the ANSI Win32 API
  variants, WinCE paths, Cygwin helpers and the UUID/worker-thread
  entries.  Restore the osIsNT() NT-vs-9x dispatch and the ANSI
  branches of the file, directory, link and environment operations.
  Restore winGetFileSizeEx()/winSetFilePointerEx() wrappers that map
  the Ex APIs onto GetFileSize()/SetFilePointer() for platforms that
  lack them, and the non-UWP Heap*/CreateFileMapping* entries.

* src/mutex_w32.c: replace SRWLOCK with CRITICAL_SECTION so that the
  mutex implementation works on pre-Vista Windows.

Keep improvements that landed after the removal, including
SQLITE_ENABLE_SETLK_TIMEOUT handling, the UWP app-container paths,
SQLITE_WIN32_USE_UUID, the GetTickCount64 entropy source and the
SQLITE_WIN32_GETVERSIONEX / NTDDI_VERSION feature detection.  A new
SQLITE_WIN32_HAS_GETTICKCOUNT64 macro selects GetTickCount64 on Vista
and later and falls back to GetTickCount() on XP and 9x.

tool/renumber-win-thunk.tcl reports the table as consistent.
… shims

The winGetFileSizeEx() and winSetFilePointerEx() wrappers that were
restored to emulate the GetFileSizeEx()/SetFilePointerEx() APIs on
Windows 9x, NT4 and 2000 were declared with the default (cdecl)
calling convention, but the aSyscall[] thunk table invokes them through
function pointers cast to WINAPI (__stdcall):

  #define osGetFileSizeEx \
    ((BOOL(WINAPI*)(HANDLE,PLARGE_INTEGER))aSyscall[25].pCurrent)

Calling a cdecl function through a stdcall pointer makes the callee
return without popping its arguments, so the caller's stack stays eight
bytes short.  Every subsequent frame is then misaligned: in winFileSize()
the address of the local LARGE_INTEGER passed to osGetFileSizeEx() was
eight bytes off from the one the callee wrote to, so the file size was
read back as garbage (a heap pointer rather than the real size) and the
pager subsequently crashed with an access violation on the very first
write transaction.

Declare both shims WINAPI so they match the thunk table casts.

Verified with an MSVC x86 build: CREATE TABLE, INSERT, SELECT and
"PRAGMA journal_mode=WAL" now all succeed against a real database file.
winDelete() declares its converted filename as LPWSTR, but
winConvertFromUtf8Filename() only returns a wide string when osIsNT()
is true.  On Windows 9x/ME -- where the ANSI API set is used instead --
it returns a narrow char* that was being handed to the A-suffixed
osGetFileAttributesA() and osDeleteFileA() thunks without a cast, which
compilers flag as an incompatible pointer type and which relies on the
runtime representation happening to match.

Cast the converted filename at each use site, matching what winOpen()
already does in both of its branches.  This is the last warning from a
-Wall gcc build of the restored VFS, so the legacy configurations now
compile cleanly:

  default (_WIN32_WINNT=0x0502)  OK
  XP      (_WIN32_WINNT=0x0501)  OK
  Vista   (_WIN32_WINNT=0x0601)  OK
  Win9x   (_WIN32_WINNT=0x0400, SQLITE_OS_WINNT=0, OMIT_WAL, no mmap) OK

Verified with an MSVC x86 build: CREATE TABLE, INSERT, SELECT and
"PRAGMA journal_mode=WAL" all succeed.
…paths

test/testopen.c is a small self-contained C program that drives the
Windows VFS end to end: CREATE TABLE, INSERT, UPDATE, SELECT, DELETE,
REINDEX, VACUUM, PRAGMA integrity_check and, with --wal, a trip through
WAL mode.  Every database call is wrapped in a structured exception
filter that prints the exception code, faulting address and whether the
fault was a read or a write.  That is the only way to get anything
useful out of the calling-convention bugs this work has been chasing;
without it an access violation inside the VFS just kills the process
with no diagnostic at all, which is how the GetFileSizeEx mismatch
first showed up.

The interesting option is --ansi.  The restored Windows 9x/ME and
NT4/2000 code paths in os_win.c are all selected by osIsNT(), and on
any Windows release from XP onward osIsNT() is the compile-time
constant 1, so the A-suffixed branches are unreachable on a modern
machine and the normal test suite never touches them.  os_win.c already
anticipates this: it declares sqlite3_os_type as a process global
rather than a static when SQLITE_TEST is defined, and the comment above
it notes that the test fixture "can manually set this value to 1 to
emulate Win98 behavior".  Setting it to 1 before the first database
access makes osIsNT() false and routes everything through the ANSI
APIs.

SQLITE_WIN32_GETVERSIONEX=1 is required alongside it, because on modern
SDKs GetVersionEx() is deprecated and SQLITE_WIN32_GETVERSIONEX
therefore defaults to 0, which makes osIsNT() the literal (1) and
bypasses sqlite3_os_type entirely.

Building with SQLITE_WIN32_NO_WIDE does NOT work for this and is not
used here.  It leaves SQLITE_WIN32_HAS_WIDE undefined, which zeroes out
the W-suffixed syscall entries, but it does not change the value of
SQLITE_WIN32_GETVERSIONEX, so osIsNT() still returns 1 and the first
call into the NT branch reaches a now-NULL GetFullPathNameW thunk and
faults at address 0.

Both variants build warning-free with MSVC x86 and pass:

    cl -DSQLITE_OS_WIN=1 -DSQLITE_THREADSAFE=1 ...          ALL PASSED
    cl ... -DSQLITE_TEST -DSQLITE_WIN32_GETVERSIONEX=1 ...  ALL PASSED

The second of those is the first runtime exercise the restored legacy
code paths have ever had.
GetNativeSystemInfo first appeared in Windows XP, but its entry in the
aSyscall[] table was the only one with no availability guard.  Because
the table takes the address of every syscall at static initialisation,
the linker adds GetNativeSystemInfo to the executable's import table
whether or not any code path calls it, so a binary built for Windows
9x/ME or NT4 refuses to load there at all: the loader cannot satisfy
the import before main() ever runs.  A runtime guard inside the table
is not sufficient for that reason.

It was also dead weight.  osGetNativeSystemInfo is defined but never
referenced anywhere in the tree, so removing the import costs nothing.

Add SQLITE_WIN32_HAS_GETNATIVESYSTEMINFO alongside the existing
SQLITE_WIN32_HAS_GETTICKCOUNT64, defaulting to true when _WIN32_WINNT
is at least 0x0501 or is not specified at all, and use it to select
between the real symbol and a NULL thunk.  The entry stays at index 69
so no renumbering is required.

Verified with i686-w64-mingw32-gcc 8.1.0, which is the toolchain that
matters here because it links msvcrt.dll rather than the Vista-and-later
ucrtbase.  Building shell.c + sqlite3.c for three targets gives 0
errors, and objdump of the resulting import tables shows:

    _WIN32_WINNT=0x0400   GetNativeSystemInfo absent
    _WIN32_WINNT=0x0501   GetNativeSystemInfo present
    _WIN32_WINNT=0x0600   GetNativeSystemInfo present, GetTickCount64 present

The 0x0400 binary now imports only KERNEL32, msvcrt.dll and USER32, and
carries an OS version of 4.0 in its PE header.  All three targets still
build warning-free and run CREATE/INSERT/SELECT plus WAL mode.

The MSVC build is unaffected because its SDK sets NTDDI_VERSION at or
above NTDDI_WINBLUE, which selects the same table entry as before.
Confirmed with an MSVC x86 build of test/testopen.c in both the default
and Win98-emulation modes, tclsh tool/renumber-win-thunk.tcl
src/os_win.c, and 0 errors out of 13,780 testfixture tests.
…g Win9x loads

These are the same class of defect as GetNativeSystemInfo in the previous
commit: an API that the target platform lacks ends up in the executable's
import table anyway, so the loader refuses to start the program before any
runtime guard inside SQLite can run.

GetSystemTimeAsFileTime first appeared in Windows 98 and Windows NT 4.0,
so Windows 95 does not have it.  Its thunk table entry was guarded only by
!SQLITE_OS_WINCE.  Replace that with SQLITE_WIN32_HAS_GETSYSTEMTIMEASFILETIME.

Distinguishing the two platforms needs care, because Windows 95 and
Windows NT 4.0 both report _WIN32_WINNT as 0x0400.  SQLITE_OS_WINNT is
what separates them, so the macro turns off only for a build that both
opts out of NT with SQLITE_OS_WINNT=0 and targets a version below 0x0410.
That means every existing configuration keeps compiling the same code as
before, and only an explicit Win95 build changes.

The fallback is not new.  winCurrentTimeInt64() already had one, written
for Windows CE, that builds the FILETIME out of GetSystemTime plus
SystemTimeToFileTime.  Both of those are unconditional thunk entries and
both exist on Windows 95, so the WinCE guard is simply widened to cover
this case too, and the awkward "if it fails it returns zero" comment
becomes a real check that returns SQLITE_ERROR.  winRandomness() gained
the same fallback.  Unlike GetSystemTimeAsFileTime(), SystemTimeToFileTime()
can fail, and the old code there would have XORed uninitialized stack
bytes into the entropy pool, so it now skips the contribution on failure.

TryEnterCriticalSection first appeared in Windows NT 4.0 Service Pack 3
and Windows 98, and was never in Windows 95.  mutex_w32.c guarded its use
with _WIN32_WINNT >= 0x0400, which is true of a Windows 95 build as well,
so the code compiled in and dragged the import along even though the
surrounding if( winMutex_isNt && ... ) could never be true there.  Fold
that condition together with SQLITE_OS_WINNT into a single
SQLITE_MUTEX_HAVE_TRYENTER and guard the winMutex_isNt variable and the
sqlite3_win32_is_nt() prototype with it too, since nothing else in the
file uses them.

Verified with i686-w64-mingw32-gcc 8.1.0.  Note that a SQLITE_OS_WINNT=0
build must also pass SQLITE_OMIT_WAL=1 and SQLITE_MAX_MMAP_SIZE=0; that
is an existing upstream requirement, since WAL and mmap rely on the NT
kernel, and SQLite raises an #error if they are left on.  All five of
these configurations build with 0 errors and only the pre-existing
sigterm_handler warning from shell.c:

    _WIN32_WINNT=0x0400                     nt4
    _WIN32_WINNT=0x0400 SQLITE_OS_WINNT=0   win95
    _WIN32_WINNT=0x0410 SQLITE_OS_WINNT=0   win98
    _WIN32_WINNT=0x0501                     xp
    _WIN32_WINNT=0x0600                     vista

objdump of the import tables shows TryEnterCriticalSection absent from the
win95 and win98 binaries and present in the other three, and all five run
CREATE/INSERT/SELECT/VACUUM/integrity_check.  GetTickCount64 appears only
in the vista binary, as before.

One caveat worth recording: GetSystemTimeAsFileTime is still listed as
imported in every MinGW binary, including the win95 one.  That is not
SQLite's doing.  A trivial program consisting of nothing but
"int main(void){ return 0; }" built with the same toolchain imports it
too, at the same hint number, so it comes from the MinGW CRT startup
code.  A MinGW-linked executable therefore cannot run on plain Windows 95
no matter what SQLite does.  This change still removes SQLite's own
dependence on the API and collapses the WinCE and Win95 fallbacks into
one path, which is what matters for any other toolchain.

The MSVC build is unaffected, since its SDK sets _WIN32_WINNT well above
0x0410.  Confirmed with an MSVC x86 build of test/testopen.c in both the
default and Win98-emulation modes, tclsh tool/renumber-win-thunk.tcl
src/os_win.c, and 0 errors out of 13,780 testfixture tests.
SQLite's own Windows code no longer imports anything introduced after
Windows 95.  A MinGW-W64 linked executable still could not load on
Windows 95, though, because of the toolchain rather than the database
engine.

crt2.o, the MinGW C runtime startup, calls __security_init_cookie() on
every program start in order to seed the stack-protector guard.  The only
definition of that function in the toolchain lives in an archive member
of libmingw32.a -- lib32_libmingw32_a-gs_support.o -- that also imports
GetSystemTimeAsFileTime, an API that first appeared in Windows 98.  A
static import has to be satisfied by the loader before main() runs, so
the executable refused to start on Windows 95 regardless of what SQLite
did.  No compiler flag avoids it: the reference comes from crt2.o, not
from user code, so -fno-stack-protector and friends make no difference.

tool/win95gs.c defines the four symbols that archive member provides, so
the linker resolves them locally and never extracts the member at all.
The Win98-only import therefore never reaches the import table.  The seed
is built out of GetSystemTime, SystemTimeToFileTime, the process and
thread ids, the tick count and the performance counter -- every one of
which exists on Windows 95.

Two details in there are load-bearing and worth recording, because getting
either wrong fails silently by falling back to the archive member:

  * The C identifiers have two underscores, so that i386 cdecl decoration
    yields the three that crt2.o asks for.

  * They are cdecl, not WINAPI.  A stdcall definition decorates to
    name@N and will not match crt2.o's reference.

This file is not part of the amalgamation and no makefile references it,
so an ordinary build is unaffected.  It is linked in by hand, only for a
build that is deliberately targeting Windows 95:

    i686-w64-mingw32-gcc -O2 -DSQLITE_OS_WIN=1 -DSQLITE_OS_WINNT=0 \
        -D_WIN32_WINNT=0x0400 -DWINVER=0x0400 \
        -DSQLITE_OMIT_WAL=1 -DSQLITE_MAX_MMAP_SIZE=0 \
        -o sqlite3.exe shell.c sqlite3.c tool/win95gs.c -ladvapi32

Verified with i686-w64-mingw32-gcc 8.1.0.  objdump of the resulting import
table shows it containing only KERNEL32, msvcrt.dll and USER32, with no
GetSystemTimeAsFileTime, GetNativeSystemInfo, TryEnterCriticalSection or
GetTickCount64, and the PE header still declaring OS version 4.0.  The
binary builds with 0 errors and runs CREATE TABLE, INSERT, SELECT, VACUUM
and PRAGMA integrity_check.  journal_mode=WAL correctly reports "delete",
since Windows 95 has neither WAL nor memory mapped files and the build
passes SQLITE_OMIT_WAL and SQLITE_MAX_MMAP_SIZE=0 accordingly.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant