Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
b3fa36a
test(fuse): consolidate FUSE tests into test/cli/fuse
lidel Apr 3, 2026
b9c7332
docs: document FUSE test split between unit and e2e
lidel Apr 3, 2026
0c263b2
ci: prevent stale FUSE mounts from failing fuse-tests
lidel Apr 3, 2026
7935dcd
ci: only symlink fusermount3 when fusermount is missing
lidel Apr 3, 2026
a588c23
fix(fuse): remove goroutine leak in IPNS Flush handler
lidel Apr 3, 2026
5816dfa
fix(fuse): add mutex to IPNS file handle operations
lidel Apr 3, 2026
3b663aa
refactor(fuse): remove dead File.Forget method
lidel Apr 3, 2026
50c5a8e
fix(fuse): flush IPNS directory after Remove and Rename
lidel Apr 3, 2026
b261516
fix(fuse): inherit CID builder and flush on IPNS Create
lidel Apr 3, 2026
3c9550c
test(fuse): add IPNS Remove and non-empty rmdir tests
lidel Apr 3, 2026
55cfd7b
feat(fuse): read UnixFS mode/mtime, add StoreMtime/StoreMode config
lidel Apr 4, 2026
6ba3437
feat(fuse): add ipfs.cid xattr to all mounts
lidel Apr 4, 2026
d334282
feat(fuse): switch from bazil.org/fuse to hanwen/go-fuse v2
lidel Apr 7, 2026
78c71e2
fix(fuse): close fd on error in Open to prevent leak
lidel Apr 7, 2026
b7f1273
fix(fuse): detect external unmount via server.Wait
lidel Apr 7, 2026
db29899
fix(fuse): return actual error from Unlink/Rmdir, not ENOENT
lidel Apr 7, 2026
f3df205
fix(fuse): reuse DagReader per open, pass ctx to all reads
lidel Apr 7, 2026
fc849b5
chore(fuse): cleanup dead code, add var comments
lidel Apr 7, 2026
944e7e8
chore(fuse): replace OSXFUSE 2.x check with macFUSE detection
lidel Apr 7, 2026
8bc9536
fix(fuse): include mountpoint path in mount errors
lidel Apr 7, 2026
9be4d04
chore(ci): remove bazil fusermount workaround
lidel Apr 7, 2026
7095eca
docs: update v0.41 changelog for FUSE rewrite
lidel Apr 7, 2026
c54071d
Merge remote-tracking branch 'origin/master' into feat/consolidate-fu…
lidel Apr 7, 2026
c73d2cf
chore(deps): bump boxo for full FileDescriptor serialization
lidel Apr 7, 2026
c15b3a8
chore(deps): bump boxo to merged ipfs/boxo#1133
lidel Apr 7, 2026
01c5669
feat(fuse): CAP_ATOMIC_O_TRUNC, new integration tests
lidel Apr 7, 2026
438b296
fix(fuse): rename-over-existing, bump boxo for flushUp race fix
lidel Apr 7, 2026
3435d87
fix(fuse): unskip VimSavePattern, bump boxo for setNodeData fix
lidel Apr 7, 2026
7818e07
fix(fuse): build tags for cross-compilation
lidel Apr 8, 2026
ce529dd
fix(test): use fusermount3 in CLI FUSE tests
lidel Apr 8, 2026
29b4e9c
feat(fuse): symlink support on writable mounts
lidel Apr 8, 2026
77038ff
fix(fuse): checked type assertion in MFS Rename
lidel Apr 8, 2026
a353eac
fix(test): add missing continue in stress test
lidel Apr 8, 2026
ae40117
fix(fuse): return error from Readdir when DAG.Get fails
lidel Apr 8, 2026
0e4fb22
docs: remove duplicate fsync bullet in changelog
lidel Apr 8, 2026
b8a0823
ci: clean up stale FUSE mounts in fuse-tests job
lidel Apr 8, 2026
c08936a
chore(deps): bump boxo to merged ipfs/boxo#1134
lidel Apr 8, 2026
819016a
docs: add build tag comments, normalize tag style
lidel Apr 8, 2026
41a4f22
fix(fuse): add Setattr to directories for chmod and mtime
lidel Apr 8, 2026
245a3c1
docs: clarify directory support and spec link for StoreMtime/StoreMode
lidel Apr 8, 2026
002f5bf
fix(fuse): use proper mode conversion, document 9-bit limit
lidel Apr 8, 2026
05bdd59
feat(fuse): symlink Setattr with mtime persistence
lidel Apr 8, 2026
09612e1
fix(fuse): return EIO instead of panicking on unknown node type
lidel Apr 8, 2026
a2182a4
docs: update FUSE docs for go-fuse migration
lidel Apr 8, 2026
3844ba6
refactor(fuse): extract shared writable types and test suite
lidel Apr 8, 2026
a3c720f
feat(fuse): add macOS-specific mount options
lidel Apr 8, 2026
6181682
fix(fuse): detect symlinks in readdir, fix stale refs
lidel Apr 8, 2026
f16a2f0
fix(fuse): normalize deprecated ipfs_cid xattr to ipfs.cid
lidel Apr 8, 2026
4dfb855
fix(fuse): serialize concurrent reads on readonly file handles
lidel Apr 9, 2026
939caf7
fix(fuse): bypass MFS locking for read-only opens
lidel Apr 9, 2026
87b969e
fix(fuse): support truncate(path, size) without open fd
lidel Apr 9, 2026
4f09f7b
ci(fuse): get stack traces on test hangs
lidel Apr 9, 2026
c5ef4ec
Merge remote-tracking branch 'origin/master' into feat/consolidate-fu…
lidel Apr 9, 2026
0e05597
Merge remote-tracking branch 'origin/master' into feat/consolidate-fu…
lidel Apr 9, 2026
930be4a
fix(fuse): fill attrs in FileInode.Setattr response
lidel Apr 9, 2026
963d525
docs(config): clarify Mounts.IPNS writability scope
lidel Apr 9, 2026
dcf517c
fuse: review cleanup for go-fuse migration
lidel Apr 9, 2026
95645a1
docs(config): list StoreMtime and StoreMode in Mounts TOC
lidel Apr 9, 2026
70bdd7e
fix(fuse): fill EntryOut attrs in Dir.Create and Dir.Mkdir
lidel Apr 9, 2026
c05d46b
fix(fuse): map context cancellation to EINTR in read paths
lidel Apr 9, 2026
fffc2cc
test(fuse): add OExcl, DirRename, SparseWrite, FsyncCrossHandle
lidel Apr 9, 2026
fbbff08
test(fuse): cover external unmount on /ipns and /mfs
lidel Apr 9, 2026
91d3d31
fix(commands): align 'ipfs mount' output columns
lidel Apr 9, 2026
7410e6b
fix(fuse): invalidate kernel cache on Fsync
lidel Apr 9, 2026
59f3f09
chore(gitignore): ignore test_fuse_unit and test_fuse_cli json output
lidel Apr 9, 2026
0c79ec6
test(fuse): end-to-end coverage with real POSIX tools
lidel Apr 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 29 additions & 5 deletions .github/workflows/gotest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -150,12 +150,21 @@ jobs:
if: failure() || success()

# FUSE filesystem tests (require /dev/fuse and fusermount)
# Runs both FUSE unit tests (./fuse/...) and CLI integration tests (./test/cli/fuse/...)
fuse-tests:
if: github.repository == 'ipfs/kubo' || github.event_name == 'workflow_dispatch'
runs-on: ${{ fromJSON(github.repository == 'ipfs/kubo' && '["self-hosted", "linux", "x64", "2xlarge"]' || '"ubuntu-latest"') }}
timeout-minutes: 5
concurrency:
group: fuse-tests-${{ github.repository }}
cancel-in-progress: false
# A normal run takes ~3min. 6min gives roughly 2x and lets Go's 4min
# test timeout fire first (printing a stack trace) on a hang, instead
# of GitHub silently cancelling the job.
timeout-minutes: 6
env:
GOTRACEBACK: single
# Dump all goroutines on a test panic, not just the panicking one,
# so we can see which test is actually hung.
GOTRACEBACK: all
TEST_FUSE: 1
defaults:
run:
Expand All @@ -169,14 +178,29 @@ jobs:
go-version-file: 'go.mod'
- name: Install FUSE
run: |
if ! command -v fusermount &>/dev/null; then
if ! command -v fusermount3 &>/dev/null && ! command -v fusermount &>/dev/null; then
sudo apt-get update
sudo apt-get install -y fuse3
# bazil.org/fuse looks for "fusermount", fuse3 only ships "fusermount3"
sudo ln -sf /usr/bin/fusermount3 /usr/local/bin/fusermount
fi
- name: Clean up stale FUSE mounts
run: |
# On shared self-hosted runners, leftover mounts from previous
# runs can exhaust the kernel FUSE mount limit (mount_max).
# Unit tests mount with FsName "kubo-test"; CLI tests mount
# under the harness temp dir (ipfs/ipns/mfs subdirectories).
awk '$1 == "kubo-test" || $2 ~ /\/tmp\/.*\/(ipfs|ipns|mfs)$/ { print $2 }' /proc/mounts 2>/dev/null \
| while read -r mp; do
fusermount3 -uz "$mp" 2>/dev/null || fusermount -uz "$mp" 2>/dev/null || true
done
- name: Run FUSE tests
run: make test_fuse
- name: Clean up FUSE mounts
if: always()
run: |
awk '$1 == "kubo-test" || $2 ~ /\/tmp\/.*\/(ipfs|ipns|mfs)$/ { print $2 }' /proc/mounts 2>/dev/null \
| while read -r mp; do
fusermount3 -uz "$mp" 2>/dev/null || fusermount -uz "$mp" 2>/dev/null || true
done

# Example tests (kubo-as-a-library)
example-tests:
Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,12 @@ go-ipfs-source.tar.gz
docs/examples/go-ipfs-as-a-library/example-folder/Qm*
/test/sharness/t0054-dag-car-import-export-data/*.car

# test artifacts from make test_unit / test_cli
# test artifacts from make test_unit / test_cli / test_fuse
/test/unit/gotest.json
/test/unit/gotest.junit.xml
/test/cli/cli-tests.json
/test/fuse/fuse-unit-tests.json
/test/fuse/fuse-cli-tests.json

# ignore build output from snapcraft
/ipfs_*.snap
Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,10 @@ If you see "version (N) is lower than repos (M)", the `ipfs` binary in `PATH` is

### Running FUSE Tests

FUSE tests require `/dev/fuse` and `fusermount` in `PATH`. On systems with only fuse3, create a symlink:
FUSE tests require `/dev/fuse` and `fusermount` in `PATH`. On systems with only fuse3, create a symlink in a temp directory (never use `sudo` to install system-wide):

```bash
ln -s /usr/bin/fusermount3 /tmp/fusermount && PATH="/tmp:$PATH" make test_fuse
FUSE_BIN="$(mktemp -d)" && ln -s /usr/bin/fusermount3 "$FUSE_BIN/fusermount" && PATH="$FUSE_BIN:$PATH" make test_fuse
```

Set `TEST_FUSE=1` to make mount failures fatal (CI does this). Without it, tests auto-detect and skip when FUSE is unavailable.
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Kubo was the first [IPFS](https://docs.ipfs.tech/concepts/what-is-ipfs/) impleme
- [HTTP Gateway](https://specs.ipfs.tech/http-gateways/) for trusted and [trustless](https://docs.ipfs.tech/reference/http/gateway/#trustless-verifiable-retrieval) content retrieval
- [HTTP RPC API](https://docs.ipfs.tech/reference/kubo/rpc/) to control the daemon
- [HTTP Routing V1](https://specs.ipfs.tech/routing/http-routing-v1/) client and server for [delegated routing](./docs/delegated-routing.md)
- [FUSE mounts](./docs/fuse.md) for mounting `/ipfs`, `/ipns`, and `/mfs` as local filesystems (experimental)
- [Content blocking](./docs/content-blocking.md) for public node operators

**Other IPFS implementations:** [Helia](https://github.com/ipfs/helia) (JavaScript), [more...](https://docs.ipfs.tech/concepts/ipfs-implementations/)
Expand Down Expand Up @@ -178,6 +179,7 @@ Kubo is available in community-maintained packages across many operating systems
| [HTTP RPC clients](docs/http-rpc-clients.md) | Client libraries for Go, JS |
| [Delegated routing](docs/delegated-routing.md) | Multi-router and HTTP routing |
| [Metrics & monitoring](docs/metrics.md) | Prometheus metrics |
| [FUSE mounts](docs/fuse.md) | Mount `/ipfs`, `/ipns`, `/mfs` as local filesystems |
| [Content blocking](docs/content-blocking.md) | Denylist for public nodes |
| [Customizing](docs/customizing.md) | Unsure if use Plugins, Boxo, or fork? |
| [Debug guide](docs/debug-guide.md) | CPU profiles, memory analysis, tracing |
Expand Down
4 changes: 3 additions & 1 deletion cmd/ipfs/kubo/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -1239,9 +1239,11 @@ func mountFuse(req *cmds.Request, cctx *oldcmds.Context) error {
if err != nil {
return err
}
// Extra space after "MFS" so "mounted at:" lines up with IPFS and
// IPNS in the column above. Matches MountCmd's output formatter.
fmt.Printf("IPFS mounted at: %s\n", fsdir)
fmt.Printf("IPNS mounted at: %s\n", nsdir)
fmt.Printf("MFS mounted at: %s\n", mfsdir)
fmt.Printf("MFS mounted at: %s\n", mfsdir)
return nil
}

Expand Down
1 change: 1 addition & 0 deletions cmd/ipfs/kubo/daemon_linux.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Systemd readiness notification (sd_notify). Linux only.
//go:build linux

package kubo
Expand Down
1 change: 1 addition & 0 deletions cmd/ipfs/kubo/daemon_other.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// No-op readiness notification on non-Linux platforms.
//go:build !linux

package kubo
Expand Down
1 change: 1 addition & 0 deletions cmd/ipfs/runmain_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Only built when collecting coverage via "go test -tags testrunmain".
//go:build testrunmain

package main_test
Expand Down
1 change: 1 addition & 0 deletions cmd/ipfs/util/signal.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Signal handling. Excluded from wasm where os.Signal is unavailable.
//go:build !wasm

package util
Expand Down
1 change: 1 addition & 0 deletions cmd/ipfs/util/ui.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// GUI detection stub. Windows has its own implementation.
//go:build !windows

package util
Expand Down
1 change: 1 addition & 0 deletions cmd/ipfs/util/ulimit_freebsd.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// FreeBSD ulimit handling via sysctl.
//go:build freebsd

package util
Expand Down
1 change: 1 addition & 0 deletions cmd/ipfs/util/ulimit_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Ulimit tests. Skipped on windows and plan9 (no getrlimit).
//go:build !windows && !plan9

package util
Expand Down
1 change: 1 addition & 0 deletions cmd/ipfs/util/ulimit_unix.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Unix ulimit handling via getrlimit/setrlimit.
//go:build darwin || linux || netbsd || openbsd

package util
Expand Down
1 change: 1 addition & 0 deletions cmd/ipfs/util/ulimit_windows.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Windows ulimit handling via SetHandleInformation.
//go:build windows

package util
Expand Down
1 change: 1 addition & 0 deletions cmd/ipfswatch/ipfswatch_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Excluded from plan9 (no fsnotify support).
//go:build !plan9

package main
Expand Down
1 change: 1 addition & 0 deletions cmd/ipfswatch/main.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Excluded from plan9 (no fsnotify support).
//go:build !plan9

package main
Expand Down
40 changes: 35 additions & 5 deletions config/mounts.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,39 @@
package config

// Mounts stores the (string) mount points.
const (
DefaultFuseAllowOther = false
DefaultStoreMtime = false
DefaultStoreMode = false
)

// Mounts stores FUSE mount point configuration.
type Mounts struct {
IPFS string
IPNS string
MFS string
FuseAllowOther bool
// IPFS is the mountpoint for the read-only /ipfs/ namespace.
IPFS string

// IPNS is the mountpoint for the /ipns/ namespace. Directories backed
// by keys this node holds are writable; all other names resolve through
// IPNS to read-only symlinks into the /ipfs mount.
IPNS string

// MFS is the mountpoint for the Mutable File System (ipfs files API).
MFS string

// FuseAllowOther sets the FUSE allow_other mount option, letting
// users other than the mounter access the mounted filesystem.
FuseAllowOther Flag

// StoreMtime controls whether writable mounts (/ipns and /mfs) persist
// the current time as mtime in UnixFS metadata when creating a file or
// opening it for writing. This changes the resulting CID even when file
// content is identical.
//
// Reading mtime from UnixFS is always enabled on all mounts.
StoreMtime Flag

// StoreMode controls whether writable mounts (/ipns and /mfs) persist
// POSIX permission bits in UnixFS metadata when a chmod request is made.
//
// Reading mode from UnixFS is always enabled on all mounts.
StoreMode Flag
}
5 changes: 4 additions & 1 deletion core/commands/mount_nofuse.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
//go:build !windows && nofuse
// Stub for non-FUSE builds: the complement of mount_unix.go's
// (linux || darwin || freebsd) && !nofuse, excluding windows
// which has its own stub in mount_windows.go.
//go:build !windows && (nofuse || !(linux || darwin || freebsd))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are we trying to achieve here? A comment would be welcome

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good idea, documented these in 819016a


package commands

Expand Down
7 changes: 5 additions & 2 deletions core/commands/mount_unix.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
//go:build !windows && !nofuse
// Real mount command. go-fuse only builds on linux, darwin, and freebsd.
//go:build (linux || darwin || freebsd) && !nofuse

package commands

Expand Down Expand Up @@ -130,9 +131,11 @@ baz
Type: config.Mounts{},
Encoders: cmds.EncoderMap{
cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, mounts *config.Mounts) error {
// Extra space after "MFS" so "mounted at:" lines up with
// IPFS and IPNS in the column above. Matches LongDescription.
fmt.Fprintf(w, "IPFS mounted at: %s\n", cmdenv.EscNonPrint(mounts.IPFS))
fmt.Fprintf(w, "IPNS mounted at: %s\n", cmdenv.EscNonPrint(mounts.IPNS))
fmt.Fprintf(w, "MFS mounted at: %s\n", cmdenv.EscNonPrint(mounts.MFS))
fmt.Fprintf(w, "MFS mounted at: %s\n", cmdenv.EscNonPrint(mounts.MFS))

return nil
}),
Expand Down
1 change: 1 addition & 0 deletions core/commands/repo_verify_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Requires Go 1.25+ for testing/synctest.
//go:build go1.25

package commands
Expand Down
1 change: 1 addition & 0 deletions core/node/libp2p/fd/sys_not_unix.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Stub returning zero on platforms without /proc or Handle APIs.
//go:build !linux && !darwin && !windows

package fd
Expand Down
1 change: 1 addition & 0 deletions core/node/libp2p/fd/sys_unix.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// File descriptor counting via /proc/self/fd (linux) or lsof (darwin).
//go:build linux || darwin

package fd
Expand Down
1 change: 1 addition & 0 deletions core/node/libp2p/fd/sys_windows.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// File descriptor counting via Windows Handle API.
//go:build windows

package fd
Expand Down
1 change: 1 addition & 0 deletions coverage/main/main.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Only built when collecting coverage via "go test -tags testrunmain".
//go:build testrunmain

package main
Expand Down
33 changes: 23 additions & 10 deletions docs/changelogs/v0.41.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ This release was brought to you by the [Shipyard](https://ipshipyard.com/) team.
- [🔧 Correct provider addresses for custom HTTP routing](#-correct-provider-addresses-for-custom-http-routing)
- [🛡️ `ipfs object patch` validates UnixFS node types](#-ipfs-object-patch-validates-unixfs-node-types)
- [🔗 MFS: fixed CidBuilder preservation](#-mfs-fixed-cidbuilder-preservation)
- [📂 FUSE Mount Fixes](#-fuse-mount-fixes)
- [📂 FUSE Mount Improvements](#-fuse-mount-improvements)
- [🐹 Go 1.26, Once More with Feeling](#-go-126-once-more-with-feeling)
- [📦️ Dependency updates](#-dependency-updates)
- [📝 Changelog](#-changelog)
Expand Down Expand Up @@ -109,18 +109,31 @@ Additionally, the MFS root directory itself now respects [`Import.CidVersion`](h

See [boxo#1125](https://github.com/ipfs/boxo/pull/1125) and [kubo#11273](https://github.com/ipfs/kubo/pull/11273).

#### 📂 FUSE Mount Fixes
#### 📂 FUSE Mount Improvements

FUSE mounts (`/ipfs`, `/ipns`, `/mfs`) now work with editors like VIM that rely on `fsync` and expect standard file ownership. FUSE support is still experimental. If you run into problems, please report them at [kubo/issues](https://github.com/ipfs/kubo/issues).
The FUSE implementation has been rewritten on top of [`hanwen/go-fuse` v2](https://github.com/hanwen/go-fuse), replacing the unmaintained `bazil.org/fuse`. This fixes long-standing architectural limitations and brings FUSE mounts much closer to what standard tools expect. FUSE support is still experimental. See [docs/fuse.md](https://github.com/ipfs/kubo/blob/master/docs/fuse.md) for setup instructions, and report problems at [kubo/issues](https://github.com/ipfs/kubo/issues).

- **No more deadlocks on save.** Editors that call `fsync` after writing would hang indefinitely on `/mfs` and `/ipns` mounts. `Fsync` is now a no-op on both; data is flushed when the file descriptor is closed.
- **Files are no longer owned by root.** Mounts now report the uid/gid of the daemon process, so access works without `allow_other`.
- **Offline IPNS writes succeed.** Writing through `/ipns` FUSE mounts no longer fails when the node has no network. IPNS records are stored locally and published when connectivity returns.
- **Smarter kernel caching.** Attribute caching is disabled for mutable mounts (`/ipns`, `/mfs`) to prevent stale reads after writes. Immutable `/ipfs` paths use long-lived caching since content addressed by CID never changes.
- **`fsync` works.** Editors (vim, emacs) and databases that call `fsync` after writing no longer get a silent no-op. Data is flushed through the open file descriptor to the DAG. The full vim save sequence (O_TRUNC + write + fsync + chmod) is tested.
- **`ftruncate` works.** Tools like `rsync --inplace` that shrink or grow files via `ftruncate(fd, size)` no longer get ENOTSUP. Opening existing files with `O_TRUNC` also works correctly.
- **`chmod` and `touch` no longer drop file content.** Setting mode or mtime on a file with `Mounts.StoreMode`/`StoreMtime` enabled previously replaced the DAG node without preserving content links, making the file appear empty.
- **Symlink creation on writable mounts.** `ln -s target link` now works on `/mfs` and `/ipns`. Symlinks are stored as UnixFS TSymlink nodes, the same format used by `ipfs add`.
- **Rename-over-existing works.** Renaming a file onto an existing name (the pattern used by rsync and atomic-save editors) now correctly replaces the target.
- **Faster reads on `/ipfs`.** Files are read sequentially from the block graph instead of re-resolving from the root on every read call.
- **Killing a stuck `cat` works.** Interrupting a read (Ctrl-C, kill) cancels in-flight block fetches instead of hanging.
- **External unmount detected.** Running `fusermount -u` from outside the daemon now correctly marks the mount as inactive.
- **Files are no longer owned by root.** Mounts report the uid/gid of the daemon process, so access works without `allow_other`.
- **Offline IPNS writes succeed.** IPNS records are stored locally and published when connectivity returns.
- **Empty directories list correctly.** Listing an empty directory on `/ipfs` or `/ipns` no longer returns an error.
- **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.
- **Rename works on `/mfs`.** Renaming a file within the same directory no longer leaves the source behind.
- **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.
- **Bare file CIDs work on `/ipfs`.** Accessing a file by its CID directly under the `/ipfs` mount now works. This was a [long-standing regression](https://github.com/ipfs/kubo/issues/9044).
- **Rename works on `/mfs` and `/ipns`.** Renaming a file within the same directory no longer leaves the source behind.
- **IPNS FUSE publish works.** Writing files to `/ipns/local/` now correctly publishes the updated DAG. Previously IPNS publishing from the FUSE mount was silently blocked.
- **Concurrent IPNS file operations no longer race.** The `/ipns` file handle serializes Read, Write, Flush, and Release, matching the `/mfs` mount.
- **IPNS directory operations flush immediately.** Remove and Rename on `/ipns` flush changes to the MFS root, preventing data loss on daemon restart.
- **New files use the correct CID version.** Files created on `/ipns` inherit the parent's CID settings instead of falling back to CIDv0.
- **UnixFS mode and mtime visible in stat.** All three mounts show POSIX mode and mtime from [UnixFS](https://specs.ipfs.tech/unixfs/) metadata when present. When absent, sensible POSIX defaults are used (files: `0644`/`0444`, directories: `0755`/`0555`).
- **Opt-in `Mounts.StoreMtime` and `Mounts.StoreMode`.** Writable mounts can persist mtime on file creation/write and POSIX mode on `chmod` for both files and directories. `touch` on directories also works, which tools like `tar` and `rsync` rely on. Both flags are off by default because they change the resulting CID. See [`Mounts.StoreMtime`](https://github.com/ipfs/kubo/blob/master/docs/config.md#mountsstoremtime) and [`Mounts.StoreMode`](https://github.com/ipfs/kubo/blob/master/docs/config.md#mountsstoremode).
- **`ipfs.cid` xattr on all mounts.** All three mounts expose the node's CID via the `ipfs.cid` extended attribute on files and directories. The legacy `ipfs_cid` xattr name (used in earlier versions of `/mfs`) is no longer supported; use `ipfs.cid` instead.
- **Platform compatibility.** macOS detection updated from OSXFUSE 2.x to macFUSE 4.x. Linux no longer needs a `fusermount` symlink; [`hanwen/go-fuse`](https://github.com/hanwen/go-fuse) finds `fusermount3` natively.

#### 🐹 Go 1.26, Once More with Feeling

Expand Down
Loading
Loading