@@ -101,6 +101,10 @@ struct CachedMeasurement {
101101 measurements : TdxMeasurements ,
102102}
103103
104+ /// Cap on `<image_cache_dir>/measurements/` (~400 B per entry). Entries never go
105+ /// stale, but a caller varying the VM shape adds one per shape.
106+ const MEASUREMENT_CACHE_MAX_ENTRIES : usize = 1024 ;
107+
104108struct ImagePaths {
105109 image_dir : PathBuf ,
106110 fw_path : PathBuf ,
@@ -224,6 +228,39 @@ impl CvmVerifier {
224228 )
225229 } ) ?;
226230 debug ! ( "Stored measurement cache entry {}" , cache_key) ;
231+ if let Err ( e) = Self :: prune_measurement_cache ( & cache_dir, MEASUREMENT_CACHE_MAX_ENTRIES ) {
232+ warn ! ( "failed to prune the measurement cache: {e:?}" ) ;
233+ }
234+ Ok ( ( ) )
235+ }
236+
237+ /// Best effort: drop the oldest `.json` entries (not in-flight temp files)
238+ /// until at most `max_entries` remain.
239+ fn prune_measurement_cache ( cache_dir : & Path , max_entries : usize ) -> Result < ( ) > {
240+ let mut entries = Vec :: new ( ) ;
241+ for entry in fs_err:: read_dir ( cache_dir) . context ( "failed to read measurement cache" ) ? {
242+ let entry = entry. context ( "failed to read measurement cache entry" ) ?;
243+ let path = entry. path ( ) ;
244+ if path. extension ( ) != Some ( OsStr :: new ( "json" ) ) {
245+ continue ;
246+ }
247+ let modified = entry
248+ . metadata ( )
249+ . and_then ( |metadata| metadata. modified ( ) )
250+ . unwrap_or ( std:: time:: UNIX_EPOCH ) ;
251+ entries. push ( ( modified, path) ) ;
252+ }
253+ if entries. len ( ) <= max_entries {
254+ return Ok ( ( ) ) ;
255+ }
256+ let excess = entries. len ( ) - max_entries;
257+ entries. sort ( ) ;
258+ for ( _, path) in entries. iter ( ) . take ( excess) {
259+ if let Err ( e) = fs_err:: remove_file ( path) {
260+ debug ! ( "failed to evict measurement cache entry: {e:?}" ) ;
261+ }
262+ }
263+ debug ! ( "evicted {excess} measurement cache entries" ) ;
227264 Ok ( ( ) )
228265 }
229266
@@ -1636,6 +1673,42 @@ mod tests {
16361673 . is_none( ) ) ;
16371674 }
16381675
1676+ #[ test]
1677+ fn measurement_cache_evicts_the_oldest_entries_and_spares_temporaries ( ) {
1678+ let dir = tempfile:: tempdir ( ) . expect ( "temp cache dir" ) ;
1679+ let cache_dir = dir. path ( ) ;
1680+ let base = std:: time:: UNIX_EPOCH + Duration :: from_secs ( 1_700_000_000 ) ;
1681+ for index in 0 ..8u64 {
1682+ let path = cache_dir. join ( format ! ( "{index:02}.json" ) ) ;
1683+ fs_err:: write ( & path, b"{}" ) . unwrap ( ) ;
1684+ let file = fs_err:: File :: open ( & path) . unwrap ( ) ;
1685+ file. set_times (
1686+ std:: fs:: FileTimes :: new ( ) . set_modified ( base + Duration :: from_secs ( index) ) ,
1687+ )
1688+ . unwrap ( ) ;
1689+ }
1690+ // An in-flight `NamedTempFile`: no extension until `persist`.
1691+ let in_flight = cache_dir. join ( ".tmpABCDEF" ) ;
1692+ fs_err:: write ( & in_flight, b"partial" ) . unwrap ( ) ;
1693+
1694+ CvmVerifier :: prune_measurement_cache ( cache_dir, 3 ) . unwrap ( ) ;
1695+
1696+ let mut remaining: Vec < String > = fs_err:: read_dir ( cache_dir)
1697+ . unwrap ( )
1698+ . map ( |entry| entry. unwrap ( ) . file_name ( ) . to_string_lossy ( ) . into_owned ( ) )
1699+ . collect ( ) ;
1700+ remaining. sort ( ) ;
1701+ assert_eq ! (
1702+ remaining,
1703+ vec![
1704+ ".tmpABCDEF" . to_string( ) ,
1705+ "05.json" . to_string( ) ,
1706+ "06.json" . to_string( ) ,
1707+ "07.json" . to_string( ) ,
1708+ ]
1709+ ) ;
1710+ }
1711+
16391712 #[ test]
16401713 fn concurrent_measurement_cache_writes_are_atomic ( ) {
16411714 let directory = tempfile:: tempdir ( ) . unwrap ( ) ;
0 commit comments