fix(mfs): fix fsync deadlock, set attrs, disable default caching (#11255)
* fix(MFS): fix deadlock, attrs, caching * unmount ipns and mfs in mount tests; allow offline * set attrs Uid, Gid, and Valid for readonly and /ipns * doc: update changelog * fix(fuse): maximize kernel cache for immutable /ipfs paths /ipfs content is addressed by CID and never changes, so kernel attribute caching is safe and avoids unnecessary FUSE round-trips. Also sets uid/gid on Root.Attr for consistency. * docs: move FUSE changelog to v0.41 highlights * fix(fuse): make IPNS fsync a no-op Calling fsync on a file opened through /ipns deadlocks and eventually panics, taking down the entire IPNS mount. The Fsync handler called mfs.File.Flush(), which tries to open a second write descriptor on the same file. Only one write descriptor can exist at a time (desclock is exclusive), and the first one from Open is still held. The new one blocks forever waiting for the lock. After the FUSE timeout, Release tries to close the original descriptor and hits a nil pointer panic in DagModifier.Sync. Make Fsync a no-op, matching the MFS mount. Data gets flushed when the file is closed. Also improve the MFS Fsync comment to explain the same constraint. * fix(fuse): set uid/gid on IPNS symlinks The "local" symlink in /ipns showed uid=0 gid=0 (root) while directories and files showed the daemon's uid/gid. Set uid/gid and disable attr caching to match other mutable IPNS nodes. Also add TODO comments across all three FUSE mounts for using Mode and Mtime from UnixFS records when present, and for wiring IPNS record TTL into attr cache duration. * fix(fuse): return empty listing for empty directories IPNS Directory.ReadDirAll and readonly Node.ReadDirAll returned ENOENT when a directory had no children. An empty directory still exists, it just has nothing in it. Return an empty slice instead. MFS already handles this correctly. The readonly Root.ReadDirAll correctly returns EPERM (you can't list all of /ipfs). The IPNS Root.ReadDirAll always has entries (peer keys), so it was never affected. This matters for /ipfs because empty directories are valid content-addressed objects (e.g. QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn is a well-known CID of an empty UnixFS directory). * fix(fuse): always sync MFS writes to root on close The Sync flag was computed as `req.Flags|fuse.OpenSync > 0` (bitwise OR), which is always true because fuse.OpenSync is non-zero. Replace with an explicit `true` to match the IPNS mount and make the intent clear: FUSE writes must always propagate to the MFS root when the file is closed, regardless of whether the caller set O_SYNC. * docs: update FUSE changelog for new fixes * test(fuse): add empty directory listing tests Verify that listing an empty directory returns an empty result instead of an error, for all three FUSE mounts: - /mfs: empty root + empty subdirectory - /ipns: empty peer directory + empty subdirectory - /ipfs: empty UnixFS directory added to the DAG * test(fuse): add append and byte-at-a-time write tests for MFS IPNS had TestAppendFile and TestMultiWrite but MFS did not. Add matching tests to cover appending to an existing file and writing one byte at a time. * ci(fuse): add dedicated FUSE test job with auto-detection Add a fuse-tests CI job that installs fuse3, sets TEST_FUSE=1, and runs FUSE unit tests. Previously these tests were compiled out by the nofuse build tag (set when TEST_FUSE=0 in the unit-tests job). Introduce fuse/fusetest package with shared test helpers: - SkipUnlessFUSE: respects TEST_FUSE env var (0=skip, 1=run) with auto-detection fallback that checks for fusermount in PATH - MountError: fatals when TEST_FUSE=1 (CI expects FUSE to work), skips when auto-detecting (local dev without FUSE) Replace the old ci.NoFuse() (checked TEST_NO_FUSE, a dead env var nobody set) and per-file maybeSkipFuseTests wrappers. On Linux, bazil.org/fuse hardcodes "fusermount" but modern distros only ship "fusermount3". The CI job creates a symlink; the auto-detect gives a helpful skip message when only fusermount3 is found locally. * fix(fuse): handle EINTR on close in IPNS concurrent write test TestConcurrentWrites was flaky because Go's goroutine preemption signal (SIGURG) can interrupt the FUSE FLUSH inside close(), returning EINTR. The write itself already succeeded and the kernel will still send RELEASE to the daemon, so the data is safe. Replace os.WriteFile with explicit open/write/close so we can ignore EINTR on close while still catching real errors. * fix(fuse): resolve bare file CIDs on /ipfs mount Accessing a file by its CID at the /ipfs FUSE mount root returned ENOENT because ProtoNodeConverter cannot handle UnixFS file ADLs. Decode dag-pb blocks directly from bytes instead. Closes https://github.com/ipfs/kubo/issues/9044 * fix(fuse): fix same-directory rename on /mfs Renaming a file within the same MFS directory left the source behind. The directory's entry cache was re-synced before the old name was removed. Unlink the source before AddChild to match the working IPNS pattern. * test(fuse): add mixed dag-pb/raw directory test Covers the scenario from https://github.com/ipfs/kubo/issues/9044: a directory with both dag-pb and raw-leaf children read through the /ipfs FUSE mount. * test(fuse): remove redundant testing.Short() checks SkipUnlessFUSE(t) already handles skipping via TEST_FUSE. The testing.Short() guard was a second skip gate that served no purpose since FUSE tests only run under make test_fuse. * fix(fuse): get DAG node before unlinking source in rename Move GetNode() before Unlink() in both mfs and ipns Rename so that a GetNode() failure does not leave the source entry already removed. Also add FUSE test instructions to AGENTS.md. * ci: skip fuse3 install when fusermount exists Self-hosted runners persist state, so after the first run fuse3 and the symlink are already in place. Skip apt-get update and install entirely when fusermount is in PATH. --------- Co-authored-by: Andrew Gillis <11790789+gammazero@users.noreply.github.com> Co-authored-by: Marcin Rataj <lidel@lidel.org>