rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 heartwoodb8b94db543e832a0cebd0e9858605ac134a85789
{
"request": "trigger",
"version": 1,
"event_type": "patch",
"repository": {
"id": "rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5",
"name": "heartwood",
"description": "Radicle Heartwood Protocol & Stack",
"private": false,
"default_branch": "master",
"delegates": [
"did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT",
"did:key:z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW",
"did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM",
"did:key:z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz",
"did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz"
]
},
"action": "Created",
"patch": {
"id": "e485eac65fae2de4ae2ad6a96cae1c78eb2f35c9",
"author": {
"id": "did:key:z6MktwkohCx8aHZ1QCjVZUiLmX92oPZFxRiFZkbq32Tk5Tkm",
"alias": "2color"
},
"title": "node: Skip unreadable address rows in `entries()`",
"state": {
"status": "open",
"conflicts": []
},
"before": "b482845e712d54fdee00b0548c828e6694a3f359",
"after": "b8b94db543e832a0cebd0e9858605ac134a85789",
"commits": [
"b8b94db543e832a0cebd0e9858605ac134a85789"
],
"target": "b482845e712d54fdee00b0548c828e6694a3f359",
"labels": [],
"assignees": [],
"revisions": [
{
"id": "e485eac65fae2de4ae2ad6a96cae1c78eb2f35c9",
"author": {
"id": "did:key:z6MktwkohCx8aHZ1QCjVZUiLmX92oPZFxRiFZkbq32Tk5Tkm",
"alias": "2color"
},
"description": "- Decode each row independently so one corrupt entry no longer aborts\n the whole address-book iterator\n- Log a warn with the raw `value` and continue past parse failures\n- Add a regression test that mimics the post-migration-8 state where a\n legacy `[]:8776` row lands as `type='ipv6'`\n\nA pre-`df8e4e6c` parser admitted `[]:8776` as `HostName::Dns(\"[]\")`.\nMigration 8 retyped any `[..]:N` dns row to `ipv6` without validating\nthe inner part, so on read `Address::from_str` rejects the row with\n\"invalid IPv6 address syntax\", which bubbled up and caused\n`available_peers()` to silently return empty, breaking peer discovery\nfor the affected node.",
"base": "b482845e712d54fdee00b0548c828e6694a3f359",
"oid": "b8b94db543e832a0cebd0e9858605ac134a85789",
"timestamp": 1777468534
}
]
}
}
{
"response": "triggered",
"run_id": {
"id": "a97870d8-8ab8-42fd-83ae-186ffec2be4c"
},
"info_url": "https://cci.rad.levitte.org//a97870d8-8ab8-42fd-83ae-186ffec2be4c.html"
}
Started at: 2026-04-29 15:15:44.766072+02:00
Commands:
$ rad clone rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 .
✓ Creating checkout in ./...
✓ Remote cloudhead@z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT added
✓ Remote-tracking branch cloudhead@z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT/master created for z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT
✓ Remote cloudhead@z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW added
✓ Remote-tracking branch cloudhead@z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW/master created for z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW
✓ Remote fintohaps@z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM added
✓ Remote-tracking branch fintohaps@z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM/master created for z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM
✓ Remote erikli@z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz added
✓ Remote-tracking branch erikli@z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz/master created for z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz
✓ Remote lorenz@z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz added
✓ Remote-tracking branch lorenz@z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz/master created for z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz
✓ Repository successfully cloned under /opt/radcis/ci.rad.levitte.org/cci/state/a97870d8-8ab8-42fd-83ae-186ffec2be4c/w/
╭────────────────────────────────────╮
│ heartwood │
│ Radicle Heartwood Protocol & Stack │
│ 157 issues · 41 patches │
╰────────────────────────────────────╯
Run `cd ./.` to go to the repository directory.
Exit code: 0
$ rad patch checkout e485eac65fae2de4ae2ad6a96cae1c78eb2f35c9
✓ Switched to branch patch/e485eac at revision e485eac
✓ Branch patch/e485eac setup to track rad/patches/e485eac65fae2de4ae2ad6a96cae1c78eb2f35c9
Exit code: 0
$ git config advice.detachedHead false
Exit code: 0
$ git checkout b8b94db543e832a0cebd0e9858605ac134a85789
HEAD is now at b8b94db5 node: Skip unreadable address rows in `entries()`
Exit code: 0
$ rad patch show e485eac65fae2de4ae2ad6a96cae1c78eb2f35c9 -p
╭────────────────────────────────────────────────────────────────────────╮
│ Title node: Skip unreadable address rows in `entries()` │
│ Patch e485eac65fae2de4ae2ad6a96cae1c78eb2f35c9 │
│ Author 2color z6Mktwk…2Tk5Tkm │
│ Head b8b94db543e832a0cebd0e9858605ac134a85789 │
│ Base b482845e712d54fdee00b0548c828e6694a3f359 │
│ Branches patch/e485eac │
│ Commits ahead 1, behind 0 │
│ Status open │
│ │
│ - Decode each row independently so one corrupt entry no longer aborts │
│ the whole address-book iterator │
│ - Log a warn with the raw `value` and continue past parse failures │
│ - Add a regression test that mimics the post-migration-8 state where a │
│ legacy `[]:8776` row lands as `type='ipv6'` │
│ │
│ A pre-`df8e4e6c` parser admitted `[]:8776` as `HostName::Dns("[]")`. │
│ Migration 8 retyped any `[..]:N` dns row to `ipv6` without validating │
│ the inner part, so on read `Address::from_str` rejects the row with │
│ "invalid IPv6 address syntax", which bubbled up and caused │
│ `available_peers()` to silently return empty, breaking peer discovery │
│ for the affected node. │
├────────────────────────────────────────────────────────────────────────┤
│ b8b94db node: Skip unreadable address rows in `entries()` │
├────────────────────────────────────────────────────────────────────────┤
│ ● Revision e485eac @ b8b94db by 2color z6Mktwk…2Tk5Tkm 12 seconds ago │
╰────────────────────────────────────────────────────────────────────────╯
commit b8b94db543e832a0cebd0e9858605ac134a85789
Author: Daniel Norman <daniel@norman.life>
Date: Wed Apr 29 15:09:14 2026 +0200
node: Skip unreadable address rows in `entries()`
- Decode each row independently so one corrupt entry no longer aborts
the whole address-book iterator
- Log a warn with the raw `value` and continue past parse failures
- Add a regression test that mimics the post-migration-8 state where a
legacy `[]:8776` row lands as `type='ipv6'`
A pre-`df8e4e6c` parser admitted `[]:8776` as `HostName::Dns("[]")`.
Migration 8 retyped any `[..]:N` dns row to `ipv6` without validating
the inner part, so on read `Address::from_str` rejects the row with
"invalid IPv6 address syntax", which bubbled up and caused
`available_peers()` to silently return empty, breaking peer discovery
for the affected node.
diff --git a/crates/radicle/src/node/address/store.rs b/crates/radicle/src/node/address/store.rs
index dbb621a28..714bcdd32 100644
--- a/crates/radicle/src/node/address/store.rs
+++ b/crates/radicle/src/node/address/store.rs
@@ -299,31 +299,45 @@ impl Store for Database {
let mut entries = Vec::new();
while let Some(Ok(row)) = stmt.next() {
- let node = row.try_read::<NodeId, _>("node")?;
- let _type = row.try_read::<AddressType, _>("type")?;
- let addr = row.try_read::<Address, _>("value")?;
- let source = row.try_read::<Source, _>("source")?;
- let last_success = row.try_read::<Option<i64>, _>("last_success")?;
- let last_attempt = row.try_read::<Option<i64>, _>("last_attempt")?;
- let last_success = last_success.map(|t| LocalTime::from_millis(t as u128));
- let last_attempt = last_attempt.map(|t| LocalTime::from_millis(t as u128));
- let version = row.try_read::<i64, _>("version")?.try_into()?;
- let banned = row.try_read::<i64, _>("banned")?.is_positive();
- let penalty = row.try_read::<i64, _>("penalty")?;
- let penalty = Penalty(penalty as u8); // Clamped at `u8::MAX`.
-
- entries.push(AddressEntry {
- node,
- version,
- penalty,
- address: KnownAddress {
- addr,
- source,
- last_success,
- last_attempt,
- banned,
- },
- });
+ // Decode each row independently so a single corrupt entry doesn't poison the whole address book.
+ let entry = (|| -> Result<AddressEntry, Error> {
+ let node = row.try_read::<NodeId, _>("node")?;
+ let _type = row.try_read::<AddressType, _>("type")?;
+ let addr = row.try_read::<Address, _>("value")?;
+ let source = row.try_read::<Source, _>("source")?;
+ let last_success = row.try_read::<Option<i64>, _>("last_success")?;
+ let last_attempt = row.try_read::<Option<i64>, _>("last_attempt")?;
+ let last_success = last_success.map(|t| LocalTime::from_millis(t as u128));
+ let last_attempt = last_attempt.map(|t| LocalTime::from_millis(t as u128));
+ let version = row.try_read::<i64, _>("version")?.try_into()?;
+ let banned = row.try_read::<i64, _>("banned")?.is_positive();
+ let penalty = row.try_read::<i64, _>("penalty")?;
+ let penalty = Penalty(penalty as u8); // Clamped at `u8::MAX`.
+
+ Ok(AddressEntry {
+ node,
+ version,
+ penalty,
+ address: KnownAddress {
+ addr,
+ source,
+ last_success,
+ last_attempt,
+ banned,
+ },
+ })
+ })();
+
+ match entry {
+ Ok(e) => entries.push(e),
+ Err(e) => {
+ let value = row.try_read::<&str, _>("value").unwrap_or("?");
+ log::warn!(
+ target: "service",
+ "Skipping unreadable address book row (value={value:?}): {e}"
+ );
+ }
+ }
}
Ok(Box::new(entries.into_iter()))
}
@@ -977,6 +991,63 @@ mod test {
assert!(db.is_ip_banned(ip2.into()).unwrap());
}
+ #[test]
+ fn test_entries_skips_unparseable_address() {
+ let alice = arbitrary::r#gen::<NodeId>(1);
+ let bob = arbitrary::r#gen::<NodeId>(2);
+ let mut cache = Database::memory().unwrap();
+ let timestamp = Timestamp::from(LocalTime::now());
+ let ua = UserAgent::default();
+ let features = node::Features::SEED;
+ let good_addr: Address = "[2001:db8::1]:8776".parse().unwrap();
+ let good_ka = KnownAddress {
+ addr: good_addr.clone(),
+ source: Source::Peer,
+ last_success: None,
+ last_attempt: None,
+ banned: false,
+ };
+ cache
+ .insert(
+ &alice,
+ 3,
+ features,
+ &Alias::new("alice"),
+ 0,
+ &ua,
+ timestamp,
+ [good_ka.clone()],
+ )
+ .unwrap();
+ // Insert bob's node row via the normal path with no addresses, then
+ // smuggle in a malformed row that mimics post-migration-8 corruption.
+ cache
+ .insert(
+ &bob,
+ 3,
+ features,
+ &Alias::new("bob"),
+ 0,
+ &ua,
+ timestamp,
+ [],
+ )
+ .unwrap();
+ cache
+ .db
+ .execute(format!(
+ "INSERT INTO addresses (node, type, value, source, timestamp)
+ VALUES ('{bob}', 'ipv6', '[]:8776', 'peer', 0)"
+ ))
+ .unwrap();
+
+ // entries() must succeed and yield alice's good row.
+ let entries = cache.entries().unwrap().collect::<Vec<_>>();
+ assert_eq!(entries.len(), 1);
+ assert_eq!(entries[0].node, alice);
+ assert_eq!(entries[0].address, good_ka);
+ }
+
#[test]
fn test_node_aliases() {
let mut db = Database::memory().unwrap();
Exit code: 0
shell: 'export RUSTDOCFLAGS=''-D warnings'' cargo --version rustc --version cargo fmt --check cargo clippy --all-targets --workspace -- --deny warnings cargo build --all-targets --workspace cargo doc --workspace --no-deps --all-features cargo test --workspace --no-fail-fast '
Commands:
$ podman run --name a97870d8-8ab8-42fd-83ae-186ffec2be4c -v /opt/radcis/ci.rad.levitte.org/cci/state/a97870d8-8ab8-42fd-83ae-186ffec2be4c/s:/a97870d8-8ab8-42fd-83ae-186ffec2be4c/s:ro -v /opt/radcis/ci.rad.levitte.org/cci/state/a97870d8-8ab8-42fd-83ae-186ffec2be4c/w:/a97870d8-8ab8-42fd-83ae-186ffec2be4c/w -w /a97870d8-8ab8-42fd-83ae-186ffec2be4c/w -v /opt/radcis/ci.rad.levitte.org/.radicle:/${id}/.radicle:ro -e RAD_HOME=/${id}/.radicle rust:trixie bash /a97870d8-8ab8-42fd-83ae-186ffec2be4c/s/script.sh
+ export 'RUSTDOCFLAGS=-D warnings'
+ RUSTDOCFLAGS='-D warnings'
+ cargo --version
info: syncing channel updates for '1.90-x86_64-unknown-linux-gnu'
info: latest update on 2025-09-18, rust version 1.90.0 (1159e78c4 2025-09-14)
info: downloading component 'cargo'
info: downloading component 'clippy'
info: downloading component 'rust-docs'
info: downloading component 'rust-src'
info: downloading component 'rust-std'
info: downloading component 'rustc'
info: downloading component 'rustfmt'
info: installing component 'cargo'
info: installing component 'clippy'
info: installing component 'rust-docs'
info: installing component 'rust-src'
info: installing component 'rust-std'
info: installing component 'rustc'
info: installing component 'rustfmt'
cargo 1.90.0 (840b83a10 2025-07-30)
+ rustc --version
rustc 1.90.0 (1159e78c4 2025-09-14)
+ cargo fmt --check
Diff in /a97870d8-8ab8-42fd-83ae-186ffec2be4c/w/crates/radicle/src/node/address/store.rs:1022:
// Insert bob's node row via the normal path with no addresses, then
// smuggle in a malformed row that mimics post-migration-8 corruption.
cache
- .insert(
- &bob,
- 3,
- features,
- &Alias::new("bob"),
- 0,
- &ua,
- timestamp,
- [],
- )
+ .insert(&bob, 3, features, &Alias::new("bob"), 0, &ua, timestamp, [])
.unwrap();
cache
.db
Exit code: 1
{
"response": "finished",
"result": "failure"
}