Skip to content

Fixes for "Firebird Internals: Inside a Firebird Database" v1.5 #232

@fdcastel

Description

@fdcastel

The following is an initial comparison made by Claude of the current "Firebird Internals" documentation (v1.5) against the actual Firebird source code (latest master, v6.0).


Document reviewed: "Firebird Internals: Inside a Firebird Database" v1.5 (9 February 2026) by Norman Dunbar and Mark Rotteveel.

Reference: Firebird source code, specifically src/jrd/ods.h and src/jrd/btr.h from the current Firebird 6.x codebase (which also contains the ODS 12/13 definitions used by Firebird 3.x/4.x/5.x).

Note: The document explicitly states it describes ODS 11.1 (Firebird 2.1) with a 4,096-byte page size and includes a caution that newer ODS versions may differ. However, many structural descriptions are presented as general truths without ODS-version qualification. The errors below are cases where the document's descriptions are factually incorrect even for ODS 11.1, or where the document makes forward-looking claims that are now verifiably wrong.


Error 1: Base Page Header — pag_checksum Description Is Stale

Section: 3. Standard Database Page Header

Document says:

pag_checksum — Two bytes, unsigned. Bytes 0x02 - 0x03. Checksum for the whole page. No longer used, always 12345, 0x3039.

"From Firebird 3.0, it is possible that this field in the page header will probably have a new name and function."

What actually happened (source: ods.h, line 249):
In ODS 12.0 (Firebird 3.0), pag_checksum was removed entirely. The field at offset 2 is now pag_reserved (USHORT) — it exists solely for alignment padding and carries no semantic meaning.

Additionally, the old pag_reserved at offset 12 was renamed to pag_pageno and repurposed as a page number self-check field for validation.

The speculative language ("it is possible that" / "will probably") should be updated to reflect the actual change that was made over 10 years ago.

Source reference: src/jrd/ods.h lines 246–256:

struct pag
{
    UCHAR pag_type;
    UCHAR pag_flags;
    USHORT pag_reserved;        // not used but anyway present because of alignment rules
    ULONG pag_generation;
    ULONG pag_scn;
    ULONG pag_pageno;           // for validation
};

Error 2: Base Page Header — Missing crypted_page Flag

Section: 3. Standard Database Page Header

Document says: The pag_flags byte "holds various flags for the page" (no specifics given for the base header level).

What's missing: Since ODS 12.0, bit 7 (0x80) of pag_flags is used as the crypted_page flag on all page types. When set, it indicates the page's data (everything after the 16-byte pag header) is encrypted on disk.

Source reference: src/jrd/ods.h line 242:

inline constexpr UCHAR crypted_page = 0x80;

Error 3: Page Type 0x0a — Described as "Write Ahead Log Page"

Section: 3. Standard Database Page Header (page type table) & Section 13

Document says:

0x0a — Page 2 of any database is a Write Ahead Log page. These pages are no longer used.

"From Firebird 3.0 it is likely there will not be a WAL page in any new databases."

Actual: In ODS 12.0 (Firebird 3.0), page type 10 (0x0a) was repurposed as pag_scns — the SCN Inventory Page. Page 2 (FIRST_SCN_PAGE) is now always a SCN page, not a WAL page. WAL pages no longer exist at all.

Source reference: src/jrd/ods.h lines 210, 216:

inline constexpr SCHAR pag_scns = 10;   // SCN's inventory page
inline constexpr ULONG FIRST_SCN_PAGE = 2;

Error 4: Header Page — Struct Layout Does Not Match ODS 12.0+

Section: 4. Database Header Page — Type 0x01

Document shows the header page struct as:

struct header_page
{
    pag hdr_header;
    USHORT hdr_page_size;
    USHORT hdr_ods_version;
    SLONG hdr_PAGES;
    ULONG hdr_next_page;
    SLONG hdr_oldest_transaction;
    SLONG hdr_oldest_active;
    SLONG hdr_next_transaction;
    USHORT hdr_sequence;
    USHORT hdr_flags;
    SLONG hdr_creation_date[2];
    SLONG hdr_attachment_id;
    ...
    UCHAR hdr_data[1];
};

Actual (ODS 12.0+, source: ods.h lines 512–543):

The struct was completely reorganized. Key differences:

Issue Document Actual (ODS 12.0+)
hdr_ods_minor position Offset 0x3e (near end) Offset 20 (right after hdr_ods_version)
Transaction fields type SLONG (4 bytes) FB_UINT64 (8 bytes)
hdr_attachment_id type SLONG (4 bytes) FB_UINT64 (8 bytes)
hdr_sequence Present Removed
hdr_next_page Present Removed
hdr_implementation SSHORT Replaced by hdr_db_impl struct (4 bytes: cpu, os, cc, compat)
hdr_ods_minor_original Present Removed
hdr_bumped_transaction Present Removed
hdr_backup_pages Present Removed
hdr_misc[3] Present (12 bytes) Removed
hdr_backup_mode In hdr_flags bits Separate UCHAR field at offset 24
hdr_shutdown_mode In hdr_flags bits Separate UCHAR field at offset 25
hdr_replica_mode N/A New UCHAR field at offset 26
hdr_guid N/A New UCHAR[16] at offset 84
hdr_crypt_page N/A New ULONG at offset 112
hdr_crypt_plugin N/A New TEXT[32] at offset 116
Total fixed size ~96 bytes 152 bytes
hdr_data offset 0x60 0x94 (148)

While the document does say it describes ODS 11.1, it would benefit from at least noting that the header page layout changed fundamentally in ODS 12.0.


Error 5: Header Page Flags — Incomplete and Partially Wrong for Modern ODS

Section: 4. Database Header Page — hdr_flags

Document shows hdr_shutdown_mask as 0x1080 (bits 7 and 12) packed into hdr_flags.

Actual (ODS 12.0+): Shutdown mode and backup mode are no longer packed into hdr_flags. They have their own dedicated byte fields (hdr_shutdown_mode and hdr_backup_mode).

The current hdr_flags are (ods.h lines 592–598):

Flag Value Description
hdr_active_shadow 0x01 Active shadow file
hdr_force_write 0x02 Forced writes
hdr_crypt_process 0x04 Encryption status changing (was unused bit 2)
hdr_no_reserve 0x08 Don't reserve space for versions (was 0x20)
hdr_SQL_dialect_3 0x10 SQL dialect 3 (was 0x100)
hdr_read_only 0x20 Read-only (was 0x200)
hdr_encrypted 0x40 Database is encrypted (new)

Note: The flag values have been reassigned. In ODS 11.x, hdr_no_reserve was 0x20 and hdr_SQL_dialect_3 was 0x100. In ODS 12.0+, they are 0x08 and 0x10 respectively.


Error 6: Header Page Clumplets — Values Are Renumbered in ODS 12.0+

Section: 4. Database Header Page — clumplet types

Document shows HDR_sweep_interval = 0x06 and HDR_difference_file = 0x0c.

Actual (ODS 12.0+, ods.h lines 575–588):

Value Document (ODS 11.x) Actual (ODS 12.0+)
0 HDR_end HDR_end
1 HDR_root_file_name HDR_root_file_name
2 HDR_journal_server (commented out as HDR_file)
3 HDR_file (commented out as HDR_last_page)
4 HDR_last_page HDR_sweep_interval ⚠️
5 HDR_unlicensed HDR_crypt_checksum ⚠️
6 HDR_sweep_interval HDR_difference_file ⚠️
7 HDR_log_name HDR_backup_guid ⚠️
8 HDR_journal_file HDR_crypt_key ⚠️
9 HDR_password_file_key HDR_crypt_hash ⚠️
10 HDR_backup_info (commented out: HDR_db_guid)
11 HDR_cache_file HDR_repl_seq ⚠️
12 HDR_difference_file (no longer exists)
13 HDR_backup_guid (no longer exists)

The clumplet numbering was completely reshuffled. Several old types were removed and new encryption/replication types were added.


Error 7: PIP Page — Missing New Fields

Section: 5. Page Inventory Page — Type 0x02

Document shows the PIP struct as:

struct page_inv_page
{
    pag pip_header;
    SLONG pip_min;
    UCHAR pip_bits[1];
};

Actual (ODS 12.0+, ods.h lines 619–625):

struct page_inv_page
{
    pag pip_header;
    ULONG pip_min;           // was SLONG
    ULONG pip_extent;        // NEW - lowest free extent
    ULONG pip_used;          // NEW - number of pages allocated from this PIP
    UCHAR pip_bits[1];
};

The total fixed size grew from 20 bytes to 32 bytes. The pip_bits array now starts at offset 28 instead of 20. pip_min changed from SLONG to ULONG.


Error 8: Pointer Page — ppg_max_space Field

Section: 7. Pointer Page — Type 0x04

Document says: ppg_max_space exists at offset 0x1e, "intended to indicate the last entry in the ppg_page array holding a page number which has free space in the page, but it has never been used."

Actual (ODS 12.0+, ods.h lines 690–699): The ppg_max_space field has been removed entirely. The pointer page struct no longer contains this field. ppg_page[] now starts at offset 32 (was 0x20).

Also, the per-data-page bitmaps at the end of the pointer page now use 8 bits (one full byte) per data page instead of the 2-bit encoding described in the document. The flags are (ods.h lines 713–717):

Flag Value Description
ppg_dp_full 0x01 Data page is FULL
ppg_dp_large 0x02 Large object on data page
ppg_dp_swept 0x04 Sweep has nothing to do (new)
ppg_dp_secondary 0x08 Primary record versions not stored (new)
ppg_dp_empty 0x10 Data page is empty (new)

Error 9: Data Page — Offsets Are Wrong for ODS 12.0+

Section: 8. Data Page — Type 0x05

Document says:

dpg_sequence — Offset 0x10 on the page
dpg_relation — Offset 0x14 on the page
dpg_count — Offset 0x16 on the page
dpg_rpt — begins at offset 0x18 on the page

Actual (ODS 12.0+, ods.h lines 341–352):

The offsets are still correct (base pag header is still 16 bytes, so data page fields start at offset 16), but:

  • dpg_sequence is now ULONG (was SLONG) — offset 0x10 ✅
  • dpg_relation — offset 0x14 ✅
  • dpg_count — offset 0x16 ✅
  • dpg_rpt — offset 0x18 ✅

The offsets happen to still be correct. However, the document says dpg_rpt is at offset 0x18, while in version 1.2 of the document there was an off-by-2 error that was already fixed.

The new flags dpg_swept (0x08) and dpg_secondary (0x10) are not mentioned.


Error 10: Record Header — Missing rhde Struct and New Flags

Section: 8.1. Record Header

Document shows only rhd (unfragmented) and rhdf (fragmented) record headers.

What's missing (ODS 12.0+, ods.h lines 794–804):

There is a third record header variant: rhde — the extended record header used when rhd_long_tranum (0x400) is set. This struct has a rhde_tra_high field (USHORT) that stores the upper 16 bits of a 48-bit transaction number.

Similarly, rhdf now also includes rhdf_tra_high between rhdf_format and rhdf_f_page.

New record flags not documented:

Flag Value Description
rhd_uk_modified 0x200 Unique key field values changed
rhd_long_tranum 0x400 Transaction number is 64-bit (use rhde/rhdf)
rhd_not_packed 0x800 Record stored as-is (no RLE compression)

Source reference: src/jrd/ods.h lines 886–897.


Error 11: Index Root Page — Struct Layout Significantly Changed

Section: 9. Index Root Page — Type 0x06

Document shows irt_repeat as 12 bytes (0x0b in the text — which is actually 11, likely a typo):

struct irt_repeat {
    SLONG irt_root;
    union {
        float irt_selectivity;
        SLONG irt_transaction;
    } irt_stuff;
    USHORT irt_desc;
    UCHAR irt_keys;
    UCHAR irt_flags;
};

Actual (ODS 12.0+, ods.h lines 393–421): irt_repeat is now 24 bytes:

Offset Field Type Size
0 irt_transaction FB_UINT64 8
8 irt_page_num ULONG 4
12 irt_page_space_id ULONG 4
16 irt_desc USHORT 2
18 irt_flags USHORT 2 (was UCHAR)
20 irt_state UCHAR 1 (new)
21 irt_keys UCHAR 1
22 irt_dummy USHORT 2 (alignment)

Key changes:

  • irt_root replaced by irt_page_num + irt_page_space_id
  • irt_selectivity removed from the repeat struct (lives only in key descriptors irtd)
  • irt_transaction is now FB_UINT64 (was SLONG in union)
  • irt_flags widened from UCHAR to USHORT
  • New irt_state field replaces the old in-progress behavior
  • New flag: irt_condition (0x20) for conditional indices

The document also says descriptors are "0x0b bytes long" — this should be 12 bytes for ODS 11.x (4+4+2+1+1 = 12), and is 24 bytes for ODS 12.0+.


Error 12: Index Root Page — Index Flag Values

Section: 9. Index Root Page — irt_flags

Document shows:

Bit 3: Index is a foreign key index
Bit 4: Index is a primary key index

Actual (ODS 12.0+, ods.h lines 442–447):

inline constexpr USHORT irt_unique      = 1;   // bit 0
inline constexpr USHORT irt_descending  = 2;   // bit 1
inline constexpr USHORT irt_foreign     = 4;   // bit 2 ← was bit 3 in document
inline constexpr USHORT irt_primary     = 8;   // bit 3 ← was bit 4 in document
inline constexpr USHORT irt_expression  = 16;  // bit 4 ← was bit 5 in document
inline constexpr USHORT irt_condition   = 32;  // bit 5 ← NEW

The document has the foreign key flag at bit 3 (value 8) and primary key at bit 4 (value 16). The actual source code has them at bit 2 (value 4) and bit 3 (value 8) respectively. The document's bit numbering appears to have been wrong even for ODS 11.x — these flag values have not changed between ODS versions.

Cross-checking with btr.h (lines 112–117) confirms:

inline constexpr int idx_unique     = 1;
inline constexpr int idx_descending = 2;
inline constexpr int idx_foreign    = 4;
inline constexpr int idx_primary    = 8;
inline constexpr int idx_expression = 16;
inline constexpr int idx_condition  = 32;

However, looking at the old fbdump source code, the old fbdump also used these same values (bit 2 = in_progress, bit 3 = foreign, bit 4 = primary, bit 5 = expression), and its output correctly shows "Foreign Key" for flag value 8 and "Primary Key" for flag value 17. So the document's description of the bit positions is wrong, but the old fbdump code was actually correct (it tests flags & 4 for "Creating", flags & 8 for "Foreign Key", flags & 16 for "Primary Key").

Wait — re-reading the document more carefully, it says:

  • Bit 2: "Index [creation?] is in progress"
  • Bit 3: "Index is a foreign key index"
  • Bit 4: "Index is a primary key index"

And the source says:

  • Bit 2 (irt_foreign = 4): Foreign key
  • Bit 3 (irt_primary = 8): Primary key
  • Bit 4 (irt_expression = 16): Expression

So the document has irt_in_progress at bit 2 where the source has irt_foreign. In the current source, the "in progress" concept is handled by irt_state, not as a flag bit. In older ODS, bit 2 was irt_in_progress (value 4). The bit assignments for foreign (bit 3) and primary (bit 4) in the document match the old ODS 11.x code.

Corrected assessment: For ODS 11.x, the document's flag assignments may have been correct. But in ODS 12.0+, irt_in_progress was removed as a flag and the remaining flags were renumbered:

  • irt_foreign moved from bit 3 → bit 2
  • irt_primary moved from bit 4 → bit 3
  • irt_expression moved from bit 5 → bit 4
  • irt_condition added at bit 5

Error 13: B-tree Page — Jump Info Structure Differs

Section: 10.2. Index Jump Info

Document says the jump info is a separate struct following the btree header at byte 0x22:

struct IndexJumpInfo
{
    USHORT firstNodeOffset;
    USHORT jumpAreaSize;
    UCHAR  jumpers;
};

And says jumpers is at "Offset 0x05 in the structure" (this is wrong — it should be offset 0x04 within the 5-byte struct).

Actual (ODS 12.0+, ods.h lines 293–312):

The jump info fields are now integrated directly into the btree_page struct:

struct btree_page
{
    pag btr_header;          // offset 0, 16 bytes
    ULONG btr_sibling;       // offset 16
    ULONG btr_left_sibling;  // offset 20
    SLONG btr_prefix_total;  // offset 24
    USHORT btr_relation;     // offset 28
    USHORT btr_length;       // offset 30
    UCHAR btr_id;            // offset 32
    UCHAR btr_level;         // offset 33
    USHORT btr_jump_interval; // offset 34 ← replaces firstNodeOffset
    USHORT btr_jump_size;     // offset 36 ← replaces jumpAreaSize
    UCHAR btr_jump_count;    // offset 38 ← replaces jumpers
    UCHAR btr_nodes[1];      // offset 39 ← node data starts here
};

There is no separate IndexJumpInfo struct. The three jump fields are regular members of btree_page. Also:

  • The field formerly called firstNodeOffset is now btr_jump_interval (jump interval between nodes, different semantic)
  • jumpAreaSizebtr_jump_size
  • jumpersbtr_jump_count
  • Node data starts at offset 39 (was 0x27 = 39 if you add the 5-byte jump info to 0x22, so the byte offset is coincidentally the same but the structural approach is different)

Error 14: B-tree Page — pag_flags Bit Numbering

Section: 10.1. B-Tree Header — pag_flags

Document says:

Bit 3: set means that this page/bucket is part of a descending index
Bit 4: set means that non-leaf nodes will contain record number information
Bit 5: set means that large keys are permitted/used
Bit 6: set means that the page contains index jump nodes

Actual (ODS 12.0+, ods.h lines 331–337):

The old B-tree page flags are all commented out in the current source:

//const UCHAR btr_dont_gc       = 1;    // Don't garbage-collect this page
//const UCHAR btr_descending    = 2;    // Page/bucket is part of a descending index
//const UCHAR btr_jump_info     = 16;   // AB: 2003-index-structure enhancement
inline constexpr UCHAR btr_released = 32; // Page was released from b-tree

The only remaining active flag is btr_released = 32 (bit 5). The descending flag was at bit 1 (value 2), not bit 3 as the document states. The document's bit 3 = "descending" is wrong — btr_descending = 2 = bit 1.

Note: the old fbdump source code at fbdump.c line ~822 also has the descending flag at bit 2 (value 4), which also doesn't match the Firebird source's value of 2.


Error 15: Generator Page — gpg_waste Fields Changed

Section: 12. Generator Page — Type 0x09

Document shows:

struct generator_page
{
    pag gpg_header;
    SLONG gpg_sequence;
    SLONG gpg_waste1;
    USHORT gpg_waste2;
    USHORT gpg_waste3;
    USHORT gpg_waste4;
    USHORT gpg_waste5;
    SINT64 gpg_values[1];
};

Actual (ODS 12.0+, ods.h lines 752–758):

struct generator_page
{
    pag gpg_header;
    ULONG gpg_sequence;      // was SLONG
    ULONG gpg_dummy1;        // single dummy for alignment
    SINT64 gpg_values[1];    // starts at offset 24
};

The five waste fields (gpg_waste1 through gpg_waste5, totaling 12 bytes) have been replaced by a single gpg_dummy1 (ULONG, 4 bytes). This means gpg_values[] now starts at offset 24 instead of offset 32. The capacity per page is therefore higher in ODS 12.0+: (pageSize - 24) / 8 generators per page (vs. the old (pageSize - 32) / 8).

For an 8192-byte page: 1021 generators per page (ODS 12.0+) vs. 1020 (ODS 11.x with 4-byte-aligned start at offset 32 — though the document says the formula is (page_size - 32) / 8 which gives 508 for 4K pages).


Error 16: Index Jump Node — Struct Contains Pointers (Not On-Disk)

Section: 10.3. Index Jump Nodes

Document shows:

struct IndexJumpNode
{
    UCHAR* nodePointer;
    USHORT prefix;
    USHORT length;
    USHORT offset;
    UCHAR* data;
};

Issue: This struct contains pointers (UCHAR* nodePointer and UCHAR* data). These are in-memory runtime structures, not on-disk formats. Pointers are 4 or 8 bytes depending on platform and are meaningless on disk. The actual on-disk representation of jump nodes is a packed sequence of: prefix (variable-length encoded), length (variable-length encoded), offset (variable-length encoded), and data bytes — not a fixed struct with pointers.

The document presents this as if it were the on-disk layout, which is misleading.


Summary

# Section Severity Category
1 Base pag header — checksum Medium Stale/speculative text
2 Base pag header — crypted_page flag Low Missing info
3 Page type 0x0a — WAL vs SCN High Factually wrong for ODS 12+
4 Header page — struct layout High Completely different in ODS 12+
5 Header page — flag values High Values reassigned in ODS 12+
6 Header page — clumplet values High Renumbered in ODS 12+
7 PIP page — missing fields Medium Missing pip_extent, pip_used
8 Pointer page — ppg_max_space Medium Removed; per-page bits changed from 2→8
9 Data page — new flags Low Missing dpg_swept, dpg_secondary
10 Record header — missing rhde Medium Missing struct and new flags
11 Index root — irt_repeat size High 24 bytes, not 12
12 Index root — flag bit positions Medium Renumbered in ODS 12+
13 B-tree page — jump info Medium Integrated into struct
14 B-tree page — pag_flags bits Medium Wrong bit for descending
15 Generator page — waste fields Medium Reduced; gpg_values offset changed
16 Jump node — pointer fields Low Runtime struct, not on-disk format

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions