gossip: add COMPLETED bit to mark records which are complete.
What changed, and why it matters
This commit hardens how Core Lightning writes its gossip store file. It adds a 'COMPLETED' flag that is set only after a full record has been safely written, so the program can detect and skip partial/corrupted records on restart. The change is defensive: it makes crashes or unclean shutdowns less likely to leave the gossip store in a broken state, especially on filesystems other than Linux ext4. There is no direct evidence in the commit of an exploitable security vulnerability.
Treat as a reliability/hardening fix. Review whether partial gossip-store records could previously lead to crashes, memory corruption, or denial-of-service on startup, especially on non-ext4 filesystems. Monitor for follow-up fixes or a security advisory from the project.
Security signals we found
Adds explicit COMPLETED flag to detect partial writes
Separates record write from completion flag write via pwrite()
Bumps gossip store minor version (14 -> 15) with migration logic
Commit message notes prior synchronization assumptions 'not so well elsewhere'
No explicit vulnerability, CVE, or exploit mechanism described
Evidence from the diff
The patch introduces GOSSIP_STORE_COMPLETED_BIT (0x2000) in the gossip store header flags and bumps the store minor version to 15. When appending a record, the code now performs the header+payload write first, then issues a separate single-byte pwrite() to set the completed bit. On read/upgrade, records lacking the completed bit can be treated as incomplete. The change also updates upgrade logic so v14 stores are migrated to v15 and marked complete. This is a robustness improvement against torn/partial writes and inconsistent synchronization assumptions across filesystems.
Changed components
gossipd/gossip_store.ccommon/gossip_store.hdevtools/create-gossipstore.cdevtools/dump-gossipstore.ccontrib/pyln-client/pyln/client/gossmap.pyInspect captured patch +26 / −8
diff --git a/common/gossip_store.h b/common/gossip_store.h
index 98abd5f9..013f8003 100644
--- a/common/gossip_store.h
+++ b/common/gossip_store.h
@@ -15,7 +15,7 @@ struct gossip_rcvd_filter;
/* First byte of file is the version.
*
* Top three bits mean incompatible change.
- * As of this writing, major == 0, minor == 13.
+ * As of this writing, major == 0, minor == 15.
*/
#define GOSSIP_STORE_MAJOR_VERSION_MASK 0xE0
#define GOSSIP_STORE_MINOR_VERSION_MASK 0x1F
@@ -29,6 +29,11 @@ struct gossip_rcvd_filter;
*/
#define GOSSIP_STORE_DELETED_BIT 0x8000U
+/**
+ * Bit of flags indicating record has been written.
+ */
+#define GOSSIP_STORE_COMPLETED_BIT 0x2000U
+
/**
* Bit of flags used to mark a channel announcement closed (not deleted for 12 blocks)
*/
diff --git a/contrib/pyln-client/pyln/client/gossmap.py b/contrib/pyln-client/pyln/client/gossmap.py
index 876b255a..f2bc3218 100755
--- a/contrib/pyln-client/pyln/client/gossmap.py
+++ b/contrib/pyln-client/pyln/client/gossmap.py
@@ -16,6 +16,7 @@ GOSSIP_STORE_MAJOR_VERSION = (0 << 5)
GOSSIP_STORE_MAJOR_VERSION_MASK = 0xE0
GOSSIP_STORE_LEN_DELETED_BIT = 0x8000
GOSSIP_STORE_LEN_PUSH_BIT = 0x4000
+GOSSIP_STORE_LEN_COMPLETE_BIT = 0x2000
# These duplicate constants in lightning/gossipd/gossip_store_wiregen.h
WIRE_GOSSIP_STORE_PRIVATE_CHANNEL = 4104
diff --git a/devtools/create-gossipstore.c b/devtools/create-gossipstore.c
index bcab2c4d..b9376f47 100644
--- a/devtools/create-gossipstore.c
+++ b/devtools/create-gossipstore.c
@@ -47,7 +47,8 @@ static void write_outmsg(int outfd, const u8 *outmsg, u32 timestamp)
{
struct gossip_hdr hdr;
- hdr.len = cpu_to_be32(tal_count(outmsg));
+ hdr.flags = CPU_TO_BE16(GOSSIP_STORE_COMPLETED_BIT);
+ hdr.len = cpu_to_be16(tal_count(outmsg));
hdr.crc = cpu_to_be32(crc32c(timestamp, outmsg, tal_count(outmsg)));
hdr.timestamp = cpu_to_be32(timestamp);
diff --git a/devtools/dump-gossipstore.c b/devtools/dump-gossipstore.c
index 70cee3eb..632d26be 100644
--- a/devtools/dump-gossipstore.c
+++ b/devtools/dump-gossipstore.c
@@ -11,7 +11,7 @@
/* Current versions we support */
#define GSTORE_MAJOR 0
-#define GSTORE_MINOR 14
+#define GSTORE_MINOR 15
int main(int argc, char *argv[])
{
diff --git a/gossipd/gossip_store.c b/gossipd/gossip_store.c
index 906b6c10..3b61737a 100644
--- a/gossipd/gossip_store.c
+++ b/gossipd/gossip_store.c
@@ -23,7 +23,7 @@
#define GOSSIP_STORE_TEMP_FILENAME "gossip_store.tmp"
/* We write it as major version 0, minor version 14 */
-#define GOSSIP_STORE_VER ((0 << 5) | 14)
+#define GOSSIP_STORE_VER ((0 << 5) | 15)
struct gossip_store {
/* Back pointer. */
@@ -66,6 +66,7 @@ static bool append_msg(int fd, const u8 *msg, u32 timestamp, u64 *len)
struct gossip_hdr hdr;
u32 msglen;
struct iovec iov[2];
+ const u8 complete_byte = (GOSSIP_STORE_COMPLETED_BIT >> 8);
/* Don't ever overwrite the version header! */
assert(*len);
@@ -88,6 +89,11 @@ static bool append_msg(int fd, const u8 *msg, u32 timestamp, u64 *len)
iov[1].iov_len = msglen;
if (gossip_pwritev(fd, iov, ARRAY_SIZE(iov), *len) != sizeof(hdr) + msglen)
return false;
+
+ /* Update the hdr with the complete bit as a single-byte write */
+ if (pwrite(fd, &complete_byte, 1, *len) != 1)
+ return false;
+
*len += sizeof(hdr) + msglen;
return true;
}
@@ -98,10 +104,11 @@ static bool append_msg(int fd, const u8 *msg, u32 timestamp, u64 *len)
* v12 added the zombie flag for expired channel updates
* v13 removed private gossip entries
* v14 removed zombie and spam flags
+ * v15 added the complete flag
*/
static bool can_upgrade(u8 oldversion)
{
- return oldversion >= 9 && oldversion <= 13;
+ return oldversion >= 9 && oldversion <= 14;
}
/* On upgrade, do best effort on private channels: hand them to
@@ -155,7 +162,7 @@ static void give_lightningd_canned_private_update(struct daemon *daemon,
static bool upgrade_field(u8 oldversion,
struct daemon *daemon,
- u16 hdr_flags,
+ be16 *hdr_flags,
u8 **msg)
{
int type = fromwire_peektype(*msg);
@@ -179,10 +186,14 @@ static bool upgrade_field(u8 oldversion,
}
if (oldversion <= 13) {
/* Discard any zombies */
- if (hdr_flags & GOSSIP_STORE_ZOMBIE_BIT_V13) {
+ if (be16_to_cpu(*hdr_flags) & GOSSIP_STORE_ZOMBIE_BIT_V13) {
*msg = tal_free(*msg);
}
}
+ if (oldversion <= 14) {
+ /* Add completed field */
+ *hdr_flags |= CPU_TO_BE16(GOSSIP_STORE_COMPLETED_BIT);
+ }
return true;
}
@@ -285,7 +296,7 @@ static int gossip_store_compact(struct daemon *daemon,
if (oldversion != version) {
if (!upgrade_field(oldversion, daemon,
- be16_to_cpu(hdr.flags), &msg)) {
+ &hdr.flags, &msg)) {
tal_free(msg);
bad = "upgrade of store failed";
goto badmsg;
Why this scored 44/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.