Skip to content

Commit 6c7a96f

Browse files
authored
Fix Database::getHeaderInfo() signed-shift UB and use fixed-width Header types (#558)
## Summary Fixes two findings from the deep code review (DB-02, DB-03) in `Database::getHeaderInfo()` and the public `Header` struct. ### DB-02 — signed left-shift UB in the big-endian assembly The multi-byte header fields were assembled from `unsigned char` operands, which promote to `int` before the `<< 24` shift. For any byte with the high bit set this is signed-left-shift undefined behaviour, and the `int` result was then sign-extended when widened to the (LP64) `unsigned long` fields. Each byte is now cast to `std::uint32_t` before shifting, through a small local `readBE32` helper that also collapses the 14 repeated 4-line blocks. ### DB-03 — platform-variable field widths `Header` used `unsigned long`, which is 32-bit on LLP64 (Windows) but 64-bit on LP64 (Linux/macOS), so the struct layout and field widths differed across platforms. The struct now uses fixed-width `<cstdint>` types (`std::uint8_t` / `std::uint32_t`). ## Test plan - Built `SQLiteCpp_tests` (MSVC, Debug) — clean. - `ctest -C Debug` — 100% passed, including `Database.getHeaderInfo`. No public API behaviour changes; the parsed values are identical, only the field types and the (previously UB) assembly path change.
2 parents 1dc77ce + c4ee389 commit 6c7a96f

3 files changed

Lines changed: 47 additions & 107 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,3 +308,4 @@ Version 3.4.0 - 2026 ???
308308
- Fix execute_many() to clear stale bindings between parameter sets (#554)
309309
- Fix Column::operator<< to stream the exact column bytes via getString() (#556)
310310
- Restore the Coverity Scan static analysis as a GitHub Actions workflow, replacing the old Travis CI job
311+
- Fix Database::getHeaderInfo() signed-shift UB and use fixed-width types for the Header struct (#558)

include/SQLiteCpp/Database.h

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ namespace filesystem = experimental::filesystem;
6464

6565
#endif // SQLITECPP_HAVE_STD_EXPERIMENTAL_FILESYSTEM
6666

67+
#include <cstdint>
6768
#include <memory>
6869
#include <string.h>
6970

@@ -120,28 +121,28 @@ SQLITECPP_API int getLibVersionNumber() noexcept;
120121
// Public structure for representing all fields contained within the SQLite header.
121122
// Official documentation for fields: https://www.sqlite.org/fileformat.html#the_database_header
122123
struct Header {
123-
unsigned char headerStr[16];
124-
unsigned int pageSizeBytes;
125-
unsigned char fileFormatWriteVersion;
126-
unsigned char fileFormatReadVersion;
127-
unsigned char reservedSpaceBytes;
128-
unsigned char maxEmbeddedPayloadFrac;
129-
unsigned char minEmbeddedPayloadFrac;
130-
unsigned char leafPayloadFrac;
131-
unsigned long fileChangeCounter;
132-
unsigned long databaseSizePages;
133-
unsigned long firstFreelistTrunkPage;
134-
unsigned long totalFreelistPages;
135-
unsigned long schemaCookie;
136-
unsigned long schemaFormatNumber;
137-
unsigned long defaultPageCacheSizeBytes;
138-
unsigned long largestBTreePageNumber;
139-
unsigned long databaseTextEncoding;
140-
unsigned long userVersion;
141-
unsigned long incrementalVaccumMode;
142-
unsigned long applicationId;
143-
unsigned long versionValidFor;
144-
unsigned long sqliteVersion;
124+
std::uint8_t headerStr[16];
125+
std::uint32_t pageSizeBytes;
126+
std::uint8_t fileFormatWriteVersion;
127+
std::uint8_t fileFormatReadVersion;
128+
std::uint8_t reservedSpaceBytes;
129+
std::uint8_t maxEmbeddedPayloadFrac;
130+
std::uint8_t minEmbeddedPayloadFrac;
131+
std::uint8_t leafPayloadFrac;
132+
std::uint32_t fileChangeCounter;
133+
std::uint32_t databaseSizePages;
134+
std::uint32_t firstFreelistTrunkPage;
135+
std::uint32_t totalFreelistPages;
136+
std::uint32_t schemaCookie;
137+
std::uint32_t schemaFormatNumber;
138+
std::uint32_t defaultPageCacheSizeBytes;
139+
std::uint32_t largestBTreePageNumber;
140+
std::uint32_t databaseTextEncoding;
141+
std::uint32_t userVersion;
142+
std::uint32_t incrementalVaccumMode;
143+
std::uint32_t applicationId;
144+
std::uint32_t versionValidFor;
145+
std::uint32_t sqliteVersion;
145146
};
146147

147148
/**

src/Database.cpp

Lines changed: 23 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -329,97 +329,35 @@ Header Database::getHeaderInfo(const std::string& aFilename)
329329
throw SQLite::Exception("Invalid or encrypted SQLite header in file " + aFilename);
330330
}
331331

332-
h.pageSizeBytes = (buf[16] << 8) | buf[17];
332+
const auto readBE32 = [&buf](std::size_t offset) -> std::uint32_t
333+
{
334+
return (static_cast<std::uint32_t>(buf[offset]) << 24) |
335+
(static_cast<std::uint32_t>(buf[offset + 1]) << 16) |
336+
(static_cast<std::uint32_t>(buf[offset + 2]) << 8) |
337+
(static_cast<std::uint32_t>(buf[offset + 3]) << 0);
338+
};
339+
340+
h.pageSizeBytes = (static_cast<std::uint32_t>(buf[16]) << 8) | static_cast<std::uint32_t>(buf[17]);
333341
h.fileFormatWriteVersion = buf[18];
334342
h.fileFormatReadVersion = buf[19];
335343
h.reservedSpaceBytes = buf[20];
336344
h.maxEmbeddedPayloadFrac = buf[21];
337345
h.minEmbeddedPayloadFrac = buf[22];
338346
h.leafPayloadFrac = buf[23];
339-
340-
h.fileChangeCounter =
341-
(buf[24] << 24) |
342-
(buf[25] << 16) |
343-
(buf[26] << 8) |
344-
(buf[27] << 0);
345-
346-
h.databaseSizePages =
347-
(buf[28] << 24) |
348-
(buf[29] << 16) |
349-
(buf[30] << 8) |
350-
(buf[31] << 0);
351-
352-
h.firstFreelistTrunkPage =
353-
(buf[32] << 24) |
354-
(buf[33] << 16) |
355-
(buf[34] << 8) |
356-
(buf[35] << 0);
357-
358-
h.totalFreelistPages =
359-
(buf[36] << 24) |
360-
(buf[37] << 16) |
361-
(buf[38] << 8) |
362-
(buf[39] << 0);
363-
364-
h.schemaCookie =
365-
(buf[40] << 24) |
366-
(buf[41] << 16) |
367-
(buf[42] << 8) |
368-
(buf[43] << 0);
369-
370-
h.schemaFormatNumber =
371-
(buf[44] << 24) |
372-
(buf[45] << 16) |
373-
(buf[46] << 8) |
374-
(buf[47] << 0);
375-
376-
h.defaultPageCacheSizeBytes =
377-
(buf[48] << 24) |
378-
(buf[49] << 16) |
379-
(buf[50] << 8) |
380-
(buf[51] << 0);
381-
382-
h.largestBTreePageNumber =
383-
(buf[52] << 24) |
384-
(buf[53] << 16) |
385-
(buf[54] << 8) |
386-
(buf[55] << 0);
387-
388-
h.databaseTextEncoding =
389-
(buf[56] << 24) |
390-
(buf[57] << 16) |
391-
(buf[58] << 8) |
392-
(buf[59] << 0);
393-
394-
h.userVersion =
395-
(buf[60] << 24) |
396-
(buf[61] << 16) |
397-
(buf[62] << 8) |
398-
(buf[63] << 0);
399-
400-
h.incrementalVaccumMode =
401-
(buf[64] << 24) |
402-
(buf[65] << 16) |
403-
(buf[66] << 8) |
404-
(buf[67] << 0);
405-
406-
h.applicationId =
407-
(buf[68] << 24) |
408-
(buf[69] << 16) |
409-
(buf[70] << 8) |
410-
(buf[71] << 0);
411-
412-
h.versionValidFor =
413-
(buf[92] << 24) |
414-
(buf[93] << 16) |
415-
(buf[94] << 8) |
416-
(buf[95] << 0);
417-
418-
h.sqliteVersion =
419-
(buf[96] << 24) |
420-
(buf[97] << 16) |
421-
(buf[98] << 8) |
422-
(buf[99] << 0);
347+
h.fileChangeCounter = readBE32(24);
348+
h.databaseSizePages = readBE32(28);
349+
h.firstFreelistTrunkPage = readBE32(32);
350+
h.totalFreelistPages = readBE32(36);
351+
h.schemaCookie = readBE32(40);
352+
h.schemaFormatNumber = readBE32(44);
353+
h.defaultPageCacheSizeBytes = readBE32(48);
354+
h.largestBTreePageNumber = readBE32(52);
355+
h.databaseTextEncoding = readBE32(56);
356+
h.userVersion = readBE32(60);
357+
h.incrementalVaccumMode = readBE32(64);
358+
h.applicationId = readBE32(68);
359+
h.versionValidFor = readBE32(92);
360+
h.sqliteVersion = readBE32(96);
423361

424362
return h;
425363
}

0 commit comments

Comments
 (0)