From 7888e49c00ed03822ae899463ab68f9a09661e60 Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Sun, 5 Dec 2021 14:05:32 -0500 Subject: [PATCH] Fix race condition when loading from selfie All the auxiliary data structures must be fully loaded before the data structure used as entry point is populated. The race condition could lead to a case of the entry point data structure being populated while the auxiliary data structures are still unpopulated, potentially causing exceptions to be thrown at launch when the static network filtering engine is queried. I haven't been able to reproduce such exceptions -- but it could happen on browsers which do not support being suspended at launch time (i.e. chromium-based browsers). Additionally, added convenience methods to easily serialize/unserialize when SNFE is used as a npm package. --- src/js/static-net-filtering.js | 99 ++++++++++++++++++++-------------- 1 file changed, 59 insertions(+), 40 deletions(-) diff --git a/src/js/static-net-filtering.js b/src/js/static-net-filtering.js index 231a92bb6..0eaa7eaf1 100644 --- a/src/js/static-net-filtering.js +++ b/src/js/static-net-filtering.js @@ -3580,9 +3580,6 @@ FilterContainer.prototype.prime = function() { FilterContainer.prototype.reset = function() { this.processedFilterCount = 0; this.acceptedCount = 0; - this.rejectedCount = 0; - this.allowFilterCount = 0; - this.blockFilterCount = 0; this.discardedCount = 0; this.goodFilters.clear(); this.badFilters.clear(); @@ -3793,11 +3790,11 @@ FilterContainer.prototype.toSelfie = function(storage, path) { return Promise.all([ storage.put( - `${path}/FilterHostnameDict.trieContainer`, + `${path}/destHNTrieContainer`, destHNTrieContainer.serialize(sparseBase64) ), storage.put( - `${path}/FilterOrigin.trieContainer`, + `${path}/origHNTrieContainer`, origHNTrieContainer.serialize(sparseBase64) ), storage.put( @@ -3818,9 +3815,6 @@ FilterContainer.prototype.toSelfie = function(storage, path) { version: this.selfieVersion, processedFilterCount: this.processedFilterCount, acceptedCount: this.acceptedCount, - rejectedCount: this.rejectedCount, - allowFilterCount: this.allowFilterCount, - blockFilterCount: this.blockFilterCount, discardedCount: this.discardedCount, bitsToBucketIndices: this.bitsToBucketIndices, buckets: bucketsToSelfie(), @@ -3830,27 +3824,35 @@ FilterContainer.prototype.toSelfie = function(storage, path) { ]); }; +FilterContainer.prototype.serialize = async function() { + const selfie = []; + const storage = { + put(name, data) { + selfie.push([ name, data ]); + } + }; + await this.toSelfie(storage, ''); + return JSON.stringify(selfie); +}; + /******************************************************************************/ -FilterContainer.prototype.fromSelfie = function(storage, path) { +FilterContainer.prototype.fromSelfie = async function(storage, path) { if ( storage instanceof Object === false || storage.get instanceof Function === false ) { - return Promise.resolve(); + return; } - const bucketsFromSelfie = selfie => { - for ( let i = 0; i < selfie.length; i++ ) { - this.buckets[i] = new Map(selfie[i]); - } - }; + this.reset(); - return Promise.all([ - storage.get(`${path}/FilterHostnameDict.trieContainer`).then(details => + const results = await Promise.all([ + storage.get(`${path}/main`), + storage.get(`${path}/destHNTrieContainer`).then(details => destHNTrieContainer.unserialize(details.content, sparseBase64) ), - storage.get(`${path}/FilterOrigin.trieContainer`).then(details => + storage.get(`${path}/origHNTrieContainer`).then(details => origHNTrieContainer.unserialize(details.content, sparseBase64) ), storage.get(`${path}/bidiTrie`).then(details => @@ -3862,28 +3864,45 @@ FilterContainer.prototype.fromSelfie = function(storage, path) { storage.get(`${path}/filterRefs`).then(details => filterRefsFromSelfie(details.content) ), - storage.get(`${path}/main`).then(details => { - let selfie; - try { - selfie = JSON.parse(details.content); - } catch (ex) { - } - if ( selfie instanceof Object === false ) { return false; } - if ( selfie.version !== this.selfieVersion ) { return false; } - this.processedFilterCount = selfie.processedFilterCount; - this.acceptedCount = selfie.acceptedCount; - this.rejectedCount = selfie.rejectedCount; - this.allowFilterCount = selfie.allowFilterCount; - this.blockFilterCount = selfie.blockFilterCount; - this.discardedCount = selfie.discardedCount; - this.bitsToBucketIndices = selfie.bitsToBucketIndices; - bucketsFromSelfie(selfie.buckets); - urlTokenizer.fromSelfie(selfie.urlTokenizer); - return true; - }), - ]).then(results => - results.every(v => v === true) - ); + ]); + + if ( results.slice(1).every(v => v === true) === false ) { return false; } + + const bucketsFromSelfie = selfie => { + for ( let i = 0; i < selfie.length; i++ ) { + this.buckets[i] = new Map(selfie[i]); + } + }; + + const details = results[0]; + if ( details instanceof Object === false ) { return false; } + if ( typeof details.content !== 'string' ) { return false; } + if ( details.content === '' ) { return false; } + let selfie; + try { + selfie = JSON.parse(details.content); + } catch (ex) { + } + if ( selfie instanceof Object === false ) { return false; } + if ( selfie.version !== this.selfieVersion ) { return false; } + this.processedFilterCount = selfie.processedFilterCount; + this.acceptedCount = selfie.acceptedCount; + this.discardedCount = selfie.discardedCount; + this.bitsToBucketIndices = selfie.bitsToBucketIndices; + bucketsFromSelfie(selfie.buckets); + urlTokenizer.fromSelfie(selfie.urlTokenizer); + return true; +}; + + +FilterContainer.prototype.unserialize = async function(s) { + const selfie = new Map(JSON.parse(s)); + const storage = { + get(name) { + return Promise.resolve(selfie.get(name)); + } + }; + return this.fromSelfie(storage, ''); }; /******************************************************************************/