Surfaced by an automated multi-agent correctness audit.
Summary
DuckLakeTable holds the encryption factory as a single per-instance shared slot (Arc<Mutex<Option<Arc<dyn EncryptionFactory>>>>). scan() rebuilds and unconditionally overwrites it per metadata page, then reads it later when building each page's ParquetSource — so concurrent or self-referencing scans on the same instance can clobber each other's decryption keys.
Location
src/table.rs:662 (shared field)
src/table.rs:1016-1038 (configure_encryption_factory unconditional overwrite)
src/table.rs:1007-1013 (create_parquet_source reads current slot)
- scan loop ~
src/table.rs:2382-2509
Failure scenario
Two scan() calls interleave on one shared DuckLakeTable Arc (e.g. a self-join, or concurrent queries): scan A configures the factory for its page and awaits; scan B overwrites the slot for a different page; scan A's create_parquet_source then uses B's key set → decryption failure, or (on file-path collision) decryption with the wrong key.
Suggested fix
Build a per-scan-page factory and thread it directly into ParquetSource construction instead of a single shared mutable slot on the table instance.
Severity: High for encrypted tables under concurrent/self-join scans; feature-gated (encryption), narrower blast radius.
Surfaced by an automated multi-agent correctness audit.
Summary
DuckLakeTableholds the encryption factory as a single per-instance shared slot (Arc<Mutex<Option<Arc<dyn EncryptionFactory>>>>).scan()rebuilds and unconditionally overwrites it per metadata page, then reads it later when building each page'sParquetSource— so concurrent or self-referencing scans on the same instance can clobber each other's decryption keys.Location
src/table.rs:662(shared field)src/table.rs:1016-1038(configure_encryption_factoryunconditional overwrite)src/table.rs:1007-1013(create_parquet_sourcereads current slot)src/table.rs:2382-2509Failure scenario
Two
scan()calls interleave on one sharedDuckLakeTableArc(e.g. a self-join, or concurrent queries): scan A configures the factory for its page and awaits; scan B overwrites the slot for a different page; scan A'screate_parquet_sourcethen uses B's key set → decryption failure, or (on file-path collision) decryption with the wrong key.Suggested fix
Build a per-scan-page factory and thread it directly into
ParquetSourceconstruction instead of a single shared mutable slot on the table instance.Severity: High for encrypted tables under concurrent/self-join scans; feature-gated (
encryption), narrower blast radius.