ipfs / ipfs/kubo

namesys is permanently locked out of an IPNS name if a pubsub RPC subscription joins its record topic first

Open
#11,454 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Go
Stars
17.1k
Forks
3.2k
Avg merge
3d 18h
Merged PRs (30d)
11

Description

### Checklist

- [x] This is a bug report, not a question.
- [x] I have searched on the issue tracker for my bug.
- [x] I am running the latest kubo version or have an issue updating.

### Installation method

third-party binary (the `kubo` npm package)

### Version

```
Kubo version: 0.43.0-e9914bb47
Repo version: 18
System version: amd64/linux
Golang version: go1.26.5
```

### Config

Two default `--profile=test` repos with `Pubsub.Enabled=true` and `Ipns.UsePubsub=true`. On the resolving daemon `Routing.Type=none`, so IPNS can only resolve over pubsub (with the DHT on, the DHT masks the failure but the namesys subscription is still never created, see below).

### Description

If a client opens a `pubsub sub` on an IPNS record topic (`/record/`) **before** namesys has resolved that name, namesys can never join the topic on that daemon: `name resolve ` fails with `could not resolve name`, `name pubsub subs` never lists the name, and cancelling the RPC subscription does not repair it. Only a daemon restart does. Nothing is logged, even with `namesys` and `pubsub-valuestore` at debug.

The topic name is deterministic from the IPNS name, so any application that uses the pubsub RPC to watch for IPNS record arrivals (a `kubo-rpc-client` user, for instance) can lock its own daemon out of a name with one ordering mistake.

#### Steps to reproduce

Daemon B is the publisher, daemon A the resolver. Both have pubsub enabled and are connected to each other; A has `Routing.Type=none`.

```sh
# B: two keys, both published
ipfs key gen victim
ipfs key gen control
CID=$(echo hello | ipfs add -q)
ipfs name publish --key=victim --allow-offline --ttl=1m /ipfs/$CID
ipfs name publish --key=control --allow-offline --ttl=1m /ipfs/$CID
```

The record topic for a name is `/record/` + base64url (no padding) of the bytes `"/ipns/" + `, i.e. what `KeyToTopic` in go-libp2p-pubsub-router produces for the key `/ipns/`.

```sh
# A: control name, resolve first (namesys joins the topic)
ipfs name resolve --nocache
# -> /ipfs/Qm... (0.2s)
ipfs name pubsub subs
# -> /ipns/

# A: victim name, RPC-subscribe to its record topic first, then resolve
ipfs pubsub sub /record/ &
ipfs name resolve --nocache
# -> Error: could not resolve name (0.2s)
ipfs name pubsub subs
# -> only /ipns/; the victim never appears

# A: cancel the RPC subscription, resolve again
kill %1
ipfs pubsub ls
# -> the victim topic is still listed
ipfs name resolve --nocache
# -> Error: could not resolve name

# A: restart the daemon, resolve first, THEN subscribe over the RPC
ipfs name resolve --nocache
# -> /ipfs/Qm...
ipfs pubsub sub /record/ &
# B republishes: the RPC stream receives the record and name resolve returns the new value
```

#### Root cause

- The pubsub RPC subscribes with the deprecated `PubSub.Subscribe(topic)` (`core/coreapi/pubsub.go`), which creates the `Topic` handle via `tryJoin` and keeps it in `myTopics`. Cancelling the subscription only calls `Subscription.Cancel()`, never `Topic.Close()`, so the handle stays for the life of the process.
- namesys' `PubsubValueStore.Subscribe` (`go-libp2p-pubsub-router/pubsub.go`, `createTopicHandler`) calls `p.ps.Join(topic)`, which returns `topic already exists` when the handle is already in `myTopics` (`go-libp2p-pubsub/pubsub.go`, `Join`).
- That error propagates out of `GetValue`/`SearchValue`, so the resolve fails, and it is not logged anywhere.

The reverse order works because `Subscribe` tolerates an existing topic handle (`tryJoin` returns the existing one) while `Join` does not.

#### Expected behavior

Either order should work: an RPC subscription on a record topic should not prevent namesys from resolving that name. Failing that, the error should at least be logged and surfaced by `name resolve` instead of the generic `could not resolve name`, and `pubsub sub` cancellation should release the topic handle when it was the only subscriber.

#### Possible fixes

- Make the pubsub RPC and namesys share topic handles (e.g. a per-node topic registry, or have `PubsubValueStore.createTopicHandler` fall back to the existing handle when `Join` reports it already exists).
- Have the coreapi `Subscribe` use `Join` + `Topic.Subscribe` and close the topic on cancel when it was the last subscriber.
- Log the `Join` error in `PubsubValueStore.Subscribe`.

Contributor guide

Open the contributing guide

Research direction

Start by reproducing the ordering issue with the commands in the report, then read core/coreapi/pubsub.go, go-libp2p-pubsub-router/pubsub.go, and go-libp2p-pubsub/pubsub.go around Subscribe, createTopicHandler, tryJoin, Join, and cancellation. Done means an RPC subscription before namesys resolution no longer prevents resolution, or the failure and cleanup behavior is explicitly corrected and covered by the relevant tests.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
api, backend-api-design, networking
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
58/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.