@cryptotaxi247 / kubo / commits / 7bfc8240c

fix(fuse): IPNS writes actually publish (#11271)

* fix(fuse): persist IPNS writes across restarts The IPNS FUSE mount's MFS republisher calls Name.Publish to persist changes, but checkPublishAllowed blocks all publishes while the mount is active. This means writes through the FUSE mount are silently dropped and lost on daemon restart. Add a context key so the mount's internal publishes bypass the guard while manual `ipfs name publish` from CLI/RPC remains blocked. - core/coreiface/name.go: context key and helpers for mount publish - core/coreapi: checkPublishAllowed checks context before blocking - fuse/ipns: ipnsPubFunc marks its context as mount-internal - fuse/ipns: tests set node.Mounts.Ipns to exercise the guard Fixes #2168 * docs: add IPNS FUSE persistence fix to v0.41 changelog * refactor(fuse): move publish bypass to internal package Move the FUSE mount publish context key from the public coreiface package to internal/fusemount, preventing external consumers from bypassing the publish guard. - internal/fusemount/context.go: new internal package with context helpers - core/coreiface/name.go: remove exported ContextWithMountPublish / IsMountPublish - core/coreapi/coreapi.go: use fusemount.IsPublish for the guard check - fuse/ipns/ipns_unix.go: use fusemount.ContextWithPublish to tag context

Marcin Rataj committed Apr 3, 2026 at 02:08 UTC 7bfc8240cd87f5b85d972a64a5221825e4d2f997
6 files changed +60 -3
core/coreapi/coreapi.go
+6 -2
@@ -28,6 +28,7 @@ import (
28 "github.com/ipfs/kubo/config"
29 coreiface "github.com/ipfs/kubo/core/coreiface"
30 "github.com/ipfs/kubo/core/coreiface/options"
31 + "github.com/ipfs/kubo/internal/fusemount"
32 pubsub "github.com/libp2p/go-libp2p-pubsub"
33 record "github.com/libp2p/go-libp2p-record"
34 ci "github.com/libp2p/go-libp2p/core/crypto"
@@ -74,7 +75,7 @@ type CoreAPI struct {
75
76 pubSub *pubsub.PubSub
77
77 - checkPublishAllowed func() error
78 + checkPublishAllowed func(ctx context.Context) error
79 checkOnline func(allowOffline bool) error
80
81 // ONLY for re-applying options in WithOptions, DO NOT USE ANYWHERE ELSE
@@ -201,7 +202,10 @@ func (api *CoreAPI) WithOptions(opts ...options.ApiOption) (coreiface.CoreAPI, e
202 return nil
203 }
204
204 - subAPI.checkPublishAllowed = func() error {
205 + subAPI.checkPublishAllowed = func(ctx context.Context) error {
206 + if fusemount.IsPublish(ctx) {
207 + return nil
208 + }
209 if n.Mounts.Ipns != nil && n.Mounts.Ipns.IsActive() {
210 return errors.New("cannot manually publish while IPNS is mounted")
211 }
core/coreapi/name.go
+1 -1
@@ -28,7 +28,7 @@ func (api *NameAPI) Publish(ctx context.Context, p path.Path, opts ...caopts.Nam
28 ctx, span := tracing.Span(ctx, "CoreAPI.NameAPI", "Publish", trace.WithAttributes(attribute.String("path", p.String())))
29 defer span.End()
30
31 - if err := api.checkPublishAllowed(); err != nil {
31 + if err := api.checkPublishAllowed(ctx); err != nil {
32 return ipns.Name{}, err
33 }
34
docs/changelogs/v0.41.md
+1
@@ -110,6 +110,7 @@ FUSE mounts (`/ipfs`, `/ipns`, `/mfs`) now work with editors like VIM that rely
110 - **Empty directories list correctly.** Listing an empty directory on `/ipfs` or `/ipns` no longer returns an error.
111 - **Bare file CIDs work on `/ipfs`.** Accessing a file by its CID directly under the `/ipfs` mount (e.g. `/ipfs/<CID>`) no longer returns "not found". This was a [long-standing regression](https://github.com/ipfs/kubo/issues/9044) that only affected files; directories were not affected.
112 - **Rename works on `/mfs`.** Renaming a file within the same directory no longer leaves the source behind.
113 +- **IPNS FUSE publish works.** Writing files to `/ipns/local/` now correctly publishes the updated DAG to IPNS. Before this fix, IPNS publishing from the FUSE mount was silently blocked, so the DAG would disappear after a daemon restart.
114
115 #### 📦️ Dependency updates
116
fuse/ipns/ipns_test.go
+16
@@ -111,6 +111,16 @@ func (m *mountWrap) Close() error {
111 return nil
112 }
113
114 +// fakeMount is a minimal mount.Mount that reports itself as active.
115 +// This simulates the real daemon path where node.Mounts.Ipns is set
116 +// after the FUSE filesystem is mounted, ensuring that checkPublishAllowed
117 +// is actually exercised during tests (see issue #2168).
118 +type fakeMount struct{}
119 +
120 +func (fakeMount) MountPoint() string { return "/fake/ipns" }
121 +func (fakeMount) Unmount() error { return nil }
122 +func (fakeMount) IsActive() bool { return true }
123 +
124 func setupIpnsTest(t *testing.T, node *core.IpfsNode) (*core.IpfsNode, *mountWrap) {
125 t.Helper()
126 fusetest.SkipUnlessFUSE(t)
@@ -140,6 +150,12 @@ func setupIpnsTest(t *testing.T, node *core.IpfsNode) (*core.IpfsNode, *mountWra
150 mnt, err := fstest.MountedT(t, fs, nil)
151 fusetest.MountError(t, err)
152
153 + // Simulate the real daemon: set node.Mounts.Ipns so that
154 + // checkPublishAllowed sees an active IPNS mount. Before the
155 + // context key fix (issue #2168), this would cause the MFS
156 + // republisher's publishes to be silently rejected.
157 + node.Mounts.Ipns = fakeMount{}
158 +
159 return node, &mountWrap{
160 Mount: mnt,
161 Fs: fs,
fuse/ipns/ipns_unix.go
+5
@@ -25,6 +25,7 @@ import (
25 logging "github.com/ipfs/go-log/v2"
26 iface "github.com/ipfs/kubo/core/coreiface"
27 options "github.com/ipfs/kubo/core/coreiface/options"
28 + "github.com/ipfs/kubo/internal/fusemount"
29 )
30
31 func init() {
@@ -86,6 +87,10 @@ type Root struct {
87
88 func ipnsPubFunc(ipfs iface.CoreAPI, key iface.Key) mfs.PubFunc {
89 return func(ctx context.Context, c cid.Cid) error {
90 + // Bypass the "cannot publish while IPNS is mounted" guard.
91 + // Without this the mount's own publishes are blocked,
92 + // causing silent data loss on daemon restart (issue #2168).
93 + ctx = fusemount.ContextWithPublish(ctx)
94 _, err := ipfs.Name().Publish(ctx, path.FromCid(c), options.Name.Key(key.Name()), options.Name.AllowOffline(true))
95 return err
96 }
internal/fusemount/context.go new
+31
@@ -0,0 +1,31 @@
1 +// Package fusemount provides internal helpers shared between the FUSE
2 +// mount layer and the core API. It lives under internal/ so that
3 +// external consumers of kubo cannot bypass publish guards.
4 +package fusemount
5 +
6 +import "context"
7 +
8 +// publishKey is a context key that lets the IPNS FUSE mount's
9 +// internal MFS republisher bypass the "cannot manually publish while
10 +// IPNS is mounted" guard in the Name API. Without this bypass the
11 +// guard blocks the mount's own publishes and silently drops IPNS
12 +// updates, causing data written through the FUSE mount to be lost
13 +// on daemon restart (see https://github.com/ipfs/kubo/issues/2168).
14 +//
15 +// TODO: the /ipns/ FUSE mount does not detect changes when a
16 +// locally-owned key is published via `ipfs name publish` (RPC/CLI).
17 +// A larger refactor is needed so the mountpoint's MFS representation
18 +// is updated to reflect external publishes to locally-owned keys,
19 +// rather than silently overwriting them on the next MFS flush.
20 +type publishKey struct{}
21 +
22 +// ContextWithPublish marks ctx as originating from the FUSE mount's
23 +// internal publish path.
24 +func ContextWithPublish(ctx context.Context) context.Context {
25 + return context.WithValue(ctx, publishKey{}, true)
26 +}
27 +
28 +// IsPublish reports whether ctx was marked by [ContextWithPublish].
29 +func IsPublish(ctx context.Context) bool {
30 + return ctx.Value(publishKey{}) != nil
31 +}