Any connected peer can send a `Subscribe` with `since` after `until`.
The service passes the range to `Store::filtered`, which asserts
`from <= to`. The panic stops the service thread.
Remove the assertion. The query already returns no rows when
`from > to`, so the peer gets no history.
There is no apparent reason why announcements with a timestamp of zero
cannot be stored. Avoid panicking.
Co-authored-by: Lorenz Leutgeb <lorenz.leutgeb@radicle.dev>
To determine whether a patch has been merged to the default branch, we
tally merges based on the default branch quorum: allow set and
threshold. With the introduction of custom branches, the tallying was
not updated to use the quorum rules for the target branch, instead
merging on the identity document threshold and delegate set.
This fix uses the `allow` set and the `threshold` of the most specific
rule that matches the target branch of patch.
Note that a reference that no rule matches never becomes canonical, so a
patch that targets one can never reach a quorum.
The crates `radicle` and `radicle-node` need the default hasher for
`IndexMap<K, V>` and `IndexSet<T>`, which currently gets enabled
indirectly by `serde_json`, which depends on `indexmap` with `std`
enabled by default.
Make the dependency on the `std` feature of `indexmap` explicit.
Co-authored-by: Lorenz Leutgeb <lorenz.leutgeb@radicle.dev>
`sync_routing` calls `get_inventory(&from)` once per inbound inventory
announcement, to work out which of a peer's previously-announced repos are
no longer in its inventory:
// crates/radicle-protocol/src/service.rs
synced.removed.extend(
self.db.routing().get_inventory(&from)?
.into_iter()
.filter(|rid| !included.contains(rid)),
);
That is `SELECT repo FROM routing WHERE node = ?`. The table's only index is
the implicit one for the primary key `(repo, node)`, whose leading column is
`repo`, so the lookup cannot use it and scans instead. The index fixes that.
`disconnect_unresponsive_peers` drops any session whose `last_active` is
older than STALE_CONNECTION_TIMEOUT (hard-coded to 2 minutes).
`last_active` is written in only two places: `to_connected`, and
`handle_message` (the gossip message handler).
Pack data never reaches `handle_message` which is what marks . It arrives as `FrameData::Git`
and goes straight to the worker thread over a channel. So a peer busy
sending us a large pack appears completely idle, and
after two minutes we hang up on it mid-transfer. gitoxide then sees a
truncated stream and the fetch fails with:
Clone of rad:<rid> from <nid> failed with
'gix fetch error: Failed to consume the pack sent by the remote'
This only affects large repositories. Small ones finish inside the
window. Neither `limits.fetchPackReceive` nor `limits.fetchTimeout`
bounds it, and it reproduces against every peer, since any peer sending a
big pack goes gossip-quiet for the duration.
We now treat arriving pack bytes as liveness, symmetrically with the
`FrameData::Gossip` arm immediately above, which already calls
`received_message`.
Note that a remote that spends more than STALE_CONNECTION_TIMEOUT
server-side (e.g. enumerating or compressing a large pack before sending
its first byte), is still reaped, and `limits.fetchTimeout` does not help.
This is not a change in behaviour. Gossip silence would have reaped it anyway.
`limits.routingMaxSize` is documented as "Number of routing table entries
before we start pruning", but the implementation diverges from this.
DELETE FROM routing WHERE node <> ?1 AND rowid IN
(SELECT rowid FROM routing WHERE timestamp < ?2 ORDER BY timestamp LIMIT ?3)
`?2` is `now - routingMaxAge`. So if nothing is older than routingMaxAge,
nothing is deleted, however far over routingMaxSize the table has grown. On a
seed following a busy network, entries are refreshed by gossip faster than
they age out, and the cap is never reachable.
For example, on a permissive node with routingMaxSize=1000, routingMaxAge=7d,
I observed:
routing rows 94,254
older than routingMaxAge, ie. prunable 16
Dropping routingMaxAge to 1 day cleared the backlog the 7-day setting had
accumulated -- from 94,254 down to 82,400 rows -- but then stopped, with
8 rows prunable and the table still ... 82x over the cap. The `routing` table
size is therefore decided by arrival rate; the configured max size has no
effect on it.
Split the two evictions so each does what its name says:
- size eviction removes the oldest entries down to `routingMaxSize`,
regardless of age;
- age eviction then removes anything still older than
`routingMaxAge`.
Size runs first: it takes the oldest rows, so the age pass only fires when
the table was near its limit.
All released versions of Radicle are affected by vulnerabilities [1] in the
network protocol: traffic is unencrypted and peers are not authenticated.
A network observer can read the contents of private repositories, and an
attacker can impersonate a node that to fetch them. No fix has been released
yet.
Since users may rely on private repositories being confidential, `rad
init` now warns about this before the interactive "Visibility" prompt,
so the warning is printed before the choice is made. The warning is printed
again in the epilogue whenever the created repository is private. This
also covers `--private`, which skips the prompt.
Update the `rad-init-private` and `rad-init-private-no-seed` examples
to match the new output.
[1]: https://radicle.dev/2026/09/23/disclosure-of-vulnerability-in-network-protocol
`VarInt` can get large enough to exhaust available memory on many
machines. Instead of allocating directly, check the size of the
allocation against some limit.
The limit chosen for frames is 128 KiB, which is roughly twice as much
as actual gossip and Git protocol frames are expected to be. However,
128 KiB is arguably short enough to not burden a standard consumer
machine too much.
This redundancy does not add much. These functions are already marked
via `#[test]`, and often times they are in a separate module named
"test" or "tests".
Read the initial identity document from the blob addressed by the
repository ID and include its founder in the set of namespaces to
be fetched.
This lets clones resolve the identity COB root even if the founder
is not a delegate anymore.
Add a test that demonstrates erroneous behavior in case the founder of a
repository is not a delegate anymore.
The change in commit 388d985578 changed
the semantics of loading the identity COB so that the founder is now
crucial.
Serialization and deserialization of symbolic references was implemented
with assumptions on the order of pairs of key and value in JSON objects,
and further, that the enabling the `preserve_order` of `serde_json` would
always guarante that these assumptions hold. See
<https://docs.rs/crate/serde_json/1.0.149/features#preserve_order>.
This is mistaken. The identity document is serialized via `Doc::encode`
which uses `radicle::…::CanonicalFormatter`, which enforces a particular
ordering. From the point of view of the implementation of symbolic
references, this means that the ordering changes, thus violating the
ordering assumption.
The effects are that during deserialization, some symbolic references
might be considered dangling, when they are only missing other symbolic
references that were reordered to occur "later" in the JSON object.
To remedy, introduce `RawSymbolicRefs`, in analogy to `RawRules`. Just
like `RawRules` may be converted to `Rules`, `RawSymbolicRefs` may be
converted to `SymbolicRefs`. This gives the most flexibility for
handling `RawSymbolicRefs`, and one clear point at which validation
happens.
The fact that the implementation cannot depend on ordering anymore means
that all symbolic references have to be validated together, which fits
the pattern of `Raw{,*}` well.
Tests expose the issue.
Co-authored-by: Lorenz Leutgeb <lorenz.leutgeb@radicle.dev>
Adding the "public seeds" of the network to the `preferred_seeds`
configuration on all nodes causes all nodes to connect to the preferred
seeds and to have them attempt to keep the connection open at all times.
Assuming some growth in the network, and no growth in the number of
"public seeds", this means that the number of inbound connections to the
public seeds grows as well. Once the public seeds are at capacity,
the network as a whole is in some sense bottlenecked by the capacity of
the public seeds: As the network grows further, more and more outbound
connections to the public seeds will fail.
To avoid such issues, do not populate `preferred_seeds`.
Testing revealed that the implementation violated RIP-0006, which
requires the most specific rule matching the target of `HEAD`
to use an `allow` of `delegates` and the document threshold.
`validate_head_rule` took the first match from `RawRules`, which orders
patterns lexicographically, so a glob could be validated in place of the
rule that actually governs the branch.
The issue here is that the correct order for matching patterns is only
guaranteed on `Rules`, but not on `RawRules`.
To prevent such errors in the future, remove `RawRules::matches`. This
requires some refactoring, and makes synthesis of the default branch
rule slightly less efficient in the worst case, but we are willing to
take that hit for a cleaner implementation.
Co-authored-by: Lorenz Leutgeb <lorenz.leutgeb@radicle.dev>
The functions `PayloadId::{project,canonical_refs}` have two
weaknesses:
1. They always execute the checks in `from_str` for the same
`str` over and over again.
2. They do not return a reference, thus, callers that only
need a reference make a copy that they then discard.
For improved performance, switch to `LazyLocks`. Keep the less efficient
functions, but deprecate them.
Many codepaths error if the `xyz.radicle.project` payload is not present
in the identity document. Change these to fall back gracefully.
Loading of payloads is unified in `GetPayload`.
An important detail is that `PayloadError::NotFound` is removed, and
`Option` is used instead.
When executing programs on Windows, we suppress creating a new window,
and, if the process should continue to run in the background, we
explicitly create a new process group and detach.
Spawning `rad diff` works, but prints a deprecation notice, because
`rad diff` itself is deprecated. `rad patch diff` however is not
deprecated and should just take a shortcut.
Make `Identity::new` private, to have more control over the initial
revision of the COB. Also assert that the author of the revision is the
solve delegate of the initial revision, i.e., the "founder".
Do not iterate over all user namespaces to find the ID of the identity
COB, but load from the founder's namespace specifically.
If the namespace of the founder is gone, attempt to recover by
walking back from `rad/id`.
Plenty conservative `debug_assert!`s, cause panics as soon as two
actions that should be added to the timeline appear within the same
operation. But there is no code in place to prevent this.
It also does not seem to be problematic to visit the same operation
multiple times (once per contained action).
A set that retains iteration order does the job.
Analysis of this bug was done over Zulip, see
<https://radicle.zulipchat.com/#narrow/channel/369873-Support/topic/radicle-node.201.2E10.20segfaults/with/616991470>.
`Update::Reviewed::review` is boxed due to:
error: large size difference between variants
--> crates/radicle-cli/src/terminal/patch/timeline.rs:158:1
|
158 | / enum Update<'a> {
159 | | /// A revision of the patch was reviewed.
160 | | Reviewed { review: Review },
| | --------------------------- the largest variant contains at least 352 bytes
161 | | /// A revision of the patch was merged.
162 | |/ Merged {
163 | || author: Author<'a>,
164 | || /// If the merge is none, this means that it was a fast-forward merge.
165 | || merge: Option<Merge>,
166 | || },
| ||_____- the second-largest variant contains at least 120 bytes
167 | | }
| |__^ the entire enum is at least 352 bytes
This is conditional on the target operating system being FreeBSDs.
HardenedBSD is FreeBSD but with changes. We determine which one it is,
by running 'sysctl --values hardening.version'.
Co-authored-by: Shawn Webb <shawn.webb@hardenedbsd.org>
The tests `rad-warn-ipv6.md` and `rad-warn-old-nodes.md` are meant to
primarily test the output of warnings. They test the output twice, via
`rad debug` and via `rad node status`.
Testing the output via one command should be sufficient.
When `Service::handle_message` handles `Message::Subscribe`, constraints
regarding visibility of repositories are not considered, this leads to
leaking RIDs of private repositories.
Add a corresponding check.
Add a test to verify that reference announcements relayed via subscribe
correctly apply visibility filtering, there by ensuring references of
private repositories are only propagated to nodes on the allow list.
The test regarding deduplication needs a fix: Ensure that all mock
repositories are public and in storage so that they do not
get filtered out in case of unknown visiblity.
Co-authored-by: Lorenz Leutgeb <lorenz.leutgeb@radicle.dev>