44
55import { executeCodeQLCommand } from './cli-executor' ;
66import { logger } from '../utils/logger' ;
7- import { writeFileSync , readFileSync , statSync } from 'fs' ;
7+ import { closeSync , fstatSync , mkdirSync , openSync , readFileSync , writeFileSync } from 'fs' ;
88import { dirname , isAbsolute } from 'path' ;
9- import { mkdirSync } from 'fs' ;
109
1110export interface QueryEvaluationResult {
1211 success : boolean ;
@@ -37,7 +36,11 @@ export type BuiltInEvaluator = keyof typeof BUILT_IN_EVALUATORS;
3736/**
3837 * In-memory cache for extracted query metadata, keyed by file path.
3938 * Stores the file modification time to invalidate when the file changes.
39+ * Bounded to {@link METADATA_CACHE_MAX} entries; least-recently-used entries
40+ * (by access) are evicted when the limit is reached. Entries are refreshed
41+ * on cache hits via delete+set, so Map iteration order reflects LRU state.
4042 */
43+ const METADATA_CACHE_MAX = 256 ;
4144const metadataCache = new Map < string , { mtime : number ; metadata : QueryMetadata } > ( ) ;
4245
4346/**
@@ -46,15 +49,23 @@ const metadataCache = new Map<string, { mtime: number; metadata: QueryMetadata }
4649 */
4750export async function extractQueryMetadata ( queryPath : string ) : Promise < QueryMetadata > {
4851 try {
49- // Check cache with mtime validation
50- const stat = statSync ( queryPath ) ;
51- const mtime = stat . mtimeMs ;
52- const cached = metadataCache . get ( queryPath ) ;
53- if ( cached && cached . mtime === mtime ) {
54- return cached . metadata ;
52+ // Open once, then fstat + read via the fd to avoid TOCTOU race (CWE-367).
53+ const fd = openSync ( queryPath , 'r' ) ;
54+ let queryContent : string ;
55+ let mtime : number ;
56+ try {
57+ mtime = fstatSync ( fd ) . mtimeMs ;
58+ const cached = metadataCache . get ( queryPath ) ;
59+ if ( cached && cached . mtime === mtime ) {
60+ // Refresh position in Map to implement true LRU behavior.
61+ metadataCache . delete ( queryPath ) ;
62+ metadataCache . set ( queryPath , cached ) ;
63+ return cached . metadata ;
64+ }
65+ queryContent = readFileSync ( fd , 'utf-8' ) ;
66+ } finally {
67+ closeSync ( fd ) ;
5568 }
56-
57- const queryContent = readFileSync ( queryPath , 'utf-8' ) ;
5869 const metadata : QueryMetadata = { } ;
5970
6071 // Extract metadata from comments using regex patterns
@@ -75,6 +86,11 @@ export async function extractQueryMetadata(queryPath: string): Promise<QueryMeta
7586 metadata . tags = tagsMatch [ 1 ] . split ( / \s + / ) . filter ( t => t . length > 0 ) ;
7687 }
7788
89+ // Evict oldest entries when the cache exceeds the size limit.
90+ if ( metadataCache . size >= METADATA_CACHE_MAX ) {
91+ const firstKey = metadataCache . keys ( ) . next ( ) . value ;
92+ if ( firstKey !== undefined ) metadataCache . delete ( firstKey ) ;
93+ }
7894 metadataCache . set ( queryPath , { mtime, metadata } ) ;
7995 return metadata ;
8096 } catch ( error ) {
0 commit comments