Skip to content

Commit 36c75f6

Browse files
committed
db: add STRICT tables with migration for old databases
Enables STRICT tables in developer mode, but old databases (~2019) may have BLOB values in TEXT columns. Migration converts BLOB faildetail to TEXT with UTF-8 validation, NULLs invalid data. STRICT is only applied to fresh databases; existing databases being upgraded skip STRICT to avoid type affinity issues with legacy data. Also adds security pragmas in developer mode: trusted_schema=OFF, cell_size_check=ON. Fixes #7913. Changelog-Added: Database: STRICT tables and security pragmas in developer mode Changelog-Fixed: Database migration for old BLOB-typed faildetail values
1 parent 22f7a62 commit 36c75f6

File tree

8 files changed

+146
-2
lines changed

8 files changed

+146
-2
lines changed

db/common.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ struct db {
6666

6767
/* Fatal if we try to write to db */
6868
bool readonly;
69+
70+
/* Set during migrations to prevent STRICT mode on table creation */
71+
bool in_migration;
6972
};
7073

7174
struct db_query {

db/db_sqlite3.c

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,16 +203,46 @@ static bool db_sqlite3_setup(struct db *db, bool create)
203203
"PRAGMA foreign_keys = ON;", -1, &stmt, NULL);
204204
err = sqlite3_step(stmt);
205205
sqlite3_finalize(stmt);
206-
return err == SQLITE_DONE;
206+
207+
if (err != SQLITE_DONE)
208+
return false;
209+
210+
if (db->developer) {
211+
sqlite3_prepare_v2(conn2sql(db->conn),
212+
"PRAGMA trusted_schema = OFF;", -1, &stmt, NULL);
213+
sqlite3_step(stmt);
214+
sqlite3_finalize(stmt);
215+
216+
sqlite3_prepare_v2(conn2sql(db->conn),
217+
"PRAGMA cell_size_check = ON;", -1, &stmt, NULL);
218+
sqlite3_step(stmt);
219+
sqlite3_finalize(stmt);
220+
}
221+
222+
return true;
207223
}
208224

209225
static bool db_sqlite3_query(struct db_stmt *stmt)
210226
{
211227
sqlite3_stmt *s;
212228
sqlite3 *conn = conn2sql(stmt->db->conn);
213229
int err;
230+
const char *query = stmt->query->query;
231+
char *modified_query = NULL;
232+
233+
/* STRICT tables for developer mode, and not during upgrades. */
234+
if (stmt->db->developer &&
235+
!stmt->db->in_migration &&
236+
strncasecmp(query, "CREATE TABLE", 12) == 0 &&
237+
!strstr(query, "STRICT")) {
238+
modified_query = tal_fmt(stmt, "%s STRICT", query);
239+
query = modified_query;
240+
}
241+
242+
err = sqlite3_prepare_v2(conn, query, -1, &s, NULL);
214243

215-
err = sqlite3_prepare_v2(conn, stmt->query->query, -1, &s, NULL);
244+
if (modified_query)
245+
tal_free(modified_query);
216246

217247
for (size_t i=0; i<stmt->query->placeholders; i++) {
218248
struct db_binding *b = &stmt->bindings[i];

db/utils.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,7 @@ struct db *db_open_(const tal_t *ctx, const char *filename,
364364
db->in_transaction = NULL;
365365
db->transaction_started = false;
366366
db->changes = NULL;
367+
db->in_migration = false;
367368

368369
/* This must be outside a transaction, so catch it */
369370
assert(!db->in_transaction);

devtools/sql-rewrite.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ def rewrite_single(self, query):
4545
r'BIGINT': 'INTEGER',
4646
r'BIGINTEGER': 'INTEGER',
4747
r'BIGSERIAL': 'INTEGER',
48+
r'VARCHAR(?:\(\d+\))?': 'TEXT',
49+
r'\bINT\b': 'INTEGER',
4850
r'CURRENT_TIMESTAMP\(\)': "strftime('%s', 'now')",
4951
r'INSERT INTO[ \t]+(.*)[ \t]+ON CONFLICT.*DO NOTHING;': 'INSERT OR IGNORE INTO \\1;',
5052
# Rewrite "decode('abcd', 'hex')" to become "x'abcd'"

tests/test_db.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,14 @@ def test_scid_upgrade(node_factory, bitcoind):
163163
assert l1.db_query('SELECT scid FROM channels;') == [{'scid': scid_to_int('103x1x1')}]
164164
assert l1.db_query('SELECT failscid FROM payments;') == [{'failscid': scid_to_int('103x1x1')}]
165165

166+
faildetail_types = l1.db_query(
167+
"SELECT id, typeof(faildetail) as type "
168+
"FROM payments WHERE faildetail IS NOT NULL"
169+
)
170+
for row in faildetail_types:
171+
assert row['type'] == 'text', \
172+
f"Payment {row['id']}: faildetail has type {row['type']}, expected 'text'"
173+
166174

167175
@unittest.skipIf(not COMPAT, "needs COMPAT to convert obsolete db")
168176
@unittest.skipIf(os.getenv('TEST_DB_PROVIDER', 'sqlite3') != 'sqlite3', "This test is based on a sqlite3 snapshot")
@@ -642,3 +650,48 @@ def test_channel_htlcs_id_change(bitcoind, node_factory):
642650
# Make some HTLCS
643651
for amt in (100, 500, 1000, 5000, 10000, 50000, 100000):
644652
l1.pay(l3, amt)
653+
654+
655+
@unittest.skipIf(os.getenv('TEST_DB_PROVIDER', 'sqlite3') != 'sqlite3', "STRICT tables are SQLite3 specific")
656+
def test_sqlite_strict_mode(node_factory):
657+
"""Test that STRICT is appended to CREATE TABLE in developer mode."""
658+
l1 = node_factory.get_node(options={'developer': None})
659+
660+
tables = l1.db_query("SELECT name, sql FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
661+
662+
strict_tables = [t for t in tables if t['sql'] and 'STRICT' in t['sql']]
663+
assert len(strict_tables) > 0, f"Expected at least one STRICT table in developer mode, found none out of {len(tables)}"
664+
665+
known_strict_tables = ['version', 'forwards', 'payments', 'local_anchors', 'addresses']
666+
for table_name in known_strict_tables:
667+
table_sql = next((t['sql'] for t in tables if t['name'] == table_name), None)
668+
if table_sql:
669+
assert 'STRICT' in table_sql, f"Expected table '{table_name}' to be STRICT in developer mode"
670+
671+
672+
@unittest.skipIf(os.getenv('TEST_DB_PROVIDER', 'sqlite3') != 'sqlite3', "SQLite3-specific test")
673+
@unittest.skipIf(not COMPAT, "needs COMPAT to test old database upgrade")
674+
@unittest.skipIf(TEST_NETWORK != 'regtest', "The network must match the DB snapshot")
675+
def test_strict_mode_with_old_database(node_factory, bitcoind):
676+
"""Test old database upgrades work (STRICT not applied during migrations)."""
677+
bitcoind.generate_block(1)
678+
679+
l1 = node_factory.get_node(dbfile='oldstyle-scids.sqlite3.xz',
680+
options={'database-upgrade': True,
681+
'developer': None})
682+
683+
assert l1.rpc.getinfo()['id'] is not None
684+
685+
# Upgraded tables won't be STRICT (only fresh databases get STRICT).
686+
strict_tables = l1.db_query(
687+
"SELECT name FROM sqlite_master "
688+
"WHERE type='table' AND sql LIKE '%STRICT%'"
689+
)
690+
assert len(strict_tables) == 0, "Upgraded database should not have STRICT tables"
691+
692+
# Verify BLOB->TEXT migration ran for faildetail cleanup.
693+
result = l1.db_query(
694+
"SELECT COUNT(*) as count FROM payments "
695+
"WHERE typeof(faildetail) = 'blob'"
696+
)
697+
assert result[0]['count'] == 0, "Found BLOB-typed faildetail after migration"

wallet/db.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ static bool db_migrate(struct lightningd *ld, struct db *db,
3535
available = num_migrations - 1;
3636
orig = current = db_get_version(db);
3737

38+
/* Disable STRICT for upgrades: legacy data may have wrong type affinity. */
39+
db->in_migration = (current != -1);
40+
3841
if (current == -1)
3942
log_info(ld->log, "Creating database");
4043
else if (available < current) {
@@ -112,6 +115,8 @@ struct db *db_setup(const tal_t *ctx, struct lightningd *ld,
112115

113116
db_commit_transaction(db);
114117

118+
db->in_migration = false;
119+
115120
/* This needs to be done outside a transaction, apparently.
116121
* It's a good idea to do this every so often, and on db
117122
* upgrade is a reasonable time. */
@@ -1069,3 +1074,4 @@ void migrate_fail_pending_payments_without_htlcs(struct lightningd *ld,
10691074
db_bind_int(stmt, payment_status_in_db(PAYMENT_PENDING));
10701075
db_exec_prepared_v2(take(stmt));
10711076
}
1077+

wallet/migrations.c

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include <ccan/tal/str/str.h>
44
#include <common/channel_id.h>
55
#include <common/htlc_state.h>
6+
#include <common/utils.h>
67
#include <db/bindings.h>
78
#include <db/common.h>
89
#include <db/exec.h>
@@ -1079,10 +1080,57 @@ static const struct db_migration dbmigrations[] = {
10791080
NULL, revert_withheld_column},
10801081
/* ^v25.12 */
10811082

1083+
/* Fix BLOB→TEXT in payments.faildetail for old databases. */
1084+
{NULL, migrate_fix_payments_faildetail_type,
1085+
/* Fixing data types is idempotent, so no revert needed */
1086+
NULL, NULL},
10821087
};
10831088

10841089
const struct db_migration *get_db_migrations(size_t *num)
10851090
{
10861091
*num = ARRAY_SIZE(dbmigrations);
10871092
return dbmigrations;
10881093
}
1094+
1095+
void migrate_fix_payments_faildetail_type(struct lightningd *ld UNUSED,
1096+
struct db *db)
1097+
{
1098+
/* Historical databases may have BLOB-typed faildetail data.
1099+
* STRICT mode rejects this, so convert or NULL out invalid UTF-8. */
1100+
struct db_stmt *stmt;
1101+
1102+
stmt = db_prepare_v2(db, SQL("SELECT id, faildetail "
1103+
"FROM payments "
1104+
"WHERE typeof(faildetail) = 'blob'"));
1105+
db_query_prepared(stmt);
1106+
1107+
while (db_step(stmt)) {
1108+
u64 id = db_col_u64(stmt, "id");
1109+
const u8 *blob = db_col_blob(stmt, "faildetail");
1110+
size_t len = db_col_bytes(stmt, "faildetail");
1111+
struct db_stmt *upd;
1112+
1113+
if (!utf8_check(blob, len)) {
1114+
/* Invalid UTF-8, NULL it out */
1115+
upd = db_prepare_v2(db,
1116+
SQL("UPDATE payments "
1117+
"SET faildetail = NULL "
1118+
"WHERE id = ?"));
1119+
db_bind_u64(upd, id);
1120+
db_exec_prepared_v2(take(upd));
1121+
continue;
1122+
}
1123+
1124+
/* Convert BLOB to TEXT */
1125+
char *text = tal_strndup(tmpctx, (char *)blob, len);
1126+
upd = db_prepare_v2(db,
1127+
SQL("UPDATE payments "
1128+
"SET faildetail = ? "
1129+
"WHERE id = ?"));
1130+
db_bind_text(upd, text);
1131+
db_bind_u64(upd, id);
1132+
db_exec_prepared_v2(take(upd));
1133+
}
1134+
1135+
tal_free(stmt);
1136+
}

wallet/migrations.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,5 @@ void migrate_remove_chain_moves_duplicates(struct lightningd *ld, struct db *db)
6262
void migrate_from_account_db(struct lightningd *ld, struct db *db);
6363
void migrate_datastore_commando_runes(struct lightningd *ld, struct db *db);
6464
void migrate_runes_idfix(struct lightningd *ld, struct db *db);
65+
void migrate_fix_payments_faildetail_type(struct lightningd *ld, struct db *db);
6566
#endif /* LIGHTNING_WALLET_MIGRATIONS_H */

0 commit comments

Comments
 (0)