1
0
mirror of https://github.com/sqlite/sqlite.git synced 2025-07-29 08:01:23 +03:00

Fix a long-standing filename digest computation bug in the OPFS SAHPool VFS which caused all VFS-stored filenames to have a digest value of 0. See [/forumpost/042d53c928382021] and for full details.

FossilOrigin-Name: 493cbe74504e8eb1ca8f2edf49fdab6bebc7fe36ffab06932a4b8c5a4eea86cd
This commit is contained in:
stephan
2025-03-16 14:05:42 +00:00
6 changed files with 326 additions and 19 deletions

View File

@ -79,6 +79,48 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
capi.SQLITE_OPEN_MAIN_JOURNAL |
capi.SQLITE_OPEN_SUPER_JOURNAL |
capi.SQLITE_OPEN_WAL;
const FLAG_COMPUTE_DIGEST_V2 = capi.SQLITE_OPEN_MEMORY
/* Part of the fix for
https://github.com/sqlite/sqlite-wasm/issues/97
Summary: prior to version 3.50.0 computeDigest() always computes
a value of [0,0] due to overflows, so it does not do anything
useful. Fixing it invalidates old persistent files, so we
instead only fix it for files created or updated since the bug
was discovered and fixed.
This flag determines whether we use the broken legacy
computeDigest() or the v2 variant. We only use this flag for
newly-created/overwritten files. Pre-existing files have the
broken digest stored in them so need to continue to use that.
What this means, in terms of db file compatibility between
versions:
- DBs created with versions older than this fix (<3.50.0)
can be read by post-fix versions. Such DBs which are written
to in-place (not replaced) by newer versions can still be read
by older versions, as the affected digest is only modified
when the SAH slot is assigned to a given filename.
- DBs created with post-fix versions will, when read by a pre-fix
version, be seen as having a "bad digest" and will be
unceremoniously replaced by that pre-fix version. When swapping
back to a post-fix version, that version will see that the file
entry is missing the FLAG_COMPUTE_DIGEST_V2 bit so will treat it
as a legacy file.
This flag is stored in the same memory as the various
SQLITE_OPEN_... flags and we must be careful here to not use a
flag bit which is otherwise relevant for the VFS.
SQLITE_OPEN_MEMORY is handled by sqlite3_open_v2() and friends,
not the VFS, so we'll repurpose that one. If we take a
currently-unused bit and it ends up, at some later point, being
used, we would have to invalidate existing VFS files in order to
move to another bit. Similarly, if the SQLITE_OPEN_MEMORY bit
were ever reassigned (which it won't be!), we'd invalidate all
VFS-side files.
*/;
/** Subdirectory of the VFS's space where "opaque" (randomly-named)
files are stored. Changing this effectively invalidates the data
@ -329,6 +371,7 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
xOpen: function f(pVfs, zName, pFile, flags, pOutFlags){
const pool = getPoolForVfs(pVfs);
try{
flags &= ~FLAG_COMPUTE_DIGEST_V2;
pool.log(`xOpen ${wasm.cstrToJs(zName)} ${flags}`);
// First try to open a path that already exists in the file system.
const path = (zName && wasm.peek8(zName))
@ -624,7 +667,8 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
const fileDigest = new Uint32Array(HEADER_DIGEST_SIZE / 4);
sah.read(fileDigest, {at: HEADER_OFFSET_DIGEST});
const compDigest = this.computeDigest(this.#apBody);
const compDigest = this.computeDigest(this.#apBody, flags);
//warn("getAssociatedPath() flags",'0x'+flags.toString(16), "compDigest", compDigest);
if(fileDigest.every((v,i) => v===compDigest[i])){
// Valid digest
const pathBytes = this.#apBody.findIndex((v)=>0===v);
@ -633,6 +677,7 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
// leaving stale db data laying around.
sah.truncate(HEADER_OFFSET_DATA);
}
//warn("getAssociatedPath() flags",'0x'+flags.toString(16), "compDigest", compDigest,"pathBytes",pathBytes);
return pathBytes
? textDecoder.decode(this.#apBody.subarray(0,pathBytes))
: '';
@ -655,10 +700,17 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
if(HEADER_MAX_PATH_SIZE <= enc.written + 1/*NUL byte*/){
toss("Path too long:",path);
}
if(path && flags){
/* When creating or re-writing files, update their digest, if
needed, to v2. We continue to use v1 for the (!path) case
(empty files) because there's little reason not to use a
digest of 0 for empty entries. */
flags |= FLAG_COMPUTE_DIGEST_V2;
}
this.#apBody.fill(0, enc.written, HEADER_MAX_PATH_SIZE);
this.#dvBody.setUint32(HEADER_OFFSET_FLAGS, flags);
const digest = this.computeDigest(this.#apBody);
const digest = this.computeDigest(this.#apBody, flags);
//console.warn("setAssociatedPath(",path,") digest",digest);
sah.write(this.#apBody, {at: 0});
sah.write(digest, {at: HEADER_OFFSET_DIGEST});
sah.flush();
@ -679,15 +731,22 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
metadata for each file as a validation check. Changing this
algorithm invalidates all existing databases for this VFS, so
don't do that.
See the docs for FLAG_COMPUTE_DIGEST_V2 for more details.
*/
computeDigest(byteArray){
let h1 = 0xdeadbeef;
let h2 = 0x41c6ce57;
for(const v of byteArray){
h1 = 31 * h1 + (v * 307);
h2 = 31 * h2 + (v * 307);
computeDigest(byteArray, fileFlags){
if( fileFlags & FLAG_COMPUTE_DIGEST_V2 ){
let h1 = 0xdeadbeef;
let h2 = 0x41c6ce57;
for(const v of byteArray){
h1 = Math.imul(h1 ^ v, 2654435761);
h2 = Math.imul(h2 ^ v, 104729);
}
return new Uint32Array([h1>>>0, h2>>>0]);
}else{
/* this is what the buggy legacy computation worked out to */
return new Uint32Array([0,0]);
}
return new Uint32Array([h1>>>0, h2>>>0]);
}
/**