SQLite syntax error due to incorrect static initialization and missing null-termination in Pages::conn_config()

XMLWordPrintableJSON

    • Type: Bug
    • Resolution: Fixed
    • Priority: Major - P3
    • WT12.0.0, 8.3.0-rc0
    • Affects Version/s: None
    • Component/s: PALite
    • None
    • Storage Engines, Storage Engines - Foundations
    • SE Foundations - 2025-12-05
    • 1

      The Pages::conn_config() method is returning garbage SQL statements to SQLite, causing "syntax error" failures when attempting to configure database connections.

      Root Cause:
      The issue has two parts:

      • Static initialization with instance-dependent values: The conn_config() method declares a static array of strings formatted with values from instance member variables (config.mmap_size_mb, config.cache_size_mb, etc.). Because the array is static, it is initialized only once - potentially before any valid config object exists or with garbage values from the first initialization. Subsequent calls reuse the incorrectly initialized static array rather than computing fresh values.
      • Null-termination handling: The configure() template method passes SQL statements to sqlite3_exec() using .data() on various string-like types. When the container holds std::string_view objects (or range views), .data() does not guarantee null-termination. Since sqlite3_exec() requires a null-terminated C string, this causes SQLite to read beyond the actual SQL statement into garbage memory, resulting in syntax errors like: near "\337\227\317": syntax error.

      Observed Error:

       [1764274623:184675][28135:0xffff9b9ccd60], file:collection_0.wt_stable, eviction-server: [WT_VERB_EXTENSION][ERROR]: ext/page_log/palite/palite.cpp:818: void Connection::configure(const Container&) [with Container = std::ranges::ref_view<const std::__cxx11::basic_string<char> [3]>]: sqlite3_exec; return: 1 (SQL logic error); details: 1 (near "g": syntax error); args: '281473030825080', 'g<', '0x0', '0x0', '0x0'
      

      Solution:

      1. Change Pages::conn_config() to return std::array<std::string, 3> by value instead of a view to a static array. This will ensure fresh SQL statements are generated with correct configuration values on each call.
      2. Modify the configure() template method to explicitly construct a std::string from each statement before calling sqlite3_exec(), then use .c_str() to obtain a guaranteed null-terminated C string:

      Reproducer:

      Run: ./test_disagg_failover_perf -cc 1 -kc 1000 -ks 10 -vs 1014 -lc -ingest_size_mb 50 -ve 1
      On branch: perf-test-disagg-failover
      

      Patch for fix:

      diff --git a/ext/page_log/palite/palite.cpp b/ext/page_log/palite/palite.cpp
      index 1b7f2767fb..7fbe930138 100644
      --- a/ext/page_log/palite/palite.cpp
      +++ b/ext/page_log/palite/palite.cpp
      @@ -815,7 +815,8 @@ public:
           configure(const Container &cfg_statements)
           {
               for (const auto &stmt : cfg_statements) {
      -            SQL_CALL_CHECK(db, sqlite3_exec, db, stmt.data(), nullptr, nullptr, nullptr);
      +            std::string sql(stmt);
      +            SQL_CALL_CHECK(db, sqlite3_exec, db, sql.c_str(), nullptr, nullptr, nullptr);
               }
           }
      
      @@ -826,7 +827,8 @@ public:
               statements.clear();
               for (const auto &stmt : sql_statements) {
                   sqlite3_stmt *prepared_stmt = nullptr;
      -            SQL_CALL_CHECK(db, sqlite3_prepare_v2, db, stmt.data(), -1, &prepared_stmt, nullptr);
      +            std::string sql(stmt);
      +            SQL_CALL_CHECK(db, sqlite3_prepare_v2, db, sql.c_str(), -1, &prepared_stmt, nullptr);
                   statements.push_back(prepared_stmt);
               }
           }
      @@ -1544,13 +1546,13 @@ struct Pages : public Table<Pages> {
           Pages &operator=(const Pages &) = delete;
      
           auto
      -    conn_config() -> std::ranges::view auto
      +    conn_config()
           {
               const uint64_t MMAP_SIZE = config.mmap_size_mb * 1_MB;
               const uint64_t PAGE_SIZE = 16_KB;
               const uint64_t CACHE_PAGES = (config.cache_size_mb * 1_MB) / PAGE_SIZE;
      
      -        const static std::string config_statements[] = {
      +        return std::array{
                 /*
                  * Uses memory mapping instead of read/write calls when the database is < mmap_size in
                  * bytes.
      @@ -1568,8 +1570,6 @@ struct Pages : public Table<Pages> {
                  * of pages.
                  */
                 std::format("PRAGMA cache_size = {};", CACHE_PAGES)};
      -
      -        return std::views::all(config_statements);
           }
      
           void
      
      

            Assignee:
            Alex Blekhman
            Reporter:
            Luke Pearson
            Votes:
            0 Vote for this issue
            Watchers:
            2 Start watching this issue

              Created:
              Updated:
              Resolved: