Skip to content

Add libfuse 3 support with libfuse 2 fallback#603

Open
matejk wants to merge 5 commits into
LinearTapeFileSystem:mainfrom
matejk:fuse3-port
Open

Add libfuse 3 support with libfuse 2 fallback#603
matejk wants to merge 5 commits into
LinearTapeFileSystem:mainfrom
matejk:fuse3-port

Conversation

@matejk

@matejk matejk commented Jun 11, 2026

Copy link
Copy Markdown

Summary

Port of LTFS to libfuse 3 with a compile-time libfuse 2 fallback, plus the FUSE 3 performance features. This PR is based directly on main (5 commits: xattr.h include fix, scheduler data-loss fix, the port, the performance features, macOS support); the test suite, the remaining bug fixes found during this work, and the base CMake build are filed as separate independent PRs.

FUSE 3 support

  • Builds against fuse3 (>= 3.4) by default on Linux, with automatic fallback to fuse2 when fuse3 is absent; --with-fuse2 forces the legacy API (still the default on macOS and the BSDs).
  • Handlers use FUSE 3 signatures: getattr/truncate absorb the handle variants, chmod/utimens use the file handle when the path is NULL (nullpath_ok), rename handles RENAME_NOREPLACE/RENAME_EXCHANGE, use_ino/hard_remove/nullpath_ok move into fuse_config, and FUSE_CAP_ASYNC_READ is cleared to keep tape reads ordered (-o sync_read no longer exists).
  • fuse_file_info.parallel_direct_writes is feature-detected (the field appeared in libfuse 3.15; version checks broke on ubuntu-24.04's 3.14).
  • macOS: with macFUSE 5 (which ships a libfuse 3), the FUSE 3 build works via FUSE_DARWIN_ENABLE_EXTENSIONS=0; macOS keeps fuse2 as its default.

Performance

  • 1 MiB FUSE requests (-o max_write=N): measured 1 MiB writes and O_DIRECT reads reaching the daemon, vs 128 KiB on fuse2.
  • -o direct_io: all I/O bypasses the kernel page cache; ~70 % higher sequential write throughput on the file backend and a flat page cache during large streaming jobs.
  • readdirplus: ls -l over 100 files needs 2 getattr requests instead of 102 (effective with libfuse >= 3.17).

Tests

The performance commit ships its integration tests (tests/t/1012: request sizes, direct_io, readdirplus). The scripts are inert until the test-suite PR (#611) merges — its harness discovers t/*.sh automatically (wildcard in automake, glob in ctest), so no registration conflicts arise in either merge order.

Relationship to the other PRs

Verification

With #611 merged alongside: all 14 integration tests pass with fuse3 and fuse2, via make check and ctest, against libfuse 3.14 and 3.17; zero compiler warnings at -Wall -Wextra; macOS compile-verified for both APIs against the macFUSE 5.2 SDK.

Issues

@matejk matejk force-pushed the fuse3-port branch 2 times, most recently from 3068ef8 to 051a223 Compare June 12, 2026 09:35
@matejk matejk changed the title Port to FUSE 3, performance features, test suite, CMake build, and bug fixes Add libfuse 3 support with libfuse 2 fallback Jun 12, 2026
@matejk matejk marked this pull request as draft June 12, 2026 09:40
@matejk matejk force-pushed the fuse3-port branch 2 times, most recently from 793bd14 to 9371bee Compare June 12, 2026 10:07
xattr.h included fuse.h without ltfs_fuse_version.h. libfuse 2 headers
default to an old API level when FUSE_USE_VERSION is undefined, but
libfuse 3 headers reject it, breaking every translation unit that
pulls in xattr.h. The release branch received the same change as part
of the FreeBSD build fix (b3e3355).
@matejk matejk marked this pull request as ready for review June 12, 2026 10:22
matejk added 4 commits June 12, 2026 12:27
_unified_insert_new_request copies at most one cache block (the tape
block size) but returned the full requested count, so the append loop
in unified_write advanced past data that was never stored. Writes
larger than the block size silently lost everything after the first
block while reporting success.

Latent with libfuse 2, which caps requests at 128 KiB, below the
default 512 KiB block; reachable today through the I/O scheduler API
and triggered by FUSE 3 request sizes. Return the number of bytes
actually stored.

A regression test (multi-block single writes of random data, content
verified) accompanies the integration test suite.

Fixes LinearTapeFileSystem#591
configure prefers fuse3 (>= 3.4.0) on Linux and falls back to fuse2
when the fuse3 development files are absent, so existing build
environments keep working; --with-fuse2 forces the legacy API and
remains the default on macOS, FreeBSD, and NetBSD. FUSE_USE_VERSION
becomes 31 for fuse3 builds. README documents the new prerequisites
and mount options.

API changes for FUSE 3:
- getattr/truncate absorb fgetattr/ftruncate via the fuse_file_info
  argument; chmod, chown, and utimens gain the argument and use the
  file handle when the path is NULL (possible under nullpath_ok).
- rename handles flags: RENAME_NOREPLACE returns EEXIST when the
  target exists, RENAME_EXCHANGE is rejected with EINVAL.
- readdir and the directory filler gain flag arguments.
- init receives struct fuse_config; use_ino, hard_remove, and
  nullpath_ok move there from mount options.
- FUSE_CAP_ASYNC_READ is cleared in init to keep tape reads ordered;
  FUSE 3 removed the -o sync_read option and enables asynchronous
  reads by default.
- big_writes is gone (always enabled); fuse_parse_cmdline uses
  struct fuse_cmdline_opts.

The fuse2 code paths are unchanged and selected with --with-fuse2.
Three independently measurable improvements, each with an integration
test that the test-suite harness discovers automatically:

- Request sizes up to 1 MiB: init sets conn->max_write (tunable with
  -o max_write=N) and libfuse >= 3.6 negotiates the matching max_pages
  with the kernel. Measured 1 MiB writes and O_DIRECT reads reaching
  the daemon, against 128 KiB with fuse2 big_writes; one request now
  carries two default-size tape blocks.

- -o direct_io: sets FOPEN_DIRECT_IO on every open, so reads and
  writes bypass the kernel page cache and arrive at the application's
  I/O size. Streaming large archives no longer fills the page cache
  and data is not buffered twice; mmap does not work on files opened
  this way and small-block applications lose readahead, so the option
  is off by default. When fuse_file_info has parallel_direct_writes it
  is set as well; the field appeared in libfuse 3.15, so it is
  detected at configure time instead of via version checks (which
  broke on ubuntu-24.04's libfuse 3.14).

- readdirplus: ltfs_fsops_readdir_attr hands each entry's attributes
  to the filler straight from the in-memory index, and init clears
  FUSE_CAP_READDIRPLUS_AUTO so every listing chunk carries attributes.
  ls -l over 100 files needs 2 getattr requests instead of 102. The
  kernel prefill is only effective with libfuse >= 3.17, so the test
  asserts the suppression conditionally.
macFUSE 5 ships a libfuse 3 whose operation signatures default to
Darwin-specific types (struct fuse_darwin_attr, struct statfs, a
Darwin directory filler). Define FUSE_DARWIN_ENABLE_EXTENSIONS=0 in
ltfs_fuse_version.h so the upstream-compatible signatures are used;
the library exports both symbol flavors.

The macFUSE position argument of the xattr handlers exists only in
the fuse2 API and the Darwin-extension mode, so the FUSE 3 build now
uses the upstream xattr signatures (LTFS_XATTR_POSITION).

Verified on macOS 26 (arm64) against the macFUSE 5.2.0 SDK
(libfuse 3.18.2): autotools (--with-fuse2=no) and CMake
(-DLTFS_WITH_FUSE2=OFF) both build without new warnings, link
libfuse3.4.dylib, and the binaries run; parallel_direct_writes is
detected. The macOS fuse2 builds and the Linux test suites
(autotools make check and ctest, 14/14) are unaffected.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

silent data corruption on ARM64?

1 participant