Fix concurrency bug leading to multiple fetches of the malware database

This commit is contained in:
Sander Declerck 2026-04-21 09:26:07 +02:00
parent 3e71398430
commit 2930894624
No known key found for this signature in database
2 changed files with 51 additions and 55 deletions

View file

@ -15,8 +15,12 @@ import { getEcoSystem, ECOSYSTEM_PY } from "../config/settings.js";
* @property {function(string, string): boolean} isMalware
*/
/** @type {MalwareDatabase | null} */
let cachedMalwareDatabase = null;
// Caching the Promise (rather than the resolved database) prevents duplicate fetches. If we cached the resolved
// value, multiple callers could pass the null-check before the first fetch completes (because each `await` yields
// control back to the event loop, allowing other callers to run). Since the Promise assignment is synchronous, all
// concurrent callers see it immediately and share a single fetch.
/** @type {Promise<MalwareDatabase> | null} */
let cachedMalwareDatabasePromise = null;
/**
* Normalize package name for comparison.
@ -34,45 +38,41 @@ function normalizePackageName(name) {
return name;
}
export async function openMalwareDatabase() {
if (cachedMalwareDatabase) {
return cachedMalwareDatabase;
}
export function openMalwareDatabase() {
if (!cachedMalwareDatabasePromise) {
cachedMalwareDatabasePromise = getMalwareDatabase().then((malwareDatabase) => {
/**
* @param {string} name
* @param {string} version
* @returns {string}
*/
function getPackageStatus(name, version) {
const normalizedName = normalizePackageName(name);
const packageData = malwareDatabase.find(
(pkg) => {
const normalizedPkgName = normalizePackageName(pkg.package_name);
return normalizedPkgName === normalizedName &&
(pkg.version === version || pkg.version === "*");
}
);
const malwareDatabase = await getMalwareDatabase();
if (!packageData) {
return MALWARE_STATUS_OK;
}
/**
* @param {string} name
* @param {string} version
* @returns {string}
*/
function getPackageStatus(name, version) {
const normalizedName = normalizePackageName(name);
const packageData = malwareDatabase.find(
(pkg) => {
const normalizedPkgName = normalizePackageName(pkg.package_name);
return normalizedPkgName === normalizedName &&
(pkg.version === version || pkg.version === "*");
return packageData.reason;
}
);
if (!packageData) {
return MALWARE_STATUS_OK;
}
return packageData.reason;
return {
getPackageStatus,
isMalware: (name, version) => {
const status = getPackageStatus(name, version);
return isMalwareStatus(status);
},
};
});
}
// This implicitly caches the malware database
// that's closed over by the getPackageStatus function
cachedMalwareDatabase = {
getPackageStatus,
isMalware: (name, version) => {
const status = getPackageStatus(name, version);
return isMalwareStatus(status);
},
};
return cachedMalwareDatabase;
return cachedMalwareDatabasePromise;
}
/**