Skip to content

fix: prevent catalog scan use-after-free#501

Merged
staticlibs merged 2 commits into
duckdb:mainfrom
hello-world-bfree:fix/postgres-catalogset-scan-uaf
Jul 8, 2026
Merged

fix: prevent catalog scan use-after-free#501
staticlibs merged 2 commits into
duckdb:mainfrom
hello-world-bfree:fix/postgres-catalogset-scan-uaf

Conversation

@hello-world-bfree

@hello-world-bfree hello-world-bfree commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Resolves #502

Problem

PostgresCatalogSet::Scan hands its callback a bare CatalogEntry& without pinning it into the calling transaction first. GetEntry and CreateEntry both keep the underlying shared_ptr<CatalogEntry> alive in PostgresTransaction::referenced_entries for the rest of the transaction. Scan does not.

If pg_clear_cache runs after a Scan but before every reference handed out by that Scan is done being used, ClearEntries drops the shared_ptr for those entries and destroys them. Anything still holding a reference from the earlier Scan is then reading freed memory; i.e., funcs from Duckdb core like duckdb_tables() or duckdb_vies()

Fix

Pinning every entry into the calling transaction inside Scan's existing lock scope, the same way GetEntry and CreateEntry already do:

for (auto &entry : entries) {
        transaction.ReferenceEntry(entry.second);
        callback(*entry.second);
}

ReferenceEntry/referenced_entries is already the established solution for this exact problem. Scan just wasn't using it yet

@staticlibs

Copy link
Copy Markdown
Member

@hello-world-bfree

Hi, thanks for the PR!

I wonder whether the ReferenceEntry call itself needs to be synchronized? Copying thread-sanitizer warning just in case the job logs will go away:

WARNING: ThreadSanitizer: data race (pid=23278)
  Write of size 8 at 0x72300003f520 by main thread (mutexes: write M0, write M1, write M2):
    #0 std::_Hashtable<std::reference_wrapper<duckdb::CatalogEntry>, std::pair<std::reference_wrapper<duckdb::CatalogEntry> const, duckdb::shared_ptr<duckdb::CatalogEntry, true> >, std::allocator<std::pair<std::reference_wrapper<duckdb::CatalogEntry> const, duckdb::shared_ptr<duckdb::CatalogEntry, true> > >, std::__detail::_Select1st, duckdb::ReferenceEquality<duckdb::CatalogEntry>, duckdb::ReferenceHashFunction<duckdb::CatalogEntry>, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<true, false, true> >::_M_insert_unique_node(unsigned long, unsigned long, std::__detail::_Hash_node<std::pair<std::reference_wrapper<duckdb::CatalogEntry> const, duckdb::shared_ptr<duckdb::CatalogEntry, true> >, true>*, unsigned long) /usr/include/c++/13/bits/hashtable.h:2187 (postgres_scanner.duckdb_extension+0x9cff5c) (BuildId: b1189a5934cb7cb8642220a3c7aa9d4961c47b4b)
    #1 std::pair<std::__detail::_Node_iterator<std::pair<std::reference_wrapper<duckdb::CatalogEntry> const, duckdb::shared_ptr<duckdb::CatalogEntry, true> >, false, true>, bool> std::_Hashtable<std::reference_wrapper<duckdb::CatalogEntry>, std::pair<std::reference_wrapper<duckdb::CatalogEntry> const, duckdb::shared_ptr<duckdb::CatalogEntry, true> >, std::allocator<std::pair<std::reference_wrapper<duckdb::CatalogEntry> const, duckdb::shared_ptr<duckdb::CatalogEntry, true> > >, std::__detail::_Select1st, duckdb::ReferenceEquality<duckdb::CatalogEntry>, duckdb::ReferenceHashFunction<duckdb::CatalogEntry>, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<true, false, true> >::_M_emplace<duckdb::CatalogEntry&, duckdb::shared_ptr<duckdb::CatalogEntry, true>&>(std::integral_constant<bool, true>, duckdb::CatalogEntry&, duckdb::shared_ptr<duckdb::CatalogEntry, true>&) /usr/include/c++/13/bits/hashtable.h:2105 (postgres_scanner.duckdb_extension+0x9d056f) (BuildId: b1189a5934cb7cb8642220a3c7aa9d4961c47b4b)
    #2 std::pair<std::__detail::_Node_iterator<std::pair<std::reference_wrapper<duckdb::CatalogEntry> const, duckdb::shared_ptr<duckdb::CatalogEntry, true> >, false, true>, bool> std::_Hashtable<std::reference_wrapper<duckdb::CatalogEntry>, std::pair<std::reference_wrapper<duckdb::CatalogEntry> const, duckdb::shared_ptr<duckdb::CatalogEntry, true> >, std::allocator<std::pair<std::reference_wrapper<duckdb::CatalogEntry> const, duckdb::shared_ptr<duckdb::CatalogEntry, true> > >, std::__detail::_Select1st, duckdb::ReferenceEquality<duckdb::CatalogEntry>, duckdb::ReferenceHashFunction<duckdb::CatalogEntry>, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<true, false, true> >::emplace<duckdb::CatalogEntry&, duckdb::shared_ptr<duckdb::CatalogEntry, true>&>(duckdb::CatalogEntry&, duckdb::shared_ptr<duckdb::CatalogEntry, true>&) /usr/include/c++/13/bits/hashtable.h:961 (postgres_scanner.duckdb_extension+0x9d056f)
    #3 std::pair<std::__detail::_Node_iterator<std::pair<std::reference_wrapper<duckdb::CatalogEntry> const, duckdb::shared_ptr<duckdb::CatalogEntry, true> >, false, true>, bool> std::unordered_map<std::reference_wrapper<duckdb::CatalogEntry>, duckdb::shared_ptr<duckdb::CatalogEntry, true>, duckdb::ReferenceHashFunction<duckdb::CatalogEntry>, duckdb::ReferenceEquality<duckdb::CatalogEntry>, std::allocator<std::pair<std::reference_wrapper<duckdb::CatalogEntry> const, duckdb::shared_ptr<duckdb::CatalogEntry, true> > > >::emplace<duckdb::CatalogEntry&, duckdb::shared_ptr<duckdb::CatalogEntry, true>&>(duckdb::CatalogEntry&, duckdb::shared_ptr<duckdb::CatalogEntry, true>&) /usr/include/c++/13/bits/unordered_map.h:396 (postgres_scanner.duckdb_extension+0x9d056f)
    #4 duckdb::PostgresTransaction::ReferenceEntry(duckdb::shared_ptr<duckdb::CatalogEntry, true>&) /home/runner/work/duckdb-postgres/duckdb-postgres/src/storage/postgres_transaction.cpp:129 (postgres_scanner.duckdb_extension+0x9d056f)
    #5 duckdb::PostgresCatalogSet::CreateEntry(duckdb::PostgresTransaction&, duckdb::shared_ptr<duckdb::CatalogEntry, true>) /home/runner/work/duckdb-postgres/duckdb-postgres/src/storage/postgres_catalog_set.cpp:96 (postgres_scanner.duckdb_extension+0x9458b4) (BuildId: b1189a5934cb7cb8642220a3c7aa9d4961c47b4b)
    #6 duckdb::PostgresInSchemaSet::CreateEntry(duckdb::PostgresTransaction&, duckdb::shared_ptr<duckdb::CatalogEntry, true>) /home/runner/work/duckdb-postgres/duckdb-postgres/src/storage/postgres_catalog_set.cpp:119 (postgres_scanner.duckdb_extension+0x946c78) (BuildId: b1189a5934cb7cb8642220a3c7aa9d4961c47b4b)
    #7 duckdb::PostgresTableSet::CreateEntries(duckdb::PostgresTransaction&, duckdb::PostgresResult&, unsigned long, unsigned long) /home/runner/work/duckdb-postgres/duckdb-postgres/src/storage/postgres_table_set.cpp:183 (postgres_scanner.duckdb_extension+0x9c3bc6) (BuildId: b1189a5934cb7cb8642220a3c7aa9d4961c47b4b)
    #8 duckdb::PostgresTableSet::LoadEntries(duckdb::ClientContext&, duckdb::PostgresTransaction&) /home/runner/work/duckdb-postgres/duckdb-postgres/src/storage/postgres_table_set.cpp:189 (postgres_scanner.duckdb_extension+0x9c4bfa) (BuildId: b1189a5934cb7cb8642220a3c7aa9d4961c47b4b)
    #9 duckdb::PostgresCatalogSet::TryLoadEntries(duckdb::ClientContext&, duckdb::PostgresTransaction&) /home/runner/work/duckdb-postgres/duckdb-postgres/src/storage/postgres_catalog_set.cpp:56 (postgres_scanner.duckdb_extension+0x943e1d) (BuildId: b1189a5934cb7cb8642220a3c7aa9d4961c47b4b)
    #10 duckdb::PostgresCatalogSet::Scan(duckdb::ClientContext&, duckdb::PostgresTransaction&, std::function<void (duckdb::CatalogEntry&)> const&) /home/runner/work/duckdb-postgres/duckdb-postgres/src/storage/postgres_catalog_set.cpp:85 (postgres_scanner.duckdb_extension+0x9445d5) (BuildId: b1189a5934cb7cb8642220a3c7aa9d4961c47b4b)
    #11 duckdb::PostgresSchemaEntry::Scan(duckdb::ClientContext&, duckdb::CatalogType, std::function<void (duckdb::CatalogEntry&)> const&) /home/runner/work/duckdb-postgres/duckdb-postgres/src/storage/postgres_schema_entry.cpp:178 (postgres_scanner.duckdb_extension+0x988d88) (BuildId: b1189a5934cb7cb8642220a3c7aa9d4961c47b4b)
    #12 DuckDBColumnsInit /home/runner/work/duckdb-postgres/duckdb-postgres/duckdb/src/function/table/system/duckdb_columns.cpp:100 (libduckdb.so+0x2411cb6) (BuildId: 522787844199fbfa6a5d21195010717f011c4dc5)
    #13 duckdb::TableScanGlobalSourceState::TableScanGlobalSourceState(duckdb::ClientContext&, duckdb::PhysicalTableScan const&) /home/runner/work/duckdb-postgres/duckdb-postgres/duckdb/src/execution/operator/scan/physical_table_scan.cpp:45 (libduckdb.so+0x3374c7a) (BuildId: 522787844199fbfa6a5d21195010717f011c4dc5)
    #14 duckdb::TemplatedUniqueIf<duckdb::TableScanGlobalSourceState, true>::templated_unique_single_t duckdb::make_uniq<duckdb::TableScanGlobalSourceState, duckdb::ClientContext&, duckdb::PhysicalTableScan const&>(duckdb::ClientContext&, duckdb::PhysicalTableScan const&) /home/runner/work/duckdb-postgres/duckdb-postgres/duckdb/src/include/duckdb/common/helper.hpp:66 (libduckdb.so+0x337567b) (BuildId: 522787844199fbfa6a5d21195010717f011c4dc5)
    #15 duckdb::PhysicalTableScan::GetGlobalSourceState(duckdb::ClientContext&) const /home/runner/work/duckdb-postgres/duckdb-postgres/duckdb/src/execution/operator/scan/physical_table_scan.cpp:101 (libduckdb.so+0x337567b)
    #16 duckdb::Pipeline::ResetSource(bool) /home/runner/work/duckdb-postgres/duckdb-postgres/duckdb/src/parallel/pipeline.cpp:306 (libduckdb.so+0x3776036) (BuildId: 522787844199fbfa6a5d21195010717f011c4dc5)
    #17 duckdb::Pipeline::Reset() /home/runner/work/duckdb-postgres/duckdb-postgres/duckdb/src/parallel/pipeline.cpp:270 (libduckdb.so+0x3776b71) (BuildId: 522787844199fbfa6a5d21195010717f011c4dc5)
    #18 duckdb::Pipeline::Schedule(duckdb::shared_ptr<duckdb::Event, true>&) /home/runner/work/duckdb-postgres/duckdb-postgres/duckdb/src/parallel/pipeline.cpp:196 (libduckdb.so+0x379f72f) (BuildId: 522787844199fbfa6a5d21195010717f011c4dc5)
    #19 duckdb::PipelineEvent::Schedule() /home/runner/work/duckdb-postgres/duckdb-postgres/duckdb/src/parallel/pipeline_event.cpp:13 (libduckdb.so+0x379fa1c) (BuildId: 522787844199fbfa6a5d21195010717f011c4dc5)
    #20 duckdb::Event::CompleteDependency() /home/runner/work/duckdb-postgres/duckdb-postgres/duckdb/src/parallel/event.cpp:20 (libduckdb.so+0x3775d4a) (BuildId: 522787844199fbfa6a5d21195010717f011c4dc5)
    #21 duckdb::Event::Finish() /home/runner/work/duckdb-postgres/duckdb-postgres/duckdb/src/parallel/event.cpp:38 (libduckdb.so+0x3775a3e) (BuildId: 522787844199fbfa6a5d21195010717f011c4dc5)
    #22 duckdb::Event::FinishTask() /home/runner/work/duckdb-postgres/duckdb-postgres/duckdb/src/parallel/event.cpp:62 (libduckdb.so+0x3775e57) (BuildId: 522787844199fbfa6a5d21195010717f011c4dc5)
    #23 duckdb::PipelineInitializeTask::ExecuteTask(duckdb::TaskExecutionMode) /home/runner/work/duckdb-postgres/duckdb-postgres/duckdb/src/parallel/pipeline_initialize_event.cpp:22 (libduckdb.so+0x37768b1) (BuildId: 522787844199fbfa6a5d21195010717f011c4dc5)
    #24 duckdb::ExecutorTask::Execute(duckdb::TaskExecutionMode) /home/runner/work/duckdb-postgres/duckdb-postgres/duckdb/src/parallel/executor_task.cpp:52 (libduckdb.so+0x3781190) (BuildId: 522787844199fbfa6a5d21195010717f011c4dc5)
    #25 duckdb::Executor::ExecuteTask(bool) /home/runner/work/duckdb-postgres/duckdb-postgres/duckdb/src/parallel/executor.cpp:594 (libduckdb.so+0x379c1c2) (BuildId: 522787844199fbfa6a5d21195010717f011c4dc5)
    #26 duckdb::ClientContext::ExecuteTaskInternal(duckdb::ClientContextLock&, duckdb::BaseQueryResult&, bool) /home/runner/work/duckdb-postgres/duckdb-postgres/duckdb/src/main/client_context.cpp:745 (libduckdb.so+0x370f725) (BuildId: 522787844199fbfa6a5d21195010717f011c4dc5)
    #27 duckdb::PendingQueryResult::ExecuteTaskInternal(duckdb::ClientContextLock&) /home/runner/work/duckdb-postgres/duckdb-postgres/duckdb/src/main/pending_query_result.cpp:68 (libduckdb.so+0x373233f) (BuildId: 522787844199fbfa6a5d21195010717f011c4dc5)
    #28 duckdb::PendingQueryResult::ExecuteInternal(duckdb::ClientContextLock&) /home/runner/work/duckdb-postgres/duckdb-postgres/duckdb/src/main/pending_query_result.cpp:75 (libduckdb.so+0x37325c3) (BuildId: 522787844199fbfa6a5d21195010717f011c4dc5)
    #29 duckdb::ClientContext::ExecutePendingQueryInternal(duckdb::ClientContextLock&, duckdb::PendingQueryResult&) /home/runner/work/duckdb-postgres/duckdb-postgres/duckdb/src/main/client_context.cpp:1250 (libduckdb.so+0x373282c) (BuildId: 522787844199fbfa6a5d21195010717f011c4dc5)
    #30 duckdb::ClientContext::Query(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, duckdb::QueryParameters) /home/runner/work/duckdb-postgres/duckdb-postgres/duckdb/src/main/client_context.cpp:1140 (libduckdb.so+0x375bde4) (BuildId: 522787844199fbfa6a5d21195010717f011c4dc5)
    #31 duckdb::Command::ExecuteQuery(duckdb::ExecuteContext&, std::reference_wrapper<duckdb::Connection>, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, unsigned long) const /home/runner/work/duckdb-postgres/duckdb-postgres/duckdb/test/sqlite/sqllogic_command.cpp:226 (unittest+0x1759c0) (BuildId: 7d28b662d2257c751e49bf0bc3205cf21884185d)
    #32 duckdb::Statement::ExecuteInternal(duckdb::ExecuteContext&) const /home/runner/work/duckdb-postgres/duckdb-postgres/duckdb/test/sqlite/sqllogic_command.cpp:849 (unittest+0x178148) (BuildId: 7d28b662d2257c751e49bf0bc3205cf21884185d)
    #33 duckdb::Command::Execute(duckdb::ExecuteContext&) const /home/runner/work/duckdb-postgres/duckdb-postgres/duckdb/test/sqlite/sqllogic_command.cpp:325 (unittest+0x1746f6) (BuildId: 7d28b662d2257c751e49bf0bc3205cf21884185d)
    #34 duckdb::SQLLogicTestRunner::ExecuteCommand(duckdb::unique_ptr<duckdb::Command, std::default_delete<duckdb::Command>, true>) /home/runner/work/duckdb-postgres/duckdb-postgres/duckdb/test/sqlite/sqllogic_test_runner.cpp:118 (unittest+0x1753fd) (BuildId: 7d28b662d2257c751e49bf0bc3205cf21884185d)
    #35 duckdb::SQLLogicTestRunner::ExecuteInternal(duckdb::SQLLogicParser&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) /home/runner/work/duckdb-postgres/duckdb-postgres/duckdb/test/sqlite/sqllogic_test_runner.cpp:863 (unittest+0x1819bf) (BuildId: 7d28b662d2257c751e49bf0bc3205cf21884185d)
    #36 duckdb::SQLLogicTestRunner::ExecuteFile(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >) /home/runner/work/duckdb-postgres/duckdb-postgres/duckdb/test/sqlite/sqllogic_test_runner.cpp:696 (unittest+0x184bb3) (BuildId: 7d28b662d2257c751e49bf0bc3205cf21884185d)
    #37 RunSQLLogicTest<false> /home/runner/work/duckdb-postgres/duckdb-postgres/duckdb/test/sqlite/test_sqllogictest.cpp:100 (unittest+0x186281) (BuildId: 7d28b662d2257c751e49bf0bc3205cf21884185d)
    #38 testRunner<> /home/runner/work/duckdb-postgres/duckdb-postgres/duckdb/test/sqlite/test_sqllogictest.cpp:142 (unittest+0x186570) (BuildId: 7d28b662d2257c751e49bf0bc3205cf21884185d)
    #39 Catch::TestInvokerAsFunction::invoke() const /home/runner/work/duckdb-postgres/duckdb-postgres/duckdb/test/../third_party/catch/catch.hpp:14583 (unittest+0x9b06f) (BuildId: 7d28b662d2257c751e49bf0bc3205cf21884185d)
    #40 Catch::TestCase::invoke() const /home/runner/work/duckdb-postgres/duckdb-postgres/duckdb/test/../third_party/catch/catch.hpp:14418 (unittest+0x9b06f)
    #41 Catch::RunContext::invokeActiveTestCase() /home/runner/work/duckdb-postgres/duckdb-postgres/duckdb/test/../third_party/catch/catch.hpp:13169 (unittest+0x9b06f)
    #42 Catch::RunContext::runCurrentTest(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&) /home/runner/work/duckdb-postgres/duckdb-postgres/duckdb/test/../third_party/catch/catch.hpp:13142 (unittest+0xdd1af) (BuildId: 7d28b662d2257c751e49bf0bc3205cf21884185d)
    #43 Catch::RunContext::runTest(Catch::TestCase const&) /home/runner/work/duckdb-postgres/duckdb-postgres/duckdb/test/../third_party/catch/catch.hpp:12887 (unittest+0x1124ec) (BuildId: 7d28b662d2257c751e49bf0bc3205cf21884185d)
    #44 execute /home/runner/work/duckdb-postgres/duckdb-postgres/duckdb/test/../third_party/catch/catch.hpp:13596 (unittest+0x1124ec)
    #45 Catch::Session::runInternal() /home/runner/work/duckdb-postgres/duckdb-postgres/duckdb/test/../third_party/catch/catch.hpp:13811 (unittest+0x1124ec)
    #46 Catch::Session::run() /home/runner/work/duckdb-postgres/duckdb-postgres/duckdb/test/../third_party/catch/catch.hpp:13767 (unittest+0x11368f) (BuildId: 7d28b662d2257c751e49bf0bc3205cf21884185d)
    #47 int Catch::Session::run<char>(int, char const* const*) /home/runner/work/duckdb-postgres/duckdb-postgres/duckdb/test/../third_party/catch/catch.hpp:13380 (unittest+0x1143fa) (BuildId: 7d28b662d2257c751e49bf0bc3205cf21884185d)
    #48 main /home/runner/work/duckdb-postgres/duckdb-postgres/duckdb/test/unittest.cpp:93 (unittest+0x1143fa)

  Previous read of size 8 at 0x72300003f520 by thread T1154 (mutexes: write M3):
    #0 std::_Hashtable<std::reference_wrapper<duckdb::CatalogEntry>, std::pair<std::reference_wrapper<duckdb::CatalogEntry> const, duckdb::shared_ptr<duckdb::CatalogEntry, true> >, std::allocator<std::pair<std::reference_wrapper<duckdb::CatalogEntry> const, duckdb::shared_ptr<duckdb::CatalogEntry, true> > >, std::__detail::_Select1st, duckdb::ReferenceEquality<duckdb::CatalogEntry>, duckdb::ReferenceHashFunction<duckdb::CatalogEntry>, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<true, false, true> >::size() const /usr/include/c++/13/bits/hashtable.h:648 (postgres_scanner.duckdb_extension+0x9d034f) (BuildId: b1189a5934cb7cb8642220a3c7aa9d4961c47b4b)
    #1 std::pair<std::__detail::_Node_iterator<std::pair<std::reference_wrapper<duckdb::CatalogEntry> const, duckdb::shared_ptr<duckdb::CatalogEntry, true> >, false, true>, bool> std::_Hashtable<std::reference_wrapper<duckdb::CatalogEntry>, std::pair<std::reference_wrapper<duckdb::CatalogEntry> const, duckdb::shared_ptr<duckdb::CatalogEntry, true> >, std::allocator<std::pair<std::reference_wrapper<duckdb::CatalogEntry> const, duckdb::shared_ptr<duckdb::CatalogEntry, true> > >, std::__detail::_Select1st, duckdb::ReferenceEquality<duckdb::CatalogEntry>, duckdb::ReferenceHashFunction<duckdb::CatalogEntry>, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<true, false, true> >::_M_emplace<duckdb::CatalogEntry&, duckdb::shared_ptr<duckdb::CatalogEntry, true>&>(std::integral_constant<bool, true>, duckdb::CatalogEntry&, duckdb::shared_ptr<duckdb::CatalogEntry, true>&) /usr/include/c++/13/bits/hashtable.h:2089 (postgres_scanner.duckdb_extension+0x9d034f)
    #2 std::pair<std::__detail::_Node_iterator<std::pair<std::reference_wrapper<duckdb::CatalogEntry> const, duckdb::shared_ptr<duckdb::CatalogEntry, true> >, false, true>, bool> std::_Hashtable<std::reference_wrapper<duckdb::CatalogEntry>, std::pair<std::reference_wrapper<duckdb::CatalogEntry> const, duckdb::shared_ptr<duckdb::CatalogEntry, true> >, std::allocator<std::pair<std::reference_wrapper<duckdb::CatalogEntry> const, duckdb::shared_ptr<duckdb::CatalogEntry, true> > >, std::__detail::_Select1st, duckdb::ReferenceEquality<duckdb::CatalogEntry>, duckdb::ReferenceHashFunction<duckdb::CatalogEntry>, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<true, false, true> >::emplace<duckdb::CatalogEntry&, duckdb::shared_ptr<duckdb::CatalogEntry, true>&>(duckdb::CatalogEntry&, duckdb::shared_ptr<duckdb::CatalogEntry, true>&) /usr/include/c++/13/bits/hashtable.h:961 (postgres_scanner.duckdb_extension+0x9d034f)
    #3 std::pair<std::__detail::_Node_iterator<std::pair<std::reference_wrapper<duckdb::CatalogEntry> const, duckdb::shared_ptr<duckdb::CatalogEntry, true> >, false, true>, bool> std::unordered_map<std::reference_wrapper<duckdb::CatalogEntry>, duckdb::shared_ptr<duckdb::CatalogEntry, true>, duckdb::ReferenceHashFunction<duckdb::CatalogEntry>, duckdb::ReferenceEquality<duckdb::CatalogEntry>, std::allocator<std::pair<std::reference_wrapper<duckdb::CatalogEntry> const, duckdb::shared_ptr<duckdb::CatalogEntry, true> > > >::emplace<duckdb::CatalogEntry&, duckdb::shared_ptr<duckdb::CatalogEntry, true>&>(duckdb::CatalogEntry&, duckdb::shared_ptr<duckdb::CatalogEntry, true>&) /usr/include/c++/13/bits/unordered_map.h:396 (postgres_scanner.duckdb_extension+0x9d034f)
    #4 duckdb::PostgresTransaction::ReferenceEntry(duckdb::shared_ptr<duckdb::CatalogEntry, true>&) /home/runner/work/duckdb-postgres/duckdb-postgres/src/storage/postgres_transaction.cpp:129 (postgres_scanner.duckdb_extension+0x9d034f)
    #5 duckdb::PostgresCatalogSet::Scan(duckdb::ClientContext&, duckdb::PostgresTransaction&, std::function<void (duckdb::CatalogEntry&)> const&) /home/runner/work/duckdb-postgres/duckdb-postgres/src/storage/postgres_catalog_set.cpp:88 (postgres_scanner.duckdb_extension+0x94460e) (BuildId: b1189a5934cb7cb8642220a3c7aa9d4961c47b4b)
    #6 duckdb::PostgresSchemaEntry::Scan(duckdb::ClientContext&, duckdb::CatalogType, std::function<void (duckdb::CatalogEntry&)> const&) /home/runner/work/duckdb-postgres/duckdb-postgres/src/storage/postgres_schema_entry.cpp:178 (postgres_scanner.duckdb_extension+0x988d88) (BuildId: b1189a5934cb7cb8642220a3c7aa9d4961c47b4b)
    #7 DuckDBColumnsInit /home/runner/work/duckdb-postgres/duckdb-postgres/duckdb/src/function/table/system/duckdb_columns.cpp:100 (libduckdb.so+0x2411cb6) (BuildId: 522787844199fbfa6a5d21195010717f011c4dc5)
    #8 duckdb::TableScanGlobalSourceState::TableScanGlobalSourceState(duckdb::ClientContext&, duckdb::PhysicalTableScan const&) /home/runner/work/duckdb-postgres/duckdb-postgres/duckdb/src/execution/operator/scan/physical_table_scan.cpp:45 (libduckdb.so+0x3374c7a) (BuildId: 522787844199fbfa6a5d21195010717f011c4dc5)
    #9 duckdb::TemplatedUniqueIf<duckdb::TableScanGlobalSourceState, true>::templated_unique_single_t duckdb::make_uniq<duckdb::TableScanGlobalSourceState, duckdb::ClientContext&, duckdb::PhysicalTableScan const&>(duckdb::ClientContext&, duckdb::PhysicalTableScan const&) /home/runner/work/duckdb-postgres/duckdb-postgres/duckdb/src/include/duckdb/common/helper.hpp:66 (libduckdb.so+0x337567b) (BuildId: 522787844199fbfa6a5d21195010717f011c4dc5)
    #10 duckdb::PhysicalTableScan::GetGlobalSourceState(duckdb::ClientContext&) const /home/runner/work/duckdb-postgres/duckdb-postgres/duckdb/src/execution/operator/scan/physical_table_scan.cpp:101 (libduckdb.so+0x337567b)
    #11 duckdb::Pipeline::ResetSource(bool) /home/runner/work/duckdb-postgres/duckdb-postgres/duckdb/src/parallel/pipeline.cpp:306 (libduckdb.so+0x3776036) (BuildId: 522787844199fbfa6a5d21195010717f011c4dc5)
    #12 duckdb::Pipeline::Reset() /home/runner/work/duckdb-postgres/duckdb-postgres/duckdb/src/parallel/pipeline.cpp:270 (libduckdb.so+0x3776b71) (BuildId: 522787844199fbfa6a5d21195010717f011c4dc5)
    #13 duckdb::Pipeline::Schedule(duckdb::shared_ptr<duckdb::Event, true>&) /home/runner/work/duckdb-postgres/duckdb-postgres/duckdb/src/parallel/pipeline.cpp:196 (libduckdb.so+0x379f72f) (BuildId: 522787844199fbfa6a5d21195010717f011c4dc5)
    #14 duckdb::PipelineEvent::Schedule() /home/runner/work/duckdb-postgres/duckdb-postgres/duckdb/src/parallel/pipeline_event.cpp:13 (libduckdb.so+0x379fa1c) (BuildId: 522787844199fbfa6a5d21195010717f011c4dc5)
    #15 duckdb::Event::CompleteDependency() /home/runner/work/duckdb-postgres/duckdb-postgres/duckdb/src/parallel/event.cpp:20 (libduckdb.so+0x3775d4a) (BuildId: 522787844199fbfa6a5d21195010717f011c4dc5)
    #16 duckdb::Event::Finish() /home/runner/work/duckdb-postgres/duckdb-postgres/duckdb/src/parallel/event.cpp:38 (libduckdb.so+0x3775a3e) (BuildId: 522787844199fbfa6a5d21195010717f011c4dc5)
    #17 duckdb::Event::FinishTask() /home/runner/work/duckdb-postgres/duckdb-postgres/duckdb/src/parallel/event.cpp:62 (libduckdb.so+0x3775e57) (BuildId: 522787844199fbfa6a5d21195010717f011c4dc5)
    #18 duckdb::PipelineInitializeTask::ExecuteTask(duckdb::TaskExecutionMode) /home/runner/work/duckdb-postgres/duckdb-postgres/duckdb/src/parallel/pipeline_initialize_event.cpp:22 (libduckdb.so+0x37768b1) (BuildId: 522787844199fbfa6a5d21195010717f011c4dc5)
    #19 duckdb::ExecutorTask::Execute(duckdb::TaskExecutionMode) /home/runner/work/duckdb-postgres/duckdb-postgres/duckdb/src/parallel/executor_task.cpp:52 (libduckdb.so+0x3781190) (BuildId: 522787844199fbfa6a5d21195010717f011c4dc5)
    #20 duckdb::TaskScheduler::TryDequeueAndProcessTask(duckdb::DBConfig const&, duckdb::TaskSchedulerQueue&, duckdb::shared_ptr<duckdb::Task, true>&) /home/runner/work/duckdb-postgres/duckdb-postgres/duckdb/src/parallel/task_scheduler.cpp:125 (libduckdb.so+0x379cca0) (BuildId: 522787844199fbfa6a5d21195010717f011c4dc5)
    #21 duckdb::TaskScheduler::ExecuteForever(std::atomic<bool>*, duckdb::TaskSchedulerType) /home/runner/work/duckdb-postgres/duckdb-postgres/duckdb/src/parallel/task_scheduler.cpp:191 (libduckdb.so+0x379d2f3) (BuildId: 522787844199fbfa6a5d21195010717f011c4dc5)
    #22 ThreadExecuteTasks /home/runner/work/duckdb-postgres/duckdb-postgres/duckdb/src/parallel/task_scheduler_pool.cpp:122 (libduckdb.so+0x379d5a8) (BuildId: 522787844199fbfa6a5d21195010717f011c4dc5)
    #23 void std::__invoke_impl<void, void (*)(duckdb::TaskScheduler*, std::atomic<bool>*, duckdb::TaskSchedulerType), duckdb::TaskScheduler*, std::atomic<bool>*, duckdb::TaskSchedulerType>(std::__invoke_other, void (*&&)(duckdb::TaskScheduler*, std::atomic<bool>*, duckdb::TaskSchedulerType), duckdb::TaskScheduler*&&, std::atomic<bool>*&&, duckdb::TaskSchedulerType&&) /usr/include/c++/13/bits/invoke.h:61 (libduckdb.so+0x37695a7) (BuildId: 522787844199fbfa6a5d21195010717f011c4dc5)
    #24 std::__invoke_result<void (*)(duckdb::TaskScheduler*, std::atomic<bool>*, duckdb::TaskSchedulerType), duckdb::TaskScheduler*, std::atomic<bool>*, duckdb::TaskSchedulerType>::type std::__invoke<void (*)(duckdb::TaskScheduler*, std::atomic<bool>*, duckdb::TaskSchedulerType), duckdb::TaskScheduler*, std::atomic<bool>*, duckdb::TaskSchedulerType>(void (*&&)(duckdb::TaskScheduler*, std::atomic<bool>*, duckdb::TaskSchedulerType), duckdb::TaskScheduler*&&, std::atomic<bool>*&&, duckdb::TaskSchedulerType&&) /usr/include/c++/13/bits/invoke.h:96 (libduckdb.so+0x37695a7)
    #25 void std::thread::_Invoker<std::tuple<void (*)(duckdb::TaskScheduler*, std::atomic<bool>*, duckdb::TaskSchedulerType), duckdb::TaskScheduler*, std::atomic<bool>*, duckdb::TaskSchedulerType> >::_M_invoke<0ul, 1ul, 2ul, 3ul>(std::_Index_tuple<0ul, 1ul, 2ul, 3ul>) /usr/include/c++/13/bits/std_thread.h:292 (libduckdb.so+0x37695a7)
    #26 std::thread::_Invoker<std::tuple<void (*)(duckdb::TaskScheduler*, std::atomic<bool>*, duckdb::TaskSchedulerType), duckdb::TaskScheduler*, std::atomic<bool>*, duckdb::TaskSchedulerType> >::operator()() /usr/include/c++/13/bits/std_thread.h:299 (libduckdb.so+0x37695a7)
    #27 std::thread::_State_impl<std::thread::_Invoker<std::tuple<void (*)(duckdb::TaskScheduler*, std::atomic<bool>*, duckdb::TaskSchedulerType), duckdb::TaskScheduler*, std::atomic<bool>*, duckdb::TaskSchedulerType> > >::_M_run() /usr/include/c++/13/bits/std_thread.h:244 (libduckdb.so+0x37695a7)
    #28 <null> <null> (libstdc++.so.6+0xecdb3) (BuildId: 753c6c8608b61d4e67be8f0c890e03e0aa046b8b)

@hello-world-bfree hello-world-bfree force-pushed the fix/postgres-catalogset-scan-uaf branch from d8befe0 to f8537b8 Compare July 8, 2026 14:38
@hello-world-bfree

Copy link
Copy Markdown
Contributor Author

I wonder whether the ReferenceEntry call itself needs to be synchronized?

Thanks for catching this!

@staticlibs staticlibs left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the update!

@staticlibs staticlibs merged commit dbaf7f4 into duckdb:main Jul 8, 2026
7 checks passed
staticlibs added a commit to staticlibs/duckdb-postgres that referenced this pull request Jul 8, 2026
This is a backport of the PR duckdb#501 to v1.5-variegata stable branch.

Resolves duckdb#502

`PostgresCatalogSet::Scan` hands its callback a bare `CatalogEntry&`
without pinning it into the calling transaction first. `GetEntry` and
`CreateEntry` both keep the underlying `shared_ptr<CatalogEntry>`
alive in `PostgresTransaction::referenced_entries` for the rest of the
transaction. `Scan` does not.

If `pg_clear_cache` runs after a `Scan` but before every reference
handed out by that `Scan` is done being used, `ClearEntries` drops
the `shared_ptr` for those entries and destroys them. Anything still
holding a reference from the earlier `Scan` is then reading freed
memory; i.e., funcs from Duckdb core like `duckdb_tables()` or
`duckdb_vies()`

Pinning every entry into the calling transaction inside `Scan`'s
existing lock scope, the same way `GetEntry` and `CreateEntry` already
do:

```cpp
for (auto &entry : entries) {
        transaction.ReferenceEntry(entry.second);
        callback(*entry.second);
}
```

`ReferenceEntry`/`referenced_entries` is already the established
solution for this exact problem. `Scan` just wasn't using it yet
staticlibs added a commit that referenced this pull request Jul 8, 2026
This is a backport of the PR #501 to v1.5-variegata stable branch.

Resolves #502

`PostgresCatalogSet::Scan` hands its callback a bare `CatalogEntry&`
without pinning it into the calling transaction first. `GetEntry` and
`CreateEntry` both keep the underlying `shared_ptr<CatalogEntry>`
alive in `PostgresTransaction::referenced_entries` for the rest of the
transaction. `Scan` does not.

If `pg_clear_cache` runs after a `Scan` but before every reference
handed out by that `Scan` is done being used, `ClearEntries` drops
the `shared_ptr` for those entries and destroys them. Anything still
holding a reference from the earlier `Scan` is then reading freed
memory; i.e., funcs from Duckdb core like `duckdb_tables()` or
`duckdb_vies()`

Pinning every entry into the calling transaction inside `Scan`'s
existing lock scope, the same way `GetEntry` and `CreateEntry` already
do:

```cpp
for (auto &entry : entries) {
        transaction.ReferenceEntry(entry.second);
        callback(*entry.second);
}
```

`ReferenceEntry`/`referenced_entries` is already the established
solution for this exact problem. `Scan` just wasn't using it yet
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Catalog scan leads to use-after-free under concurrent pressure

2 participants