From eb8c20695643923598d0516e86b1fcef144f65fa Mon Sep 17 00:00:00 2001 From: SulfurNitride Date: Wed, 11 Mar 2026 04:02:27 -0500 Subject: Fix FUSE passthrough: use proper API, drop LD_LIBRARY_PATH, auto-apply cap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use fuse_passthrough_open/close API instead of raw fd assignment to fi->backing_id, which caused EIO on every file open through the VFS - Remove LD_LIBRARY_PATH from launcher script — DT_RPATH handles lib resolution, and LD_LIBRARY_PATH triggers AT_SECURE which drops file capabilities (cap_sys_admin) needed for passthrough - Auto-detect missing cap_sys_admin at mount time and prompt via pkexec to re-apply it (handles binary copy/update/extract losing the cap) - Disable passthrough for session if fuse_passthrough_open fails, avoiding "Operation not permitted" spam in terminal - Fix Nix flake: tinyxml2 → tinyxml-2, xorg.lib* → top-level names - CI: eliminate artifact zip round-trip for GitHub releases Co-Authored-By: Claude Opus 4.6 --- .github/workflows/ci.yml | 18 +++----------- docker/build-inner.sh | 5 +++- flake.nix | 55 ++++++++++++++++++++++--------------------- src/src/organizercore.cpp | 42 ++++++++++++++++++++++++++++++++- src/src/vfs/mo2filesystem.cpp | 30 ++++++++++++++++++----- src/src/vfs/mo2filesystem.h | 1 + 6 files changed, 101 insertions(+), 50 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d3ca6be..0212a31 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: build: runs-on: ubuntu-latest permissions: - contents: read + contents: write packages: write steps: @@ -75,23 +75,11 @@ jobs: with: name: Fluorine-Manager path: artifact/fluorine-manager.tar.gz + compression-level: 0 if-no-files-found: warn - release: - if: startsWith(github.ref, 'refs/tags/v') - needs: build - runs-on: ubuntu-latest - permissions: - contents: write - - steps: - - name: Download artifacts - uses: actions/download-artifact@v4 - with: - name: Fluorine-Manager - path: artifact/ - - name: Create GitHub Release + if: startsWith(github.ref, 'refs/tags/v') uses: softprops/action-gh-release@v2 with: files: artifact/fluorine-manager.tar.gz diff --git a/docker/build-inner.sh b/docker/build-inner.sh index d035e34..e9add61 100755 --- a/docker/build-inner.sh +++ b/docker/build-inner.sh @@ -349,7 +349,10 @@ RUN="${BIN_DST}" PYTHON_DIR="${RUN}/python" export PATH="${RUN}:${PATH}" -export LD_LIBRARY_PATH="${RUN}/lib:${PYTHON_DIR}/lib:${LD_LIBRARY_PATH:-}" +# NOTE: Do NOT set LD_LIBRARY_PATH here. The binary uses DT_RPATH +# ($ORIGIN/lib:$ORIGIN/python/lib) to find its libraries. Setting +# LD_LIBRARY_PATH on a capability-bearing binary (cap_sys_admin for FUSE +# passthrough) triggers AT_SECURE mode, which drops all file capabilities. export MO2_BASE_DIR="${RUN}" export MO2_PLUGINS_DIR="${RUN}/plugins" export MO2_DLLS_DIR="${RUN}/dlls" diff --git a/flake.nix b/flake.nix index d982917..6e66a5e 100644 --- a/flake.nix +++ b/flake.nix @@ -142,7 +142,7 @@ # System libraries sqlite - tinyxml2 + tinyxml-2 spdlog fuse3 lz4 @@ -382,7 +382,8 @@ RUN="''${BIN_DST}" PYTHON_DIR="''${RUN}/python" export PATH="''${RUN}:''${PATH}" -export LD_LIBRARY_PATH="''${RUN}/lib:''${PYTHON_DIR}/lib:''${LD_LIBRARY_PATH:-}" +# NOTE: Do NOT set LD_LIBRARY_PATH — DT_RPATH handles library resolution. +# Setting it on a capability-bearing binary drops file capabilities (AT_SECURE). export MO2_BASE_DIR="''${RUN}" export MO2_PLUGINS_DIR="''${RUN}/plugins" export MO2_DLLS_DIR="''${RUN}/dlls" @@ -443,19 +444,19 @@ LAUNCH vulkan-loader mesa - xorg.libX11 - xorg.libxcb - xorg.libXext - xorg.libXrandr - xorg.libXcursor - xorg.libXi - xorg.libXfixes - xorg.libXrender - xorg.libXcomposite - xorg.libXdamage - xorg.libXtst - xorg.libXScrnSaver - xorg.libXinerama + libx11 + libxcb + libxext + libxrandr + libxcursor + libxi + libxfixes + libxrender + libxcomposite + libxdamage + libxtst + libxscrnsaver + libxinerama libxkbcommon wayland @@ -490,18 +491,18 @@ LAUNCH fontconfig zlib glib - xorg.libX11 - xorg.libxcb - xorg.libXext - xorg.libXrandr - xorg.libXcursor - xorg.libXi - xorg.libXfixes - xorg.libXrender - xorg.libXcomposite - xorg.libXdamage - xorg.libXtst - xorg.libXinerama + libx11 + libxcb + libxext + libxrandr + libxcursor + libxi + libxfixes + libxrender + libxcomposite + libxdamage + libxtst + libxinerama wayland libxkbcommon gnutls diff --git a/src/src/organizercore.cpp b/src/src/organizercore.cpp index 3896039..b42c3ed 100644 --- a/src/src/organizercore.cpp +++ b/src/src/organizercore.cpp @@ -663,9 +663,49 @@ void OrganizerCore::prepareVFS() } // FUSE passthrough: read the per-instance setting and pass it to the VFS. + // The binary needs CAP_SYS_ADMIN for passthrough to work — it can be lost + // when the binary is copied, updated, or extracted to a new location. { - const bool passthrough = + bool passthrough = QSettings().value("fluorine/fuse_passthrough", false).toBool(); + + if (passthrough) { + // Check if the binary actually has cap_sys_admin right now. + const QString binary = QCoreApplication::applicationFilePath(); + QProcess getcap; + getcap.setProgram("getcap"); + getcap.setArguments({binary}); + getcap.start(); + getcap.waitForFinished(5000); + const QString caps = QString::fromUtf8(getcap.readAllStandardOutput()); + const bool hasCap = caps.contains("cap_sys_admin"); + + if (!hasCap) { + std::fprintf(stderr, + "[VFS] passthrough enabled but binary lacks cap_sys_admin " + "— requesting via pkexec...\n"); + QProcess setcap; + setcap.setProgram("pkexec"); + setcap.setArguments({"setcap", "cap_sys_admin+ep", binary}); + setcap.start(); + setcap.waitForFinished(60000); + + if (setcap.exitCode() != 0) { + std::fprintf(stderr, + "[VFS] failed to set cap_sys_admin — disabling passthrough\n"); + passthrough = false; + } else { + // The capability is on the file now, but the running process won't + // have it until next exec. Disable passthrough for this session — + // it will work automatically on next launch. + std::fprintf(stderr, + "[VFS] cap_sys_admin applied to binary — passthrough " + "will activate on next launch\n"); + passthrough = false; + } + } + } + m_USVFS.setPassthroughEnabled(passthrough); } #endif diff --git a/src/src/vfs/mo2filesystem.cpp b/src/src/vfs/mo2filesystem.cpp index 42e90c0..6c8cb87 100644 --- a/src/src/vfs/mo2filesystem.cpp +++ b/src/src/vfs/mo2filesystem.cpp @@ -1056,12 +1056,25 @@ void mo2_open(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) ptFd = open(realPath.c_str(), O_RDONLY); } if (ptFd >= 0) { - fi->backing_id = static_cast(ptFd); - // Store the fd so we can close it on release - std::scoped_lock lock(ctx->open_files_mutex); - auto it = ctx->open_files.find(fh); - if (it != ctx->open_files.end()) { - it->second.fd = ptFd; + int backingId = fuse_passthrough_open(req, ptFd); + if (backingId > 0) { + fi->backing_id = backingId; + // Store the fd so we can close it on release + std::scoped_lock lock(ctx->open_files_mutex); + auto it = ctx->open_files.find(fh); + if (it != ctx->open_files.end()) { + it->second.fd = ptFd; + it->second.backing_id = backingId; + } + } else { + // Passthrough registration failed (e.g. missing cap_sys_admin at + // runtime). Disable passthrough for the rest of this session to + // avoid spamming "Operation not permitted" on every open. + close(ptFd); + ctx->passthrough_active = false; + std::fprintf(stderr, + "[VFS] fuse_passthrough_open failed — disabling " + "passthrough for this session\n"); } } } @@ -1667,6 +1680,11 @@ void mo2_release(fuse_req_t req, fuse_ino_t /*ino*/, struct fuse_file_info* fi) std::scoped_lock lock(ctx->open_files_mutex); auto it = ctx->open_files.find(fi->fh); if (it != ctx->open_files.end()) { + if constexpr (FUSE_CAP_PASSTHROUGH != 0) { + if (it->second.backing_id > 0) { + fuse_passthrough_close(req, it->second.backing_id); + } + } if (it->second.fd >= 0) { close(it->second.fd); } diff --git a/src/src/vfs/mo2filesystem.h b/src/src/vfs/mo2filesystem.h index 2883a18..9b27d76 100644 --- a/src/src/vfs/mo2filesystem.h +++ b/src/src/vfs/mo2filesystem.h @@ -39,6 +39,7 @@ struct Mo2FsContext struct OpenFile { int fd = -1; + int backing_id = 0; // FUSE passthrough backing ID (0 = not using passthrough) std::string real_path; bool writable = false; bool is_backing = false; -- cgit v1.3.1