diff --git a/.dir-locals.el b/.dir-locals.el index 628f512ab..4616d4003 100644 --- a/.dir-locals.el +++ b/.dir-locals.el @@ -1,47 +1,64 @@ ((python-mode . ((indent-tabs-mode . nil))) - (autoconf-mode . ((indent-tabs-mode . t))) - (c++-mode . ((c-file-style . "k&r") - (indent-tabs-mode . nil) - (c-basic-offset . 4) - (c-file-offsets . - ((block-close . 0) - (brace-list-close . 0) - (brace-list-entry . 0) - (brace-list-intro . +) - (case-label . 0) - (class-close . 0) - (defun-block-intro . +) - (defun-close . 0) - (defun-open . 0) - (else-clause . 0) - (inclass . +) - (label . 0) - (statement . 0) - (statement-block-intro . +) - (statement-case-intro . +) - (statement-cont . +) - (substatement . +) - (topmost-intro . 0))))) - (c-mode . ((c-file-style . "stroustrup") - (indent-tabs-mode . t) - (tab-width . 8) + (c++-mode . ((c-file-style . "linux-kernel") + (c-basic-offset . 8) + (c-label-minimum-indentation . 0) + (c-offsets-alist . ( + (arglist-close . c-lineup-arglist-tabs-only) + (arglist-cont-nonempty . + (c-lineup-gcc-asm-reg c-lineup-arglist-tabs-only)) + (arglist-intro . +) + (brace-list-intro . +) + (c . c-lineup-C-comments) + (case-label . 0) + (comment-intro . c-lineup-comment) + (cpp-define-intro . +) + (cpp-macro . -1000) + (cpp-macro-cont . +) + (defun-block-intro . +) + (else-clause . 0) + (func-decl-cont . +) + (inclass . +) + (inher-cont . c-lineup-multi-inher) + (knr-argdecl-intro . 0) + (label . -1000) + (statement . 0) + (statement-block-intro . +) + (statement-case-intro . +) + (statement-cont . +) + (substatement . +) + )) + (indent-tabs-mode . t) + (show-trailing-whitespace . t) + )) + (c-mode . ((c-file-style . "linux-kernel") (c-basic-offset . 8) - (c-file-offsets . - ((block-close . 0) - (brace-list-close . 0) - (brace-list-entry . 0) - (brace-list-intro . +) - (case-label . 0) - (class-close . 0) - (defun-block-intro . +) - (defun-close . 0) - (defun-open . 0) - (else-clause . 0) - (inclass . +) - (label . 0) - (statement . 0) - (statement-block-intro . +) - (statement-case-intro . +) - (statement-cont . +) - (substatement . +) - (topmost-intro . 0)))))) + (c-label-minimum-indentation . 0) + (c-offsets-alist . ( + (arglist-close . c-lineup-arglist-tabs-only) + (arglist-cont-nonempty . + (c-lineup-gcc-asm-reg c-lineup-arglist-tabs-only)) + (arglist-intro . +) + (brace-list-intro . +) + (c . c-lineup-C-comments) + (case-label . 0) + (comment-intro . c-lineup-comment) + (cpp-define-intro . +) + (cpp-macro . -1000) + (cpp-macro-cont . +) + (defun-block-intro . +) + (else-clause . 0) + (func-decl-cont . +) + (inclass . +) + (inher-cont . c-lineup-multi-inher) + (knr-argdecl-intro . 0) + (label . -1000) + (statement . 0) + (statement-block-intro . +) + (statement-case-intro . +) + (statement-cont . +) + (substatement . +) + )) + (indent-tabs-mode . t) + (show-trailing-whitespace . t) + )) +) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..4677c34d2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,27 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. Debian] + - Version [e.g. 12] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/issue-report.md b/.github/ISSUE_TEMPLATE/issue-report.md deleted file mode 100644 index 492ce0a1c..000000000 --- a/.github/ISSUE_TEMPLATE/issue-report.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -name: Issue report -about: Create a report to help us improve -title: '' -labels: '' -assignees: '' - ---- - -PLEASE READ BEFORE REPORTING AN ISSUE - -libfuse does not have any active, regular contributors or developers. The current maintainer continues to apply pull requests and tries to make regular releases, but unfortunately has no capacity to do any development beyond addressing high-impact issues. When reporting bugs, please understand that unless you are including a pull request or are reporting a critical issue, you will probably not get a response. - -To prevent the issue tracker from being flooded with issues that no-one is intending to work on, and to give more visibility to critical issues that users should be aware of and that most urgently need attention, I will also close most bug reports once they've been inactive for a while. - -Please note that this isn't meant to imply that you haven't found a bug - you most likely have and I'm grateful that you took the time to report it. Unfortunately, libfuse is a purely volunteer driven project, and at the moment there simply aren't any volunteers. diff --git a/.github/workflows/abicheck.sh b/.github/workflows/abicheck.sh new file mode 100755 index 000000000..89d426a2b --- /dev/null +++ b/.github/workflows/abicheck.sh @@ -0,0 +1,126 @@ +#!/bin/bash +# ABI compatibility check script for libfuse + +set -e + +show_help() { + cat << EOF +Usage: $0 [OPTIONS] + +Check ABI compatibility between two versions of libfuse. + +OPTIONS: + --prev-tag Previous release tag/hash to compare against + (default: auto-detect latest release tag) + + --current-dir Path to current version checkout + (default: create temporary checkout from current directory) + + --previous-dir Path to previous version checkout + (default: create temporary checkout at --prev-tag) + + -h, --help Show this help message and exit + +EXAMPLES: + # Auto-detect previous release and create temporary checkouts + $0 + + # Compare against a specific tag + $0 --prev-tag fuse-3.16.2 + + # Use existing checkout directories (useful in CI) + $0 --current-dir ./current --previous-dir ./previous + +NOTES: + - If both --current-dir and --previous-dir are provided, the script will use + those directories directly without creating temporary checkouts. + + - If only --prev-tag is provided (or auto-detected), the script will create + temporary checkouts in /tmp for both versions. + +EOF +} + +PREV_TAG="" +CURRENT_DIR="" +PREVIOUS_DIR="" +WORK_DIR="/tmp/libfuse-abicheck-$$" + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + -h|--help) + show_help + exit 0 + ;; + --prev-tag) + PREV_TAG="$2" + shift 2 + ;; + --current-dir) + CURRENT_DIR="$2" + shift 2 + ;; + --previous-dir) + PREVIOUS_DIR="$2" + shift 2 + ;; + *) + # Support old positional argument format + PREV_TAG="$1" + shift + ;; + esac +done + +# If directories are provided, use them; otherwise create our own +if [ -n "$CURRENT_DIR" ] && [ -n "$PREVIOUS_DIR" ]; then + # Use provided directories - convert to absolute paths + CURRENT_PATH="$(cd "$CURRENT_DIR" && pwd)" + PREVIOUS_PATH="$(cd "$PREVIOUS_DIR" && pwd)" + echo "Using provided directories:" + echo " Current: $CURRENT_PATH" + echo " Previous: $PREVIOUS_PATH" + CLEANUP_WORK_DIR=false +else + # Auto-detect previous release tag if not provided + if [ -z "$PREV_TAG" ]; then + PREV_TAG=$(.github/workflows/find_previous_release_tag.sh) + fi + + echo "Previous release: $PREV_TAG" + + # Create working directory + mkdir -p "$WORK_DIR" + trap "rm -rf $WORK_DIR" EXIT + CLEANUP_WORK_DIR=true + + # Checkout current version + cp -r . "$WORK_DIR/current" + + # Checkout previous version + git clone . "$WORK_DIR/previous" + (cd "$WORK_DIR/previous" && git checkout "$PREV_TAG") + + CURRENT_PATH="$WORK_DIR/current" + PREVIOUS_PATH="$WORK_DIR/previous" +fi + +# Build current version +cd "$CURRENT_PATH" +meson setup build --buildtype=debug +meson compile -C build + +# Build previous version +cd "$PREVIOUS_PATH" +meson setup build --buildtype=debug +meson compile -C build + +# Run abidiff +abidiff \ + --no-added-syms \ + --suppressions "$CURRENT_PATH/.github/workflows/abidiff_suppressions.abignore" \ + --headers-dir1 "$PREVIOUS_PATH/include/" \ + --headers-dir2 "$CURRENT_PATH/include/" \ + "$PREVIOUS_PATH/build/lib/libfuse3.so" \ + "$CURRENT_PATH/build/lib/libfuse3.so" diff --git a/.github/workflows/abicheck.yml b/.github/workflows/abicheck.yml index 297af751e..169c22a52 100644 --- a/.github/workflows/abicheck.yml +++ b/.github/workflows/abicheck.yml @@ -26,40 +26,25 @@ jobs: if: runner.os == 'Linux' run: | sudo apt-get update - sudo apt-get -y install abigail-tools clang gcc + sudo apt-get -y install abigail-tools clang gcc liburing-dev libnuma-dev - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: path: current - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: path: previous ref: ${{ github.event.pull_request.base.ref }} - - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 + - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: python-version: '3.12' - - name: Build current - working-directory: current + - name: Install Python dependencies run: | - pip install -r requirements.txt - meson setup build --buildtype=debug - meson compile -C build + pip install -r current/requirements.txt - - name: Build previous - working-directory: previous + - name: Run ABI check run: | - echo "Commit-id before PR: $(git show HEAD)" - pip install -r requirements.txt - meson setup build --buildtype=debug - meson compile -C build - - - name: Run abidiff - run: abidiff - --no-added-syms - --headers-dir1 previous/include/ - --headers-dir2 current/include/ - previous/build/lib/libfused.so - current/build/lib/libfused.so + current/.github/workflows/abicheck.sh --current-dir current --previous-dir previous diff --git a/.github/workflows/abicheck_prev_release.yml b/.github/workflows/abicheck_prev_release.yml new file mode 100644 index 000000000..cc33ac12d --- /dev/null +++ b/.github/workflows/abicheck_prev_release.yml @@ -0,0 +1,59 @@ +--- +name: 'libfuse ABI check against previous major release' + +on: + push: + branches: + - master + - 'fuse-[0-9]+.[0-9]+*' # This will match branches like 3.17, 3.18, 4.0, etc. + pull_request: + branches: + - master + - 'fuse-[0-9]+.[0-9]+*' +permissions: + contents: read + +jobs: + abi: + runs-on: '${{ matrix.os }}' + strategy: + matrix: + os: + - ubuntu-latest + + steps: + - name: Install dependencies (Ubuntu) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get -y install abigail-tools clang gcc liburing-dev libnuma-dev + + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + path: current + fetch-depth: 0 # Fetch all history and tags + + - name: Determine previous major release tag + id: prev_release + run: | + cd current + PREV_TAG=$(.github/workflows/find_previous_release_tag.sh) + echo "prev_tag=$PREV_TAG" >> $GITHUB_OUTPUT + + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + path: previous + ref: ${{ steps.prev_release.outputs.prev_tag }} + + - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + with: + python-version: '3.12' + + - name: Install Python dependencies + run: | + pip install -r current/requirements.txt + + - name: Run ABI check + run: | + echo "Previous release tag: ${{ steps.prev_release.outputs.prev_tag }}" + current/.github/workflows/abicheck.sh --current-dir current --previous-dir previous diff --git a/.github/workflows/abidiff_suppressions.abignore b/.github/workflows/abidiff_suppressions.abignore new file mode 100644 index 000000000..cf53bb3c2 --- /dev/null +++ b/.github/workflows/abidiff_suppressions.abignore @@ -0,0 +1,43 @@ +# Suppression file for abidiff false positives in libfuse +# This file suppresses ABI changes that are actually compatible but flagged by abidiff + +[suppress_type] +# Suppress the fuse_conn_info reserved array transformation +# This change is ABI-compatible: uint32_t[16] -> uint16_t request_timeout + uint16_t[31] +# Both use exactly 64 bytes (16*4 = 32*2 = 64 bytes) +name = fuse_conn_info +# Suppress changes to the reserved field that are size/offset related +change_kind = size-or-offset-change +has_data_member_inserted_at = offset_in_bits(512) + +[suppress_type] +# Also suppress the general struct size change for fuse_conn_info +# since the total size remains the same (128 bytes) by a static assertion +# in the code +name = fuse_conn_info +change_kind = size-change + +[suppress_type] +# Suppress ALL changes to fuse_operations struct +# These are backward compatible due to the op_size mechanism in fuse_main() +# Applications pass sizeof(struct fuse_operations) at compile time, +# and the library uses memcpy(&fs->op, op, op_size) to safely copy only +# the fields the application knows about. New fields remain NULL. +name = fuse_operations +has_data_member_inserted_at = end +has_size_change = yes + +[suppress_type] +# Suppress ALL changes to fuse_lowlevel_ops struct +# These are backward compatible due to the op_size mechanism in fuse_session_new() +# Same pattern as fuse_operations - op_size controls safe copying +name = fuse_lowlevel_ops +has_data_member_inserted_at = end +has_size_change = yes + +[suppress_type] +# fuse_file_info documents its padding bits as the place for new flags. +# Claiming one shifts the remaining padding but keeps the struct size and +# every documented member offset, so bound this to size-preserving changes. +name = fuse_file_info +has_size_change = no diff --git a/.github/workflows/bsd.yaml b/.github/workflows/bsd.yaml new file mode 100644 index 000000000..8edb96bce --- /dev/null +++ b/.github/workflows/bsd.yaml @@ -0,0 +1,33 @@ +--- +# TODO: integrate into matrix.os in pr-ci.yml +# TODO: add NetBSD and DragonFlyBSD +name: 'Build (FreeBSD)' +on: + push: + branches: + - master + - 'fuse-[0-9]+.[0-9]+*' + pull_request: + branches: + - master + - 'fuse-[0-9]+.[0-9]+*' + +jobs: + build_bsd: + runs-on: ubuntu-latest + name: Build under FreeBSD + steps: + - name: Checkout + uses: actions/checkout@v6.0.1 + - name: Build + uses: vmactions/freebsd-vm@v1 + with: + usesh: true + prepare: | + pkg install -y meson ninja + run: | + mkdir build + cd build + meson setup .. + ninja -v +... \ No newline at end of file diff --git a/.github/workflows/checkpatch.yml b/.github/workflows/checkpatch.yml index 622542c54..c97f5acc3 100644 --- a/.github/workflows/checkpatch.yml +++ b/.github/workflows/checkpatch.yml @@ -1,20 +1,16 @@ name: Checkpatch on: - push: - branches: - - master pull_request: + types: [opened, synchronize, reopened] branches: - master -permissions: - contents: read jobs: checkpatch: runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 0 - name: Install dependencies @@ -23,14 +19,25 @@ jobs: sudo apt-get install -y perl - name: Run checkpatch.pl run: | + if [ -z "${{ github.base_ref }}" ]; then + echo "Not a pull request, skipping checkpatch" + exit 0 + fi git fetch origin ${{ github.base_ref }} base_commit=$(git merge-base origin/${{ github.base_ref }} HEAD) echo "Base commit: $base_commit" echo "Running checkpatch.pl on all commits in the PR:" - git rev-list --reverse $base_commit..HEAD | while read commit; do + # --no-merges: HEAD is GitHub's test merge, and ^.. is + # the whole PR, which checkpatch would read as a single patch. + git rev-list --reverse --no-merges $base_commit..HEAD | while read commit; do + # Skip dependabot commits - we do not have control over it + if git log -1 --format='%ae' $commit | grep -q "dependabot"; then + echo "Skipping dependabot commit: $commit" + continue + fi subject=$(git log -1 --format=%s $commit) echo "Checking commit: $commit - $subject" - if ! ./checkpatch.pl --no-tree --ignore MAINTAINERS,SPDX_LICENSE_TAG,COMMIT_MESSAGE,FILE_PATH_CHANGES,EMAIL_SUBJECT,AVOID_EXTERNS,GIT_COMMIT_ID,ENOSYS_SYSCALL -g $commit; then + if ! .github/workflows/run-checkpatch.sh $commit; then echo "checkpatch.pl found issues in commit $commit - $subject" exit 1 fi diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 474f2c8d2..914d81613 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -49,11 +49,11 @@ jobs: build-mode: manual steps: - name: Checkout repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9 + uses: github/codeql-action/init@1b168cd39490f61582a9beae412bb7057a6b2c4e # v3.29.5 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -73,12 +73,12 @@ jobs: - if: matrix.build-mode == 'manual' shell: bash run: | - sudo apt install meson ninja-build python3-pytest + sudo apt install meson ninja-build python3-pytest liburing-dev libnuma-dev meson setup build --buildtype=debug meson compile -C build - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9 + uses: github/codeql-action/analyze@1b168cd39490f61582a9beae412bb7057a6b2c4e # v3.29.5 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml index 184fbcfe1..af68e2c9d 100644 --- a/.github/workflows/codespell.yml +++ b/.github/workflows/codespell.yml @@ -21,8 +21,8 @@ jobs: steps: - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Codespell - uses: codespell-project/actions-codespell@406322ec52dd7b488e48c1c4b82e2a8b3a1bf630 # v2.1 + uses: codespell-project/actions-codespell@8f01853be192eb0f849a5c7d721450e7a467c579 # v2.2 with: skip: checkpatch.pl diff --git a/.github/workflows/find_previous_release_tag.sh b/.github/workflows/find_previous_release_tag.sh new file mode 100755 index 000000000..0e73336a1 --- /dev/null +++ b/.github/workflows/find_previous_release_tag.sh @@ -0,0 +1,69 @@ +#!/bin/bash + +set -e + +# Script to find the previous major release tag for libfuse +# Usage: ./find_previous_release_tag.sh + +# Get current version from meson.build +# Pattern matches: version : "3.18.0" or version: '3.18.0' +VERSION_PATTERN="version\s*:\s*['\"]" +VERSION_EXTRACT="s/.*version\s*:\s*['\"]([^'\"]+)['\"].*/\1/" + +CURRENT_VERSION=$(grep -E "$VERSION_PATTERN" meson.build | \ + sed -E "$VERSION_EXTRACT") +echo "Current version: $CURRENT_VERSION" >&2 + +# Extract major.minor version (e.g., 3.18 from 3.18.0) +# Pattern captures first two numbers separated by dot +MAJOR_MINOR_PATTERN='s/^([0-9]+\.[0-9]+).*/\1/' + +CURRENT_MAJOR_MINOR=$(echo "$CURRENT_VERSION" | \ + sed -E "$MAJOR_MINOR_PATTERN") +echo "Current major.minor: $CURRENT_MAJOR_MINOR" >&2 + +# Get all major.minor versions from tags, sort them, and find the one before +# current +# Pattern matches tags like: fuse-3.17.0, fuse-3.18.1, etc. +FUSE_TAG_PATTERN="^fuse-[0-9]+\.[0-9]+" +# Pattern extracts major.minor from version strings +TAG_MAJOR_MINOR_PATTERN='s/^([0-9]+\.[0-9]+).*/\1/' + +ALL_MAJOR_MINOR=$(git tag --list | \ + grep -E "$FUSE_TAG_PATTERN" | \ + sed 's/fuse-//' | \ + sed -E "$TAG_MAJOR_MINOR_PATTERN" | \ + sort -V -u) +echo "All major.minor versions found:" >&2 +echo "$ALL_MAJOR_MINOR" >&2 + +# Find the previous major.minor version +PREV_MAJOR_MINOR=$(echo "$ALL_MAJOR_MINOR" | \ + grep -B1 "^${CURRENT_MAJOR_MINOR}$" | \ + head -1) + +if [ -z "$PREV_MAJOR_MINOR" ] || [ "$PREV_MAJOR_MINOR" = "$CURRENT_MAJOR_MINOR" ]; then + echo "Error: No previous major.minor version found before $CURRENT_MAJOR_MINOR" >&2 + exit 1 +fi + +echo "Previous major.minor: $PREV_MAJOR_MINOR" >&2 + +# Get the latest tag for the previous major.minor version +# Pattern matches tags like: fuse-3.17.0, fuse-3.17.1, fuse-3.17.2, etc. +PREV_TAG_PATTERN="^fuse-${PREV_MAJOR_MINOR}\.[0-9]+" + +PREV_TAG=$(git tag --list | \ + grep -E "$PREV_TAG_PATTERN" | \ + sort -V | \ + tail -1) + +if [ -z "$PREV_TAG" ]; then + echo "Error: No previous major release tag found for version $PREV_MAJOR_MINOR" >&2 + exit 1 +fi + +echo "Previous release tag: $PREV_TAG" >&2 + +# Output the tag to stdout (this is what the workflow will capture) +echo "$PREV_TAG" diff --git a/.github/workflows/install-ubuntu-dependencies.sh b/.github/workflows/install-ubuntu-dependencies.sh new file mode 100755 index 000000000..19109d2ed --- /dev/null +++ b/.github/workflows/install-ubuntu-dependencies.sh @@ -0,0 +1,232 @@ +#!/bin/bash +# Install Ubuntu dependencies for libfuse +# Based on dependencies from .github/workflows/pr-ci.yml and codechecker.yml + +set -e + +# Package lists - define once, reuse everywhere +PACKAGES_CORE=( + gcc + meson + ninja-build + libudev-dev + liburing-dev + lsb-release + pkg-config + python3 + python3-pip + libsystemd-dev + systemd-dev +) + +PACKAGES_FULL=( + "${PACKAGES_CORE[@]}" + clang + doxygen + gcc-10 + gcc-9 + gdb + valgrind + gcc-multilib + g++-multilib + libc6-dev-i386 + libpcap0.8-dev:i386 + libudev-dev:i386 + pkg-config:i386 + libsystemd-dev + systemd-dev +) + +PACKAGES_CODECHECKER=( + "${PACKAGES_CORE[@]}" + jq +) + +PACKAGES_ABICHECK=( + "${PACKAGES_CORE[@]}" + abigail-tools + clang +) + +PACKAGES_CODEQL=( + "${PACKAGES_CORE[@]}" +) + +PACKAGES_CPPCHECK=( + cppcheck +) + +usage() { + cat << EOF +Usage: $0 [OPTIONS] + +Install Ubuntu dependencies for libfuse development and testing. + +OPTIONS: + --minimal Install only minimal build dependencies + --full Install all dependencies including multilib and testing tools (default) + --codechecker Install dependencies for CodeChecker analysis + --cppcheck Install cppcheck for static analysis + --abicheck Install dependencies for ABI compatibility checks + --codeql Install dependencies for CodeQL analysis + --infer Install Facebook Infer static analyzer + -h, --help Show this help message + +EXAMPLES: + $0 # Install full dependencies + $0 --minimal # Install minimal build dependencies + $0 --codechecker # Install CodeChecker dependencies + $0 --cppcheck # Install cppcheck + $0 --abicheck # Install ABI check dependencies + $0 --codeql # Install CodeQL dependencies + $0 --infer # Install Infer static analyzer + +EOF + exit 0 +} + +install_packages() { + local packages=("$@") + if [ ${#packages[@]} -eq 0 ]; then + echo "No packages to install" + return 0 + fi + + sudo apt-get update + sudo apt-get install -y "${packages[@]}" +} + +install_minimal() { + echo "Installing minimal build dependencies..." + install_packages "${PACKAGES_CORE[@]}" +} + +install_full() { + echo "Installing full dependencies (including multilib and testing tools)..." + + # Add i386 architecture for 32-bit support + echo "Adding i386 architecture..." + sudo dpkg --add-architecture i386 + + install_packages "${PACKAGES_FULL[@]}" + + echo "Installing Python test dependencies..." + pip install -r requirements.txt +} + +install_codechecker() { + echo "Installing CodeChecker dependencies..." + install_packages "${PACKAGES_CODECHECKER[@]}" + + echo "Installing Python packages for CodeChecker..." + pip install -r requirements.txt +} + +install_abicheck() { + echo "Installing ABI check dependencies..." + install_packages "${PACKAGES_ABICHECK[@]}" +} + +install_codeql() { + echo "Installing CodeQL dependencies..." + install_packages "${PACKAGES_CODEQL[@]}" +} + +install_cppcheck() { + echo "Installing cppcheck..." + install_packages "${PACKAGES_CPPCHECK[@]}" +} + +install_infer() { + echo "Installing Facebook Infer..." + INFER_VERSION="1.2.0" + INFER_TAR="infer-linux-x86_64-v${INFER_VERSION}.tar.xz" + curl -sSL \ + "https://github.com/facebook/infer/releases/download/v${INFER_VERSION}/${INFER_TAR}" \ + -o /tmp/infer.tar.xz + sudo tar -xJf /tmp/infer.tar.xz -C /opt + sudo ln -sf "/opt/infer-linux-x86_64-v${INFER_VERSION}/bin/infer" /usr/local/bin/infer + rm /tmp/infer.tar.xz +} + +# Default to full installation +MODE="full" +INSTALL_CPPCHECK=0 +INSTALL_INFER=0 + +# Parse command-line arguments +while [[ $# -gt 0 ]]; do + case $1 in + --minimal) + MODE="minimal" + shift + ;; + --full) + MODE="full" + shift + ;; + --codechecker) + MODE="codechecker" + shift + ;; + --cppcheck) + INSTALL_CPPCHECK=1 + shift + ;; + --abicheck) + MODE="abicheck" + shift + ;; + --codeql) + MODE="codeql" + shift + ;; + --infer) + INSTALL_INFER=1 + shift + ;; + -h|--help) + usage + ;; + *) + echo "Unknown option: $1" + echo "Use -h or --help for usage information." + exit 1 + ;; + esac +done + +# Execute the appropriate installation +case $MODE in + minimal) + install_minimal + ;; + full) + install_full + ;; + codechecker) + install_codechecker + ;; + abicheck) + install_abicheck + ;; + codeql) + install_codeql + ;; +esac + +# Install optional tools if requested +if [ $INSTALL_CPPCHECK -eq 1 ]; then + install_cppcheck +fi +if [ $INSTALL_INFER -eq 1 ]; then + install_infer +fi + +echo "" +echo "✓ Dependencies installed successfully!" +echo "" +echo "Next steps:" +echo " 1. Build: meson setup builddir && ninja -C builddir" +echo " 2. Test: meson test -C builddir" +echo "" diff --git a/.github/workflows/iwyi-check.yml b/.github/workflows/iwyi-check.yml index 3751f35de..46c5a8ad4 100644 --- a/.github/workflows/iwyi-check.yml +++ b/.github/workflows/iwyi-check.yml @@ -18,7 +18,7 @@ jobs: name: Include What You Use Check runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 0 diff --git a/.github/workflows/pr-ci.yml b/.github/workflows/pr-ci.yml index 4bb7f66de..f7026286a 100644 --- a/.github/workflows/pr-ci.yml +++ b/.github/workflows/pr-ci.yml @@ -17,25 +17,207 @@ jobs: build: runs-on: '${{ matrix.os }}' strategy: + # One configuration failing must not cancel its siblings. + fail-fast: false matrix: os: # 18.04 does not work because of ‘no_sanitize’ attribute # cc1: all warnings being treated as errors # - ubuntu-18.04 - ubuntu-latest + # One entry per CI job. Every build/test parameter is spelled out via + # include below and forwarded verbatim to test/ci-build.sh -- nothing + # about what a job builds or which pass it runs is hidden in a shell + # case branch. + config: + - gcc + - gcc-root + # GitHub Actions runners have no fuse io-uring support; kept for a + # runner that does. + # - gcc-io-uring + # - gcc-io-uring-root + - gcc-valgrind + - gcc-9-valgrind + - gcc-10-valgrind + - clang + - clang-san + - clang-san-root + # - clang-san-io-uring + - clang-san-nosymver + - clang-san-noiouring + - clang-san-m32 + # include entries must repeat the config key: an include naming none + # of the base matrix's own keys would be folded into every + # combination instead of just its own. + include: + - config: gcc + cc: gcc + - config: gcc-root + cc: gcc + root: true + # - config: gcc-io-uring + # cc: gcc + # io_uring: true + # - config: gcc-io-uring-root + # cc: gcc + # root: true + # io_uring: true + - config: gcc-valgrind + cc: gcc + valgrind: true + - config: gcc-9-valgrind + cc: gcc-9 + valgrind: true + - config: gcc-10-valgrind + cc: gcc-10 + valgrind: true + meson_opts: + - -Dc_args=-flto=auto + - config: clang + cc: clang + cxx: clang++ + - config: clang-san + cc: clang + cxx: clang++ + sanitize: true + - config: clang-san-root + cc: clang + cxx: clang++ + sanitize: true + root: true + # - config: clang-san-io-uring + # cc: clang + # cxx: clang++ + # sanitize: true + # io_uring: true + - config: clang-san-nosymver + cc: clang + cxx: clang++ + sanitize: true + meson_opts: + - -Ddisable-libc-symbol-version=true + - config: clang-san-noiouring + cc: clang + cxx: clang++ + sanitize: true + meson_opts: + - -Denable-io-uring=false + - config: clang-san-m32 + cc: clang + cxx: clang++ + sanitize: true + meson_opts: + - -Dc_args=-m32 + - -Dcpp_args=-m32 + - -Dc_link_args=-m32 + - -Dcpp_link_args=-m32 + - --pkg-config-path=/usr/lib/i386-linux-gnu/pkgconfig steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: '3.x' - name: Install dependencies (Ubuntu) if: runner.os == 'Linux' run: | - sudo dpkg --add-architecture i386 - sudo apt-get update - sudo apt-get install -y clang doxygen gcc gcc-10 gcc-9 valgrind \ - gcc-multilib g++-multilib libc6-dev-i386 \ - libpcap0.8-dev:i386 libudev-dev:i386 pkg-config:i386 - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: actions/setup-python@v5 + ./.github/workflows/install-ubuntu-dependencies.sh --full + + - run: test/ci/prepare-runner.sh ${{ github.workspace }}/fuse-tests + + - run: test/ci-build.sh --name ${{ matrix.config }} + --cc ${{ matrix.cc }} + ${{ matrix.cxx && format('--cxx {0}', matrix.cxx) || '' }} + ${{ matrix.sanitize && '--sanitize' || '' }} + ${{ matrix.valgrind && '--valgrind' || '' }} + ${{ matrix.meson_opts && format('--meson-opt {0}', join(matrix.meson_opts, ' --meson-opt ')) || '' }} + ${{ matrix.root && '--root' || '' }} + ${{ matrix.io_uring && '--io-uring' || '' }} + --work-dir ${{ github.workspace }}/fuse-tests + + - name: Print failed test logs + if: failure() + run: | + # A test's name is /, so its logs/ sits three + # levels under fuse-tests/run/: ///logs. + for dir in fuse-tests/run/*/*/*/logs; do + [ -d "$dir" ] || continue + label=${dir#fuse-tests/run/} + label=${label%/logs} + echo "::group::${label}" + for out in "$dir"/*.sh "$dir"/*.out; do + [ -e "$out" ] || continue + echo "--- ${out##*/} ---" + tail -n 100 "$out" + done + # The stack dumps and the process table are the whole report on + # a timeout, and a tail of a backtrace keeps its least useful + # end -- print them whole. + for out in "$dir"/*.txt; do + [ -e "$out" ] || continue + echo "--- ${out##*/} ---" + cat "$out" + done + echo "::endgroup::" + echo + echo + done + + echo "::group::dmesg" + sudo dmesg -T | tail -n 300 + echo "::endgroup::" + + - name: Unmount stale test mountpoints + if: always() + run: | + # A killed test never reaches fuse_umount, so its mountpoint stays a + # dead transport -- not always a "mnt" directory (examples/null.sh + # mounts onto a plain file under tmp/ instead). The upload step's + # own directory walk stat()s it and ENOTCONN fails the whole + # upload, not just that one entry, so find every FUSE mount under + # this run from /proc/self/mountinfo instead of guessing the shape. + run_dir="$(readlink -f fuse-tests/run)/" + awk -v prefix="$run_dir" ' + { + for (i = 7; i <= NF; i++) if ($i == "-") { fstype = $(i + 1); break } + if (index($5, prefix) == 1 && fstype ~ /^fuse/) print $5 + }' /proc/self/mountinfo | + sort -r | # nested mounts before their parent + while IFS= read -r mnt; do + sudo umount -f "$mnt" 2>/dev/null || true + done + + - name: Upload test logs + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - python-version: '3.12' - - run: pip install -r requirements.txt - - run: test/ci-build.sh + name: test-logs-${{ matrix.os }}-${{ matrix.config }} + # A test killed on timeout runs no cleanup, so a socket it bound in + # its log directory is still there; nothing in one can be uploaded. + path: | + fuse-tests/run/ + fuse-tests/build-*/meson-logs/ + !fuse-tests/run/**/core.* + !fuse-tests/run/**/mnt + !fuse-tests/run/**/*.sock + if-no-files-found: ignore + retention-days: 14 + + # master's branch protection still requires this exact context name + # from when the build job's matrix had only the os dimension. + build-required-check: + name: build (ubuntu-latest) + needs: build + if: always() + runs-on: ubuntu-latest + steps: + - run: '[[ "${{ needs.build.result }}" == "success" ]]' + + doxygen: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Install dependencies (Ubuntu) + run: | + ./.github/workflows/install-ubuntu-dependencies.sh --full + - run: test/ci/doxygen.sh diff --git a/.github/workflows/run-checkpatch.sh b/.github/workflows/run-checkpatch.sh new file mode 100755 index 000000000..43aede57d --- /dev/null +++ b/.github/workflows/run-checkpatch.sh @@ -0,0 +1,31 @@ +#!/bin/sh + +if [ -n "$1" ]; then + commit="$1" +else + commit="HEAD" +fi + +IGNORES="MAINTAINERS,SPDX_LICENSE_TAG,COMMIT_MESSAGE,FILE_PATH_CHANGES" +IGNORES="${IGNORES},EMAIL_SUBJECT,AVOID_EXTERNS,GIT_COMMIT_ID,ENOSYS_SYSCALL" +IGNORES="${IGNORES},ENOSYS,FROM_SIGN_OFF_MISMATCH,QUOTED_COMMIT_ID," +IGNORES="${IGNORES},PREFER_ATTRIBUTE_ALWAYS_UNUSED,PREFER_DEFINED_ATTRIBUTE_MACRO" +IGNORES="${IGNORES},STRCPY,STRNCPY,COMPLEX_MACRO,LINE_SPACING,SYMBOLIC_PERMS" +# version checks read as "FUSE_USE_VERSION >= FUSE_MAKE_VERSION(3, 19)" +IGNORES="${IGNORES},CONSTANT_COMPARISON" +# a Reported-by has no public report to link while an advisory is embargoed +IGNORES="${IGNORES},BAD_REPORTED_BY_LINK" + +# checkpatch is a C/kernel-style checker; a YAML key like "cc:" reads as a +# malformed Signed-off-by/Cc trailer to its BAD_SIGN_OFF check, so YAML is +# kept out of the diff it sees rather than silencing that check. +# -c diff.algorithm=default: histogram (a common user gitconfig default) +# aligns some hunks differently than myers, which can turn an unrelated +# pre-existing line into a "+" line or vice versa; pin so local runs match CI. +# ^.. rather than -1 : with a pathspec, -1 means "one +# commit reachable from here that touches it", so a YAML-only commit would +# hand checkpatch its predecessor and report on the wrong patch. The range +# emits nothing instead, which is the right answer for such a commit. +git -c diff.algorithm=default format-patch --stdout "${commit}^..${commit}" \ + -- . ':!*.yml' ':!*.yaml' | + ./checkpatch.pl --show-types --max-line-length=100 --no-tree --ignore ${IGNORES} - diff --git a/.gitignore b/.gitignore index 9576c6cbf..e7fdd120d 100644 --- a/.gitignore +++ b/.gitignore @@ -12,8 +12,11 @@ *.gz \#*# *.orig +*.patch *~ +.DS_Store Makefile.in +Makefile *.m4 stamp-h* config.* @@ -37,4 +40,7 @@ TAGS /GSYMS /GTAGS /test/test_setattr +/test/test_api_30 /build/ +# run-tests.py output, when pointed at the source tree with --run-dir +/fuse-tests/ diff --git a/AUTHORS b/AUTHORS index 9f6ed908d..946beef4d 100644 --- a/AUTHORS +++ b/AUTHORS @@ -260,24 +260,40 @@ Tyler Hall yangyun Abhishek -# New authors since fuse-3.17.0 +# New authors since fuse-3.17.1 Luis Henriques Zegang - -# New authors since fuse-3.17.1-rc0 -Maksim Harbachou -Vassili Tchersky - -# New authors since fuse-3.17.1-rc1 -jnr0006 -Vassili Tchersky - -# New authors since fuse-3.17.1 swj <1186093704@qq.com> +Gleb Popov <6yearold@gmail.com> +WekaJosh <80121792+WekaJosh@users.noreply.github.com> +Alexander Monakov Ben Dooks - -# New authors since fuse-3.17.2 +Ben Linsay +Dave Vasilevsky +Darrick J. Wong +Georgi Valkov +Alik Aslanyan +jnr0006 +Jingbo Xu Long Li +Maksim Harbachou +Meng Lu Wang +Vassili Tchersky +Vassili Tchersky +izxl007 +Zeno Sebastian Endemann -# New authors since fuse-3.17.3 -Georgi Valkov +# New authors since fuse-3.18.1 +Abhinav Agarwal + +# New authors since fuse-3.18.2 +Robert Doebbelin +Ashish Ahuja +Benjamin Peterson +wilkis +Jan Engelhardt +Alexander Mikhalitsyn +wangrzneu +nikola-sh +Mikhail Novosyolov +Keerthana KT diff --git a/ChangeLog.rst b/ChangeLog.rst index 730cb0ffd..dabe1318f 100644 --- a/ChangeLog.rst +++ b/ChangeLog.rst @@ -1,3 +1,162 @@ +libfuse 3.18.3 (2026-09-09) +=========================== + +Security Fixes +-------------- + +* ``fuse_session_custom_io()`` is disabled unless libfuse is built with + ``-Denable-custom-io=true``, and returns ``-ENOTSUP`` otherwise. The + ``hello_ll_uds`` example is built only with that option, and enabling it + warns at configure time. + Reason is a custom io peer might not be a kernel and can + forge requests that libfuse parses without bounds checks, crashing or + corrupting the filesystem process. See ``doc/README.custom-io``. + +* fusermount3: resolve the mountpoint once, through an ``O_PATH|O_NOFOLLOW`` + descriptor. A symlink swapped in between the type check and the second + lookup could redirect the mount. +* fusermount3: run the auto-unmount probe as the calling user. It opened the + caller-supplied mountpoint with elevated privileges before, so a symlink + could get an attacker-chosen path opened as root. +* mount_util: terminate the ``/bin/mount`` and ``/bin/umount`` argument + vectors with ``--``. ``fsname`` reaches them straight from ``-ofsname=`` in + setuid-root fusermount3, and the child raises the real uid to 0 before + ``execle()``, so an unprivileged caller controlled a positional operand of a + mount(8) that was not in restricted mode. +* mount_util: skip the mtab update entirely for option-like mount arguments. + BusyBox mount(8) does not honour ``--``. +* fusermount3: unmount through ``unmount_fuse()`` when passing the device + descriptor to the caller fails, so that path drops privileges and runs the + ``fusermount3 -u`` checks instead of calling ``umount2()`` as root on a + caller-supplied path. +* fusermount3 and lib: pass ``UMOUNT_NOFOLLOW`` on the kernel and non-setuid + unmount paths. +* fusermount3: check the ``fstat()`` return value when validating the + communication file descriptor. +* fusermount3: fix an out-of-bounds read at index -1 in ``get_mnt_opts()`` + when the option string is empty, which a read-only mount with no further + options reaches. +* util: avoid a pointer underflow when trimming ``fuse.conf`` lines. +* fusermount3: reject a negative ``mount_max`` other than the documented -1. + A typo such as -2 made the limit comparison always true and blocked every + non-root mount. +* lib: relay the KILLPRIV_V2 kill-suidgid flags to the filesystem in the new + ``fuse_file_info::kill_suidgid``. Only ``setattr`` saw them before, so a + filesystem that had taken over clearing suid/sgid never learned of it on + ``O_TRUNC`` open and on write, and the bits survived. + +Note: ``fuse_file_info::kill_suidgid`` is new in 3.18.3 and ``FUSE_VERSION`` +carries no patch level. A filesystem built against these headers but running +against an older 3.18 library finds the field permanently zero, so require +3.18.3 at run time as well. + +Important Fixes +--------------- + +* Fixed a hang on ``statx`` in builds without ``HAVE_STATX``: ``_do_statx()`` + never replied, so the kernel waited forever. +* io-uring: the CQE dispatch validated the opcode against ``fuse_ll_ops[]`` + but called through ``fuse_ll_ops2[]``, so an opcode with no handler there + was called as a null function pointer. +* io-uring: fixed the notify-retrieve reply buffer handling. The + ``fuse_notify_retrieve_in`` header sits at the start of the payload buffer, + not in the ring header. +* io-uring: fixed the ``req_header_sz`` calculation, which sized the header + buffer from the wrong struct. +* io-uring: create the rings with ``IORING_SETUP_SUBMIT_ALL``, so one failing + commit SQE no longer leaves the rest of the batch unsubmitted. +* io-uring: ``fuse_reply_none()`` commits the ring entry. A FORGET answered + that way leaked the entry and left the kernel-side request outstanding. +* fusermount3: treat ``ECONNABORTED`` like ``ENOTCONN`` when deciding whether + to auto-unmount, so a daemon that dies with io-uring registered no longer + leaves the mount behind. +* ``receive_fd()``: check ``CMSG_FIRSTHDR()`` for NULL before dereferencing it. +* ``fuse_session_loop_mt_312()`` no longer destroys ``se->mt_lock`` before + ``fuse_session_destroy()`` destroys it again, which was undefined behaviour + on every multi-threaded shutdown. +* ``fuse_loop_cfg_create()`` returning NULL is checked before the config is + dereferenced in ``fuse_session_loop_mt_312()`` and + ``fuse_session_loop_mt_31()``. +* iconv: the error check after opening the ``fromfs`` descriptor tested + ``tofs``, so a failed ``iconv_open()`` was ignored and left an invalid + descriptor behind. +* mount.fuse: a failure to clear ``FD_CLOEXEC`` went undetected, because the + result was compared against 1 rather than -1. +* ``grow_pipe_to_max()`` opens ``/proc/sys/fs/pipe-max-size`` with + ``O_CLOEXEC``; a concurrent fork+exec leaked the descriptor into the child. +* Fixed a build failure for ``FUSE_USE_VERSION`` 312 and newer without symbol + versioning, where ``fuse_loop_mt()`` expanded to an undeclared + ``fuse_loop_mt_312()``. +* Fixed a Clang 21 build failure in ``ST_MTIM_NSEC``. +* Fixed leaks: the pipe when its size cannot be grown, the mountpoint in + ``fuse_session_mount()`` and ``fuse_session_destroy()``, the pipe + descriptors when ``fork()`` or ``setsid()`` fail in ``fuse_daemonize()``, + the context when ``pthread_setspecific()`` fails, ``print_module_help()``, + and a ``fuse_pollhandle`` in ``fuse_lib_poll()``. +* Examples: ``update_fs()`` uses ``localtime_r()``. ``localtime()`` returns a + shared static ``struct tm``, so it raced with the session threads and + ``strftime()`` could format a half-overwritten time. +* Examples: ``cuse_client`` caps the transfer size at 16 MiB. ``do_rw()`` + passed the SIZE argument straight to ``calloc()``. +* Examples: memfs_ll locking, refcounting and bounds fixes, including a + use-after-free on rename overwrite and on a concurrent forget. +* Examples: passthrough_hp lock-order and lifetime fixes. The directory + stream is protected by a per-handle lock, ``fs.mutex`` is taken before + ``Inode::m`` and when ``link()`` raises nlookup, and no inode lock is held + across a syscall or a reply. + +Documentation +------------- + +* The fuse-devel mailing list moved to lists.linux.dev. +* Man page and README corrections. + + +libfuse 3.18.2 (2026-03-18) +=========================== +* Fix two io-uring issues that might be security critical + * fuse-io-uring: Fix UAF and NULL deref in startup error path + * fuse-io-uring: Fix NULL deref and memory leak in fuse_uring_init_queue + +libfuse 3.18.1 (2025-12-20) +=========================== +* Fix a critical ABI issue compared to libfuse-3.17.3+ +* Note: This breaks ABI compatibility to libfuse-3.18.0 + (given that 3.18.0 is out for 2 days only, probably the lesser evil) + +libfuse 3.18.0 (2025-12-18) +=========================== + +New Features +------------ + +* fuse-over-io-uring communication +* statx support +* Request timeouts: Prevent hung operations +* FUSE_NOTIFY_INC_EPOCH: New notification mechanism for epoch counters + +Important Fixes +---------------- + +* Fixed double unmount on FUSE_DESTROY +* Fixed junk readdirplus results when filesystem doesn't fill stat info +* Fixed memory deallocation in fuse_session_loop_remember +* Fixed COPY_FILE_RANGE interface + +Platform Support +---------------- + +* Improved FreeBSD support (mount error reporting, test runner, build fixes) +* Fixed 32-bit architecture builds +* Fixed build with musl libc and older kernels (< 5.9) + +Other Improvements +------------------ + +* Added PanFS to fusermount whitelist +* Thread naming support for easier debugging + + libfuse 3.17.4 (2025-08-19) =========================== - Try to detect mount-utils by checking for /run/mount/utab diff --git a/LICENSE b/LICENSE index 72d90bdb2..184a5b27c 100644 --- a/LICENSE +++ b/LICENSE @@ -4,14 +4,10 @@ General Public License, version 2.1 ("LGPL"): - All files in the include/ directory. - All files in the lib/ directory. - meson.build -- All files in the debian/ directory -- All files in the packaging/ directory -- Jenkinsfile -- fused.spec -- Makefile The full terms of the LGPL can be found in the LGPL2.txt file. + All other files may be used only under the terms of the GNU General Public License, version 2 ("GPL"). The full text of this license can be found in the GPL2.txt file. diff --git a/README.md b/README.md index eaf1308d5..156e49621 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ nevertheless want to adjust them, you can do so with the *meson configure* command: $ meson configure # list options - $ meson configure -D disable-mtab=true # set an optionq + $ meson configure -D disable-mtab=true # set an option $ # ensure all meson options are applied to the final build system $ meson setup --reconfigure ../ @@ -81,17 +81,19 @@ nevertheless want to adjust them, you can do so with the To build, test, and install libfuse, you then use Ninja: $ ninja - $ sudo python3 -m pytest test/ + $ sudo ../test/run-tests.py --build-dir . $ sudo ninja install -Running the tests requires the [py.test](http://www.pytest.org/) -Python module. Instead of running the tests as root, the majority of -tests can also be run as a regular user if *util/fusermount3* is made -setuid root first: +Running the tests requires bash, Python 3 and (to resolve core dumps) +gdb. Instead of running the tests as root, the majority of tests can +also be run as a regular user if *util/fusermount3* is made setuid root +first; the rest then skip themselves: - $ sudo chown root:root util/fusermount3 - $ sudo chmod 4755 util/fusermount3 - $ python3 -m pytest test/ + $ ../test/run-tests.py --build-dir . --setuid-helpers + +Each test gets its own working and log directory, and the runner prints +what every one of them cost. See the README under *test/cases* for how +to add one and where to look when one fails. Security implications --------------------- @@ -154,9 +156,10 @@ directory and at http://libfuse.github.io/doxygen. Getting Help ------------ -If you need help, please ask on the -mailing list (subscribe at -https://lists.sourceforge.net/lists/listinfo/fuse-devel). +If you need help, please ask on the +mailing list (subscribe by mailing +, archive at +https://lore.kernel.org/fuse-devel/). Please report any bugs on the GitHub issue tracker at https://github.com/libfuse/libfuse/issues. diff --git a/checkpatch.pl b/checkpatch.pl index 9eed3683a..ae480dfd9 100755 --- a/checkpatch.pl +++ b/checkpatch.pl @@ -113,7 +113,8 @@ sub help { --max-line-length=n set the maximum line length, (default $max_line_length) if exceeded, warn on patches requires --strict for use with --file - --min-conf-desc-length=n set the min description length, if shorter, warn + --min-conf-desc-length=n set the minimum description length for config symbols + in lines, if shorter, warn (default $min_conf_desc_length) --tab-size=n set the number of spaces for tab (default $tabsize) --root=PATH PATH to the kernel tree root --no-summary suppress the per-file summary @@ -150,6 +151,24 @@ sub help { exit($exitcode); } +my $DO_WHILE_0_ADVICE = q{ + do {} while (0) advice is over-stated in a few situations: + + The more obvious case is macros, like MODULE_PARM_DESC, invoked at + file-scope, where C disallows code (it must be in functions). See + $exceptions if you have one to add by name. + + More troublesome is declarative macros used at top of new scope, + like DECLARE_PER_CPU. These might just compile with a do-while-0 + wrapper, but would be incorrect. Most of these are handled by + detecting struct,union,etc declaration primitives in $exceptions. + + Theres also macros called inside an if (block), which "return" an + expression. These cannot do-while, and need a ({}) wrapper. + + Enjoy this qualification while we work to improve our heuristics. +}; + sub uniq { my %seen; return grep { !$seen{$_}++ } @_; @@ -520,7 +539,8 @@ sub hash_show_words { ____cacheline_aligned_in_smp| ____cacheline_internodealigned_in_smp| __weak| - __alloc_size\s*\(\s*\d+\s*(?:,\s*\d+\s*)?\) + __alloc_size\s*\(\s*\d+\s*(?:,\s*\d+\s*)?\)| + _Atomic }x; our $Modifier; our $Inline = qr{inline|__always_inline|noinline|__inline|__inline__}; @@ -666,6 +686,9 @@ sub hash_show_words { [\.\!:\s]* )}; +# Device ID types like found in include/linux/mod_devicetable.h. +our $dev_id_types = qr{\b[a-z]\w*_device_id\b}; + sub edit_distance_min { my (@arr) = @_; my $len = scalar @arr; @@ -834,20 +857,16 @@ sub find_standard_signature { $mode_perms_search = "(?:${mode_perms_search})"; our %deprecated_apis = ( - "synchronize_rcu_bh" => "synchronize_rcu", - "synchronize_rcu_bh_expedited" => "synchronize_rcu_expedited", - "call_rcu_bh" => "call_rcu", - "rcu_barrier_bh" => "rcu_barrier", - "synchronize_sched" => "synchronize_rcu", - "synchronize_sched_expedited" => "synchronize_rcu_expedited", - "call_rcu_sched" => "call_rcu", - "rcu_barrier_sched" => "rcu_barrier", - "get_state_synchronize_sched" => "get_state_synchronize_rcu", - "cond_synchronize_sched" => "cond_synchronize_rcu", "kmap" => "kmap_local_page", "kunmap" => "kunmap_local", "kmap_atomic" => "kmap_local_page", "kunmap_atomic" => "kunmap_local", + #These should be enough to drive away new IDR users + "DEFINE_IDR" => "DEFINE_XARRAY", + "idr_init" => "xa_init", + "idr_init_base" => "xa_init_flags", + "rcu_read_lock_trace" => "rcu_read_lock_tasks_trace", + "rcu_read_unlock_trace" => "rcu_read_unlock_tasks_trace", ); #Create a search pattern for all these strings to speed up a loop below @@ -1084,7 +1103,9 @@ sub build_types { (?:$Storage\s+)?(?:[A-Z_][A-Z0-9]*_){0,2}(?:DEFINE|DECLARE)(?:_[A-Z0-9]+){1,6}\s*\(| (?:$Storage\s+)?[HLP]?LIST_HEAD\s*\(| (?:SKCIPHER_REQUEST|SHASH_DESC|AHASH_REQUEST)_ON_STACK\s*\(| - (?:$Storage\s+)?(?:XA_STATE|XA_STATE_ORDER)\s*\( + (?:$Storage\s+)?(?:XA_STATE|XA_STATE_ORDER)\s*\(| + __cacheline_group_(?:begin|end)(?:_aligned)?\s*\(| + __dma_from_device_group_(?:begin|end)\s*\( )}; our %allow_repeated_words = ( @@ -2624,6 +2645,11 @@ sub exclude_global_initialisers { $realfile =~ m@/bpf/.*\.bpf\.c$@; } +sub is_userspace { + my ($realfile) = @_; + return ($realfile =~ m@^tools/@ || $realfile =~ m@^scripts/@); +} + sub process { my $filename = shift; @@ -2875,7 +2901,7 @@ sub process { if ($realfile =~ m@^include/asm/@) { ERROR("MODIFIED_INCLUDE_ASM", - "do not modify files in include/asm, change architecture specific files in include/asm-\n" . "$here$rawline\n"); + "do not modify files in include/asm, change architecture specific files in arch//include/asm\n" . "$here$rawline\n"); } $found_file = 1; } @@ -3010,6 +3036,16 @@ sub process { } } +# Check for invalid patch separator + if ($in_commit_log && + $line =~ /^---.+/) { + if (ERROR("BAD_COMMIT_SEPARATOR", + "Invalid commit separator - some tools may have problems applying this\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ s/-/=/g; + } + } + # Check for patch separator if ($line =~ /^---$/) { $has_patch_separator = 1; @@ -3230,19 +3266,19 @@ sub process { my $tag_case = not ($tag eq "Fixes:"); my $tag_space = not ($line =~ /^fixes:? [0-9a-f]{5,40} ($balanced_parens)/i); - my $id_length = not ($orig_commit =~ /^[0-9a-f]{12}$/i); + my $id_length = not ($orig_commit =~ /^[0-9a-f]{12,40}$/i); my $id_case = not ($orig_commit !~ /[A-F]/); my $id = "0123456789ab"; my ($cid, $ctitle) = git_commit_info($orig_commit, $id, $title); - if ($ctitle ne $title || $tag_case || $tag_space || - $id_length || $id_case || !$title_has_quotes) { + if (defined($cid) && ($ctitle ne $title || $tag_case || $tag_space || $id_length || $id_case || !$title_has_quotes)) { + my $fixed = "Fixes: $cid (\"$ctitle\")"; if (WARN("BAD_FIXES_TAG", - "Please use correct Fixes: style 'Fixes: <12 chars of sha1> (\"\")' - ie: 'Fixes: $cid (\"$ctitle\")'\n" . $herecurr) && + "Please use correct Fixes: style 'Fixes: <12+ chars of sha1> (\"<title line>\")' - ie: '$fixed'\n" . $herecurr) && $fix) { - $fixed[$fixlinenr] = "Fixes: $cid (\"$ctitle\")"; + $fixed[$fixlinenr] = $fixed; } } } @@ -3282,7 +3318,7 @@ sub process { # file delta changes $line =~ /^\s*(?:[\w\.\-\+]*\/)++[\w\.\-\+]+:/ || # filename then : - $line =~ /^\s*(?:Fixes:|$link_tags_search|$signature_tags)/i || + $line =~ /^\s*(?:Fixes:|https?:|$link_tags_search|$signature_tags)/i || # A Fixes:, link or signature tag line $commit_log_possible_stack_dump)) { WARN("COMMIT_LOG_LONG_LINE", @@ -3328,6 +3364,13 @@ sub process { } } +# Check for auto-generated unhandled placeholder text (mostly for cover letters) + if (($in_commit_log || $in_header_lines) && + $rawline =~ /(?:SUBJECT|BLURB) HERE/) { + ERROR("PLACEHOLDER_USE", + "Placeholder text detected\n" . $herecurr); + } + # Check for git id commit length and improperly formed commit descriptions # A correctly formed commit description is: # commit <SHA-1 hash length 12+ chars> ("Complete commit subject") @@ -3491,9 +3534,10 @@ sub process { # Check for various typo / spelling mistakes if (defined($misspellings) && ($in_commit_log || $line =~ /^(?:\+|Subject:)/i)) { - while ($rawline =~ /(?:^|[^\w\-'`])($misspellings)(?:[^\w\-'`]|$)/gi) { + my $rawline_utf8 = decode("utf8", $rawline); + while ($rawline_utf8 =~ /(?:^|[^\w\-'`])($misspellings)(?:[^\w\-'`]|$)/gi) { my $typo = $1; - my $blank = copy_spacing($rawline); + my $blank = copy_spacing($rawline_utf8); my $ptr = substr($blank, 0, $-[1]) . "^" x length($typo); my $hereptr = "$hereline$ptr\n"; my $typo_fix = $spelling_fix{lc($typo)}; @@ -3655,7 +3699,7 @@ sub process { $help_length < $min_conf_desc_length) { my $stat_real = get_stat_real($linenr, $ln - 1); WARN("CONFIG_DESCRIPTION", - "please write a help paragraph that fully describes the config symbol\n" . "$here\n$stat_real\n"); + "please write a help paragraph that fully describes the config symbol with at least $min_conf_desc_length lines\n" . "$here\n$stat_real\n"); } } @@ -3699,20 +3743,6 @@ sub process { } } - if (($realfile =~ /Makefile.*/ || $realfile =~ /Kbuild.*/) && - ($line =~ /\+(EXTRA_[A-Z]+FLAGS).*/)) { - my $flag = $1; - my $replacement = { - 'EXTRA_AFLAGS' => 'asflags-y', - 'EXTRA_CFLAGS' => 'ccflags-y', - 'EXTRA_CPPFLAGS' => 'cppflags-y', - 'EXTRA_LDFLAGS' => 'ldflags-y', - }; - - WARN("DEPRECATED_VARIABLE", - "Use of $flag is deprecated, please use \`$replacement->{$flag} instead.\n" . $herecurr) if ($replacement->{$flag}); - } - # check for DT compatible documentation if (defined $root && (($realfile =~ /\.dtsi?$/ && $line =~ /^\+\s*compatible\s*=\s*\"/) || @@ -3744,6 +3774,18 @@ sub process { } } +# Check for RGMII phy-mode with delay on PCB + if ($realfile =~ /\.(dts|dtsi|dtso)$/ && + $line =~ /^\+\s*(phy-mode|phy-connection-type)\s*=\s*"/ && + !ctx_has_comment($first_line, $linenr)) { + my $prop = $1; + my $mode = get_quoted_string($line, $rawline); + if ($mode =~ /^"rgmii(?:|-rxid|-txid)"$/) { + WARN("UNCOMMENTED_RGMII_MODE", + "$prop $mode without comment -- delays on the PCB should be described, otherwise use \"rgmii-id\"\n" . $herecurr); + } + } + # check for using SPDX license tag at beginning of files if ($realline == $checklicenseline) { if ($rawline =~ /^[ \+]\s*\#\!\s*\//) { @@ -4827,7 +4869,7 @@ sub process { } # do not use BUG() or variants - if ($line =~ /\b(?!AA_|BUILD_|DCCP_|IDA_|KVM_|RWLOCK_|snd_|SPIN_)(?:[a-zA-Z_]*_)?BUG(?:_ON)?(?:_[A-Z_]+)?\s*\(/) { + if ($line =~ /\b(?!AA_|BUILD_|IDA_|KVM_|RWLOCK_|snd_|SPIN_)(?:[a-zA-Z_]*_)?BUG(?:_ON)?(?:_[A-Z_]+)?\s*\(/) { my $msg_level = \&WARN; $msg_level = \&CHK if ($file); &{$msg_level}("AVOID_BUG", @@ -5513,9 +5555,9 @@ sub process { } } -# check for unnecessary parentheses around comparisons in if uses -# when !drivers/staging or command-line uses --strict - if (($realfile !~ m@^(?:drivers/staging/)@ || $check_orig) && +# check for unnecessary parentheses around comparisons +# except in drivers/staging + if (($realfile !~ m@^(?:drivers/staging/)@) && $perl_version_ok && defined($stat) && $stat =~ /(^.\s*if\s*($balanced_parens))/) { my $if_stat = $1; @@ -5843,6 +5885,8 @@ sub process { #CamelCase if ($var !~ /^$Constant$/ && $var =~ /[A-Z][a-z]|[a-z][A-Z]/ && +#Ignore C keywords + $var !~ /^_Generic$/ && #Ignore some autogenerated defines and enum values $var !~ /^(?:[A-Z]+_){1,5}[A-Z]{1,3}[a-z]/ && #Ignore Page<foo> variants @@ -5904,9 +5948,9 @@ sub process { } } -# multi-statement macros should be enclosed in a do while loop, grab the -# first statement and ensure its the whole macro if its not enclosed -# in a known good container +# Usually multi-statement macros should be enclosed in a do {} while +# (0) loop. Grab the first statement and ensure its the whole macro +# if its not enclosed in a known good container if ($realfile !~ m@/vmlinux.lds.h$@ && $line =~ /^.\s*\#\s*define\s*$Ident(\()?/) { my $ln = $linenr; @@ -5959,10 +6003,13 @@ sub process { my $exceptions = qr{ $Declare| + # named exceptions module_param_named| MODULE_PARM_DESC| DECLARE_PER_CPU| DEFINE_PER_CPU| + static_assert| + # declaration primitives __typeof__\(| union| struct| @@ -5997,11 +6044,11 @@ sub process { ERROR("MULTISTATEMENT_MACRO_USE_DO_WHILE", "Macros starting with if should be enclosed by a do - while loop to avoid possible if/else logic defects\n" . "$herectx"); } elsif ($dstat =~ /;/) { - ERROR("MULTISTATEMENT_MACRO_USE_DO_WHILE", - "Macros with multiple statements should be enclosed in a do - while loop\n" . "$herectx"); + WARN("MULTISTATEMENT_MACRO_USE_DO_WHILE", + "Non-declarative macros with multiple statements should be enclosed in a do - while loop\n" . "$herectx\nBUT SEE:\n$DO_WHILE_0_ADVICE"); } else { ERROR("COMPLEX_MACRO", - "Macros with complex values should be enclosed in parentheses\n" . "$herectx"); + "Macros with complex values should be enclosed in parentheses\n" . "$herectx\nBUT SEE:\n$DO_WHILE_0_ADVICE"); } } @@ -6045,7 +6092,7 @@ sub process { } # check if this is an unused argument - if ($define_stmt !~ /\b$arg\b/) { + if ($define_stmt !~ /\b$arg\b/ && $define_stmt) { WARN("MACRO_ARG_UNUSED", "Argument '$arg' is not used in function-like macro\n" . "$herectx"); } @@ -6701,6 +6748,13 @@ sub process { } } +# check for context_unsafe without a comment. + if ($line =~ /\bcontext_unsafe\b/ && + !ctx_has_comment($first_line, $linenr)) { + WARN("CONTEXT_UNSAFE", + "context_unsafe without comment\n" . $herecurr); + } + # check of hardware specific defines if ($line =~ m@^.\s*\#\s*if.*\b(__i386__|__powerpc64__|__sun__|__s390x__)\b@ && $realfile !~ m@include/asm-@) { CHK("ARCH_DEFINES", @@ -6912,7 +6966,7 @@ sub process { ($extension eq "f" && defined $qualifier && $qualifier !~ /^w/) || ($extension eq "4" && - defined $qualifier && $qualifier !~ /^cc/)) { + defined $qualifier && $qualifier !~ /^c(?:[hlbc]|hR)$/)) { $bad_specifier = $specifier; last; } @@ -7002,21 +7056,20 @@ sub process { # } # } # } - # strcpy uses that should likely be strscpy - if ($line =~ /\bstrcpy\s*\(/) { + if ($line =~ /\bstrcpy\s*\(/ && !is_userspace($realfile)) { WARN("STRCPY", "Prefer strscpy over strcpy - see: https://github.com/KSPP/linux/issues/88\n" . $herecurr); } # strlcpy uses that should likely be strscpy - if ($line =~ /\bstrlcpy\s*\(/) { + if ($line =~ /\bstrlcpy\s*\(/ && !is_userspace($realfile)) { WARN("STRLCPY", "Prefer strscpy over strlcpy - see: https://github.com/KSPP/linux/issues/89\n" . $herecurr); } # strncpy uses that should likely be strscpy or strscpy_pad - if ($line =~ /\bstrncpy\s*\(/) { + if ($line =~ /\bstrncpy\s*\(/ && !is_userspace($realfile)) { WARN("STRNCPY", "Prefer strscpy, strscpy_pad, or __nonstring over strncpy - see: https://github.com/KSPP/linux/issues/90\n" . $herecurr); } @@ -7227,17 +7280,42 @@ sub process { "Prefer $3(sizeof(*$1)...) over $3($4...)\n" . $herecurr); } -# check for (kv|k)[mz]alloc with multiplies that could be kmalloc_array/kvmalloc_array/kvcalloc/kcalloc +# check for (kv|k)[mz]alloc that could be kmalloc_obj/kvmalloc_obj/kzalloc_obj/kvzalloc_obj + if ($perl_version_ok && + defined $stat && + $stat =~ /^\+\s*($Lval)\s*\=\s*(?:$balanced_parens)?\s*((?:kv|k)[mz]alloc)\s*\(\s*($FuncArg)\s*,/) { + my $oldfunc = $3; + my $a1 = $4; + my $newfunc = "kmalloc_obj"; + $newfunc = "kvmalloc_obj" if ($oldfunc eq "kvmalloc"); + $newfunc = "kvzalloc_obj" if ($oldfunc eq "kvzalloc"); + $newfunc = "kzalloc_obj" if ($oldfunc eq "kzalloc"); + + if ($a1 =~ s/^sizeof\s*\S\(?([^\)]*)\)?$/$1/) { + my $cnt = statement_rawlines($stat); + my $herectx = get_stat_here($linenr, $cnt, $here); + + if (WARN("ALLOC_WITH_SIZEOF", + "Prefer $newfunc over $oldfunc with sizeof\n" . $herectx) && + $cnt == 1 && + $fix) { + $fixed[$fixlinenr] =~ s/\b($Lval)\s*\=\s*(?:$balanced_parens)?\s*((?:kv|k)[mz]alloc)\s*\(\s*($FuncArg)\s*,/$1 = $newfunc($a1,/; + } + } + } + + +# check for (kv|k)[mz]alloc with multiplies that could be kmalloc_objs/kvmalloc_objs/kzalloc_objs/kvzalloc_objs if ($perl_version_ok && defined $stat && $stat =~ /^\+\s*($Lval)\s*\=\s*(?:$balanced_parens)?\s*((?:kv|k)[mz]alloc)\s*\(\s*($FuncArg)\s*\*\s*($FuncArg)\s*,/) { my $oldfunc = $3; my $a1 = $4; my $a2 = $10; - my $newfunc = "kmalloc_array"; - $newfunc = "kvmalloc_array" if ($oldfunc eq "kvmalloc"); - $newfunc = "kvcalloc" if ($oldfunc eq "kvzalloc"); - $newfunc = "kcalloc" if ($oldfunc eq "kzalloc"); + my $newfunc = "kmalloc_objs"; + $newfunc = "kvmalloc_objs" if ($oldfunc eq "kvmalloc"); + $newfunc = "kvzalloc_objs" if ($oldfunc eq "kvzalloc"); + $newfunc = "kzalloc_objs" if ($oldfunc eq "kzalloc"); my $r1 = $a1; my $r2 = $a2; if ($a1 =~ /^sizeof\s*\S/) { @@ -7253,7 +7331,9 @@ sub process { "Prefer $newfunc over $oldfunc with multiply\n" . $herectx) && $cnt == 1 && $fix) { - $fixed[$fixlinenr] =~ s/\b($Lval)\s*\=\s*(?:$balanced_parens)?\s*((?:kv|k)[mz]alloc)\s*\(\s*($FuncArg)\s*\*\s*($FuncArg)/$1 . ' = ' . "$newfunc(" . trim($r1) . ', ' . trim($r2)/e; + my $sized = trim($r2); + $sized =~ s/^sizeof\s*\S\(?([^\)]*)\)?$/$1/; + $fixed[$fixlinenr] =~ s/\b($Lval)\s*\=\s*(?:$balanced_parens)?\s*((?:kv|k)[mz]alloc)\s*\(\s*($FuncArg)\s*\*\s*($FuncArg)/$1 . ' = ' . "$newfunc(" . $sized . ', ' . trim($r1)/e; } } } @@ -7676,6 +7756,37 @@ sub process { WARN("DUPLICATED_SYSCTL_CONST", "duplicated sysctl range checking value '$1', consider using the shared one in include/linux/sysctl.h\n" . $herecurr); } + +# Check that *_device_id tables have sentinel entries. + if (defined $stat && $line =~ /struct\s+$dev_id_types\s+\w+\s*\[\s*\]\s*=\s*\{/) { + my $stripped = $stat; + + # Strip diff line prefixes. + $stripped =~ s/(^|\n)./$1/g; + # Line continuations. + $stripped =~ s/\\\n/\n/g; + # Strip whitespace, empty strings, zeroes, and commas. + $stripped =~ s/""//g; + $stripped =~ s/0x0//g; + $stripped =~ s/[\s$;,0]//g; + # Strip field assignments. + $stripped =~ s/\.$Ident=//g; + + if (!(substr($stripped, -4) eq "{}};" || + substr($stripped, -6) eq "{{}}};" || + $stripped =~ /ISAPNP_DEVICE_SINGLE_END}};$/ || + $stripped =~ /ISAPNP_CARD_END}};$/ || + $stripped =~ /NULL};$/ || + $stripped =~ /PCMCIA_DEVICE_NULL};$/)) { + ERROR("MISSING_SENTINEL", "missing sentinel in ID array\n" . "$here\n$stat\n"); + } + } + +# check for uninitialized pointers with __free attribute + while ($line =~ /\*\s*($Ident)\s+__free\s*\(\s*$Ident\s*\)\s*[,;]/g) { + ERROR("UNINITIALIZED_PTR_WITH_FREE", + "pointer '$1' with __free attribute should be initialized\n" . $herecurr); + } } # If we have no input at all, then there is nothing to report on diff --git a/dev-docs/release-process.md b/dev-docs/release-process.md index fe4c225d1..d96c87306 100644 --- a/dev-docs/release-process.md +++ b/dev-docs/release-process.md @@ -19,7 +19,7 @@ Release Process * `md build && (cd build && meson .. && ninja)` * `sudo sudo chown root:root build/util/fusermount3` * `sudo chmod 4755 build/util/fusermount3` - * `(cd build; python3 -m pytest test/)` + * `(cd build; ../test/run-tests.py --build-dir .)` * Upload API docs: * `rm -r ../libfuse.github.io/doxygen && cp -a doc/html ../libfuse.github.io/doxygen` * `git -C ../libfuse.github.io add doxygen/` @@ -33,7 +33,7 @@ Release Process Announcement email template ``` -To: fuse-devel@lists.sourceforge.net +To: fuse-devel@lists.linux.dev Subject: [ANNOUNCE] libfuse XXXX has been released Dear all, diff --git a/doc/README.custom-io b/doc/README.custom-io new file mode 100644 index 000000000..7b82a21b5 --- /dev/null +++ b/doc/README.custom-io @@ -0,0 +1,187 @@ +FUSE over a Custom Transport +============================ + +This document describes fuse_session_custom_io(), which lets a session speak +the FUSE protocol over a file descriptor the caller provides instead of over +/dev/fuse, and the validation such a caller has to do. + +The example that ships with libfuse, example/hello_ll_uds.c, uses an AF_UNIX +socket, which is where the shorthand "UDS" comes from. The API itself does not +care what the descriptor is: a socket, a pipe pair, anything a read() and a +writev() work on. + + +Disabled by Default +------------------- + +fuse_session_custom_io() returns -ENOTSUP and logs a message unless libfuse +was built with: + + meson setup build -Denable-custom-io=true + +The hello_ll_uds example is built only with that option, and the test that +drives it skips without it. The reason is the trust model described below, +not the transport itself. + +The symbols remain exported in every build, so an application that calls +fuse_session_custom_io() keeps linking and loading; it is refused at run time. + + +How It Works +------------ + +A normal session gets its descriptor from the kernel and the kernel is the +peer at the other end: + + fuse_session_new() + | + v + fuse_session_mount() --> open /dev/fuse, mount() + | + v + fuse_session_loop() --> read()/writev() on the /dev/fuse fd + peer: the kernel FUSE module + +A custom io session never mounts anything. The caller connects the descriptor +itself and hands it, plus the io callbacks, to the session: + + fuse_session_new() + | + v + caller creates fd --> socket(), connect()/accept(), ... + | + v + fuse_session_custom_io(se, &io, sizeof(io), fd) + | + v + fuse_session_loop() --> io->read()/io->writev() on that fd + peer: whatever is at the other end + +From FUSE_INIT onwards the protocol is identical. The peer sends requests in +the same wire format the kernel would, and the session replies the same way. + + +The io Callbacks +---------------- + +struct fuse_custom_io has five members. read and writev are mandatory: +fuse_session_custom_io() returns -EINVAL without them, because libfuse cannot +fall back to plain read()/writev() on a descriptor it does not own. + +1. **read(fd, buf, buf_len, userdata)**: must deliver exactly one request per + call. buf_len is the session buffer size. A return value smaller than + sizeof(struct fuse_in_header) ends the session loop with -EIO and the + message "short read on fuse device", which is also what a peer closing the + connection looks like. + +2. **writev(fd, iov, count, userdata)**: must write every iovec. + +3. **splice_send** and **splice_receive**: optional. Without them the session + does not offer FUSE_CAP_SPLICE_WRITE, FUSE_CAP_SPLICE_MOVE and + FUSE_CAP_SPLICE_READ, so replies and requests are copied instead. + +4. **clone_fd**: optional, called by the multi-threaded loop to get a + descriptor per worker. Without it every worker shares the one descriptor. + +The descriptor is closed by fuse_session_destroy(). + + +What a Custom io Session Does Not Have +-------------------------------------- + +1. **No mountpoint.** fuse_session_mount() and fusermount3 are not involved, + so none of the mount options, the setuid helper or /etc/mtab bookkeeping + apply. + +2. **No kernel.** FUSE_INIT arrives from the peer, so protocol version and + feature negotiation are whatever the peer asks for. + +3. **No io_uring.** fuse_session_custom_io() clears se->uring.enable: the ring + is issued against a /dev/fuse descriptor and would bypass the io callbacks + entirely. A session reports this on its own stderr as + "FUSE_INIT: io_uring=off:custom_io". + +4. **No trustworthy credentials.** The uid, gid and pid that fuse_req_ctx() + returns are the ones the peer wrote into fuse_in_header. The allow_root + option compares that same uid against the session owner, so it states what + the peer claims, not who the peer is. + + +The Trust Model: Unverified Opcodes +----------------------------------- + +libfuse was written against one producer, the Linux kernel, which does not +need to validate the requests it sends because it builds them itself. A +kernel-built request always has: + +- a fuse_in_header whose len matches the bytes written, +- the per-opcode header the opcode calls for, +- name arguments NUL terminated inside the message, +- count and size fields describing data that is actually present. + +libfuse repeats almost none of these checks. What it does check: + +- FUSE_INIT and CUSE_INIT: enough bytes for the major/minor fields, at + dispatch time, otherwise EPROTO. +- FUSE_WRITE handled by op.write_buf: fuse_write_in.size against the bytes + received, otherwise EIO. +- FUSE_NOTIFY_REPLY: the same check. +- The extension block: FUSE_EXT_SIZE(total_extlen) against the buffer length. + +Every other opcode is taken at face value, in three classes: + +1. **Entry counts.** fuse_batch_forget_in.count bounds the loop over the + fuse_forget_one array that follows it. Nothing compares it to the bytes + that arrived, so an inflated count reads count * 16 bytes past the array + and hands them to op.forget or op.forget_multi as inode numbers and + lookup counts. + +2. **Payload lengths.** FUSE_SETXATTR passes fuse_setxattr_in.size to + op.setxattr as the length of the value, without checking that many bytes + were received. + +3. **Strings.** Name arguments are handed to the filesystem as a const char * + and are expected to be terminated. FUSE_SYMLINK, FUSE_RENAME, FUSE_RENAME2 + and FUSE_SETXATTR locate their second argument at name + strlen(name) + 1. + A name without a NUL runs off the end of the receive buffer. + +A malformed request therefore reads memory outside the receive buffer. The +usual outcome is a crash of the filesystem process; the batch forget case also +feeds the out-of-bounds words into the filesystem's inode bookkeeping. + +**A custom io peer owes the session the validation the kernel would have made +unnecessary.** The read callback is the place for it, because that is the last +point before libfuse parses the bytes. At minimum, per message: + +- reject a fuse_in_header whose len is below sizeof(struct fuse_in_header) or + above the buffer size, and deliver exactly len bytes; +- reject an opcode whose per-opcode header does not fit in what arrived; +- check every count and size field against the remaining bytes; +- check that every name argument is NUL terminated within the message. + +A peer that forwards requests it did not build, or that accepts connections +from anything but itself, has to treat this as the security boundary it is. + + +Example +------- + +example/hello_ll_uds.c serves the protocol on an AF_UNIX socket: + + se = fuse_session_new(&args, &hello_ll_oper, sizeof(hello_ll_oper), NULL); + cfd = create_socket(socket_path); /* socket, bind, listen, accept */ + fuse_session_custom_io(se, &io, sizeof(io), cfd); + fuse_session_loop(se); + +Its stream_read() reads the header, takes the length from it and reads the +rest of the message, which is the framing the read callback contract asks for +but none of the validation the section above asks for. It is an example of the +API, not a model for a peer that is not trusted. + + +See Also +-------- + +- doc/README.fuse-io-uring - the other non-default transport +- doc/README.mount - what a /dev/fuse session does instead +- include/fuse_lowlevel.h - fuse_session_custom_io() and struct fuse_custom_io diff --git a/doc/README.fuse-io-uring b/doc/README.fuse-io-uring new file mode 100644 index 000000000..62c71bf75 --- /dev/null +++ b/doc/README.fuse-io-uring @@ -0,0 +1,38 @@ +fuse-over-io-uring uses io-uring for transport of kernel/userspace +messages. See also https://docs.kernel.org/filesystems/fuse/fuse-io-uring.html + +In order to enable it, the kernel module needs to have it enabled: +echo 1 > /sys/module/fuse/parameters/enable_uring + +Additionally, FUSE_CAP_OVER_IO_URING needs to be set and +se->uring.enable has to be true. The latter can be +achieved with the libfuse option '-o io_uring'. + +Default queue-depth is 8 and can be changed with the parameter +'-oio_uring_q_depth'. + +As of now there is always one queue per core. A reduced number +of cores in development. + +Benefits: +- Improved performance by using io_uring for kernel/userspace communication +- Reduced system call overhead compared to traditional FUSE +- Asynchronous I/O operations + +Usage: +To enable io_uring support when mounting a FUSE filesystem: +1. Enable kernel support: echo 1 > /sys/module/fuse/parameters/enable_uring +2. Mount with io_uring option: -o io_uring +3. Optionally adjust queue depth: -o io_uring_q_depth=<depth> + +Example: +./my_fuse_fs /source /mountpoint -o io_uring -o io_uring_q_depth=16 + +Requirements: +- Linux kernel with io_uring and FUSE io_uring support enabled +- libfuse compiled with io_uring support + +Build Dependencies: +- liburing (for io_uring support) +- libnuma (required alongside liburing) +- meson build system with option: -Denable-io-uring=true diff --git a/doc/README.fuse_reply_errors b/doc/README.fuse_reply_errors new file mode 100644 index 000000000..d60fd7a29 --- /dev/null +++ b/doc/README.fuse_reply_errors @@ -0,0 +1,5 @@ +Under normal operation, a call to any fuse_reply_* function should not result +in an error. + +Should the kernel abort the fuse connection, a fuse_reply_* call can return +-ENOENT. diff --git a/doc/README.notifications b/doc/README.notifications new file mode 100644 index 000000000..2ca6204b1 --- /dev/null +++ b/doc/README.notifications @@ -0,0 +1,37 @@ +During the life-cycle of a user-space filesystem the usual flow is: + + 1. User-space application does a filesystem-related syscall + 2. Kernel VFS calls into the FUSE kernel driver + 3. FUSE kernel redirects request to the user-space filesystem + 4. User-space server replies to request + 5. FUSE returns reply to VFS + 6. User-space application gets reply from the kernel + +However, there are occasions where the filesystem needs to send notifications to +the kernel that are not in reply to any particular request. If, for example, +when a READ request of 4096 bytes results in the filesystem having more data +available for the specific inode, it may be useful to provide this extra data to +the kernel cache so that future read operations will be faster. + +FUSE provides mechanisms for a user-space server to send the kernel certain +types of asynchronous notifications. Currently, these are the available +notifications: + +|-------------+----------------------------------| +| Operation | libfuse function | +|-------------+----------------------------------| +| POLL | fuse_lowlevel_notify_poll | +| INVAL_INODE | fuse_lowlevel_notify_inval_inode | +| ENTRY | fuse_lowlevel_notify_inval_entry | +| STORE | fuse_lowlevel_notify_store | +| RETRIEVE | fuse_lowlevel_notify_retrieve | +| DELETE | fuse_lowlevel_notify_delete | +| RESEND | - | +|-------------+----------------------------------| + +One important restriction is that these asynchronous operations SHALL NOT be +performed while executing other FUSE requests. Doing so will likely result in +deadlocking the user-space filesystem server. In the example above, if the +server is replying to a READ request and has extra data to add to the kernel +cache, it needs to reply to the READ request first, and, e.g., signal a +different thread to do the STORE. diff --git a/doc/fusermount3.1 b/doc/fusermount3.1 index 1455742b4..d45f061be 100644 --- a/doc/fusermount3.1 +++ b/doc/fusermount3.1 @@ -7,7 +7,7 @@ \fBfusermount3\fR [\fIOPTIONS\fR] \fIMOUNTPOINT\fR .SH DESCRIPTION -Filesystem in Userspace (FUSE) is a simple interface for userspace programs to export a virtual filesystem to the Linux kernel. It also aims to provide a secure method for non privileged users to create and mount their own filesystem implementations. +Filesystem in Userspace (FUSE) is a simple interface for userspace programs to export a virtual filesystem to the Linux kernel. It provides a secure method for non privileged users to create and mount their own filesystem implementations. .PP \fBfusermount3\fR is a program to mount and unmount FUSE filesystems. It should be called directly only for unmounting FUSE @@ -33,7 +33,7 @@ lazy unmount. \fIfuse\fR(4), .SH HOMEPAGE -More information about fusermount3 and the FUSE project can be found at <\fIhttp://fuse.sourceforge.net/\fR>. +More information about fusermount3 and the FUSE project can be found at <\fIhttps://github.com/libfuse/\fR>. .SH AUTHORS .LP diff --git a/doc/libfuse-operations.txt b/doc/libfuse-operations.txt index a56f89be5..457ff2cdc 100644 --- a/doc/libfuse-operations.txt +++ b/doc/libfuse-operations.txt @@ -1,5 +1,5 @@ List of libfuse operations with their in/out arguments, created with -help of chatgpt. As of kernel 6.9 (protocol 7.40). The list +help of chatgpt. As of kernel 6.18 (protocol 7.45). The list was only partly human verified - use with care. 1. FUSE_LOOKUP (1) @@ -125,7 +125,7 @@ was only partly human verified - use with care. - out_args[2]: Not used 17. FUSE_STATFS (17) - - in_args[0]: Size of fuse_statfs_in (16 bytes) + - in_args[0]: Not used - in_args[1]: Not used - in_args[2]: Not used - out_args[0]: Size of fuse_statfs_out (typically 96 bytes) @@ -368,7 +368,7 @@ was only partly human verified - use with care. - out_args[2]: Not used 47. FUSE_COPY_FILE_RANGE (47) - - in_args[0]: Size of fuse_copy_file_range_in (48 bytes) + - in_args[0]: Size of fuse_copy_file_range_in (56 bytes) - in_args[1]: Not used - in_args[2]: Not used - out_args[0]: Size of fuse_write_out (24 bytes) @@ -416,3 +416,11 @@ was only partly human verified - use with care. - out_args[0]: Size of fuse_statx_out (typically 256 bytes) - out_args[1]: Not used - out_args[2]: Not used + +53. FUSE_COPY_FILE_RANGE_64 (53) + - in_args[0]: Size of fuse_copy_file_range_in (56 bytes) + - in_args[1]: Not used + - in_args[2]: Not used + - out_args[0]: Size of fuse_copy_file_range_out (8 bytes) + - out_args[1]: Not used + - out_args[2]: Not used diff --git a/doc/mount.fuse3.8 b/doc/mount.fuse3.8 index 32862fcce..4380c84b7 100644 --- a/doc/mount.fuse3.8 +++ b/doc/mount.fuse3.8 @@ -2,7 +2,7 @@ .SH NAME fuse \- configuration and mount options for FUSE file systems .SH DESCRIPTION -FUSE (Filesystem in Userspace) is a simple interface for userspace programs to export a virtual filesystem to the Linux kernel. FUSE also aims to provide a secure method for non privileged users to create and mount their own filesystem implementations. +FUSE (Filesystem in Userspace) is a simple interface for userspace programs to export a virtual filesystem to the Linux kernel. FUSE also provides a secure method for non privileged users to create and mount their own filesystem implementations. .SH DEFINITIONS .TP \fBFUSE\fP diff --git a/example/README.compile b/example/README.compile new file mode 100644 index 000000000..a7b681982 --- /dev/null +++ b/example/README.compile @@ -0,0 +1,4 @@ +Note: + * If the pkg-config command fails due to the absence of the fuse3.pc file, + you should configure the path to the fuse3.pc file in the PKG_CONFIG_PATH + variable. \ No newline at end of file diff --git a/example/cuse.c b/example/cuse.c index 330124e87..92b6b09a5 100644 --- a/example/cuse.c +++ b/example/cuse.c @@ -4,7 +4,7 @@ Copyright (C) 2008-2009 Tejun Heo <tj@kernel.org> This program can be distributed under the terms of the GNU GPLv2. - See the file COPYING. + See the file GPL2.txt. */ @@ -24,7 +24,7 @@ * * To compile this example, run * - * gcc -Wall cuse.c `pkg-config fused --cflags --libs` -o cuse + * gcc -Wall cuse.c `pkg-config fuse3 --cflags --libs` -o cuse * * ## Source code ## * \include cuse.c diff --git a/example/cuse_client.c b/example/cuse_client.c index 903ffc63f..50f70f3bf 100644 --- a/example/cuse_client.c +++ b/example/cuse_client.c @@ -4,7 +4,7 @@ Copyright (C) 2008 Tejun Heo <teheo@suse.de> This program can be distributed under the terms of the GNU GPLv2. - See the file COPYING. + See the file GPL2.txt. */ /** @file @@ -48,6 +48,9 @@ #include <unistd.h> #include "ioctl.h" +/* keeps a mistyped SIZE from asking for the machine's memory */ +#define MAX_XFER_SIZE (16 * 1024 * 1024) + const char *usage = "Usage: cuse_client FIOC_FILE COMMAND\n" "\n" @@ -63,6 +66,12 @@ static int do_rw(int fd, int is_read, size_t size, off_t offset, struct fioc_rw_arg arg = { .offset = offset }; ssize_t ret; + if (size > MAX_XFER_SIZE) { + fprintf(stderr, "size %zu exceeds limit of %d bytes\n", + size, MAX_XFER_SIZE); + return -1; + } + arg.buf = calloc(1, size); if (!arg.buf) { fprintf(stderr, "failed to allocated %zu bytes\n", size); diff --git a/example/hello.c b/example/hello.c index b18563d25..6288162d8 100644 --- a/example/hello.c +++ b/example/hello.c @@ -3,7 +3,7 @@ Copyright (C) 2001-2007 Miklos Szeredi <miklos@szeredi.hu> This program can be distributed under the terms of the GNU GPLv2. - See the file COPYING. + See the file GPL2.txt. */ /** @file @@ -12,7 +12,7 @@ * * Compile with: * - * gcc -Wall hello.c `pkg-config fused --cflags --libs` -o hello + * gcc -Wall hello.c `pkg-config fuse3 --cflags --libs` -o hello * * ## Source code ## * \include hello.c diff --git a/example/hello_ll.c b/example/hello_ll.c index 16fb5e517..bd5fecdcd 100644 --- a/example/hello_ll.c +++ b/example/hello_ll.c @@ -3,7 +3,7 @@ Copyright (C) 2001-2007 Miklos Szeredi <miklos@szeredi.hu> This program can be distributed under the terms of the GNU GPLv2. - See the file COPYING. + See the file GPL2.txt. */ /** @file @@ -12,7 +12,11 @@ * * Compile with: * - * gcc -Wall hello_ll.c `pkg-config fused --cflags --libs` -o hello_ll + * gcc -Wall hello_ll.c `pkg-config fuse3 --cflags --libs` -o hello_ll + * + * Note: If the pkg-config command fails due to the absence of the fuse3.pc + * file, you should configure the path to the fuse3.pc file in the + * PKG_CONFIG_PATH variable. * * ## Source code ## * \include hello_ll.c diff --git a/example/hello_ll_uds.c b/example/hello_ll_uds.c index 80471ab09..aa906b71a 100644 --- a/example/hello_ll_uds.c +++ b/example/hello_ll_uds.c @@ -4,7 +4,7 @@ Copyright (C) 2022 Tofik Sonono <tofik.sonono@intel.com> This program can be distributed under the terms of the GNU GPLv2. - See the file COPYING. + See the file GPL2.txt. */ /** @file @@ -14,7 +14,7 @@ * * Compile with: * - * gcc -Wall hello_ll_uds.c `pkg-config fused --cflags --libs` -o hello_ll_uds + * gcc -Wall hello_ll_uds.c `pkg-config fuse3 --cflags --libs` -o hello_ll_uds * * ## Source code ## * \include hello_ll.c @@ -42,6 +42,19 @@ static const char *hello_str = "Hello World!\n"; static const char *hello_name = "hello"; +#define DEFAULT_SOCKET_PATH "/tmp/libfuse-hello-ll.sock" + +static struct options { + const char *socket_path; +} options; + +#define OPTION(t, p) \ + { t, offsetof(struct options, p), 1 } +static const struct fuse_opt option_spec[] = { + OPTION("--socket=%s", socket_path), + FUSE_OPT_END +}; + static int hello_stat(fuse_ino_t ino, struct stat *stbuf) { stbuf->st_ino = ino; @@ -306,7 +319,9 @@ static void fuse_cmdline_help_uds(void) { printf(" -h --help print help\n" " -V --version print version\n" - " -d -o debug enable debug output (implies -f)\n"); + " -d -o debug enable debug output (implies -f)\n" + " --socket=PATH serve on PATH\n" + " (default: " DEFAULT_SOCKET_PATH ")\n"); } int main(int argc, char *argv[]) @@ -323,6 +338,13 @@ int main(int argc, char *argv[]) int cfd = -1; int ret = -1; + /* strdup: fuse_opt_parse frees the old value if --socket is given */ + options.socket_path = strdup(DEFAULT_SOCKET_PATH); + if (options.socket_path == NULL) + return 1; + + if (fuse_opt_parse(&args, &options, option_spec, NULL) == -1) + return 1; if (fuse_parse_cmdline(&args, &opts) != 0) return 1; if (opts.show_help) { @@ -346,7 +368,7 @@ int main(int argc, char *argv[]) if (fuse_set_signal_handlers(se) != 0) goto err_out2; - cfd = create_socket("/tmp/libfuse-hello-ll.sock"); + cfd = create_socket(options.socket_path); if (cfd == -1) goto err_out3; diff --git a/example/invalidate_path.c b/example/invalidate_path.c index 9d22d2e4b..3e2d24509 100644 --- a/example/invalidate_path.c +++ b/example/invalidate_path.c @@ -4,7 +4,7 @@ (C) 2017 EditShare LLC <slawek.rudnicki@editshare.com> This program can be distributed under the terms of the GNU GPLv2. - See the file COPYING. + See the file GPL2.txt. */ /** @file @@ -19,7 +19,7 @@ * * ## Compilation ## * - * gcc -Wall invalidate_path.c `pkg-config fused --cflags --libs` -o invalidate_path + * gcc -Wall invalidate_path.c `pkg-config fuse3 --cflags --libs` -o invalidate_path * * ## Source code ## * \include invalidate_path.c @@ -163,10 +163,11 @@ static const struct fuse_operations xmp_oper = { static void update_fs(void) { static int count = 0; + struct tm tmbuf; struct tm *now; time_t t; t = time(NULL); - now = localtime(&t); + now = localtime_r(&t, &tmbuf); assert(now != NULL); int time_file_size = strftime(time_file_contents, MAX_STR_LEN, diff --git a/example/ioctl.c b/example/ioctl.c index 3b915f959..2548fd3f0 100644 --- a/example/ioctl.c +++ b/example/ioctl.c @@ -4,7 +4,7 @@ Copyright (C) 2008 Tejun Heo <teheo@suse.de> This program can be distributed under the terms of the GNU GPLv2. - See the file COPYING. + See the file GPL2.txt. */ /** @file @@ -16,7 +16,7 @@ * * Compile with: * - * gcc -Wall ioctl.c `pkg-config fused --cflags --libs` -o ioctl + * gcc -Wall ioctl.c `pkg-config fuse3 --cflags --libs` -o ioctl * * ## Source code ## * \include ioctl.c diff --git a/example/ioctl.h b/example/ioctl.h index a4f054cc3..ca7e97541 100644 --- a/example/ioctl.h +++ b/example/ioctl.h @@ -4,7 +4,7 @@ Copyright (C) 2008 Tejun Heo <teheo@suse.de> This program can be distributed under the terms of the GNU GPLv2. - See the file COPYING. + See the file GPL2.txt. */ /** @file diff --git a/example/ioctl_client.c b/example/ioctl_client.c index 019d03031..99e27505e 100644 --- a/example/ioctl_client.c +++ b/example/ioctl_client.c @@ -3,10 +3,8 @@ Copyright (C) 2008 SUSE Linux Products GmbH Copyright (C) 2008 Tejun Heo <teheo@suse.de> - This program tests the ioctl.c example file systsem. - This program can be distributed under the terms of the GNU GPLv2. - See the file COPYING. + See the file GPL2.txt. */ /** @file diff --git a/example/memfs_ll.cc b/example/memfs_ll.cc index e5877565b..e0e428352 100644 --- a/example/memfs_ll.cc +++ b/example/memfs_ll.cc @@ -3,11 +3,10 @@ Copyright (C) 2024 DataDirect Networks. This program can be distributed under the terms of the GNU GPLv2. - See the file COPYING. + See the file GPL2.txt. */ -#include <linux/limits.h> -#define FUSE_USE_VERSION 317 +#define FUSE_USE_VERSION FUSE_MAKE_VERSION(3, 18) #include <algorithm> #include <stdio.h> @@ -28,6 +27,9 @@ #include <string_view> #include <cstdint> #include <fuse_lowlevel.h> +#ifdef HAVE_LINUX_LIMITS_H +#include <linux/limits.h> +#endif #define MEMFS_ATTR_TIMEOUT 0.0 #define MEMFS_ENTRY_TIMEOUT 0.0 @@ -41,11 +43,11 @@ class Dentry; static void memfs_panic(std::string_view message); struct DirHandle { - std::vector<std::pair<std::string, const Dentry *> > entries; + std::vector<std::pair<std::string, std::shared_ptr<Inode> > > entries; size_t offset; - DirHandle(const std::vector<std::pair<std::string, const Dentry *> > - &entries) + DirHandle(const std::vector< + std::pair<std::string, std::shared_ptr<Inode> > > &entries) : entries(entries) , offset(0) { @@ -136,10 +138,12 @@ class Inode { } time_t get_mtime() const { + std::lock_guard<std::mutex> lock(mutex); return mtime; } mode_t get_mode() const { + std::lock_guard<std::mutex> lock(attr_mutex); return mode; } @@ -191,13 +195,13 @@ class Inode { void set_mtime(const struct timespec &_mtime) { - std::lock_guard<std::mutex> lock(attr_mutex); + std::lock_guard<std::mutex> lock(mutex); mtime = _mtime.tv_sec; } void truncate(off_t size) { - std::lock_guard<std::mutex> attr_lock(attr_mutex); + std::lock_guard<std::mutex> attr_lock(mutex); if (size < content.size()) { content.resize(size); } else if (size > content.size()) { @@ -208,7 +212,9 @@ class Inode { void get_attr(struct stat *stbuf) const { - std::lock_guard<std::mutex> lock(attr_mutex); + // consistent cross-domain snapshot: content lock then attr lock + std::lock_guard<std::mutex> lock(mutex); + std::lock_guard<std::mutex> attr_lock(attr_mutex); stbuf->st_ino = ino; stbuf->st_mode = mode; stbuf->st_nlink = nlink; @@ -223,6 +229,7 @@ class Inode { bool is_empty() const { + std::lock_guard<std::mutex> lock(mutex); return dentries.empty(); } @@ -241,13 +248,18 @@ class Inode { return old_value - 1; } + nlink_t get_nlink() const + { + return nlink.load(std::memory_order_relaxed); + } + /** * Methods that need Dentry knowledge */ int add_child_locked(const std::string &name, Dentry *child_dentry); int add_child(const std::string &name, Dentry *child_dentry); int remove_child(const std::string &name); - std::vector<std::pair<std::string, const Dentry *> > + std::vector<std::pair<std::string, std::shared_ptr<Inode> > > get_children() const; Dentry *find_child_locked(const std::string &name) const; Dentry *find_child(const std::string &name) const; @@ -256,11 +268,11 @@ class Inode { class Dentry { public: std::string name; - Inode *inode; + std::shared_ptr<Inode> inode; - Dentry(const std::string &n, Inode *i) + Dentry(const std::string &n, std::shared_ptr<Inode> i) : name(n) - , inode(i) + , inode(std::move(i)) { } @@ -296,7 +308,7 @@ class Dentry { Inode *get_inode() const { - return inode; + return inode.get(); } void inc_lookup() @@ -307,15 +319,14 @@ class Dentry { class Inodes { private: - std::unordered_map<uint64_t, std::unique_ptr<Inode> > inodes; + std::unordered_map<uint64_t, std::shared_ptr<Inode> > inodes; mutable std::shared_mutex inodes_mutex; std::atomic<uint64_t> next_ino{ FUSE_ROOT_ID + 1 }; - std::mutex mutex; public: Inodes() { - auto root = std::make_unique<Inode>(FUSE_ROOT_ID, "/", true); + auto root = std::make_shared<Inode>(FUSE_ROOT_ID, "/", true); root->mode = S_IFDIR | 0755; root->nlink = 2; // . and .. inodes[FUSE_ROOT_ID] = std::move(root); @@ -346,27 +357,28 @@ class Inodes { erase_locked(inode); } - Inode *find_locked(fuse_ino_t ino) + std::shared_ptr<Inode> find_locked(fuse_ino_t ino) { auto it = inodes.find(ino); if (it == inodes.end()) { return nullptr; } - return it->second.get(); + return it->second; } - Inode *find(fuse_ino_t ino) + std::shared_ptr<Inode> find(fuse_ino_t ino) { std::shared_lock lock(inodes_mutex); return find_locked(ino); } - Inode *create(const std::string &name, bool is_dir, mode_t mode) + std::shared_ptr<Inode> create(const std::string &name, bool is_dir, + mode_t mode) { std::unique_lock<std::shared_mutex> lock(inodes_mutex); uint64_t ino = next_ino.fetch_add(1, std::memory_order_relaxed); - auto new_inode = std::make_unique<Inode>(ino, name, is_dir); + auto new_inode = std::make_shared<Inode>(ino, name, is_dir); new_inode->set_mode(mode); auto [it, inserted] = inodes.emplace(ino, std::move(new_inode)); @@ -376,12 +388,12 @@ class Inodes { return nullptr; } - return it->second.get(); + return it->second; } - size_t size() + size_t size() const { - std::lock_guard<std::mutex> lock(mutex); + std::shared_lock<std::shared_mutex> lock(inodes_mutex); return inodes.size(); } }; @@ -460,19 +472,19 @@ Dentry *Inode::find_child(const std::string &name) const return find_child_locked(name); } -std::vector<std::pair<std::string, const Dentry *> > Inode::get_children() const +std::vector<std::pair<std::string, std::shared_ptr<Inode> > > +Inode::get_children() const { + std::lock_guard<std::mutex> lock(mutex); if (!is_dir_) { return {}; // Return an empty vector if this is not a directory } - std::vector<std::pair<std::string, const Dentry *> > children; + std::vector<std::pair<std::string, std::shared_ptr<Inode> > > children; children.reserve(dentries.size()); - for (size_t i = 0; i < dentries.size(); ++i) { - const Dentry *dentry = dentries[i]; - std::string name = dentry->get_name(); - children.emplace_back(name, dentry); + for (const Dentry *dentry : dentries) { + children.emplace_back(dentry->name, dentry->inode); } return children; @@ -481,7 +493,7 @@ static Inodes Inodes; static void memfs_lookup(fuse_req_t req, fuse_ino_t parent, const char *name) { - auto *parentInode = Inodes.find(parent); + auto parentInode = Inodes.find(parent); if (!parentInode) { fuse_reply_err(req, ENOENT); @@ -493,22 +505,26 @@ static void memfs_lookup(fuse_req_t req, fuse_ino_t parent, const char *name) return; } - Dentry *child = parentInode->find_child(name); + parentInode->lock(); + Dentry *child = parentInode->find_child_locked(name); if (!child) { + parentInode->unlock(); fuse_reply_err(req, ENOENT); return; } + std::shared_ptr<Inode> child_inode = child->inode; + parentInode->unlock(); struct fuse_entry_param e; memset(&e, 0, sizeof(e)); - e.ino = child->get_ino(); + e.ino = child_inode->get_ino(); e.attr_timeout = MEMFS_ATTR_TIMEOUT; e.entry_timeout = MEMFS_ENTRY_TIMEOUT; - e.attr.st_ino = child->get_ino(); - e.attr.st_mode = child->get_mode(); - e.attr.st_nlink = child->is_dir() ? 2 : 1; + e.attr.st_ino = child_inode->get_ino(); + e.attr.st_mode = child_inode->get_mode(); + e.attr.st_nlink = child_inode->is_dir() ? 2 : 1; - child->inc_lookup(); + child_inode->inc_lookup(); fuse_reply_entry(req, &e); } @@ -522,7 +538,7 @@ static void memfs_getattr(fuse_req_t req, fuse_ino_t ino, return; } - auto *inode_data = Inodes.find(actual_ino); + auto inode_data = Inodes.find(actual_ino); if (!inode_data) { fuse_reply_err(req, ENOENT); return; @@ -538,7 +554,7 @@ static void memfs_getattr(fuse_req_t req, fuse_ino_t ino, static void memfs_create(fuse_req_t req, fuse_ino_t parent, const char *name, mode_t mode, struct fuse_file_info *fi) { - auto *parentInode = Inodes.find(parent); + auto parentInode = Inodes.find(parent); if (!parentInode || !parentInode->is_dir()) { fuse_reply_err(req, ENOENT); return; @@ -549,7 +565,7 @@ static void memfs_create(fuse_req_t req, fuse_ino_t parent, const char *name, return; } - Inode *new_inode = Inodes.create(name, false, mode); + auto new_inode = Inodes.create(name, false, mode); if (!new_inode) { fuse_reply_err(req, EIO); return; @@ -562,7 +578,13 @@ static void memfs_create(fuse_req_t req, fuse_ino_t parent, const char *name, // << (void *)new_dentry << ", name: '" << name // << "', inode address: " << (void *)new_inode << std::endl; - parentInode->add_child(name, new_dentry); + int error = parentInode->add_child(name, new_dentry); + if (error != 0) { + delete new_dentry; + Inodes.erase(new_inode.get()); + fuse_reply_err(req, error); + return; + } struct fuse_entry_param e; memset(&e, 0, sizeof(e)); @@ -579,7 +601,7 @@ static void memfs_write(fuse_req_t req, fuse_ino_t ino, const char *buf, size_t size, off_t offset, [[maybe_unused]] struct fuse_file_info *fi) { - Inode *inode = Inodes.find(ino); + auto inode = Inodes.find(ino); if (!inode) { fuse_reply_err(req, ENOENT); return; @@ -590,6 +612,19 @@ static void memfs_write(fuse_req_t req, fuse_ino_t ino, const char *buf, return; } + // A negative offset, or an end position past the content vector's index + // type, would convert to a huge value in write_content and abort the + // resize. PTRDIFF_MAX is the std::vector size/index ceiling on both 32- + // and 64-bit builds. + if (offset < 0) { + fuse_reply_err(req, EINVAL); + return; + } + if ((uint64_t)offset > (uint64_t)PTRDIFF_MAX - size) { + fuse_reply_err(req, EFBIG); + return; + } + inode->write_content(buf, size, offset); fuse_reply_write(req, size); } @@ -597,12 +632,19 @@ static void memfs_write(fuse_req_t req, fuse_ino_t ino, const char *buf, static void memfs_read(fuse_req_t req, fuse_ino_t ino, size_t size, off_t offset, [[maybe_unused]] struct fuse_file_info *fi) { - Inode *inode = Inodes.find(ino); + auto inode = Inodes.find(ino); if (!inode || inode->is_dir()) { fuse_reply_err(req, ENOENT); return; } + // A negative offset would slip past the offset >= content_size() guard + // on a 32-bit build (signed compare) and index before the buffer. + if (offset < 0) { + fuse_reply_err(req, EINVAL); + return; + } + inode->lock(); if (offset >= inode->content_size()) { @@ -623,7 +665,7 @@ static void memfs_read(fuse_req_t req, fuse_ino_t ino, size_t size, static void memfs_open(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info *fi) { - auto *inode_data = Inodes.find(ino); + auto inode_data = Inodes.find(ino); if (!inode_data || inode_data->is_dir()) { fuse_reply_err(req, ENOENT); return; @@ -637,7 +679,7 @@ static void memfs_open(fuse_req_t req, fuse_ino_t ino, static void memfs_opendir(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info *fi) { - auto *inode = Inodes.find(ino); + auto inode = Inodes.find(ino); if (!inode || !inode->is_dir()) { fuse_reply_err(req, ENOTDIR); return; @@ -668,12 +710,12 @@ static void memfs_readdir(fuse_req_t req, [[maybe_unused]] fuse_ino_t ino, i < static_cast<off_t>(dir_handle->entries.size()); ++i) { const auto &entry = dir_handle->entries[i]; const std::string &name = entry.first; - const Dentry *dentry = entry.second; + const std::shared_ptr<Inode> &inode = entry.second; struct stat stbuf; memset(&stbuf, 0, sizeof(stbuf)); - stbuf.st_ino = dentry->get_inode()->get_ino(); - dentry->get_inode()->get_attr(&stbuf); + stbuf.st_ino = inode->get_ino(); + inode->get_attr(&stbuf); size_t entry_size = fuse_add_direntry(req, nullptr, 0, name.c_str(), nullptr, 0); @@ -716,12 +758,12 @@ static void memfs_panic(std::string_view message) static void memfs_forget(fuse_req_t req, fuse_ino_t ino, uint64_t nlookup) { Inodes.lock(); - Inode *inode = Inodes.find_locked(ino); + auto inode = Inodes.find_locked(ino); uint64_t res; if (inode) { res = inode->dec_lookup(nlookup); - if (res == 0) - Inodes.erase_locked(inode); + if (res == 0 && inode->get_nlink() == 0) + Inodes.erase_locked(inode.get()); } Inodes.unlock(); fuse_reply_none(req); @@ -730,14 +772,14 @@ static void memfs_forget(fuse_req_t req, fuse_ino_t ino, uint64_t nlookup) static void memfs_forget_multi(fuse_req_t req, size_t count, struct fuse_forget_data *forgets) { + Inodes.lock(); for (size_t i = 0; i < count; i++) { - fuse_ino_t ino = forgets[i].ino; - uint64_t nlookup = forgets[i].nlookup; - auto *inode_data = Inodes.find(ino); - if (inode_data) { - inode_data->dec_lookup(nlookup); - } + auto inode = Inodes.find_locked(forgets[i].ino); + if (inode && inode->dec_lookup(forgets[i].nlookup) == 0 && + inode->get_nlink() == 0) + Inodes.erase_locked(inode.get()); } + Inodes.unlock(); fuse_reply_none(req); } @@ -750,14 +792,12 @@ static void memfs_setattr(fuse_req_t req, fuse_ino_t ino, struct stat *attr, return; } - auto *inode_data = Inodes.find(actual_ino); + auto inode_data = Inodes.find(actual_ino); if (!inode_data) { fuse_reply_err(req, ENOENT); return; } - inode_data->lock(); - if (to_set & FUSE_SET_ATTR_MODE) inode_data->set_mode(attr->st_mode); if (to_set & FUSE_SET_ATTR_UID) @@ -773,7 +813,6 @@ static void memfs_setattr(fuse_req_t req, fuse_ino_t ino, struct stat *attr, struct stat st; inode_data->get_attr(&st); - inode_data->unlock(); fuse_reply_attr(req, &st, MEMFS_ATTR_TIMEOUT); } @@ -782,8 +821,8 @@ static void memfs_mkdir(fuse_req_t req, fuse_ino_t parent, const char *name, mode_t mode) { int error = 0; - Inode *parentInode = nullptr; - Inode *new_inode = nullptr; + std::shared_ptr<Inode> parentInode = nullptr; + std::shared_ptr<Inode> new_inode = nullptr; Dentry *new_dentry = nullptr; struct fuse_entry_param e; @@ -821,13 +860,13 @@ static void memfs_mkdir(fuse_req_t req, fuse_ino_t parent, const char *name, out_cleanup: delete new_dentry; - Inodes.erase_locked(new_inode); + Inodes.erase(new_inode.get()); goto out; } static void memfs_rmdir(fuse_req_t req, fuse_ino_t parent, const char *name) { - auto *parentInode = Inodes.find(parent); + auto parentInode = Inodes.find(parent); if (!parentInode || !parentInode->is_dir()) { fuse_reply_err(req, ENOENT); return; @@ -861,7 +900,7 @@ static void memfs_rmdir(fuse_req_t req, fuse_ino_t parent, const char *name) static void memfs_unlink(fuse_req_t req, fuse_ino_t parent, const char *name) { - auto *parentInode = Inodes.find(parent); + auto parentInode = Inodes.find(parent); if (!parentInode || !parentInode->is_dir()) { fuse_reply_err(req, ENOENT); return; @@ -896,16 +935,22 @@ static void memfs_rename(fuse_req_t req, fuse_ino_t parent, const char *name, unsigned int flags) { int error = 0; - Inode *parentInode = nullptr; - Inode *newparentInode = nullptr; + std::shared_ptr<Inode> parentInode = nullptr; + std::shared_ptr<Inode> newparentInode = nullptr; Dentry *child_dentry = nullptr; Dentry *child_dentry_copy = nullptr; Dentry *existing_dentry = nullptr; + Inode *inode_a = nullptr; + Inode *inode_b = nullptr; +#if defined(RENAME_EXCHANGE) && defined(RENAME_NOREPLACE) if (flags & (RENAME_EXCHANGE | RENAME_NOREPLACE)) { fuse_reply_err(req, EINVAL); return; } +#else + (void)flags; +#endif Inodes.lock(); @@ -917,10 +962,13 @@ static void memfs_rename(fuse_req_t req, fuse_ino_t parent, const char *name, goto out_unlock_global; } - parentInode->lock(); - if (parent != newparent) { - newparentInode->lock(); - } + inode_a = parentInode.get(); + inode_b = (parent != newparent) ? newparentInode.get() : nullptr; + if (inode_b && inode_a->get_ino() > inode_b->get_ino()) + std::swap(inode_a, inode_b); // always lock lower ino first + inode_a->lock(); + if (inode_b) + inode_b->lock(); child_dentry = parentInode->find_child_locked(name); if (child_dentry == nullptr) { @@ -930,26 +978,25 @@ static void memfs_rename(fuse_req_t req, fuse_ino_t parent, const char *name, existing_dentry = newparentInode->find_child_locked(newname); if (existing_dentry) { - if (existing_dentry->is_dir()) { - if (!existing_dentry->get_inode()->is_empty()) { - error = ENOTEMPTY; - goto out_unlock; - } - newparentInode->dec_nlink(); + Inode *existing_inode = existing_dentry->get_inode(); + if (existing_inode->is_dir() && !existing_inode->is_empty()) { + error = ENOTEMPTY; + goto out_unlock; } + // remove_child() frees the dentry and already decrements + // newparent's nlink for a dir child; resolve the inode first. newparentInode->remove_child(newname); - existing_dentry->get_inode()->dec_nlink(); + existing_inode->dec_nlink(); } - child_dentry_copy = new Dentry(newname, child_dentry->get_inode()); + child_dentry_copy = new Dentry(newname, child_dentry->inode); parentInode->remove_child(name); newparentInode->add_child_locked(newname, child_dentry_copy); out_unlock: - parentInode->unlock(); - if (parent != newparent) { - newparentInode->unlock(); - } + if (inode_b) + inode_b->unlock(); + inode_a->unlock(); out_unlock_global: Inodes.unlock(); @@ -960,20 +1007,20 @@ static void memfs_link(fuse_req_t req, fuse_ino_t ino, fuse_ino_t newparent, const char *newname) { int error = 0; - Inode *src_inode = nullptr; - Inode *parent_inode = nullptr; + std::shared_ptr<Inode> src_inode = nullptr; + std::shared_ptr<Inode> parent_inode = nullptr; struct fuse_entry_param e; - std::unique_ptr<Dentry> new_dentry; + Dentry *new_dentry = nullptr; Inodes.lock(); - src_inode = Inodes.find(ino); + src_inode = Inodes.find_locked(ino); if (!src_inode) { error = ENOENT; goto out_unlock_global; } - parent_inode = Inodes.find(newparent); + parent_inode = Inodes.find_locked(newparent); if (!parent_inode || !parent_inode->is_dir()) { error = ENOENT; goto out_unlock_global; @@ -989,8 +1036,16 @@ static void memfs_link(fuse_req_t req, fuse_ino_t ino, fuse_ino_t newparent, src_inode->inc_nlink(); - new_dentry = std::make_unique<Dentry>(newname, src_inode); - parent_inode->add_child(newname, new_dentry.get()); + new_dentry = new Dentry(newname, src_inode); + error = parent_inode->add_child_locked(newname, new_dentry); + if (error != 0) { + delete new_dentry; + src_inode->dec_nlink(); + goto out_unlock_parent; + } + + // the reply below hands the kernel a new lookup reference + src_inode->inc_lookup(); memset(&e, 0, sizeof(e)); e.ino = ino; @@ -1041,53 +1096,30 @@ static void memfs_statfs(fuse_req_t req, [[maybe_unused]] fuse_ino_t ino) fuse_reply_statfs(req, &stbuf); } +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wmissing-field-initializers" static const struct fuse_lowlevel_ops memfs_oper = { - .init = nullptr, - .destroy = nullptr, .lookup = memfs_lookup, .forget = memfs_forget, .getattr = memfs_getattr, .setattr = memfs_setattr, - .readlink = nullptr, - .mknod = nullptr, .mkdir = memfs_mkdir, .unlink = memfs_unlink, .rmdir = memfs_rmdir, - .symlink = nullptr, .rename = memfs_rename, .link = memfs_link, .open = memfs_open, .read = memfs_read, .write = memfs_write, - .flush = nullptr, .release = memfs_release, - .fsync = nullptr, .opendir = memfs_opendir, .readdir = memfs_readdir, .releasedir = memfs_releasedir, - .fsyncdir = nullptr, .statfs = memfs_statfs, - .setxattr = nullptr, - .getxattr = nullptr, - .listxattr = nullptr, - .removexattr = nullptr, - .access = nullptr, .create = memfs_create, - .getlk = nullptr, - .setlk = nullptr, - .bmap = nullptr, - .ioctl = nullptr, - .poll = nullptr, - .write_buf = nullptr, - .retrieve_reply = nullptr, .forget_multi = memfs_forget_multi, - .flock = nullptr, - .fallocate = nullptr, - .readdirplus = nullptr, - .copy_file_range = nullptr, - .lseek = nullptr, - .tmpfile = nullptr, }; +#pragma GCC diagnostic pop int main(int argc, char *argv[]) { diff --git a/example/meson.build b/example/meson.build index b2e896cc4..f32f892bb 100644 --- a/example/meson.build +++ b/example/meson.build @@ -4,7 +4,11 @@ examples = [ 'passthrough', 'passthrough_fh', 'ioctl', 'cuse', 'cuse_client' ] if not platform.endswith('bsd') and platform != 'dragonfly' - examples += [ 'passthrough_ll', 'hello_ll_uds' ] + examples += [ 'passthrough_ll' ] + + if private_cfg.get('HAVE_CUSTOM_IO', false) + examples += [ 'hello_ll_uds' ] + endif # According to Conrad Meyer <cem@freebsd.org>, FreeBSD doesn't # support mounting files, This is enforced in vfs_domount_first() @@ -31,13 +35,13 @@ foreach ex : threaded_examples install: false) endforeach -if not platform.endswith('bsd') and platform != 'dragonfly' and add_languages('cpp', required : false) +if platform != 'dragonfly' and add_languages('cpp', required : false) executable('passthrough_hp', 'passthrough_hp.cc', dependencies: [ thread_dep, libfuse_dep ], install: false) executable('memfs_ll', 'memfs_ll.cc', dependencies: [ thread_dep, libfuse_dep ], - cpp_args : '-std=c++17', + cpp_args : '-std=c++20', install: false) endif diff --git a/example/notify_inval_entry.c b/example/notify_inval_entry.c index 0eda6ca4c..69ce39329 100644 --- a/example/notify_inval_entry.c +++ b/example/notify_inval_entry.c @@ -3,7 +3,7 @@ Copyright (C) 2016 Nikolaus Rath <Nikolaus@rath.org> This program can be distributed under the terms of the GNU GPLv2. - See the file COPYING. + See the file GPL2.txt. */ /** @file @@ -67,15 +67,20 @@ * To use the function fuse_lowlevel_notify_expire_entry() instead of * fuse_lowlevel_notify_inval_entry(), use the command line option --only-expire * + * Another possible command-line option is --inc-epoch, which will use the FUSE + * low-level function fuse_lowlevel_notify_increment_epoch() instead. This will + * function will force the invalidation of all dentries next time they are + * revalidated. Note that --inc-epoch and --only-expire options are mutually + * exclusive. + * * ## Compilation ## * - * gcc -Wall notify_inval_entry.c `pkg-config fused --cflags --libs` -o notify_inval_entry + * gcc -Wall notify_inval_entry.c `pkg-config fuse3 --cflags --libs` -o notify_inval_entry * * ## Source code ## * \include notify_inval_entry.c */ - #define FUSE_USE_VERSION FUSE_MAKE_VERSION(3, 12) #include <fuse_lowlevel.h> @@ -99,298 +104,329 @@ static pthread_t main_thread; /* Command line parsing */ struct options { - int no_notify; - float timeout; - int update_interval; - int only_expire; + int no_notify; + float timeout; + int update_interval; + int only_expire; + int inc_epoch; }; static struct options options = { - .timeout = 5, - .no_notify = 0, - .update_interval = 1, - .only_expire = 0, + .timeout = 5, + .no_notify = 0, + .update_interval = 1, + .only_expire = 0, + .inc_epoch = 0, }; -#define OPTION(t, p) \ - { t, offsetof(struct options, p), 1 } +#define OPTION(t, p) { t, offsetof(struct options, p), 1 } static const struct fuse_opt option_spec[] = { - OPTION("--no-notify", no_notify), - OPTION("--update-interval=%d", update_interval), - OPTION("--timeout=%f", timeout), - OPTION("--only-expire", only_expire), - FUSE_OPT_END + OPTION("--no-notify", no_notify), + OPTION("--update-interval=%d", update_interval), + OPTION("--timeout=%f", timeout), + OPTION("--only-expire", only_expire), + OPTION("--inc-epoch", inc_epoch), + FUSE_OPT_END }; -static int tfs_stat(fuse_ino_t ino, struct stat *stbuf) { - stbuf->st_ino = ino; - if (ino == FUSE_ROOT_ID) { - stbuf->st_mode = S_IFDIR | 0755; - stbuf->st_nlink = 1; - } - - else if (ino == file_ino) { - stbuf->st_mode = S_IFREG | 0000; - stbuf->st_nlink = 1; - stbuf->st_size = 0; - } - - else - return -1; - - return 0; +static int tfs_stat(fuse_ino_t ino, struct stat *stbuf) +{ + stbuf->st_ino = ino; + if (ino == FUSE_ROOT_ID) { + stbuf->st_mode = S_IFDIR | 0755; + stbuf->st_nlink = 1; + } + + else if (ino == file_ino) { + stbuf->st_mode = S_IFREG | 0000; + stbuf->st_nlink = 1; + stbuf->st_size = 0; + } + + else + return -1; + + return 0; } -static void tfs_init(void *userdata, struct fuse_conn_info *conn) { +static void tfs_init(void *userdata, struct fuse_conn_info *conn) +{ (void)userdata; /* Disable the receiving and processing of FUSE_INTERRUPT requests */ conn->no_interrupt = 1; } -static void tfs_lookup(fuse_req_t req, fuse_ino_t parent, - const char *name) { - struct fuse_entry_param e; - memset(&e, 0, sizeof(e)); - - if (parent != FUSE_ROOT_ID) - goto err_out; - else if (strcmp(name, file_name) == 0) { - e.ino = file_ino; - lookup_cnt++; - } else - goto err_out; - - e.attr_timeout = options.timeout; - e.entry_timeout = options.timeout; - if (tfs_stat(e.ino, &e.attr) != 0) - goto err_out; - fuse_reply_entry(req, &e); - return; +static void tfs_lookup(fuse_req_t req, fuse_ino_t parent, const char *name) +{ + struct fuse_entry_param e; + memset(&e, 0, sizeof(e)); + + if (parent != FUSE_ROOT_ID) + goto err_out; + else if (strcmp(name, file_name) == 0) { + e.ino = file_ino; + lookup_cnt++; + } else + goto err_out; + + e.attr_timeout = options.timeout; + e.entry_timeout = options.timeout; + if (tfs_stat(e.ino, &e.attr) != 0) + goto err_out; + fuse_reply_entry(req, &e); + return; err_out: - fuse_reply_err(req, ENOENT); + fuse_reply_err(req, ENOENT); } -static void tfs_forget (fuse_req_t req, fuse_ino_t ino, - uint64_t nlookup) { - (void) req; - if(ino == file_ino) - lookup_cnt -= nlookup; - else - assert(ino == FUSE_ROOT_ID); - fuse_reply_none(req); +static void tfs_forget(fuse_req_t req, fuse_ino_t ino, uint64_t nlookup) +{ + (void)req; + if (ino == file_ino) + lookup_cnt -= nlookup; + else + assert(ino == FUSE_ROOT_ID); + fuse_reply_none(req); } static void tfs_getattr(fuse_req_t req, fuse_ino_t ino, - struct fuse_file_info *fi) { - struct stat stbuf; + struct fuse_file_info *fi) +{ + struct stat stbuf; - (void) fi; + (void)fi; - memset(&stbuf, 0, sizeof(stbuf)); - if (tfs_stat(ino, &stbuf) != 0) - fuse_reply_err(req, ENOENT); - else - fuse_reply_attr(req, &stbuf, options.timeout); + memset(&stbuf, 0, sizeof(stbuf)); + if (tfs_stat(ino, &stbuf) != 0) + fuse_reply_err(req, ENOENT); + else + fuse_reply_attr(req, &stbuf, options.timeout); } struct dirbuf { - char *p; - size_t size; + char *p; + size_t size; }; static void dirbuf_add(fuse_req_t req, struct dirbuf *b, const char *name, - fuse_ino_t ino) { - struct stat stbuf; - size_t oldsize = b->size; - b->size += fuse_add_direntry(req, NULL, 0, name, NULL, 0); - b->p = (char *) realloc(b->p, b->size); - memset(&stbuf, 0, sizeof(stbuf)); - stbuf.st_ino = ino; - fuse_add_direntry(req, b->p + oldsize, b->size - oldsize, name, &stbuf, - b->size); + fuse_ino_t ino) +{ + struct stat stbuf; + size_t oldsize = b->size; + b->size += fuse_add_direntry(req, NULL, 0, name, NULL, 0); + b->p = (char *)realloc(b->p, b->size); + memset(&stbuf, 0, sizeof(stbuf)); + stbuf.st_ino = ino; + fuse_add_direntry(req, b->p + oldsize, b->size - oldsize, name, &stbuf, + b->size); } #define min(x, y) ((x) < (y) ? (x) : (y)) static int reply_buf_limited(fuse_req_t req, const char *buf, size_t bufsize, - off_t off, size_t maxsize) { - if (off < bufsize) - return fuse_reply_buf(req, buf + off, - min(bufsize - off, maxsize)); - else - return fuse_reply_buf(req, NULL, 0); + off_t off, size_t maxsize) +{ + if (off < bufsize) + return fuse_reply_buf(req, buf + off, + min(bufsize - off, maxsize)); + else + return fuse_reply_buf(req, NULL, 0); } -static void tfs_readdir(fuse_req_t req, fuse_ino_t ino, size_t size, - off_t off, struct fuse_file_info *fi) { - (void) fi; - - if (ino != FUSE_ROOT_ID) - fuse_reply_err(req, ENOTDIR); - else { - struct dirbuf b; - - memset(&b, 0, sizeof(b)); - dirbuf_add(req, &b, file_name, file_ino); - reply_buf_limited(req, b.p, b.size, off, size); - free(b.p); - } +static void tfs_readdir(fuse_req_t req, fuse_ino_t ino, size_t size, off_t off, + struct fuse_file_info *fi) +{ + (void)fi; + + if (ino != FUSE_ROOT_ID) + fuse_reply_err(req, ENOTDIR); + else { + struct dirbuf b; + + memset(&b, 0, sizeof(b)); + dirbuf_add(req, &b, file_name, file_ino); + reply_buf_limited(req, b.p, b.size, off, size); + free(b.p); + } } static const struct fuse_lowlevel_ops tfs_oper = { - .init = tfs_init, - .lookup = tfs_lookup, - .getattr = tfs_getattr, - .readdir = tfs_readdir, - .forget = tfs_forget, + .init = tfs_init, + .lookup = tfs_lookup, + .getattr = tfs_getattr, + .readdir = tfs_readdir, + .forget = tfs_forget, }; -static void update_fs(void) { - time_t t; - struct tm *now; - ssize_t ret; +static void update_fs(void) +{ + time_t t; + struct tm tmbuf; + struct tm *now; + ssize_t ret; - t = time(NULL); - now = localtime(&t); - assert(now != NULL); + t = time(NULL); + now = localtime_r(&t, &tmbuf); + assert(now != NULL); - ret = strftime(file_name, MAX_STR_LEN, - "Time_is_%Hh_%Mm_%Ss", now); - assert(ret != 0); + ret = strftime(file_name, MAX_STR_LEN, "Time_is_%Hh_%Mm_%Ss", now); + assert(ret != 0); } -static void* update_fs_loop(void *data) { - struct fuse_session *se = (struct fuse_session*) data; - char *old_name; - - - while(!fuse_session_exited(se)) { - old_name = strdup(file_name); - update_fs(); - - if (!options.no_notify && lookup_cnt) { - if(options.only_expire) { // expire entry - int ret = fuse_lowlevel_notify_expire_entry - (se, FUSE_ROOT_ID, old_name, strlen(old_name)); - - // no kernel support - if (ret == -ENOSYS) { - printf("fuse_lowlevel_notify_expire_entry not supported by kernel\n"); - printf("Exiting...\n"); - - fuse_session_exit(se); - // Make sure to exit now, rather than on next request from userspace - pthread_kill(main_thread, SIGPIPE); - - break; - } - // 1) ret == 0: successful expire of an existing entry - // 2) ret == -ENOENT: kernel has already expired the entry / - // entry does not exist anymore in the kernel - assert(ret == 0 || ret == -ENOENT); - } else { // invalidate entry - assert(fuse_lowlevel_notify_inval_entry - (se, FUSE_ROOT_ID, old_name, strlen(old_name)) == 0); - } - } - free(old_name); - sleep(options.update_interval); - } - return NULL; +static void *update_fs_loop(void *data) +{ + struct fuse_session *se = (struct fuse_session *)data; + char *old_name; + int ret = 0; + + while (!fuse_session_exited(se)) { + old_name = strdup(file_name); + update_fs(); + + if (!options.no_notify && lookup_cnt) { + if (options.only_expire) { // expire entry + ret = fuse_lowlevel_notify_expire_entry( + se, FUSE_ROOT_ID, old_name, + strlen(old_name)); + + // no kernel support + if (ret == -ENOSYS) { + printf("%s not supported by kernel\n", + "fuse_lowlevel_notify_expire_entry"); + break; + } + + // 1) ret == 0: successful expire of an existing entry + // 2) ret == -ENOENT: kernel has already expired the entry / + // entry does not exist anymore in the kernel + assert(ret == 0 || ret == -ENOENT); + } else if (options.inc_epoch) { // increment epoch + ret = fuse_lowlevel_notify_increment_epoch(se); + + if (ret == -ENOSYS) { + printf("%s not supported by kernel\n", + "fuse_lowlevel_notify_increment_epoch"); + break; + } + assert(ret == 0); + } else { // invalidate entry + assert(fuse_lowlevel_notify_inval_entry( + se, FUSE_ROOT_ID, old_name, + strlen(old_name)) == 0); + } + } + free(old_name); + sleep(options.update_interval); + } + + if (ret == -ENOSYS) { + printf("Exiting...\n"); + + fuse_session_exit(se); + // Make sure to exit now, rather than on next request from userspace + pthread_kill(main_thread, SIGPIPE); + } + + return NULL; } static void show_help(const char *progname) { - printf("usage: %s [options] <mountpoint>\n\n", progname); - printf("File-system specific options:\n" - " --timeout=<secs> Timeout for kernel caches\n" - " --update-interval=<secs> Update-rate of file system contents\n" - " --no-notify Disable kernel notifications\n" - " --only-expire Expire entries instead of invalidating them\n" - "\n"); + printf("usage: %s [options] <mountpoint>\n\n", progname); + printf("File-system specific options:\n" + " --timeout=<secs> Timeout for kernel caches\n" + " --update-interval=<secs> Update-rate of file system contents\n" + " --no-notify Disable kernel notifications\n" + " --only-expire Expire entries instead of invalidating them\n" + " --inc-epoch Increment epoch, invalidating all dentries\n" + "\n"); } -int main(int argc, char *argv[]) { - struct fuse_args args = FUSE_ARGS_INIT(argc, argv); - struct fuse_session *se; - struct fuse_cmdline_opts opts; - struct fuse_loop_config *config; - pthread_t updater; - int ret = -1; - - if (fuse_opt_parse(&args, &options, option_spec, NULL) == -1) - return 1; - - if (fuse_parse_cmdline(&args, &opts) != 0) - return 1; - if (opts.show_help) { - show_help(argv[0]); - fuse_cmdline_help(); - fuse_lowlevel_help(); - ret = 0; - goto err_out1; - } else if (opts.show_version) { - printf("FUSE library version %s\n", fuse_pkgversion()); - fuse_lowlevel_version(); - ret = 0; - goto err_out1; - } - - /* Initial contents */ - update_fs(); - - se = fuse_session_new(&args, &tfs_oper, - sizeof(tfs_oper), &se); - if (se == NULL) - goto err_out1; - - if (fuse_set_signal_handlers(se) != 0) - goto err_out2; - - if (fuse_session_mount(se, opts.mountpoint) != 0) - goto err_out3; - - fuse_daemonize(opts.foreground); - - // Needed to ensure that the main thread continues/restarts processing as soon - // as the fuse session ends (immediately after calling fuse_session_exit() ) - // and not only on the next request from userspace - main_thread = pthread_self(); - - /* Start thread to update file contents */ - ret = pthread_create(&updater, NULL, update_fs_loop, (void *)se); - if (ret != 0) { - fprintf(stderr, "pthread_create failed with %s\n", - strerror(ret)); - goto err_out3; - } - - /* Block until ctrl+c or fusermount -u */ - if (opts.singlethread) { - ret = fuse_session_loop(se); - } else { +int main(int argc, char *argv[]) +{ + struct fuse_args args = FUSE_ARGS_INIT(argc, argv); + struct fuse_session *se; + struct fuse_cmdline_opts opts; + struct fuse_loop_config *config; + pthread_t updater; + int ret = -1; + + if (fuse_opt_parse(&args, &options, option_spec, NULL) == -1) + return 1; + + if (fuse_parse_cmdline(&args, &opts) != 0) + return 1; + if (opts.show_help) { + show_help(argv[0]); + fuse_cmdline_help(); + fuse_lowlevel_help(); + ret = 0; + goto err_out1; + } else if (opts.show_version) { + printf("FUSE library version %s\n", fuse_pkgversion()); + fuse_lowlevel_version(); + ret = 0; + goto err_out1; + } + if (options.only_expire && options.inc_epoch) { + printf("'only-expire' and 'inc-epoch' options are exclusive\n"); + ret = 0; + goto err_out1; + } + + /* Initial contents */ + update_fs(); + + se = fuse_session_new(&args, &tfs_oper, sizeof(tfs_oper), &se); + if (se == NULL) + goto err_out1; + + if (fuse_set_signal_handlers(se) != 0) + goto err_out2; + + if (fuse_session_mount(se, opts.mountpoint) != 0) + goto err_out3; + + fuse_daemonize(opts.foreground); + + // Needed to ensure that the main thread continues/restarts processing as soon + // as the fuse session ends (immediately after calling fuse_session_exit() ) + // and not only on the next request from userspace + main_thread = pthread_self(); + + /* Start thread to update file contents */ + ret = pthread_create(&updater, NULL, update_fs_loop, (void *)se); + if (ret != 0) { + fprintf(stderr, "pthread_create failed with %s\n", + strerror(ret)); + goto err_out3; + } + + /* Block until ctrl+c or fusermount -u */ + if (opts.singlethread) { + ret = fuse_session_loop(se); + } else { config = fuse_loop_cfg_create(); fuse_loop_cfg_set_clone_fd(config, opts.clone_fd); fuse_loop_cfg_set_max_threads(config, opts.max_threads); ret = fuse_session_loop_mt(se, config); fuse_loop_cfg_destroy(config); config = NULL; - } + } - fuse_session_unmount(se); + fuse_session_unmount(se); err_out3: - fuse_remove_signal_handlers(se); + fuse_remove_signal_handlers(se); err_out2: - fuse_session_destroy(se); + fuse_session_destroy(se); err_out1: - free(opts.mountpoint); - fuse_opt_free_args(&args); + free(opts.mountpoint); + fuse_opt_free_args(&args); - return ret ? 1 : 0; + return ret ? 1 : 0; } - /** * Local Variables: * mode: c diff --git a/example/notify_inval_inode.c b/example/notify_inval_inode.c index b3721267b..c02853084 100644 --- a/example/notify_inval_inode.c +++ b/example/notify_inval_inode.c @@ -3,7 +3,7 @@ Copyright (C) 2016 Nikolaus Rath <Nikolaus@rath.org> This program can be distributed under the terms of the GNU GPLv2. - See the file COPYING. + See the file GPL2.txt. */ /** @file @@ -52,13 +52,12 @@ * * ## Compilation ## * - * gcc -Wall notify_inval_inode.c `pkg-config fused --cflags --libs` -o notify_inval_inode + * gcc -Wall notify_inval_inode.c `pkg-config fuse3 --cflags --libs` -o notify_inval_inode * * ## Source code ## * \include notify_inval_inode.c */ - #define FUSE_USE_VERSION FUSE_MAKE_VERSION(3, 12) #include <fuse_lowlevel.h> @@ -88,42 +87,42 @@ static _Atomic bool is_stop = false; /* Command line parsing */ struct options { - int no_notify; - int update_interval; + int no_notify; + int update_interval; }; static struct options options = { - .no_notify = 0, - .update_interval = 1, + .no_notify = 0, + .update_interval = 1, }; -#define OPTION(t, p) \ - { t, offsetof(struct options, p), 1 } +#define OPTION(t, p) { t, offsetof(struct options, p), 1 } static const struct fuse_opt option_spec[] = { - OPTION("--no-notify", no_notify), - OPTION("--update-interval=%d", update_interval), - FUSE_OPT_END + OPTION("--no-notify", no_notify), + OPTION("--update-interval=%d", update_interval), FUSE_OPT_END }; -static int tfs_stat(fuse_ino_t ino, struct stat *stbuf) { - stbuf->st_ino = ino; - if (ino == FUSE_ROOT_ID) { - stbuf->st_mode = S_IFDIR | 0755; - stbuf->st_nlink = 1; - } - - else if (ino == FILE_INO) { - stbuf->st_mode = S_IFREG | 0444; - stbuf->st_nlink = 1; - stbuf->st_size = file_size; - } - - else - return -1; - - return 0; +static int tfs_stat(fuse_ino_t ino, struct stat *stbuf) +{ + stbuf->st_ino = ino; + if (ino == FUSE_ROOT_ID) { + stbuf->st_mode = S_IFDIR | 0755; + stbuf->st_nlink = 1; + } + + else if (ino == FILE_INO) { + stbuf->st_mode = S_IFREG | 0444; + stbuf->st_nlink = 1; + stbuf->st_size = file_size; + } + + else + return -1; + + return 0; } -static void tfs_init(void *userdata, struct fuse_conn_info *conn) { +static void tfs_init(void *userdata, struct fuse_conn_info *conn) +{ (void)userdata; /* Disable the receiving and processing of FUSE_INTERRUPT requests */ @@ -132,267 +131,273 @@ static void tfs_init(void *userdata, struct fuse_conn_info *conn) { static void tfs_destroy(void *userarg) { - (void)userarg; + (void)userarg; - is_stop = true; + is_stop = true; } -static void tfs_lookup(fuse_req_t req, fuse_ino_t parent, - const char *name) { - struct fuse_entry_param e; - memset(&e, 0, sizeof(e)); - - if (parent != FUSE_ROOT_ID) - goto err_out; - else if (strcmp(name, FILE_NAME) == 0) { - e.ino = FILE_INO; - lookup_cnt++; - } else - goto err_out; - - e.attr_timeout = NO_TIMEOUT; - e.entry_timeout = NO_TIMEOUT; - if (tfs_stat(e.ino, &e.attr) != 0) - goto err_out; - fuse_reply_entry(req, &e); - return; +static void tfs_lookup(fuse_req_t req, fuse_ino_t parent, const char *name) +{ + struct fuse_entry_param e; + memset(&e, 0, sizeof(e)); + + if (parent != FUSE_ROOT_ID) + goto err_out; + else if (strcmp(name, FILE_NAME) == 0) { + e.ino = FILE_INO; + lookup_cnt++; + } else + goto err_out; + + e.attr_timeout = NO_TIMEOUT; + e.entry_timeout = NO_TIMEOUT; + if (tfs_stat(e.ino, &e.attr) != 0) + goto err_out; + fuse_reply_entry(req, &e); + return; err_out: - fuse_reply_err(req, ENOENT); + fuse_reply_err(req, ENOENT); } -static void tfs_forget (fuse_req_t req, fuse_ino_t ino, - uint64_t nlookup) { - (void) req; - if(ino == FILE_INO) - lookup_cnt -= nlookup; - else - assert(ino == FUSE_ROOT_ID); - fuse_reply_none(req); +static void tfs_forget(fuse_req_t req, fuse_ino_t ino, uint64_t nlookup) +{ + (void)req; + if (ino == FILE_INO) + lookup_cnt -= nlookup; + else + assert(ino == FUSE_ROOT_ID); + fuse_reply_none(req); } static void tfs_getattr(fuse_req_t req, fuse_ino_t ino, - struct fuse_file_info *fi) { - struct stat stbuf; + struct fuse_file_info *fi) +{ + struct stat stbuf; - (void) fi; + (void)fi; - memset(&stbuf, 0, sizeof(stbuf)); - if (tfs_stat(ino, &stbuf) != 0) - fuse_reply_err(req, ENOENT); - else - fuse_reply_attr(req, &stbuf, NO_TIMEOUT); + memset(&stbuf, 0, sizeof(stbuf)); + if (tfs_stat(ino, &stbuf) != 0) + fuse_reply_err(req, ENOENT); + else + fuse_reply_attr(req, &stbuf, NO_TIMEOUT); } struct dirbuf { - char *p; - size_t size; + char *p; + size_t size; }; static void dirbuf_add(fuse_req_t req, struct dirbuf *b, const char *name, - fuse_ino_t ino) { - struct stat stbuf; - size_t oldsize = b->size; - b->size += fuse_add_direntry(req, NULL, 0, name, NULL, 0); - b->p = (char *) realloc(b->p, b->size); - memset(&stbuf, 0, sizeof(stbuf)); - stbuf.st_ino = ino; - fuse_add_direntry(req, b->p + oldsize, b->size - oldsize, name, &stbuf, - b->size); + fuse_ino_t ino) +{ + struct stat stbuf; + size_t oldsize = b->size; + b->size += fuse_add_direntry(req, NULL, 0, name, NULL, 0); + b->p = (char *)realloc(b->p, b->size); + memset(&stbuf, 0, sizeof(stbuf)); + stbuf.st_ino = ino; + fuse_add_direntry(req, b->p + oldsize, b->size - oldsize, name, &stbuf, + b->size); } #define min(x, y) ((x) < (y) ? (x) : (y)) static int reply_buf_limited(fuse_req_t req, const char *buf, size_t bufsize, - off_t off, size_t maxsize) { - if (off < bufsize) - return fuse_reply_buf(req, buf + off, - min(bufsize - off, maxsize)); - else - return fuse_reply_buf(req, NULL, 0); + off_t off, size_t maxsize) +{ + if (off < bufsize) + return fuse_reply_buf(req, buf + off, + min(bufsize - off, maxsize)); + else + return fuse_reply_buf(req, NULL, 0); } -static void tfs_readdir(fuse_req_t req, fuse_ino_t ino, size_t size, - off_t off, struct fuse_file_info *fi) { - (void) fi; - - if (ino != FUSE_ROOT_ID) - fuse_reply_err(req, ENOTDIR); - else { - struct dirbuf b; - - memset(&b, 0, sizeof(b)); - dirbuf_add(req, &b, FILE_NAME, FILE_INO); - reply_buf_limited(req, b.p, b.size, off, size); - free(b.p); - } +static void tfs_readdir(fuse_req_t req, fuse_ino_t ino, size_t size, off_t off, + struct fuse_file_info *fi) +{ + (void)fi; + + if (ino != FUSE_ROOT_ID) + fuse_reply_err(req, ENOTDIR); + else { + struct dirbuf b; + + memset(&b, 0, sizeof(b)); + dirbuf_add(req, &b, FILE_NAME, FILE_INO); + reply_buf_limited(req, b.p, b.size, off, size); + free(b.p); + } } -static void tfs_open(fuse_req_t req, fuse_ino_t ino, - struct fuse_file_info *fi) { - - /* Make cache persistent even if file is closed, +static void tfs_open(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info *fi) +{ + /* Make cache persistent even if file is closed, this makes it easier to see the effects */ - fi->keep_cache = 1; - - if (ino == FUSE_ROOT_ID) - fuse_reply_err(req, EISDIR); - else if ((fi->flags & O_ACCMODE) != O_RDONLY) - fuse_reply_err(req, EACCES); - else if (ino == FILE_INO) - fuse_reply_open(req, fi); - else { - // This should not happen - fprintf(stderr, "Got open for non-existing inode!\n"); - fuse_reply_err(req, ENOENT); - } + fi->keep_cache = 1; + + if (ino == FUSE_ROOT_ID) + fuse_reply_err(req, EISDIR); + else if ((fi->flags & O_ACCMODE) != O_RDONLY) + fuse_reply_err(req, EACCES); + else if (ino == FILE_INO) + fuse_reply_open(req, fi); + else { + // This should not happen + fprintf(stderr, "Got open for non-existing inode!\n"); + fuse_reply_err(req, ENOENT); + } } -static void tfs_read(fuse_req_t req, fuse_ino_t ino, size_t size, - off_t off, struct fuse_file_info *fi) { - (void) fi; +static void tfs_read(fuse_req_t req, fuse_ino_t ino, size_t size, off_t off, + struct fuse_file_info *fi) +{ + (void)fi; - assert(ino == FILE_INO); - reply_buf_limited(req, file_contents, file_size, off, size); + assert(ino == FILE_INO); + reply_buf_limited(req, file_contents, file_size, off, size); } static const struct fuse_lowlevel_ops tfs_oper = { - .init = tfs_init, - .destroy = tfs_destroy, - .lookup = tfs_lookup, - .getattr = tfs_getattr, - .readdir = tfs_readdir, - .open = tfs_open, - .read = tfs_read, - .forget = tfs_forget, + .init = tfs_init, + .destroy = tfs_destroy, + .lookup = tfs_lookup, + .getattr = tfs_getattr, + .readdir = tfs_readdir, + .open = tfs_open, + .read = tfs_read, + .forget = tfs_forget, }; -static void update_fs(void) { - struct tm *now; - time_t t; - t = time(NULL); - now = localtime(&t); - assert(now != NULL); - - file_size = strftime(file_contents, MAX_STR_LEN, - "The current time is %H:%M:%S\n", now); - assert(file_size != 0); +static void update_fs(void) +{ + struct tm tmbuf; + struct tm *now; + time_t t; + t = time(NULL); + now = localtime_r(&t, &tmbuf); + assert(now != NULL); + + file_size = strftime(file_contents, MAX_STR_LEN, + "The current time is %H:%M:%S\n", now); + assert(file_size != 0); } -static void* update_fs_loop(void *data) { - struct fuse_session *se = (struct fuse_session*) data; +static void *update_fs_loop(void *data) +{ + struct fuse_session *se = (struct fuse_session *)data; - while(!is_stop) { - update_fs(); - if (!options.no_notify && lookup_cnt) { - /* Only send notification if the kernel is aware of the inode */ + while (!is_stop) { + update_fs(); + if (!options.no_notify && lookup_cnt) { + /* Only send notification if the kernel is aware of the inode */ - /* Some errors (ENOENT, EBADF, ENODEV) have to be accepted as they + /* Some errors (ENOENT, EBADF, ENODEV) have to be accepted as they * might come up during umount, when kernel side already releases * all inodes, but does not send FUSE_DESTROY yet. */ - int ret = - fuse_lowlevel_notify_inval_inode(se, FILE_INO, 0, 0); - if ((ret != 0 && !is_stop) && - ret != -ENOENT && ret != -EBADF && ret != -ENODEV) { - fprintf(stderr, - "ERROR: fuse_lowlevel_notify_store() failed with %s (%d)\n", - strerror(-ret), -ret); - abort(); - } - } - sleep(options.update_interval); - } - return NULL; + int ret = fuse_lowlevel_notify_inval_inode(se, FILE_INO, + 0, 0); + if ((ret != 0 && !is_stop) && ret != -ENOENT && + ret != -EBADF && ret != -ENODEV) { + fprintf(stderr, + "ERROR: fuse_lowlevel_notify_store() failed with %s (%d)\n", + strerror(-ret), -ret); + abort(); + } + } + sleep(options.update_interval); + } + return NULL; } static void show_help(const char *progname) { - printf("usage: %s [options] <mountpoint>\n\n", progname); - printf("File-system specific options:\n" - " --update-interval=<secs> Update-rate of file system contents\n" - " --no-notify Disable kernel notifications\n" - "\n"); + printf("usage: %s [options] <mountpoint>\n\n", progname); + printf("File-system specific options:\n" + " --update-interval=<secs> Update-rate of file system contents\n" + " --no-notify Disable kernel notifications\n" + "\n"); } -int main(int argc, char *argv[]) { - struct fuse_args args = FUSE_ARGS_INIT(argc, argv); - struct fuse_session *se; - struct fuse_cmdline_opts opts; - struct fuse_loop_config *config; - pthread_t updater; - int ret = -1; - - if (fuse_opt_parse(&args, &options, option_spec, NULL) == -1) - return 1; - - if (fuse_parse_cmdline(&args, &opts) != 0) { - ret = 1; - goto err_out1; - } - - if (opts.show_help) { - show_help(argv[0]); - fuse_cmdline_help(); - fuse_lowlevel_help(); - ret = 0; - goto err_out1; - } else if (opts.show_version) { - printf("FUSE library version %s\n", fuse_pkgversion()); - fuse_lowlevel_version(); - ret = 0; - goto err_out1; - } - - /* Initial contents */ - update_fs(); - - se = fuse_session_new(&args, &tfs_oper, - sizeof(tfs_oper), NULL); - if (se == NULL) - goto err_out1; - - if (fuse_set_signal_handlers(se) != 0) - goto err_out2; - - if (fuse_session_mount(se, opts.mountpoint) != 0) - goto err_out3; - - fuse_daemonize(opts.foreground); - - /* Start thread to update file contents */ - ret = pthread_create(&updater, NULL, update_fs_loop, (void *)se); - if (ret != 0) { - fprintf(stderr, "pthread_create failed with %s\n", - strerror(ret)); - goto err_out3; - } - - /* Block until ctrl+c or fusermount -u */ - if (opts.singlethread) - ret = fuse_session_loop(se); - else { - config = fuse_loop_cfg_create(); - fuse_loop_cfg_set_clone_fd(config, opts.clone_fd); - fuse_loop_cfg_set_max_threads(config, opts.max_threads); - ret = fuse_session_loop_mt(se, config); - fuse_loop_cfg_destroy(config); - config = NULL; - } - - fuse_session_unmount(se); +int main(int argc, char *argv[]) +{ + struct fuse_args args = FUSE_ARGS_INIT(argc, argv); + struct fuse_session *se; + struct fuse_cmdline_opts opts; + struct fuse_loop_config *config; + pthread_t updater; + int ret = -1; + + if (fuse_opt_parse(&args, &options, option_spec, NULL) == -1) + return 1; + + if (fuse_parse_cmdline(&args, &opts) != 0) { + ret = 1; + goto err_out1; + } + + if (opts.show_help) { + show_help(argv[0]); + fuse_cmdline_help(); + fuse_lowlevel_help(); + ret = 0; + goto err_out1; + } else if (opts.show_version) { + printf("FUSE library version %s\n", fuse_pkgversion()); + fuse_lowlevel_version(); + ret = 0; + goto err_out1; + } + + /* Initial contents */ + update_fs(); + + se = fuse_session_new(&args, &tfs_oper, sizeof(tfs_oper), NULL); + if (se == NULL) + goto err_out1; + + if (fuse_set_signal_handlers(se) != 0) + goto err_out2; + + if (fuse_session_mount(se, opts.mountpoint) != 0) + goto err_out3; + + fuse_daemonize(opts.foreground); + + /* Start thread to update file contents */ + ret = pthread_create(&updater, NULL, update_fs_loop, (void *)se); + if (ret != 0) { + fprintf(stderr, "pthread_create failed with %s\n", + strerror(ret)); + goto err_out3; + } + + /* Block until ctrl+c or fusermount -u */ + if (opts.singlethread) + ret = fuse_session_loop(se); + else { + config = fuse_loop_cfg_create(); + fuse_loop_cfg_set_clone_fd(config, opts.clone_fd); + fuse_loop_cfg_set_max_threads(config, opts.max_threads); + ret = fuse_session_loop_mt(se, config); + fuse_loop_cfg_destroy(config); + config = NULL; + } + + fuse_session_unmount(se); err_out3: - fuse_remove_signal_handlers(se); + fuse_remove_signal_handlers(se); err_out2: - fuse_session_destroy(se); + fuse_session_destroy(se); err_out1: - fuse_opt_free_args(&args); - free(opts.mountpoint); + fuse_opt_free_args(&args); + free(opts.mountpoint); - return ret ? 1 : 0; + return ret ? 1 : 0; } - /** * Local Variables: * mode: c diff --git a/example/notify_store_retrieve.c b/example/notify_store_retrieve.c index cb1d27402..23d433318 100644 --- a/example/notify_store_retrieve.c +++ b/example/notify_store_retrieve.c @@ -3,7 +3,7 @@ Copyright (C) 2016 Nikolaus Rath <Nikolaus@rath.org> This program can be distributed under the terms of the GNU GPLv2. - See the file COPYING. + See the file GPL2.txt. */ /** @file @@ -51,13 +51,12 @@ * * ## Compilation ## * - * gcc -Wall notify_store_retrieve.c `pkg-config fused --cflags --libs` -o notify_store_retrieve + * gcc -Wall notify_store_retrieve.c `pkg-config fuse3 --cflags --libs` -o notify_store_retrieve * * ## Source code ## * \include notify_store_retrieve.c */ - #define FUSE_USE_VERSION FUSE_MAKE_VERSION(3, 12) #include <fuse_lowlevel.h> @@ -94,210 +93,214 @@ static bool is_umount = false; /* updater thread tid */ static pthread_t updater; - /* Command line parsing */ struct options { - int no_notify; - int update_interval; + int no_notify; + int update_interval; }; static struct options options = { - .no_notify = 0, - .update_interval = 1, + .no_notify = 0, + .update_interval = 1, }; -#define OPTION(t, p) \ - { t, offsetof(struct options, p), 1 } +#define OPTION(t, p) { t, offsetof(struct options, p), 1 } static const struct fuse_opt option_spec[] = { - OPTION("--no-notify", no_notify), - OPTION("--update-interval=%d", update_interval), - FUSE_OPT_END + OPTION("--no-notify", no_notify), + OPTION("--update-interval=%d", update_interval), FUSE_OPT_END }; -static int tfs_stat(fuse_ino_t ino, struct stat *stbuf) { - stbuf->st_ino = ino; - if (ino == FUSE_ROOT_ID) { - stbuf->st_mode = S_IFDIR | 0755; - stbuf->st_nlink = 1; - } - - else if (ino == FILE_INO) { - stbuf->st_mode = S_IFREG | 0444; - stbuf->st_nlink = 1; - stbuf->st_size = file_size; - } - - else - return -1; - - return 0; +static int tfs_stat(fuse_ino_t ino, struct stat *stbuf) +{ + stbuf->st_ino = ino; + if (ino == FUSE_ROOT_ID) { + stbuf->st_mode = S_IFDIR | 0755; + stbuf->st_nlink = 1; + } + + else if (ino == FILE_INO) { + stbuf->st_mode = S_IFREG | 0444; + stbuf->st_nlink = 1; + stbuf->st_size = file_size; + } + + else + return -1; + + return 0; } -static void tfs_init(void *userdata, struct fuse_conn_info *conn) { +static void tfs_init(void *userdata, struct fuse_conn_info *conn) +{ (void)userdata; /* Disable the receiving and processing of FUSE_INTERRUPT requests */ conn->no_interrupt = 1; } -static void tfs_lookup(fuse_req_t req, fuse_ino_t parent, - const char *name) { - struct fuse_entry_param e; - memset(&e, 0, sizeof(e)); - - if (parent != FUSE_ROOT_ID) - goto err_out; - else if (strcmp(name, FILE_NAME) == 0) { - e.ino = FILE_INO; - } else - goto err_out; - - e.attr_timeout = NO_TIMEOUT; - e.entry_timeout = NO_TIMEOUT; - if (tfs_stat(e.ino, &e.attr) != 0) - goto err_out; - fuse_reply_entry(req, &e); - - /* +static void tfs_lookup(fuse_req_t req, fuse_ino_t parent, const char *name) +{ + struct fuse_entry_param e; + memset(&e, 0, sizeof(e)); + + if (parent != FUSE_ROOT_ID) + goto err_out; + else if (strcmp(name, FILE_NAME) == 0) + e.ino = FILE_INO; + else + goto err_out; + + e.attr_timeout = NO_TIMEOUT; + e.entry_timeout = NO_TIMEOUT; + if (tfs_stat(e.ino, &e.attr) != 0) + goto err_out; + fuse_reply_entry(req, &e); + + /* * must only be set when the kernel knows about the entry, * otherwise update_fs_loop() might see a positive count, but kernel * would not have the entry yet */ - if (e.ino == FILE_INO) { - pthread_mutex_lock(&lock); - lookup_cnt++; - pthread_mutex_unlock(&lock); - } + if (e.ino == FILE_INO) { + pthread_mutex_lock(&lock); + lookup_cnt++; + pthread_mutex_unlock(&lock); + } - return; + return; err_out: - fuse_reply_err(req, ENOENT); + fuse_reply_err(req, ENOENT); } -static void tfs_forget (fuse_req_t req, fuse_ino_t ino, - uint64_t nlookup) { - (void) req; - if(ino == FILE_INO) { - pthread_mutex_lock(&lock); - lookup_cnt -= nlookup; - pthread_mutex_unlock(&lock); - } else - assert(ino == FUSE_ROOT_ID); - fuse_reply_none(req); +static void tfs_forget(fuse_req_t req, fuse_ino_t ino, uint64_t nlookup) +{ + (void)req; + if (ino == FILE_INO) { + pthread_mutex_lock(&lock); + lookup_cnt -= nlookup; + pthread_mutex_unlock(&lock); + } else + assert(ino == FUSE_ROOT_ID); + fuse_reply_none(req); } static void tfs_getattr(fuse_req_t req, fuse_ino_t ino, - struct fuse_file_info *fi) { - struct stat stbuf; + struct fuse_file_info *fi) +{ + struct stat stbuf; - (void) fi; + (void)fi; - memset(&stbuf, 0, sizeof(stbuf)); - if (tfs_stat(ino, &stbuf) != 0) - fuse_reply_err(req, ENOENT); - else - fuse_reply_attr(req, &stbuf, NO_TIMEOUT); + memset(&stbuf, 0, sizeof(stbuf)); + if (tfs_stat(ino, &stbuf) != 0) + fuse_reply_err(req, ENOENT); + else + fuse_reply_attr(req, &stbuf, NO_TIMEOUT); } struct dirbuf { - char *p; - size_t size; + char *p; + size_t size; }; static void dirbuf_add(fuse_req_t req, struct dirbuf *b, const char *name, - fuse_ino_t ino) { - struct stat stbuf; - size_t oldsize = b->size; - b->size += fuse_add_direntry(req, NULL, 0, name, NULL, 0); - b->p = (char *) realloc(b->p, b->size); - memset(&stbuf, 0, sizeof(stbuf)); - stbuf.st_ino = ino; - fuse_add_direntry(req, b->p + oldsize, b->size - oldsize, name, &stbuf, - b->size); + fuse_ino_t ino) +{ + struct stat stbuf; + size_t oldsize = b->size; + b->size += fuse_add_direntry(req, NULL, 0, name, NULL, 0); + b->p = (char *)realloc(b->p, b->size); + memset(&stbuf, 0, sizeof(stbuf)); + stbuf.st_ino = ino; + fuse_add_direntry(req, b->p + oldsize, b->size - oldsize, name, &stbuf, + b->size); } #define min(x, y) ((x) < (y) ? (x) : (y)) static int reply_buf_limited(fuse_req_t req, const char *buf, size_t bufsize, - off_t off, size_t maxsize) { - if (off < bufsize) - return fuse_reply_buf(req, buf + off, - min(bufsize - off, maxsize)); - else - return fuse_reply_buf(req, NULL, 0); + off_t off, size_t maxsize) +{ + if (off < bufsize) + return fuse_reply_buf(req, buf + off, + min(bufsize - off, maxsize)); + else + return fuse_reply_buf(req, NULL, 0); } -static void tfs_readdir(fuse_req_t req, fuse_ino_t ino, size_t size, - off_t off, struct fuse_file_info *fi) { - (void) fi; - - if (ino != FUSE_ROOT_ID) - fuse_reply_err(req, ENOTDIR); - else { - struct dirbuf b; - - memset(&b, 0, sizeof(b)); - dirbuf_add(req, &b, FILE_NAME, FILE_INO); - reply_buf_limited(req, b.p, b.size, off, size); - free(b.p); - } +static void tfs_readdir(fuse_req_t req, fuse_ino_t ino, size_t size, off_t off, + struct fuse_file_info *fi) +{ + (void)fi; + + if (ino != FUSE_ROOT_ID) + fuse_reply_err(req, ENOTDIR); + else { + struct dirbuf b; + + memset(&b, 0, sizeof(b)); + dirbuf_add(req, &b, FILE_NAME, FILE_INO); + reply_buf_limited(req, b.p, b.size, off, size); + free(b.p); + } } -static void tfs_open(fuse_req_t req, fuse_ino_t ino, - struct fuse_file_info *fi) { - - /* Make cache persistent even if file is closed, +static void tfs_open(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info *fi) +{ + /* Make cache persistent even if file is closed, this makes it easier to see the effects */ - fi->keep_cache = 1; - - if (ino == FUSE_ROOT_ID) - fuse_reply_err(req, EISDIR); - else if ((fi->flags & O_ACCMODE) != O_RDONLY) - fuse_reply_err(req, EACCES); - else if (ino == FILE_INO) { - fuse_reply_open(req, fi); - pthread_mutex_lock(&lock); - open_cnt++; - pthread_mutex_unlock(&lock); - } else { - // This should not happen - fprintf(stderr, "Got open for non-existing inode!\n"); - fuse_reply_err(req, ENOENT); - } + fi->keep_cache = 1; + + if (ino == FUSE_ROOT_ID) + fuse_reply_err(req, EISDIR); + else if ((fi->flags & O_ACCMODE) != O_RDONLY) + fuse_reply_err(req, EACCES); + else if (ino == FILE_INO) { + fuse_reply_open(req, fi); + pthread_mutex_lock(&lock); + open_cnt++; + pthread_mutex_unlock(&lock); + } else { + // This should not happen + fprintf(stderr, "Got open for non-existing inode!\n"); + fuse_reply_err(req, ENOENT); + } } -static void tfs_read(fuse_req_t req, fuse_ino_t ino, size_t size, - off_t off, struct fuse_file_info *fi) { - (void) fi; +static void tfs_read(fuse_req_t req, fuse_ino_t ino, size_t size, off_t off, + struct fuse_file_info *fi) +{ + (void)fi; - assert(ino == FILE_INO); - reply_buf_limited(req, file_contents, file_size, off, size); + assert(ino == FILE_INO); + reply_buf_limited(req, file_contents, file_size, off, size); } static void tfs_retrieve_reply(fuse_req_t req, void *cookie, fuse_ino_t ino, - off_t offset, struct fuse_bufvec *data) { - struct fuse_bufvec bufv; - char buf[MAX_STR_LEN]; - char *expected; - ssize_t ret; - - assert(ino == FILE_INO); - assert(offset == 0); - expected = (char*) cookie; - - bufv.count = 1; - bufv.idx = 0; - bufv.off = 0; - bufv.buf[0].size = MAX_STR_LEN; - bufv.buf[0].mem = buf; - bufv.buf[0].flags = 0; - - ret = fuse_buf_copy(&bufv, data, 0); - assert(ret > 0); - assert(strncmp(buf, expected, ret) == 0); - free(expected); - retrieve_status = 2; - fuse_reply_none(req); + off_t offset, struct fuse_bufvec *data) +{ + struct fuse_bufvec bufv; + char buf[MAX_STR_LEN]; + char *expected; + ssize_t ret; + + assert(ino == FILE_INO); + assert(offset == 0); + expected = (char *)cookie; + + bufv.count = 1; + bufv.idx = 0; + bufv.off = 0; + bufv.buf[0].size = MAX_STR_LEN; + bufv.buf[0].mem = buf; + bufv.buf[0].flags = 0; + + ret = fuse_buf_copy(&bufv, data, 0); + assert(ret > 0); + assert(strncmp(buf, expected, ret) == 0); + free(expected); + retrieve_status = 2; + fuse_reply_none(req); } static void tfs_destroy(void *userdata) @@ -309,163 +312,166 @@ static void tfs_destroy(void *userdata) pthread_join(updater, NULL); } - static const struct fuse_lowlevel_ops tfs_oper = { - .init = tfs_init, - .lookup = tfs_lookup, - .getattr = tfs_getattr, - .readdir = tfs_readdir, - .open = tfs_open, - .read = tfs_read, - .forget = tfs_forget, - .retrieve_reply = tfs_retrieve_reply, - .destroy = tfs_destroy, + .init = tfs_init, + .lookup = tfs_lookup, + .getattr = tfs_getattr, + .readdir = tfs_readdir, + .open = tfs_open, + .read = tfs_read, + .forget = tfs_forget, + .retrieve_reply = tfs_retrieve_reply, + .destroy = tfs_destroy, }; -static void update_fs(void) { - struct tm *now; - time_t t; - t = time(NULL); - now = localtime(&t); - assert(now != NULL); - - file_size = strftime(file_contents, MAX_STR_LEN, - "The current time is %H:%M:%S\n", now); - assert(file_size != 0); +static void update_fs(void) +{ + struct tm tmbuf; + struct tm *now; + time_t t; + t = time(NULL); + now = localtime_r(&t, &tmbuf); + assert(now != NULL); + + file_size = strftime(file_contents, MAX_STR_LEN, + "The current time is %H:%M:%S\n", now); + assert(file_size != 0); } -static void* update_fs_loop(void *data) { - struct fuse_session *se = (struct fuse_session*) data; - struct fuse_bufvec bufv; - int ret; - - while(!is_umount) { - update_fs(); - pthread_mutex_lock(&lock); - if (!options.no_notify && open_cnt && lookup_cnt) { - /* Only send notification if the kernel +static void *update_fs_loop(void *data) +{ + struct fuse_session *se = (struct fuse_session *)data; + struct fuse_bufvec bufv; + int ret; + + while (!is_umount) { + update_fs(); + pthread_mutex_lock(&lock); + if (!options.no_notify && open_cnt && lookup_cnt) { + /* Only send notification if the kernel is aware of the inode */ - bufv.count = 1; - bufv.idx = 0; - bufv.off = 0; - bufv.buf[0].size = file_size; - bufv.buf[0].mem = file_contents; - bufv.buf[0].flags = 0; - - /* + bufv.count = 1; + bufv.idx = 0; + bufv.off = 0; + bufv.buf[0].size = file_size; + bufv.buf[0].mem = file_contents; + bufv.buf[0].flags = 0; + + /* * Some errors (ENOENT, EBADF, ENODEV) have to be accepted as they * might come up during umount, when kernel side already releases * all inodes, but does not send FUSE_DESTROY yet. */ - ret = fuse_lowlevel_notify_store(se, FILE_INO, 0, &bufv, 0); - if ((ret != 0 && !is_umount) && - ret != -ENOENT && ret != -EBADF && ret != -ENODEV) { - fprintf(stderr, - "ERROR: fuse_lowlevel_notify_store() failed with %s (%d)\n", - strerror(-ret), -ret); - abort(); - } - - /* To make sure that everything worked correctly, ask the + ret = fuse_lowlevel_notify_store(se, FILE_INO, 0, &bufv, + 0); + if ((ret != 0 && !is_umount) && ret != -ENOENT && + ret != -EBADF && ret != -ENODEV) { + fprintf(stderr, + "ERROR: fuse_lowlevel_notify_store() failed with %s (%d)\n", + strerror(-ret), -ret); + abort(); + } + + /* To make sure that everything worked correctly, ask the kernel to send us back the stored data */ - ret = fuse_lowlevel_notify_retrieve(se, FILE_INO, MAX_STR_LEN, - 0, (void*) strdup(file_contents)); - assert((ret == 0 || is_umount) || ret == -ENOENT || ret == -EBADF || - ret != -ENODEV); - if(retrieve_status == 0) - retrieve_status = 1; - } - pthread_mutex_unlock(&lock); - sleep(options.update_interval); - } - return NULL; + ret = fuse_lowlevel_notify_retrieve( + se, FILE_INO, MAX_STR_LEN, 0, + (void *)strdup(file_contents)); + assert((ret == 0 || is_umount) || ret == -ENOENT || + ret == -EBADF || ret != -ENODEV); + if (retrieve_status == 0) + retrieve_status = 1; + } + pthread_mutex_unlock(&lock); + sleep(options.update_interval); + } + return NULL; } static void show_help(const char *progname) { - printf("usage: %s [options] <mountpoint>\n\n", progname); - printf("File-system specific options:\n" - " --update-interval=<secs> Update-rate of file system contents\n" - " --no-notify Disable kernel notifications\n" - "\n"); + printf("usage: %s [options] <mountpoint>\n\n", progname); + printf("File-system specific options:\n" + " --update-interval=<secs> Update-rate of file system contents\n" + " --no-notify Disable kernel notifications\n" + "\n"); } -int main(int argc, char *argv[]) { - struct fuse_args args = FUSE_ARGS_INIT(argc, argv); - struct fuse_session *se; - struct fuse_cmdline_opts opts; - struct fuse_loop_config *config; - int ret = -1; - - if (fuse_opt_parse(&args, &options, option_spec, NULL) == -1) - return 1; - - if (fuse_parse_cmdline(&args, &opts) != 0) - return 1; - if (opts.show_help) { - show_help(argv[0]); - fuse_cmdline_help(); - fuse_lowlevel_help(); - ret = 0; - goto err_out1; - } else if (opts.show_version) { - printf("FUSE library version %s\n", fuse_pkgversion()); - fuse_lowlevel_version(); - ret = 0; - goto err_out1; - } - - /* Initial contents */ - update_fs(); - - se = fuse_session_new(&args, &tfs_oper, - sizeof(tfs_oper), NULL); - if (se == NULL) - goto err_out1; - - if (fuse_set_signal_handlers(se) != 0) - goto err_out2; - - if (fuse_session_mount(se, opts.mountpoint) != 0) - goto err_out3; - - fuse_daemonize(opts.foreground); - - /* Start thread to update file contents */ - ret = pthread_create(&updater, NULL, update_fs_loop, (void *)se); - if (ret != 0) { - fprintf(stderr, "pthread_create failed with %s\n", - strerror(ret)); - goto err_out3; - } - - /* Block until ctrl+c or fusermount -u */ - if (opts.singlethread) - ret = fuse_session_loop(se); - else { - config = fuse_loop_cfg_create(); - fuse_loop_cfg_set_clone_fd(config, opts.clone_fd); - fuse_loop_cfg_set_max_threads(config, opts.max_threads); - ret = fuse_session_loop_mt(se, config); - fuse_loop_cfg_destroy(config); - config = NULL; - } - - assert(retrieve_status != 1); - fuse_session_unmount(se); +int main(int argc, char *argv[]) +{ + struct fuse_args args = FUSE_ARGS_INIT(argc, argv); + struct fuse_session *se; + struct fuse_cmdline_opts opts; + struct fuse_loop_config *config; + int ret = -1; + + if (fuse_opt_parse(&args, &options, option_spec, NULL) == -1) + return 1; + + if (fuse_parse_cmdline(&args, &opts) != 0) + return 1; + if (opts.show_help) { + show_help(argv[0]); + fuse_cmdline_help(); + fuse_lowlevel_help(); + ret = 0; + goto err_out1; + } else if (opts.show_version) { + printf("FUSE library version %s\n", fuse_pkgversion()); + fuse_lowlevel_version(); + ret = 0; + goto err_out1; + } + + /* Initial contents */ + update_fs(); + + se = fuse_session_new(&args, &tfs_oper, sizeof(tfs_oper), NULL); + if (se == NULL) + goto err_out1; + + if (fuse_set_signal_handlers(se) != 0) + goto err_out2; + + if (fuse_session_mount(se, opts.mountpoint) != 0) + goto err_out3; + + fuse_daemonize(opts.foreground); + + /* Start thread to update file contents */ + ret = pthread_create(&updater, NULL, update_fs_loop, (void *)se); + if (ret != 0) { + fprintf(stderr, "pthread_create failed with %s\n", + strerror(ret)); + goto err_out3; + } + + /* Block until ctrl+c or fusermount -u */ + if (opts.singlethread) + ret = fuse_session_loop(se); + else { + config = fuse_loop_cfg_create(); + fuse_loop_cfg_set_clone_fd(config, opts.clone_fd); + fuse_loop_cfg_set_max_threads(config, opts.max_threads); + ret = fuse_session_loop_mt(se, config); + fuse_loop_cfg_destroy(config); + config = NULL; + } + + assert(retrieve_status != 1); + fuse_session_unmount(se); err_out3: - fuse_remove_signal_handlers(se); + fuse_remove_signal_handlers(se); err_out2: - fuse_session_destroy(se); + fuse_session_destroy(se); err_out1: - free(opts.mountpoint); - fuse_opt_free_args(&args); + free(opts.mountpoint); + fuse_opt_free_args(&args); - return ret ? 1 : 0; + return ret ? 1 : 0; } - /** * Local Variables: * mode: c diff --git a/example/null.c b/example/null.c index 65670eec7..ec41def40 100644 --- a/example/null.c +++ b/example/null.c @@ -3,7 +3,7 @@ Copyright (C) 2001-2007 Miklos Szeredi <miklos@szeredi.hu> This program can be distributed under the terms of the GNU GPLv2. - See the file COPYING. + See the file GPL2.txt. */ /** @file @@ -15,7 +15,7 @@ * * Compile with: * - * gcc -Wall null.c `pkg-config fused --cflags --libs` -o null + * gcc -Wall null.c `pkg-config fuse3 --cflags --libs` -o null * * ## Source code ## * \include passthrough_fh.c diff --git a/example/passthrough.c b/example/passthrough.c index fcc9263d8..81a265aa4 100644 --- a/example/passthrough.c +++ b/example/passthrough.c @@ -4,7 +4,7 @@ Copyright (C) 2011 Sebastian Pipping <sebastian@pipping.org> This program can be distributed under the terms of the GNU GPLv2. - See the file COPYING. + See the file GPL2.txt. */ /** @file @@ -16,14 +16,14 @@ * * Compile with * - * gcc -Wall passthrough.c `pkg-config fused --cflags --libs` -o passthrough + * gcc -Wall passthrough.c `pkg-config fuse3 --cflags --libs` -o passthrough * * ## Source code ## * \include passthrough.c */ -#define FUSE_USE_VERSION 31 +#define FUSE_USE_VERSION FUSE_MAKE_VERSION(3, 18) #define _GNU_SOURCE @@ -40,10 +40,6 @@ #include <sys/stat.h> #include <dirent.h> #include <errno.h> -#ifdef __FreeBSD__ -#include <sys/socket.h> -#include <sys/un.h> -#endif #include <sys/time.h> #ifdef HAVE_SETXATTR #include <sys/xattr.h> @@ -52,12 +48,13 @@ #include "passthrough_helpers.h" static int fill_dir_plus = 0; +static int readdir_zero_ino; static void *xmp_init(struct fuse_conn_info *conn, struct fuse_config *cfg) { (void) conn; - cfg->use_ino = 1; + cfg->use_ino = !readdir_zero_ino; /* parallel_direct_writes feature depends on direct_io features. To make parallel_direct_writes valid, need either set cfg->direct_io @@ -144,6 +141,8 @@ static int xmp_readdir(const char *path, void *buf, fuse_fill_dir_t filler, st.st_ino = de->d_ino; st.st_mode = de->d_type << 12; } + if (readdir_zero_ino) + st.st_ino = 0; if (filler(buf, de->d_name, &st, 0, fill_dir_plus)) break; } @@ -399,7 +398,6 @@ static int xmp_fsync(const char *path, int isdatasync, return 0; } -#ifdef HAVE_POSIX_FALLOCATE static int xmp_fallocate(const char *path, int mode, off_t offset, off_t length, struct fuse_file_info *fi) { @@ -408,9 +406,6 @@ static int xmp_fallocate(const char *path, int mode, (void) fi; - if (mode) - return -EOPNOTSUPP; - if(fi == NULL) fd = open(path, O_WRONLY); else @@ -419,13 +414,12 @@ static int xmp_fallocate(const char *path, int mode, if (fd == -1) return -errno; - res = -posix_fallocate(fd, offset, length); + res = do_fallocate(fd, mode, offset, length); if(fi == NULL) close(fd); return res; } -#endif #ifdef HAVE_SETXATTR /* xattr operations are optional and can safely be left unimplemented */ @@ -528,6 +522,24 @@ static off_t xmp_lseek(const char *path, off_t off, int whence, struct fuse_file return res; } +#ifdef HAVE_STATX +static int xmp_statx(const char *path, int flags, int mask, struct statx *stxbuf, + struct fuse_file_info *fi) +{ + int fd = -1; + int res; + + if (fi) + fd = fi->fh; + + res = statx(fd, path, flags | AT_SYMLINK_NOFOLLOW, mask, stxbuf); + if (res == -1) + return -errno; + + return 0; +} +#endif + static const struct fuse_operations xmp_oper = { .init = xmp_init, .getattr = xmp_getattr, @@ -554,9 +566,7 @@ static const struct fuse_operations xmp_oper = { .statfs = xmp_statfs, .release = xmp_release, .fsync = xmp_fsync, -#ifdef HAVE_POSIX_FALLOCATE .fallocate = xmp_fallocate, -#endif #ifdef HAVE_SETXATTR .setxattr = xmp_setxattr, .getxattr = xmp_getxattr, @@ -567,6 +577,9 @@ static const struct fuse_operations xmp_oper = { .copy_file_range = xmp_copy_file_range, #endif .lseek = xmp_lseek, +#ifdef HAVE_STATX + .statx = xmp_statx, +#endif }; int main(int argc, char *argv[]) @@ -580,6 +593,9 @@ int main(int argc, char *argv[]) for (i=0, new_argc=0; (i<argc) && (new_argc<MAX_ARGS); i++) { if (!strcmp(argv[i], "--plus")) { fill_dir_plus = FUSE_FILL_DIR_PLUS; + } else if (!strcmp(argv[i], "--readdir-zero-inodes")) { + // Return zero inodes from readdir + readdir_zero_ino = 1; } else { new_argv[new_argc++] = argv[i]; } diff --git a/example/passthrough_fh.c b/example/passthrough_fh.c index 0268a8b0f..6403fbb74 100644 --- a/example/passthrough_fh.c +++ b/example/passthrough_fh.c @@ -4,7 +4,7 @@ Copyright (C) 2011 Sebastian Pipping <sebastian@pipping.org> This program can be distributed under the terms of the GNU GPLv2. - See the file COPYING. + See the file GPL2.txt. */ /** @file @@ -17,13 +17,13 @@ * * Compile with: * - * gcc -Wall passthrough_fh.c `pkg-config fused --cflags --libs` -lulockmgr -o passthrough_fh + * gcc -Wall passthrough_fh.c `pkg-config fuse3 --cflags --libs` -lulockmgr -o passthrough_fh * * ## Source code ## * \include passthrough_fh.c */ -#define FUSE_USE_VERSION 31 +#define FUSE_USE_VERSION FUSE_MAKE_VERSION(3, 18) #define _GNU_SOURCE @@ -47,6 +47,8 @@ #endif #include <sys/file.h> /* flock(2) */ +#include "passthrough_helpers.h" + static void *xmp_init(struct fuse_conn_info *conn, struct fuse_config *cfg) { @@ -514,18 +516,13 @@ static int xmp_fsync(const char *path, int isdatasync, return 0; } -#ifdef HAVE_POSIX_FALLOCATE static int xmp_fallocate(const char *path, int mode, off_t offset, off_t length, struct fuse_file_info *fi) { (void) path; - if (mode) - return -EOPNOTSUPP; - - return -posix_fallocate(fi->fh, offset, length); + return do_fallocate(fi->fh, mode, offset, length); } -#endif #ifdef HAVE_SETXATTR /* xattr operations are optional and can safely be left unimplemented */ @@ -619,6 +616,24 @@ static off_t xmp_lseek(const char *path, off_t off, int whence, struct fuse_file return res; } +#ifdef HAVE_STATX +static int xmp_statx(const char *path, int flags, int mask, struct statx *stxbuf, + struct fuse_file_info *fi) +{ + int fd = -1; + int res; + + if (fi) + fd = fi->fh; + + res = statx(fd, path, flags | AT_SYMLINK_NOFOLLOW, mask, stxbuf); + if (res == -1) + return -errno; + + return 0; +} +#endif + static const struct fuse_operations xmp_oper = { .init = xmp_init, .getattr = xmp_getattr, @@ -650,9 +665,7 @@ static const struct fuse_operations xmp_oper = { .flush = xmp_flush, .release = xmp_release, .fsync = xmp_fsync, -#ifdef HAVE_POSIX_FALLOCATE .fallocate = xmp_fallocate, -#endif #ifdef HAVE_SETXATTR .setxattr = xmp_setxattr, .getxattr = xmp_getxattr, @@ -667,6 +680,9 @@ static const struct fuse_operations xmp_oper = { .copy_file_range = xmp_copy_file_range, #endif .lseek = xmp_lseek, +#ifdef HAVE_STATX + .statx = xmp_statx, +#endif }; int main(int argc, char *argv[]) diff --git a/example/passthrough_helpers.h b/example/passthrough_helpers.h index 6b77c3360..326a5c7d6 100644 --- a/example/passthrough_helpers.h +++ b/example/passthrough_helpers.h @@ -23,11 +23,55 @@ * SUCH DAMAGE */ +#ifndef FUSE_EXAMPLE_PASSTHROUGH_HELPERS_H_ +#define FUSE_EXAMPLE_PASSTHROUGH_HELPERS_H_ + +#include <errno.h> +#include <fcntl.h> +#include <string.h> +#include <sys/stat.h> +#include <unistd.h> + +#ifdef __FreeBSD__ +#include <sys/socket.h> +#include <sys/un.h> +#endif + +static inline int do_fallocate(int fd, int mode, off_t offset, off_t length) +{ +#ifdef HAVE_FALLOCATE + if (fallocate(fd, mode, offset, length) == -1) + return -errno; + return 0; +#else // HAVE_FALLOCATE + +#ifdef HAVE_POSIX_FALLOCATE + if (mode == 0) + return -posix_fallocate(fd, offset, length); +#endif + +#ifdef HAVE_FSPACECTL + // 0x3 == FALLOC_FL_KEEP_SIZE | FALLOC_FL_PUNCH_HOLE + if (mode == 0x3) { + struct spacectl_range sr; + + sr.r_offset = offset; + sr.r_len = length; + if (fspacectl(fd, SPACECTL_DEALLOC, &sr, 0, NULL) == -1) + return -errno; + return 0; + } +#endif + + return -EOPNOTSUPP; +#endif // HAVE_FALLOCATE +} + /* * Creates files on the underlying file system in response to a FUSE_MKNOD * operation */ -static int mknod_wrapper(int dirfd, const char *path, const char *link, +static inline int mknod_wrapper(int dirfd, const char *path, const char *link, int mode, dev_t rdev) { int res; @@ -74,3 +118,5 @@ static int mknod_wrapper(int dirfd, const char *path, const char *link, return res; } + +#endif // FUSE_PASSTHROUGH_HELPERS_H_ diff --git a/example/passthrough_hp.cc b/example/passthrough_hp.cc index be089dc85..64f1ae5be 100644 --- a/example/passthrough_hp.cc +++ b/example/passthrough_hp.cc @@ -5,7 +5,7 @@ Copyright (C) 2018 Valve, Inc This program can be distributed under the terms of the GNU GPLv2. - See the file COPYING. + See the file GPL2.txt. */ /** @file @@ -59,11 +59,17 @@ #include <string.h> #include <sys/file.h> #include <sys/resource.h> +#ifdef HAVE_SYS_XATTR_H #include <sys/xattr.h> +#endif #include <time.h> #include <unistd.h> #include <pthread.h> #include <limits.h> +#ifdef __FreeBSD__ +#include <fcntl.h> +#include <sys/user.h> +#endif // C++ includes #include <cstddef> @@ -72,6 +78,9 @@ #include "cxxopts.hpp" #include <mutex> #include <syslog.h> +#include <atomic> + +#include "passthrough_helpers.h" using namespace std; @@ -82,15 +91,14 @@ using namespace std; sfs_dirp` elements as inodes and file handles. This means that we must be able to store pointer a pointer in both a fuse_ino_t variable and a uint64_t variable (used for file handles). */ -static_assert(sizeof(fuse_ino_t) >= sizeof(void*), - "void* must fit into fuse_ino_t"); +static_assert(sizeof(fuse_ino_t) >= sizeof(void *), + "void* must fit into fuse_ino_t"); static_assert(sizeof(fuse_ino_t) >= sizeof(uint64_t), - "fuse_ino_t must be at least 64 bits"); - + "fuse_ino_t must be at least 64 bits"); /* Forward declarations */ struct Inode; -static Inode& get_inode(fuse_ino_t ino); +static Inode &get_inode(fuse_ino_t ino); static void forget_one(fuse_ino_t ino, uint64_t n); // Uniquely identifies a file in the source directory tree. This could @@ -103,1474 +111,1601 @@ static void forget_one(fuse_ino_t ino, uint64_t n); typedef std::pair<ino_t, dev_t> SrcId; // Define a hash function for SrcId -namespace std { - template<> - struct hash<SrcId> { - size_t operator()(const SrcId& id) const { - return hash<ino_t>{}(id.first) ^ hash<dev_t>{}(id.second); - } - }; +namespace std +{ +template <> struct hash<SrcId> { + size_t operator()(const SrcId &id) const + { + return hash<ino_t>{}(id.first) ^ hash<dev_t>{}(id.second); + } +}; } // Maps files in the source directory tree to inodes typedef std::unordered_map<SrcId, Inode> InodeMap; struct Inode { - int fd {-1}; - dev_t src_dev {0}; - ino_t src_ino {0}; - int generation {0}; - int backing_id {0}; - uint64_t nopen {0}; - uint64_t nlookup {0}; - std::mutex m; - - // Delete copy constructor and assignments. We could implement - // move if we need it. - Inode() = default; - Inode(const Inode&) = delete; - Inode(Inode&& inode) = delete; - Inode& operator=(Inode&& inode) = delete; - Inode& operator=(const Inode&) = delete; - - ~Inode() { - if(fd > 0) - close(fd); - } + int fd{ -1 }; + dev_t src_dev{ 0 }; + ino_t src_ino{ 0 }; + int generation{ 0 }; + int backing_id{ 0 }; + uint64_t nopen{ 0 }; + std::atomic<uint64_t> nlookup{ 0 }; + /* Drop before fuse_reply_*: the reply may be the kernel's last + * reference, so forget_one() can erase the inode right after + */ + std::mutex m; + + // Delete copy constructor and assignments. We could implement + // move if we need it. + Inode() = default; + Inode(const Inode &) = delete; + Inode(Inode &&inode) = delete; + Inode &operator=(Inode &&inode) = delete; + Inode &operator=(const Inode &) = delete; + + ~Inode() + { + if (fd > 0) + close(fd); + } }; struct Fs { - // Must be acquired *after* any Inode.m locks. - std::mutex mutex; - InodeMap inodes; // protected by mutex - Inode root; - double timeout; - bool debug; - bool debug_fuse; - bool foreground; - std::string source; - size_t blocksize; - dev_t src_dev; - bool nosplice; - bool nocache; - size_t num_threads; - bool clone_fd; - std::string fuse_mount_options; - bool direct_io; - bool passthrough; + // Must be acquired *before* any Inode.m locks. + std::mutex mutex; + InodeMap inodes; // protected by mutex + Inode root; + double timeout; + bool debug; + bool debug_fuse; + bool foreground; + std::string source; + size_t blocksize; + dev_t src_dev; + bool nosplice; + bool nocache; + size_t num_threads; + bool clone_fd; + + std::string fuse_mount_options; + bool direct_io; + bool passthrough; }; static Fs fs{}; +#define FUSE_BUF_COPY_FLAGS \ + (fs.nosplice ? FUSE_BUF_NO_SPLICE : \ + static_cast<fuse_buf_copy_flags>(FUSE_BUF_SPLICE_MOVE)) -#define FUSE_BUF_COPY_FLAGS \ - (fs.nosplice ? \ - FUSE_BUF_NO_SPLICE : \ - static_cast<fuse_buf_copy_flags>(FUSE_BUF_SPLICE_MOVE)) - - -static Inode& get_inode(fuse_ino_t ino) { - if (ino == FUSE_ROOT_ID) - return fs.root; - - Inode* inode = reinterpret_cast<Inode*>(ino); - if(inode->fd == -1) { - cerr << "INTERNAL ERROR: Unknown inode " << ino << endl; - abort(); - } - return *inode; +static Inode &get_inode(fuse_ino_t ino) +{ + if (ino == FUSE_ROOT_ID) + return fs.root; + + Inode *inode = reinterpret_cast<Inode *>(ino); + if (inode->fd == -1) { + cerr << "INTERNAL ERROR: Unknown inode " << ino << endl; + abort(); + } + return *inode; } - -static int get_fs_fd(fuse_ino_t ino) { - int fd = get_inode(ino).fd; - return fd; +static int get_fs_fd(fuse_ino_t ino) +{ + int fd = get_inode(ino).fd; + return fd; } - -static void sfs_init(void *userdata, fuse_conn_info *conn) { - (void)userdata; - - if (!fuse_set_feature_flag(conn, FUSE_CAP_PASSTHROUGH)) - fs.passthrough = false; - - /* Passthrough and writeback cache are conflicting modes */ - if (fs.timeout && !fs.passthrough) - fuse_set_feature_flag(conn, FUSE_CAP_WRITEBACK_CACHE); - - fuse_set_feature_flag(conn, FUSE_CAP_FLOCK_LOCKS); - - if (fs.nosplice) { - // FUSE_CAP_SPLICE_READ is enabled in libfused by default, - // see do_init() in in fuse_lowlevel.c - // Just unset all, in case FUSE_CAP_SPLICE_WRITE or - // FUSE_CAP_SPLICE_MOVE would also get enabled by default. - fuse_unset_feature_flag(conn, FUSE_CAP_SPLICE_READ); - fuse_unset_feature_flag(conn, FUSE_CAP_SPLICE_WRITE); - fuse_unset_feature_flag(conn, FUSE_CAP_SPLICE_MOVE); - } else { - fuse_set_feature_flag(conn, FUSE_CAP_SPLICE_WRITE); - fuse_set_feature_flag(conn, FUSE_CAP_SPLICE_READ); - fuse_set_feature_flag(conn, FUSE_CAP_SPLICE_MOVE); - } - - /* This is a local file system - no network coherency needed */ - fuse_set_feature_flag(conn, FUSE_CAP_DIRECT_IO_ALLOW_MMAP); - - /* Disable NFS export support, which also disabled name_to_handle_at. - * Goal is to make xfstests that test name_to_handle_at to fail with - * the right error code (EOPNOTSUPP) than to open_by_handle_at to fail with - * ESTALE and let those test fail. - * Perfect NFS export support is not possible with this FUSE filesystem needs - * more kernel work, in order to passthrough nfs handle encode/decode to - * fuse-server/daemon. - */ - fuse_set_feature_flag(conn, FUSE_CAP_NO_EXPORT_SUPPORT); - - /* Disable the receiving and processing of FUSE_INTERRUPT requests */ - conn->no_interrupt = 1; - - /* Try a large IO by default */ - conn->max_write = 4 * 1024 * 1024; +static void sfs_init(void *userdata, fuse_conn_info *conn) +{ + (void)userdata; + + if (!fuse_set_feature_flag(conn, FUSE_CAP_PASSTHROUGH)) + fs.passthrough = false; + + /* Passthrough and writeback cache are conflicting modes */ + if (fs.timeout && !fs.passthrough) + fuse_set_feature_flag(conn, FUSE_CAP_WRITEBACK_CACHE); + + fuse_set_feature_flag(conn, FUSE_CAP_FLOCK_LOCKS); + + if (fs.nosplice) { + // FUSE_CAP_SPLICE_READ is enabled in libfuse3 by default, + // see do_init() in fuse_lowlevel.c + // Just unset all, in case FUSE_CAP_SPLICE_WRITE or + // FUSE_CAP_SPLICE_MOVE would also get enabled by default. + fuse_unset_feature_flag(conn, FUSE_CAP_SPLICE_READ); + fuse_unset_feature_flag(conn, FUSE_CAP_SPLICE_WRITE); + fuse_unset_feature_flag(conn, FUSE_CAP_SPLICE_MOVE); + } else { + fuse_set_feature_flag(conn, FUSE_CAP_SPLICE_WRITE); + fuse_set_feature_flag(conn, FUSE_CAP_SPLICE_READ); + fuse_set_feature_flag(conn, FUSE_CAP_SPLICE_MOVE); + } + + /* This is a local file system - no network coherency needed */ + fuse_set_feature_flag(conn, FUSE_CAP_DIRECT_IO_ALLOW_MMAP); + + /* Disable NFS export support, which also disabled name_to_handle_at. + * Goal is to make xfstests that test name_to_handle_at to fail with + * the right error code (EOPNOTSUPP) than to open_by_handle_at to fail with + * ESTALE and let those test fail. + * Perfect NFS export support is not possible with this FUSE filesystem needs + * more kernel work, in order to passthrough nfs handle encode/decode to + * fuse-server/daemon. + */ + fuse_set_feature_flag(conn, FUSE_CAP_NO_EXPORT_SUPPORT); + + /* Disable the receiving and processing of FUSE_INTERRUPT requests */ + conn->no_interrupt = 1; + + /* Try a large IO by default */ + conn->max_write = 4 * 1024 * 1024; } - static void sfs_getattr(fuse_req_t req, fuse_ino_t ino, fuse_file_info *fi) { - struct stat attr; - int fd = fi ? fi->fh : get_inode(ino).fd; - - auto res = fstatat(fd, "", &attr, AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW); - if (res == -1) { - fuse_reply_err(req, errno); - return; - } - fuse_reply_attr(req, &attr, fs.timeout); + struct stat attr; + int fd = fi ? fi->fh : get_inode(ino).fd; + + auto res = fstatat(fd, "", &attr, AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW); + if (res == -1) { + fuse_reply_err(req, errno); + return; + } + fuse_reply_attr(req, &attr, fs.timeout); } - +static int with_fd_path(int fd, const std::function<int(const char *)> &f) +{ +#ifdef __FreeBSD__ + struct kinfo_file kf; + kf.kf_structsize = sizeof(kf); + int ret = fcntl(fd, F_KINFO, &kf); + if (ret == -1) + return ret; + return f(kf.kf_path); +#else // Linux + char procname[64]; + sprintf(procname, "/proc/self/fd/%i", fd); + return f(procname); +#endif +} static void do_setattr(fuse_req_t req, fuse_ino_t ino, struct stat *attr, - int valid, struct fuse_file_info* fi) { - Inode& inode = get_inode(ino); - int ifd = inode.fd; - int res; - - if (valid & FUSE_SET_ATTR_MODE) { - if (fi) { - res = fchmod(fi->fh, attr->st_mode); - } else { - char procname[64]; - sprintf(procname, "/proc/self/fd/%i", ifd); - res = chmod(procname, attr->st_mode); - } - if (res == -1) - goto out_err; - } - if (valid & (FUSE_SET_ATTR_UID | FUSE_SET_ATTR_GID)) { - uid_t uid = (valid & FUSE_SET_ATTR_UID) ? attr->st_uid : static_cast<uid_t>(-1); - gid_t gid = (valid & FUSE_SET_ATTR_GID) ? attr->st_gid : static_cast<gid_t>(-1); - - res = fchownat(ifd, "", uid, gid, AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW); - if (res == -1) - goto out_err; - } - if (valid & FUSE_SET_ATTR_SIZE) { - if (fi) { - res = ftruncate(fi->fh, attr->st_size); - } else { - char procname[64]; - sprintf(procname, "/proc/self/fd/%i", ifd); - res = truncate(procname, attr->st_size); - } - if (res == -1) - goto out_err; - } - if (valid & (FUSE_SET_ATTR_ATIME | FUSE_SET_ATTR_MTIME)) { - struct timespec tv[2]; - - tv[0].tv_sec = 0; - tv[1].tv_sec = 0; - tv[0].tv_nsec = UTIME_OMIT; - tv[1].tv_nsec = UTIME_OMIT; - - if (valid & FUSE_SET_ATTR_ATIME_NOW) - tv[0].tv_nsec = UTIME_NOW; - else if (valid & FUSE_SET_ATTR_ATIME) - tv[0] = attr->st_atim; - - if (valid & FUSE_SET_ATTR_MTIME_NOW) - tv[1].tv_nsec = UTIME_NOW; - else if (valid & FUSE_SET_ATTR_MTIME) - tv[1] = attr->st_mtim; - - if (fi) - res = futimens(fi->fh, tv); - else { + int valid, struct fuse_file_info *fi) +{ + Inode &inode = get_inode(ino); + int ifd = inode.fd; + int res; + + if (valid & FUSE_SET_ATTR_MODE) { + if (fi) { + res = fchmod(fi->fh, attr->st_mode); + } else { + res = with_fd_path(ifd, [attr](const char *procname) { + return chmod(procname, attr->st_mode); + }); + } + if (res == -1) + goto out_err; + } + if (valid & (FUSE_SET_ATTR_UID | FUSE_SET_ATTR_GID)) { + uid_t uid = (valid & FUSE_SET_ATTR_UID) ? + attr->st_uid : + static_cast<uid_t>(-1); + gid_t gid = (valid & FUSE_SET_ATTR_GID) ? + attr->st_gid : + static_cast<gid_t>(-1); + + res = fchownat(ifd, "", uid, gid, + AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW); + if (res == -1) + goto out_err; + } + if (valid & FUSE_SET_ATTR_SIZE) { + if (fi) { + res = ftruncate(fi->fh, attr->st_size); + } else { + res = with_fd_path(ifd, [attr](const char *procname) { + return truncate(procname, attr->st_size); + }); + } + if (res == -1) + goto out_err; + } + if (valid & (FUSE_SET_ATTR_ATIME | FUSE_SET_ATTR_MTIME)) { + struct timespec tv[2]; + + tv[0].tv_sec = 0; + tv[1].tv_sec = 0; + tv[0].tv_nsec = UTIME_OMIT; + tv[1].tv_nsec = UTIME_OMIT; + + if (valid & FUSE_SET_ATTR_ATIME_NOW) + tv[0].tv_nsec = UTIME_NOW; + else if (valid & FUSE_SET_ATTR_ATIME) + tv[0] = attr->st_atim; + + if (valid & FUSE_SET_ATTR_MTIME_NOW) + tv[1].tv_nsec = UTIME_NOW; + else if (valid & FUSE_SET_ATTR_MTIME) + tv[1] = attr->st_mtim; + + if (fi) + res = futimens(fi->fh, tv); + else { #ifdef HAVE_UTIMENSAT - char procname[64]; - sprintf(procname, "/proc/self/fd/%i", ifd); - res = utimensat(AT_FDCWD, procname, tv, 0); + res = with_fd_path(ifd, [&tv](const char *procname) { + return utimensat(AT_FDCWD, procname, tv, 0); + }); #else - res = -1; - errno = EOPNOTSUPP; + res = -1; + errno = EOPNOTSUPP; #endif - } - if (res == -1) - goto out_err; - } - return sfs_getattr(req, ino, fi); + } + if (res == -1) + goto out_err; + } + return sfs_getattr(req, ino, fi); out_err: - fuse_reply_err(req, errno); + fuse_reply_err(req, errno); } - static void sfs_setattr(fuse_req_t req, fuse_ino_t ino, struct stat *attr, - int valid, fuse_file_info *fi) { - (void) ino; - do_setattr(req, ino, attr, valid, fi); + int valid, fuse_file_info *fi) +{ + (void)ino; + do_setattr(req, ino, attr, valid, fi); } - -static int do_lookup(fuse_ino_t parent, const char *name, - fuse_entry_param *e) { - if (fs.debug) - cerr << "DEBUG: lookup(): name=" << name - << ", parent=" << parent << endl; - memset(e, 0, sizeof(*e)); - e->attr_timeout = fs.timeout; - e->entry_timeout = fs.timeout; - - auto newfd = openat(get_fs_fd(parent), name, O_PATH | O_NOFOLLOW); - if (newfd == -1) - return errno; - - auto res = fstatat(newfd, "", &e->attr, AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW); - if (res == -1) { - auto saveerr = errno; - close(newfd); - if (fs.debug) - cerr << "DEBUG: lookup(): fstatat failed" << endl; - return saveerr; - } - - if (e->attr.st_dev != fs.src_dev) { - cerr << "WARNING: Mountpoints in the source directory tree will be hidden." << endl; - return ENOTSUP; - } else if (e->attr.st_ino == FUSE_ROOT_ID) { - cerr << "ERROR: Source directory tree must not include inode " - << FUSE_ROOT_ID << endl; - return EIO; - } - - SrcId id {e->attr.st_ino, e->attr.st_dev}; - unique_lock<mutex> fs_lock {fs.mutex}; - Inode* inode_p; - try { - inode_p = &fs.inodes[id]; - } catch (std::bad_alloc&) { - return ENOMEM; - } - e->ino = reinterpret_cast<fuse_ino_t>(inode_p); - Inode& inode {*inode_p}; - e->generation = inode.generation; - - if (inode.fd == -ENOENT) { // found unlinked inode - if (fs.debug) - cerr << "DEBUG: lookup(): inode " << e->attr.st_ino - << " recycled; generation=" << inode.generation << endl; - /* fallthrough to new inode but keep existing inode.nlookup */ - } - - if (inode.fd > 0) { // found existing inode - fs_lock.unlock(); - if (fs.debug) - cerr << "DEBUG: lookup(): inode " << e->attr.st_ino - << " (userspace) already known; fd = " << inode.fd << endl; - lock_guard<mutex> g {inode.m}; - - inode.nlookup++; - if (fs.debug) - cerr << "DEBUG:" << __func__ << ":" << __LINE__ << " " - << "inode " << inode.src_ino - << " count " << inode.nlookup << endl; - - - close(newfd); - } else { // no existing inode - /* This is just here to make Helgrind happy. It violates the - lock ordering requirement (inode.m must be acquired before - fs.mutex), but this is of no consequence because at this - point no other thread has access to the inode mutex */ - lock_guard<mutex> g {inode.m}; - inode.src_ino = e->attr.st_ino; - inode.src_dev = e->attr.st_dev; - - inode.nlookup++; - if (fs.debug) - cerr << "DEBUG:" << __func__ << ":" << __LINE__ << " " - << "inode " << inode.src_ino - << " count " << inode.nlookup << endl; - - inode.fd = newfd; - fs_lock.unlock(); - - if (fs.debug) - cerr << "DEBUG: lookup(): created userspace inode " << e->attr.st_ino - << "; fd = " << inode.fd << endl; - } - - return 0; +static int do_lookup(fuse_ino_t parent, const char *name, fuse_entry_param *e) +{ + if (fs.debug) + cerr << "DEBUG: lookup(): name=" << name + << ", parent=" << parent << endl; + memset(e, 0, sizeof(*e)); + e->attr_timeout = fs.timeout; + e->entry_timeout = fs.timeout; + + auto newfd = openat(get_fs_fd(parent), name, O_PATH | O_NOFOLLOW); + if (newfd == -1) + return errno; + + auto res = fstatat(newfd, "", &e->attr, + AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW); + if (res == -1) { + auto saveerr = errno; + close(newfd); + if (fs.debug) + cerr << "DEBUG: lookup(): fstatat failed" << endl; + return saveerr; + } + + if (e->attr.st_dev != fs.src_dev) { + cerr << "WARNING: Mountpoints in the source directory tree will be hidden." + << endl; + return ENOTSUP; + } else if (e->attr.st_ino == FUSE_ROOT_ID) { + cerr << "ERROR: Source directory tree must not include inode " + << FUSE_ROOT_ID << endl; + return EIO; + } + + SrcId id{ e->attr.st_ino, e->attr.st_dev }; + unique_lock<mutex> fs_lock{ fs.mutex }; + Inode *inode_p; + try { + inode_p = &fs.inodes[id]; + } catch (std::bad_alloc &) { + return ENOMEM; + } + e->ino = reinterpret_cast<fuse_ino_t>(inode_p); + Inode &inode{ *inode_p }; + e->generation = inode.generation; + + if (inode.fd == -ENOENT) { // found unlinked inode + if (fs.debug) + cerr << "DEBUG: lookup(): inode " << e->attr.st_ino + << " recycled; generation=" << inode.generation + << endl; + /* fallthrough to new inode but keep existing inode.nlookup */ + } + + if (inode.fd > 0) { // found existing inode + if (fs.debug) + cerr << "DEBUG: lookup(): inode " << e->attr.st_ino + << " (userspace) already known; fd = " << inode.fd + << endl; + + inode.nlookup++; + if (fs.debug) + cerr << "DEBUG:" << __func__ << ":" << __LINE__ << " " + << "inode " << inode.src_ino << " count " + << inode.nlookup << endl; + + fs_lock.unlock(); + close(newfd); + } else { // no existing inode + /* An unlinked inode (fd == -ENOENT) stays in fs.inodes and is + * reachable by other threads, so this is a real lock + */ + lock_guard<mutex> g{ inode.m }; + inode.src_ino = e->attr.st_ino; + inode.src_dev = e->attr.st_dev; + + inode.nlookup++; + if (fs.debug) + cerr << "DEBUG:" << __func__ << ":" << __LINE__ << " " + << "inode " << inode.src_ino << " count " + << inode.nlookup << endl; + + inode.fd = newfd; + fs_lock.unlock(); + + if (fs.debug) + cerr << "DEBUG: lookup(): created userspace inode " + << e->attr.st_ino << "; fd = " << inode.fd << endl; + } + + return 0; } - -static void sfs_lookup(fuse_req_t req, fuse_ino_t parent, const char *name) { - fuse_entry_param e {}; - auto err = do_lookup(parent, name, &e); - if (err == ENOENT) { - e.attr_timeout = fs.timeout; - e.entry_timeout = fs.timeout; - e.ino = e.attr.st_ino = 0; - fuse_reply_entry(req, &e); - } else if (err) { - if (err == ENFILE || err == EMFILE) - cerr << "ERROR: Reached maximum number of file descriptors." << endl; - fuse_reply_err(req, err); - } else { - fuse_reply_entry(req, &e); - } +static void sfs_lookup(fuse_req_t req, fuse_ino_t parent, const char *name) +{ + fuse_entry_param e{}; + auto err = do_lookup(parent, name, &e); + if (err == ENOENT) { + e.attr_timeout = fs.timeout; + e.entry_timeout = fs.timeout; + e.ino = e.attr.st_ino = 0; + fuse_reply_entry(req, &e); + } else if (err) { + if (err == ENFILE || err == EMFILE) + cerr << "ERROR: Reached maximum number of file descriptors." + << endl; + fuse_reply_err(req, err); + } else { + fuse_reply_entry(req, &e); + } } - -static void mknod_symlink(fuse_req_t req, fuse_ino_t parent, - const char *name, mode_t mode, dev_t rdev, - const char *link) { - int res; - Inode& inode_p = get_inode(parent); - auto saverr = ENOMEM; - - if (S_ISDIR(mode)) - res = mkdirat(inode_p.fd, name, mode); - else if (S_ISLNK(mode)) - res = symlinkat(link, inode_p.fd, name); - else - res = mknodat(inode_p.fd, name, mode, rdev); - saverr = errno; - if (res == -1) - goto out; - - fuse_entry_param e; - saverr = do_lookup(parent, name, &e); - if (saverr) - goto out; - - fuse_reply_entry(req, &e); - return; +static void mknod_symlink(fuse_req_t req, fuse_ino_t parent, const char *name, + mode_t mode, dev_t rdev, const char *link) +{ + int res; + Inode &inode_p = get_inode(parent); + auto saverr = ENOMEM; + + if (S_ISDIR(mode)) + res = mkdirat(inode_p.fd, name, mode); + else if (S_ISLNK(mode)) + res = symlinkat(link, inode_p.fd, name); + else + res = mknodat(inode_p.fd, name, mode, rdev); + saverr = errno; + if (res == -1) + goto out; + + fuse_entry_param e; + saverr = do_lookup(parent, name, &e); + if (saverr) + goto out; + + fuse_reply_entry(req, &e); + return; out: - if (saverr == ENFILE || saverr == EMFILE) - cerr << "ERROR: Reached maximum number of file descriptors." << endl; - fuse_reply_err(req, saverr); + if (saverr == ENFILE || saverr == EMFILE) + cerr << "ERROR: Reached maximum number of file descriptors." + << endl; + fuse_reply_err(req, saverr); } - static void sfs_mknod(fuse_req_t req, fuse_ino_t parent, const char *name, - mode_t mode, dev_t rdev) { - mknod_symlink(req, parent, name, mode, rdev, nullptr); + mode_t mode, dev_t rdev) +{ + mknod_symlink(req, parent, name, mode, rdev, nullptr); } - static void sfs_mkdir(fuse_req_t req, fuse_ino_t parent, const char *name, - mode_t mode) { - mknod_symlink(req, parent, name, S_IFDIR | mode, 0, nullptr); + mode_t mode) +{ + mknod_symlink(req, parent, name, S_IFDIR | mode, 0, nullptr); } - static void sfs_symlink(fuse_req_t req, const char *link, fuse_ino_t parent, - const char *name) { - mknod_symlink(req, parent, name, S_IFLNK, 0, link); + const char *name) +{ + mknod_symlink(req, parent, name, S_IFLNK, 0, link); } - static void sfs_link(fuse_req_t req, fuse_ino_t ino, fuse_ino_t parent, - const char *name) { - Inode& inode = get_inode(ino); - Inode& inode_p = get_inode(parent); - fuse_entry_param e {}; - - e.attr_timeout = fs.timeout; - e.entry_timeout = fs.timeout; - - char procname[64]; - sprintf(procname, "/proc/self/fd/%i", inode.fd); - auto res = linkat(AT_FDCWD, procname, inode_p.fd, name, AT_SYMLINK_FOLLOW); - if (res == -1) { - fuse_reply_err(req, errno); - return; - } - - res = fstatat(inode.fd, "", &e.attr, AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW); - if (res == -1) { - fuse_reply_err(req, errno); - return; - } - e.ino = reinterpret_cast<fuse_ino_t>(&inode); - { - lock_guard<mutex> g {inode.m}; - inode.nlookup++; - if (fs.debug) - cerr << "DEBUG:" << __func__ << ":" << __LINE__ << " " - << "inode " << inode.src_ino - << " count " << inode.nlookup << endl; - } - - fuse_reply_entry(req, &e); - return; + const char *name) +{ + Inode &inode = get_inode(ino); + Inode &inode_p = get_inode(parent); + fuse_entry_param e{}; + + e.attr_timeout = fs.timeout; + e.entry_timeout = fs.timeout; + + char procname[64]; + sprintf(procname, "/proc/self/fd/%i", inode.fd); + auto res = + linkat(AT_FDCWD, procname, inode_p.fd, name, AT_SYMLINK_FOLLOW); + if (res == -1) { + fuse_reply_err(req, errno); + return; + } + + res = fstatat(inode.fd, "", &e.attr, + AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW); + if (res == -1) { + fuse_reply_err(req, errno); + return; + } + e.ino = reinterpret_cast<fuse_ino_t>(&inode); + { + /* forget_one() reads the count under fs.mutex to decide that + * an inode is unreferenced, so raise it under fs.mutex too */ + lock_guard<mutex> g_fs{ fs.mutex }; + inode.nlookup++; + if (fs.debug) + cerr << "DEBUG:" << __func__ << ":" << __LINE__ << " " + << "inode " << inode.src_ino << " count " + << inode.nlookup << endl; + } + + fuse_reply_entry(req, &e); + return; } - -static void sfs_rmdir(fuse_req_t req, fuse_ino_t parent, const char *name) { - Inode& inode_p = get_inode(parent); - lock_guard<mutex> g {inode_p.m}; - auto res = unlinkat(inode_p.fd, name, AT_REMOVEDIR); - fuse_reply_err(req, res == -1 ? errno : 0); +static void sfs_rmdir(fuse_req_t req, fuse_ino_t parent, const char *name) +{ + Inode &inode_p = get_inode(parent); + auto res = unlinkat(inode_p.fd, name, AT_REMOVEDIR); + fuse_reply_err(req, res == -1 ? errno : 0); } - static void sfs_rename(fuse_req_t req, fuse_ino_t parent, const char *name, - fuse_ino_t newparent, const char *newname, - unsigned int flags) { - Inode& inode_p = get_inode(parent); - Inode& inode_np = get_inode(newparent); - if (flags) { - fuse_reply_err(req, EINVAL); - return; - } - - auto res = renameat(inode_p.fd, name, inode_np.fd, newname); - fuse_reply_err(req, res == -1 ? errno : 0); + fuse_ino_t newparent, const char *newname, + unsigned int flags) +{ + Inode &inode_p = get_inode(parent); + Inode &inode_np = get_inode(newparent); + if (flags) { + fuse_reply_err(req, EINVAL); + return; + } + + auto res = renameat(inode_p.fd, name, inode_np.fd, newname); + fuse_reply_err(req, res == -1 ? errno : 0); } - -static void sfs_unlink(fuse_req_t req, fuse_ino_t parent, const char *name) { - Inode& inode_p = get_inode(parent); - // Release inode.fd before last unlink like nfsd EXPORT_OP_CLOSE_BEFORE_UNLINK - // to test reused inode numbers. - // Skip this when inode has an open file and when writeback cache is enabled. - if (!fs.timeout) { - fuse_entry_param e; - auto err = do_lookup(parent, name, &e); - if (err) { - fuse_reply_err(req, err); - return; - } - if (e.attr.st_nlink == 1) { - Inode& inode = get_inode(e.ino); - lock_guard<mutex> g {inode.m}; - if (inode.fd > 0 && !inode.nopen) { - if (fs.debug) - cerr << "DEBUG: unlink: release inode " << e.attr.st_ino - << "; fd=" << inode.fd << endl; - lock_guard<mutex> g_fs {fs.mutex}; - close(inode.fd); - inode.fd = -ENOENT; - inode.generation++; - } - } - - // decrease the ref which lookup above had increased - forget_one(e.ino, 1); - } - auto res = unlinkat(inode_p.fd, name, 0); - fuse_reply_err(req, res == -1 ? errno : 0); +static void sfs_unlink(fuse_req_t req, fuse_ino_t parent, const char *name) +{ + Inode &inode_p = get_inode(parent); + // Release inode.fd before last unlink like nfsd EXPORT_OP_CLOSE_BEFORE_UNLINK + // to test reused inode numbers. + // Skip this when inode has an open file and when writeback cache is enabled. + if (!fs.timeout) { + fuse_entry_param e; + auto err = do_lookup(parent, name, &e); + if (err) { + fuse_reply_err(req, err); + return; + } + if (e.attr.st_nlink == 1) { + Inode &inode = get_inode(e.ino); + int release_fd = -1; + + { + lock_guard<mutex> g_fs{ fs.mutex }; + lock_guard<mutex> g{ inode.m }; + if (inode.fd > 0 && !inode.nopen) { + if (fs.debug) + cerr << "DEBUG: unlink: release inode " + << e.attr.st_ino + << "; fd=" << inode.fd + << endl; + release_fd = inode.fd; + inode.fd = -ENOENT; + inode.generation++; + } + } + /* closed only after the fd is unpublished, so no other + * thread can hand it to a syscall once it is reused + */ + if (release_fd != -1) + close(release_fd); + } + + // decrease the ref which lookup above had increased + forget_one(e.ino, 1); + } + auto res = unlinkat(inode_p.fd, name, 0); + fuse_reply_err(req, res == -1 ? errno : 0); } - -static void forget_one(fuse_ino_t ino, uint64_t n) { - Inode& inode = get_inode(ino); - unique_lock<mutex> l {inode.m}; - - if(n > inode.nlookup) { - cerr << "INTERNAL ERROR: Negative lookup count for inode " - << inode.src_ino << endl; - abort(); - } - inode.nlookup -= n; - - if (fs.debug) - cerr << "DEBUG:" << __func__ << ":" << __LINE__ << " " - << "inode " << inode.src_ino - << " count " << inode.nlookup << endl; - - if (!inode.nlookup) { - if (fs.debug) - cerr << "DEBUG: forget: cleaning up inode " << inode.src_ino << endl; - { - lock_guard<mutex> g_fs {fs.mutex}; - l.unlock(); - fs.inodes.erase({inode.src_ino, inode.src_dev}); - } - } else if (fs.debug) - cerr << "DEBUG: forget: inode " << inode.src_ino - << " lookup count now " << inode.nlookup << endl; +static void forget_one(fuse_ino_t ino, uint64_t n) +{ + Inode &inode = get_inode(ino); + SrcId id; + + { + lock_guard<mutex> l{ inode.m }; + + if (n > inode.nlookup) { + cerr << "INTERNAL ERROR: Negative lookup count for inode " + << inode.src_ino << endl; + abort(); + } + inode.nlookup -= n; + + if (fs.debug) + cerr << "DEBUG:" << __func__ << ":" << __LINE__ << " " + << "inode " << inode.src_ino << " count " + << inode.nlookup << endl; + + if (inode.nlookup) + return; + + id = { inode.src_ino, inode.src_dev }; + } + + /* The inode may be erased and re-created between the two locks, so + * look it up by key instead of reusing the reference and let a + * resurrecting do_lookup() win the count check + */ + lock_guard<mutex> g_fs{ fs.mutex }; + auto it = fs.inodes.find(id); + if (it == fs.inodes.end() || it->second.nlookup) + return; + + if (fs.debug) + cerr << "DEBUG: forget: cleaning up inode " << id.first << endl; + fs.inodes.erase(it); } -static void sfs_forget(fuse_req_t req, fuse_ino_t ino, uint64_t nlookup) { - forget_one(ino, nlookup); - fuse_reply_none(req); +static void sfs_forget(fuse_req_t req, fuse_ino_t ino, uint64_t nlookup) +{ + forget_one(ino, nlookup); + fuse_reply_none(req); } - static void sfs_forget_multi(fuse_req_t req, size_t count, - fuse_forget_data *forgets) { - for (int i = 0; i < count; i++) - forget_one(forgets[i].ino, forgets[i].nlookup); - fuse_reply_none(req); + fuse_forget_data *forgets) +{ + for (int i = 0; i < count; i++) + forget_one(forgets[i].ino, forgets[i].nlookup); + fuse_reply_none(req); } - -static void sfs_readlink(fuse_req_t req, fuse_ino_t ino) { - Inode& inode = get_inode(ino); - char buf[PATH_MAX + 1]; - auto res = readlinkat(inode.fd, "", buf, sizeof(buf)); - if (res == -1) - fuse_reply_err(req, errno); - else if (res == sizeof(buf)) - fuse_reply_err(req, ENAMETOOLONG); - else { - buf[res] = '\0'; - fuse_reply_readlink(req, buf); - } +static void sfs_readlink(fuse_req_t req, fuse_ino_t ino) +{ + Inode &inode = get_inode(ino); + char buf[PATH_MAX + 1]; + auto res = readlinkat(inode.fd, "", buf, sizeof(buf)); + if (res == -1) + fuse_reply_err(req, errno); + else if (res == sizeof(buf)) + fuse_reply_err(req, ENAMETOOLONG); + else { + buf[res] = '\0'; + fuse_reply_readlink(req, buf); + } } - struct DirHandle { - DIR *dp {nullptr}; - off_t offset; - - DirHandle() = default; - DirHandle(const DirHandle&) = delete; - DirHandle& operator=(const DirHandle&) = delete; - - ~DirHandle() { - if(dp) - closedir(dp); - } + DIR *dp{ nullptr }; + off_t offset; + // serialises readdir(); drop before fuse_reply_*, the reply may let + // RELEASEDIR delete this handle + std::mutex m; + + DirHandle() = default; + DirHandle(const DirHandle &) = delete; + DirHandle &operator=(const DirHandle &) = delete; + + ~DirHandle() + { + if (dp) + closedir(dp); + } }; - -static DirHandle *get_dir_handle(fuse_file_info *fi) { - return reinterpret_cast<DirHandle*>(fi->fh); +static DirHandle *get_dir_handle(fuse_file_info *fi) +{ + return reinterpret_cast<DirHandle *>(fi->fh); } - -static void sfs_opendir(fuse_req_t req, fuse_ino_t ino, fuse_file_info *fi) { - Inode& inode = get_inode(ino); - auto d = new (nothrow) DirHandle; - if (d == nullptr) { - fuse_reply_err(req, ENOMEM); - return; - } - - // Make Helgrind happy - it can't know that there's an implicit - // synchronization due to the fact that other threads cannot - // access d until we've called fuse_reply_*. - lock_guard<mutex> g {inode.m}; - - auto fd = openat(inode.fd, ".", O_RDONLY); - if (fd == -1) - goto out_errno; - - // On success, dir stream takes ownership of fd, so we - // do not have to close it. - d->dp = fdopendir(fd); - if(d->dp == nullptr) - goto out_errno; - - d->offset = 0; - - fi->fh = reinterpret_cast<uint64_t>(d); - if(fs.timeout) { - fi->keep_cache = 1; - fi->cache_readdir = 1; - } - fuse_reply_open(req, fi); - return; +static void sfs_opendir(fuse_req_t req, fuse_ino_t ino, fuse_file_info *fi) +{ + Inode &inode = get_inode(ino); + DIR *dp = nullptr; + int fd; + + auto d = new (nothrow) DirHandle; + if (d == nullptr) { + fuse_reply_err(req, ENOMEM); + return; + } + + fd = openat(inode.fd, ".", O_RDONLY); + if (fd == -1) + goto out_errno; + + // On success, dir stream takes ownership of fd, so we + // do not have to close it. + dp = fdopendir(fd); + if (dp == nullptr) + goto out_errno; + + { + // Make Helgrind happy - it can't know that there's an implicit + // synchronization due to the fact that other threads cannot + // access d until we've called fuse_reply_*. + lock_guard<mutex> g{ d->m }; + + d->dp = dp; + d->offset = 0; + } + + fi->fh = reinterpret_cast<uint64_t>(d); + if (fs.timeout) { + fi->keep_cache = 1; + fi->cache_readdir = 1; + } + fuse_reply_open(req, fi); + return; out_errno: - auto error = errno; - delete d; - if (error == ENFILE || error == EMFILE) - cerr << "ERROR: Reached maximum number of file descriptors." << endl; - fuse_reply_err(req, error); + auto error = errno; + delete d; + if (error == ENFILE || error == EMFILE) + cerr << "ERROR: Reached maximum number of file descriptors." + << endl; + fuse_reply_err(req, error); } - -static bool is_dot_or_dotdot(const char *name) { - return name[0] == '.' && - (name[1] == '\0' || (name[1] == '.' && name[2] == '\0')); +static bool is_dot_or_dotdot(const char *name) +{ + return name[0] == '.' && + (name[1] == '\0' || (name[1] == '.' && name[2] == '\0')); } - static void do_readdir(fuse_req_t req, fuse_ino_t ino, size_t size, - off_t offset, fuse_file_info *fi, const int plus) { - auto d = get_dir_handle(fi); - Inode& inode = get_inode(ino); - lock_guard<mutex> g {inode.m}; - char *p; - auto rem = size; - int err = 0, count = 0; - - if (fs.debug) - cerr << "DEBUG: readdir(): started with offset " - << offset << endl; - - auto buf = new (nothrow) char[size]; - if (!buf) { - fuse_reply_err(req, ENOMEM); - return; - } - p = buf; - - if (offset != d->offset) { - if (fs.debug) - cerr << "DEBUG: readdir(): seeking to " << offset << endl; - seekdir(d->dp, offset); - d->offset = offset; - } - - while (1) { - bool did_lookup = false; - struct dirent *entry; - errno = 0; - entry = readdir(d->dp); - if (!entry) { - if(errno) { - err = errno; - if (fs.debug) - warn("DEBUG: readdir(): readdir failed with"); - goto error; - } - break; // End of stream - } - d->offset = entry->d_off; - - fuse_entry_param e{}; - size_t entsize; - if (plus) { - if (is_dot_or_dotdot(entry->d_name)) { - /* fuse kernel ignores attributes for these and also does - * not increase lookup count (see fuse_direntplus_link) */ - e.attr.st_ino = entry->d_ino; - e.attr.st_mode = entry->d_type << 12; - } else { - err = do_lookup(ino, entry->d_name, &e); - if (err) - goto error; - did_lookup = true; - } - entsize = fuse_add_direntry_plus(req, p, rem, entry->d_name, &e, entry->d_off); - } else { - e.attr.st_ino = entry->d_ino; - e.attr.st_mode = entry->d_type << 12; - entsize = fuse_add_direntry(req, p, rem, entry->d_name, &e.attr, entry->d_off); - } - - if (entsize > rem) { - if (fs.debug) - cerr << "DEBUG: readdir(): buffer full, returning data. " << endl; - if (did_lookup) - forget_one(e.ino, 1); - break; - } - - p += entsize; - rem -= entsize; - count++; - if (fs.debug) { - cerr << "DEBUG: readdir(): added to buffer: " << entry->d_name - << ", ino " << e.attr.st_ino << ", offset " << entry->d_off << endl; - } - } - err = 0; + off_t offset, fuse_file_info *fi, const int plus) +{ + auto d = get_dir_handle(fi); + char *p; + auto rem = size; + int err = 0, count = 0; + + if (fs.debug) + cerr << "DEBUG: readdir(): started with offset " << offset + << endl; + + auto buf = new (nothrow) char[size]; + if (!buf) { + fuse_reply_err(req, ENOMEM); + return; + } + p = buf; + + unique_lock<mutex> l{ d->m }; + + if (offset != d->offset) { + if (fs.debug) + cerr << "DEBUG: readdir(): seeking to " << offset + << endl; + seekdir(d->dp, offset); + d->offset = offset; + } + + while (1) { + bool did_lookup = false; + struct dirent *entry; + errno = 0; + entry = readdir(d->dp); + if (!entry) { + if (errno) { + err = errno; + if (fs.debug) + warn("DEBUG: readdir(): readdir failed with"); + goto error; + } + break; // End of stream + } + d->offset = entry->d_off; + + fuse_entry_param e{}; + size_t entsize; + if (plus) { + if (is_dot_or_dotdot(entry->d_name)) { + /* fuse kernel ignores attributes for these and also does + * not increase lookup count (see fuse_direntplus_link) + */ + e.attr.st_ino = entry->d_ino; + e.attr.st_mode = entry->d_type << 12; + } else { + err = do_lookup(ino, entry->d_name, &e); + if (err) + goto error; + did_lookup = true; + } + entsize = fuse_add_direntry_plus( + req, p, rem, entry->d_name, &e, entry->d_off); + } else { + e.attr.st_ino = entry->d_ino; + e.attr.st_mode = entry->d_type << 12; + entsize = fuse_add_direntry(req, p, rem, entry->d_name, + &e.attr, entry->d_off); + } + + if (entsize > rem) { + if (fs.debug) + cerr << "DEBUG: readdir(): buffer full, returning data. " + << endl; + if (did_lookup) + forget_one(e.ino, 1); + break; + } + + p += entsize; + rem -= entsize; + count++; + if (fs.debug) { + cerr << "DEBUG: readdir(): added to buffer: " + << entry->d_name << ", ino " << e.attr.st_ino + << ", offset " << entry->d_off << endl; + } + } + err = 0; error: - // If there's an error, we can only signal it if we haven't stored - // any entries yet - otherwise we'd end up with wrong lookup - // counts for the entries that are already in the buffer. So we - // return what we've collected until that point. - if (err && rem == size) { - if (err == ENFILE || err == EMFILE) - cerr << "ERROR: Reached maximum number of file descriptors." << endl; - fuse_reply_err(req, err); - } else { - if (fs.debug) - cerr << "DEBUG: readdir(): returning " << count - << " entries, curr offset " << d->offset << endl; - fuse_reply_buf(req, buf, size - rem); - } - delete[] buf; - return; + // If there's an error, we can only signal it if we haven't stored + // any entries yet - otherwise we'd end up with wrong lookup + // counts for the entries that are already in the buffer. So we + // return what we've collected until that point. + if (err && rem == size) { + if (err == ENFILE || err == EMFILE) + cerr << "ERROR: Reached maximum number of file descriptors." + << endl; + l.unlock(); + fuse_reply_err(req, err); + } else { + if (fs.debug) + cerr << "DEBUG: readdir(): returning " << count + << " entries, curr offset " << d->offset << endl; + l.unlock(); + fuse_reply_buf(req, buf, size - rem); + } + delete[] buf; + return; } - static void sfs_readdir(fuse_req_t req, fuse_ino_t ino, size_t size, - off_t offset, fuse_file_info *fi) { - // operation logging is done in readdir to reduce code duplication - do_readdir(req, ino, size, offset, fi, 0); + off_t offset, fuse_file_info *fi) +{ + // operation logging is done in readdir to reduce code duplication + do_readdir(req, ino, size, offset, fi, 0); } - static void sfs_readdirplus(fuse_req_t req, fuse_ino_t ino, size_t size, - off_t offset, fuse_file_info *fi) { - // operation logging is done in readdir to reduce code duplication - do_readdir(req, ino, size, offset, fi, 1); + off_t offset, fuse_file_info *fi) +{ + // operation logging is done in readdir to reduce code duplication + do_readdir(req, ino, size, offset, fi, 1); } - -static void sfs_releasedir(fuse_req_t req, fuse_ino_t ino, fuse_file_info *fi) { - (void) ino; - auto d = get_dir_handle(fi); - delete d; - fuse_reply_err(req, 0); +static void sfs_releasedir(fuse_req_t req, fuse_ino_t ino, fuse_file_info *fi) +{ + (void)ino; + auto d = get_dir_handle(fi); + delete d; + fuse_reply_err(req, 0); } - static void do_passthrough_open(fuse_req_t req, fuse_ino_t ino, int fd, - fuse_file_info *fi) { - Inode& inode = get_inode(ino); - /* Setup a shared backing file on first open of an inode */ - if (inode.backing_id) { - if (fs.debug) - cerr << "DEBUG: reusing shared backing file " - << inode.backing_id << " for inode " << ino << endl; - fi->backing_id = inode.backing_id; - } else if (!(inode.backing_id = fuse_passthrough_open(req, fd))) { - cerr << "DEBUG: fuse_passthrough_open failed for inode " << ino - << ", disabling rw passthrough." << endl; - fs.passthrough = false; - } else { - if (fs.debug) - cerr << "DEBUG: setup shared backing file " - << inode.backing_id << " for inode " << ino << endl; - fi->backing_id = inode.backing_id; - } - /* open in passthrough mode must drop old page cache */ - if (fi->backing_id) - fi->keep_cache = false; + fuse_file_info *fi) +{ + Inode &inode = get_inode(ino); + /* Setup a shared backing file on first open of an inode */ + if (inode.backing_id) { + if (fs.debug) + cerr << "DEBUG: reusing shared backing file " + << inode.backing_id << " for inode " << ino + << endl; + fi->backing_id = inode.backing_id; + } else if (!(inode.backing_id = fuse_passthrough_open(req, fd))) { + cerr << "DEBUG: fuse_passthrough_open failed for inode " << ino + << ", disabling rw passthrough." << endl; + fs.passthrough = false; + } else { + if (fs.debug) + cerr << "DEBUG: setup shared backing file " + << inode.backing_id << " for inode " << ino + << endl; + fi->backing_id = inode.backing_id; + } + /* open in passthrough mode must drop old page cache */ + if (fi->backing_id) + fi->keep_cache = false; } static void sfs_create_open_flags(fuse_file_info *fi) { - if (fs.direct_io) - fi->direct_io = 1; - - /* - * fi->direct_io (FOPEN_DIRECT_IO) is set to benefit from - * parallel_direct_writes, which kernel cannot do for plain O_DIRECT. - * However, passthrough is preferred, but which is not possible when - * FOPEN_DIRECT_IO is set. - */ - if (!fs.passthrough) { - if (fi->flags & O_DIRECT) - fi->direct_io = 1; - } - - /* parallel_direct_writes feature depends on direct_io features. - To make parallel_direct_writes valid, need set fi->direct_io - in current function. */ - fi->parallel_direct_writes = 1; - - fi->keep_cache = (fs.timeout != 0); - fi->noflush = (fs.timeout == 0 && (fi->flags & O_ACCMODE) == O_RDONLY); + if (fs.direct_io) + fi->direct_io = 1; + + /* + * fi->direct_io (FOPEN_DIRECT_IO) is set to benefit from + * parallel_direct_writes, which kernel cannot do for plain O_DIRECT. + * However, passthrough is preferred, but which is not possible when + * FOPEN_DIRECT_IO is set. + */ + if (!fs.passthrough) { + if (fi->flags & O_DIRECT) + fi->direct_io = 1; + } + + /* parallel_direct_writes feature depends on direct_io features. + * To make parallel_direct_writes valid, need set fi->direct_io + * in current function. + */ + fi->parallel_direct_writes = 1; + + fi->keep_cache = (fs.timeout != 0); + fi->noflush = (fs.timeout == 0 && (fi->flags & O_ACCMODE) == O_RDONLY); } static void sfs_create(fuse_req_t req, fuse_ino_t parent, const char *name, - mode_t mode, fuse_file_info *fi) { - Inode& inode_p = get_inode(parent); - - auto fd = openat(inode_p.fd, name, - (fi->flags | O_CREAT) & ~O_NOFOLLOW, mode); - if (fd == -1) { - auto err = errno; - if (err == ENFILE || err == EMFILE) - cerr << "ERROR: Reached maximum number of file descriptors." << endl; - fuse_reply_err(req, err); - return; - } - - fi->fh = fd; - fuse_entry_param e; - auto err = do_lookup(parent, name, &e); - if (err) { - if (err == ENFILE || err == EMFILE) - cerr << "ERROR: Reached maximum number of file descriptors." << endl; - fuse_reply_err(req, err); - return; - } - - Inode& inode = get_inode(e.ino); - lock_guard<mutex> g {inode.m}; - inode.nopen++; - - sfs_create_open_flags(fi); - - if (fs.passthrough) - do_passthrough_open(req, e.ino, fd, fi); - fuse_reply_create(req, &e, fi); + mode_t mode, fuse_file_info *fi) +{ + Inode &inode_p = get_inode(parent); + + auto fd = openat(inode_p.fd, name, (fi->flags | O_CREAT) & ~O_NOFOLLOW, + mode); + if (fd == -1) { + auto err = errno; + if (err == ENFILE || err == EMFILE) + cerr << "ERROR: Reached maximum number of file descriptors." + << endl; + fuse_reply_err(req, err); + return; + } + + fi->fh = fd; + fuse_entry_param e; + auto err = do_lookup(parent, name, &e); + if (err) { + if (err == ENFILE || err == EMFILE) + cerr << "ERROR: Reached maximum number of file descriptors." + << endl; + fuse_reply_err(req, err); + return; + } + + Inode &inode = get_inode(e.ino); + { + lock_guard<mutex> g{ inode.m }; + inode.nopen++; + + sfs_create_open_flags(fi); + + if (fs.passthrough) + do_passthrough_open(req, e.ino, fd, fi); + } + fuse_reply_create(req, &e, fi); } +#ifdef O_TMPFILE static Inode *create_new_inode(int fd, fuse_entry_param *e) { - memset(e, 0, sizeof(*e)); - e->attr_timeout = fs.timeout; - e->entry_timeout = fs.timeout; - - auto res = fstatat(fd, "", &e->attr, AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW); - if (res == -1) { - if (fs.debug) - cerr << "DEBUG: lookup(): fstatat failed" << endl; - return NULL; - } - - SrcId id {e->attr.st_ino, e->attr.st_dev}; - unique_lock<mutex> fs_lock {fs.mutex}; - Inode* p_inode; - try { - p_inode = &fs.inodes[id]; - } catch (std::bad_alloc&) { - return NULL; - } - - e->ino = reinterpret_cast<fuse_ino_t>(p_inode); - e->generation = p_inode->generation; - - lock_guard<mutex> g {p_inode->m}; - p_inode->src_ino = e->attr.st_ino; - p_inode->src_dev = e->attr.st_dev; - - p_inode->nlookup++; - if (fs.debug) - cerr << "DEBUG:" << __func__ << ":" << __LINE__ << " " - << "inode " << p_inode->src_ino - << " count " << p_inode->nlookup << endl; - - p_inode->fd = fd; - fs_lock.unlock(); - - if (fs.debug) - cerr << "DEBUG: lookup(): created userspace inode " << e->attr.st_ino - << "; fd = " << p_inode->fd << endl; - return p_inode; -} - -static void sfs_tmpfile(fuse_req_t req, fuse_ino_t parent, - mode_t mode, fuse_file_info *fi) { - Inode& parent_inode = get_inode(parent); - - auto fd = openat(parent_inode.fd, ".", - (fi->flags | O_TMPFILE) & ~O_NOFOLLOW, mode); - if (fd == -1) { - auto err = errno; - if (err == ENFILE || err == EMFILE) - cerr << "ERROR: Reached maximum number of file descriptors." << endl; - fuse_reply_err(req, err); - return; - } - - fi->fh = fd; - fuse_entry_param e; - - Inode *inode = create_new_inode(dup(fd), &e); - if (inode == NULL) { - auto err = errno; - cerr << "ERROR: could not create new inode." << endl; - close(fd); - fuse_reply_err(req, err); - return; - } - - lock_guard<mutex> g {inode->m}; - - sfs_create_open_flags(fi); - - if (fs.passthrough) - do_passthrough_open(req, e.ino, fd, fi); - - fuse_reply_create(req, &e, fi); + memset(e, 0, sizeof(*e)); + e->attr_timeout = fs.timeout; + e->entry_timeout = fs.timeout; + + auto res = + fstatat(fd, "", &e->attr, AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW); + if (res == -1) { + if (fs.debug) + cerr << "DEBUG: lookup(): fstatat failed" << endl; + return NULL; + } + + SrcId id{ e->attr.st_ino, e->attr.st_dev }; + unique_lock<mutex> fs_lock{ fs.mutex }; + Inode *p_inode; + try { + p_inode = &fs.inodes[id]; + } catch (std::bad_alloc &) { + return NULL; + } + + e->ino = reinterpret_cast<fuse_ino_t>(p_inode); + e->generation = p_inode->generation; + + lock_guard<mutex> g{ p_inode->m }; + p_inode->src_ino = e->attr.st_ino; + p_inode->src_dev = e->attr.st_dev; + + p_inode->nlookup++; + if (fs.debug) + cerr << "DEBUG:" << __func__ << ":" << __LINE__ << " " + << "inode " << p_inode->src_ino << " count " + << p_inode->nlookup << endl; + + p_inode->fd = fd; + fs_lock.unlock(); + + if (fs.debug) + cerr << "DEBUG: lookup(): created userspace inode " + << e->attr.st_ino << "; fd = " << p_inode->fd << endl; + return p_inode; } - +static void sfs_tmpfile(fuse_req_t req, fuse_ino_t parent, mode_t mode, + fuse_file_info *fi) +{ + Inode &parent_inode = get_inode(parent); + + auto fd = openat(parent_inode.fd, ".", + (fi->flags | O_TMPFILE) & ~O_NOFOLLOW, mode); + if (fd == -1) { + auto err = errno; + if (err == ENFILE || err == EMFILE) + cerr << "ERROR: Reached maximum number of file descriptors." + << endl; + fuse_reply_err(req, err); + return; + } + + fi->fh = fd; + fuse_entry_param e; + + Inode *inode = create_new_inode(dup(fd), &e); + if (inode == NULL) { + auto err = errno; + cerr << "ERROR: could not create new inode." << endl; + close(fd); + fuse_reply_err(req, err); + return; + } + + { + lock_guard<mutex> g{ inode->m }; + + sfs_create_open_flags(fi); + + if (fs.passthrough) + do_passthrough_open(req, e.ino, fd, fi); + } + + fuse_reply_create(req, &e, fi); +} +#endif static void sfs_fsyncdir(fuse_req_t req, fuse_ino_t ino, int datasync, - fuse_file_info *fi) { - (void) ino; - int res; - int fd = dirfd(get_dir_handle(fi)->dp); - if (datasync) - res = fdatasync(fd); - else - res = fsync(fd); - fuse_reply_err(req, res == -1 ? errno : 0); + fuse_file_info *fi) +{ + (void)ino; + int res; + int fd = dirfd(get_dir_handle(fi)->dp); + if (datasync) + res = fdatasync(fd); + else + res = fsync(fd); + fuse_reply_err(req, res == -1 ? errno : 0); } +static void sfs_open(fuse_req_t req, fuse_ino_t ino, fuse_file_info *fi) +{ + Inode &inode = get_inode(ino); -static void sfs_open(fuse_req_t req, fuse_ino_t ino, fuse_file_info *fi) { - Inode& inode = get_inode(ino); - - /* With writeback cache, kernel may send read requests even + /* With writeback cache, kernel may send read requests even when userspace opened write-only */ - if (fs.timeout && (fi->flags & O_ACCMODE) == O_WRONLY) { - fi->flags &= ~O_ACCMODE; - fi->flags |= O_RDWR; - } - - /* With writeback cache, O_APPEND is handled by the kernel. This - breaks atomicity (since the file may change in the underlying - filesystem, so that the kernel's idea of the end of the file - isn't accurate anymore). However, no process should modify the - file in the underlying filesystem once it has been read, so - this is not a problem. */ - if (fs.timeout && fi->flags & O_APPEND) - fi->flags &= ~O_APPEND; - - /* Unfortunately we cannot use inode.fd, because this was opened + if (fs.timeout && (fi->flags & O_ACCMODE) == O_WRONLY) { + fi->flags &= ~O_ACCMODE; + fi->flags |= O_RDWR; + } + + /* With writeback cache, O_APPEND is handled by the kernel. This + * breaks atomicity (since the file may change in the underlying + * filesystem, so that the kernel's idea of the end of the file + * isn't accurate anymore). However, no process should modify the + * file in the underlying filesystem once it has been read, so + * this is not a problem. + */ + if (fs.timeout && fi->flags & O_APPEND) + fi->flags &= ~O_APPEND; + + /* Unfortunately we cannot use inode.fd, because this was opened with O_PATH (so it doesn't allow read/write access). */ - char buf[64]; - sprintf(buf, "/proc/self/fd/%i", inode.fd); - auto fd = open(buf, fi->flags & ~O_NOFOLLOW); - if (fd == -1) { - auto err = errno; - if (err == ENFILE || err == EMFILE) - cerr << "ERROR: Reached maximum number of file descriptors." << endl; - fuse_reply_err(req, err); - return; - } - - lock_guard<mutex> g {inode.m}; - inode.nopen++; - - sfs_create_open_flags(fi); - - fi->fh = fd; - if (fs.passthrough) - do_passthrough_open(req, ino, fd, fi); - fuse_reply_open(req, fi); + auto fd = with_fd_path(inode.fd, [fi](const char *buf) { + return open(buf, fi->flags & ~O_NOFOLLOW); + }); + if (fd == -1) { + auto err = errno; + if (err == ENFILE || err == EMFILE) + cerr << "ERROR: Reached maximum number of file descriptors." + << endl; + fuse_reply_err(req, err); + return; + } + + { + lock_guard<mutex> g{ inode.m }; + inode.nopen++; + + sfs_create_open_flags(fi); + + fi->fh = fd; + if (fs.passthrough) + do_passthrough_open(req, ino, fd, fi); + } + fuse_reply_open(req, fi); } - -static void sfs_release(fuse_req_t req, fuse_ino_t ino, fuse_file_info *fi) { - Inode& inode = get_inode(ino); - lock_guard<mutex> g {inode.m}; - inode.nopen--; - - /* Close the shared backing file on last file close of an inode */ - if (inode.backing_id && !inode.nopen) { - if (fuse_passthrough_close(req, inode.backing_id) < 0) { - cerr << "DEBUG: fuse_passthrough_close failed for inode " - << ino << " backing file " << inode.backing_id << endl; - } else if (fs.debug) { - cerr << "DEBUG: closed backing file " << inode.backing_id - << " for inode " << ino << endl; - } - inode.backing_id = 0; - } - - close(fi->fh); - fuse_reply_err(req, 0); +static void sfs_release(fuse_req_t req, fuse_ino_t ino, fuse_file_info *fi) +{ + Inode &inode = get_inode(ino); + int backing_id = 0; + + { + lock_guard<mutex> g{ inode.m }; + inode.nopen--; + + /* Close the shared backing file on last file close of an inode */ + if (inode.backing_id && !inode.nopen) { + backing_id = inode.backing_id; + inode.backing_id = 0; + } + } + + if (backing_id) { + if (fuse_passthrough_close(req, backing_id) < 0) { + cerr << "DEBUG: fuse_passthrough_close failed for inode " + << ino << " backing file " << backing_id << endl; + } else if (fs.debug) { + cerr << "DEBUG: closed backing file " << backing_id + << " for inode " << ino << endl; + } + } + + close(fi->fh); + fuse_reply_err(req, 0); } - -static void sfs_flush(fuse_req_t req, fuse_ino_t ino, fuse_file_info *fi) { - (void) ino; - auto res = close(dup(fi->fh)); - fuse_reply_err(req, res == -1 ? errno : 0); +static void sfs_flush(fuse_req_t req, fuse_ino_t ino, fuse_file_info *fi) +{ + (void)ino; + auto res = close(dup(fi->fh)); + fuse_reply_err(req, res == -1 ? errno : 0); } - static void sfs_fsync(fuse_req_t req, fuse_ino_t ino, int datasync, - fuse_file_info *fi) { - (void) ino; - int res; - if (datasync) - res = fdatasync(fi->fh); - else - res = fsync(fi->fh); - fuse_reply_err(req, res == -1 ? errno : 0); + fuse_file_info *fi) +{ + (void)ino; + int res; + if (datasync) + res = fdatasync(fi->fh); + else + res = fsync(fi->fh); + fuse_reply_err(req, res == -1 ? errno : 0); } - -static void do_read(fuse_req_t req, size_t size, off_t off, fuse_file_info *fi) { - - fuse_bufvec buf = FUSE_BUFVEC_INIT(size); - buf.buf[0].flags = static_cast<fuse_buf_flags>( - FUSE_BUF_IS_FD | FUSE_BUF_FD_SEEK); - buf.buf[0].fd = fi->fh; - buf.buf[0].pos = off; - - fuse_reply_data(req, &buf, FUSE_BUF_COPY_FLAGS); +static void do_read(fuse_req_t req, size_t size, off_t off, fuse_file_info *fi) +{ + fuse_bufvec buf = FUSE_BUFVEC_INIT(size); + char *payload = NULL; + size_t payload_size = 0; + int res = fuse_req_get_payload(req, &payload, &payload_size, NULL); + + /* + * This is a demonstration how to use io-uring payload. For FUSE_BUF_IS_FD + * it shouldn't make much of a difference because fuse_reply_data() -> + * fuse_reply_data_uring() also has access to the payload and will + * read directly from the FD into the payload. + * It is more useful for file systems that need a buffer for decryption, + * decompression, etc. + */ + if (res == 0) { + /* This is an io-uring request - write directly to the payload */ + assert(payload_size >= size); + + buf.buf[0].mem = payload; + buf.buf[0].size = payload_size; + + res = pread(fi->fh, payload, size, off); + if (res < 0) { + fuse_reply_err(req, errno); + return; + } + + buf.buf[0].size = res; + } else { + buf.buf[0].flags = static_cast<fuse_buf_flags>( + FUSE_BUF_IS_FD | FUSE_BUF_FD_SEEK); + buf.buf[0].fd = fi->fh; + buf.buf[0].pos = off; + } + + fuse_reply_data(req, &buf, FUSE_BUF_COPY_FLAGS); } static void sfs_read(fuse_req_t req, fuse_ino_t ino, size_t size, off_t off, - fuse_file_info *fi) { - (void) ino; - if (fs.passthrough && !fs.direct_io) { - cerr << "ERROR: fuse_passthrough read failed." << endl; - fuse_reply_err(req, EIO); - return; - } - do_read(req, size, off, fi); + fuse_file_info *fi) +{ + (void)ino; + if (fs.passthrough && !fs.direct_io) { + cerr << "ERROR: fuse_passthrough read failed." << endl; + fuse_reply_err(req, EIO); + return; + } + do_read(req, size, off, fi); } - static void do_write_buf(fuse_req_t req, size_t size, off_t off, - fuse_bufvec *in_buf, fuse_file_info *fi) { - fuse_bufvec out_buf = FUSE_BUFVEC_INIT(size); - out_buf.buf[0].flags = static_cast<fuse_buf_flags>( - FUSE_BUF_IS_FD | FUSE_BUF_FD_SEEK); - out_buf.buf[0].fd = fi->fh; - out_buf.buf[0].pos = off; - - auto res = fuse_buf_copy(&out_buf, in_buf, FUSE_BUF_COPY_FLAGS); - if (res < 0) - fuse_reply_err(req, -res); - else - fuse_reply_write(req, (size_t)res); + fuse_bufvec *in_buf, fuse_file_info *fi) +{ + fuse_bufvec out_buf = FUSE_BUFVEC_INIT(size); + out_buf.buf[0].flags = + static_cast<fuse_buf_flags>(FUSE_BUF_IS_FD | FUSE_BUF_FD_SEEK); + out_buf.buf[0].fd = fi->fh; + out_buf.buf[0].pos = off; + + auto res = fuse_buf_copy(&out_buf, in_buf, FUSE_BUF_COPY_FLAGS); + if (res < 0) + fuse_reply_err(req, -res); + else + fuse_reply_write(req, (size_t)res); } - static void sfs_write_buf(fuse_req_t req, fuse_ino_t ino, fuse_bufvec *in_buf, - off_t off, fuse_file_info *fi) { - (void) ino; - if (fs.passthrough && !fs.direct_io) { - cerr << "ERROR: fuse_passthrough write failed." << endl; - fuse_reply_err(req, EIO); - return; - } - auto size {fuse_buf_size(in_buf)}; - do_write_buf(req, size, off, in_buf, fi); + off_t off, fuse_file_info *fi) +{ + (void)ino; + if (fs.passthrough && !fs.direct_io) { + cerr << "ERROR: fuse_passthrough write failed." << endl; + fuse_reply_err(req, EIO); + return; + } + auto size{ fuse_buf_size(in_buf) }; + do_write_buf(req, size, off, in_buf, fi); } +static void sfs_statfs(fuse_req_t req, fuse_ino_t ino) +{ + struct statvfs stbuf; -static void sfs_statfs(fuse_req_t req, fuse_ino_t ino) { - struct statvfs stbuf; - - auto res = fstatvfs(get_fs_fd(ino), &stbuf); - if (res == -1) - fuse_reply_err(req, errno); - else - fuse_reply_statfs(req, &stbuf); + auto res = fstatvfs(get_fs_fd(ino), &stbuf); + if (res == -1) + fuse_reply_err(req, errno); + else + fuse_reply_statfs(req, &stbuf); } - -#ifdef HAVE_POSIX_FALLOCATE static void sfs_fallocate(fuse_req_t req, fuse_ino_t ino, int mode, - off_t offset, off_t length, fuse_file_info *fi) { - (void) ino; - if (mode) { - fuse_reply_err(req, EOPNOTSUPP); - return; - } - - auto err = posix_fallocate(fi->fh, offset, length); - fuse_reply_err(req, err); + off_t offset, off_t length, fuse_file_info *fi) +{ + (void)ino; + + auto err = -do_fallocate(fi->fh, mode, offset, length); + + fuse_reply_err(req, err); } -#endif static void sfs_flock(fuse_req_t req, fuse_ino_t ino, fuse_file_info *fi, - int op) { - (void) ino; - auto res = flock(fi->fh, op); - fuse_reply_err(req, res == -1 ? errno : 0); + int op) +{ + (void)ino; + auto res = flock(fi->fh, op); + fuse_reply_err(req, res == -1 ? errno : 0); } - #ifdef HAVE_SETXATTR static void sfs_getxattr(fuse_req_t req, fuse_ino_t ino, const char *name, - size_t size) { - char *value = nullptr; - Inode& inode = get_inode(ino); - ssize_t ret; - int saverr; - - char procname[64]; - sprintf(procname, "/proc/self/fd/%i", inode.fd); - - if (size) { - value = new (nothrow) char[size]; - if (value == nullptr) { - saverr = ENOMEM; - goto out; - } - - ret = getxattr(procname, name, value, size); - if (ret == -1) - goto out_err; - saverr = 0; - if (ret == 0) - goto out; - - fuse_reply_buf(req, value, ret); - } else { - ret = getxattr(procname, name, nullptr, 0); - if (ret == -1) - goto out_err; - - fuse_reply_xattr(req, ret); - } + size_t size) +{ + char *value = nullptr; + Inode &inode = get_inode(ino); + ssize_t ret; + int saverr; + + char procname[64]; + sprintf(procname, "/proc/self/fd/%i", inode.fd); + + if (size) { + value = new (nothrow) char[size]; + if (value == nullptr) { + saverr = ENOMEM; + goto out; + } + + ret = getxattr(procname, name, value, size); + if (ret == -1) + goto out_err; + saverr = 0; + if (ret == 0) + goto out; + + fuse_reply_buf(req, value, ret); + } else { + ret = getxattr(procname, name, nullptr, 0); + if (ret == -1) + goto out_err; + + fuse_reply_xattr(req, ret); + } out_free: - delete[] value; - return; + delete[] value; + return; out_err: - saverr = errno; + saverr = errno; out: - fuse_reply_err(req, saverr); - goto out_free; + fuse_reply_err(req, saverr); + goto out_free; } - -static void sfs_listxattr(fuse_req_t req, fuse_ino_t ino, size_t size) { - char *value = nullptr; - Inode& inode = get_inode(ino); - ssize_t ret; - int saverr; - - char procname[64]; - sprintf(procname, "/proc/self/fd/%i", inode.fd); - - if (size) { - value = new (nothrow) char[size]; - if (value == nullptr) { - saverr = ENOMEM; - goto out; - } - - ret = listxattr(procname, value, size); - if (ret == -1) - goto out_err; - saverr = 0; - if (ret == 0) - goto out; - - fuse_reply_buf(req, value, ret); - } else { - ret = listxattr(procname, nullptr, 0); - if (ret == -1) - goto out_err; - - fuse_reply_xattr(req, ret); - } +static void sfs_listxattr(fuse_req_t req, fuse_ino_t ino, size_t size) +{ + char *value = nullptr; + Inode &inode = get_inode(ino); + ssize_t ret; + int saverr; + + char procname[64]; + sprintf(procname, "/proc/self/fd/%i", inode.fd); + + if (size) { + value = new (nothrow) char[size]; + if (value == nullptr) { + saverr = ENOMEM; + goto out; + } + + ret = listxattr(procname, value, size); + if (ret == -1) + goto out_err; + saverr = 0; + if (ret == 0) + goto out; + + fuse_reply_buf(req, value, ret); + } else { + ret = listxattr(procname, nullptr, 0); + if (ret == -1) + goto out_err; + + fuse_reply_xattr(req, ret); + } out_free: - delete[] value; - return; + delete[] value; + return; out_err: - saverr = errno; + saverr = errno; out: - fuse_reply_err(req, saverr); - goto out_free; + fuse_reply_err(req, saverr); + goto out_free; } - static void sfs_setxattr(fuse_req_t req, fuse_ino_t ino, const char *name, - const char *value, size_t size, int flags) { - Inode& inode = get_inode(ino); - ssize_t ret; - int saverr; + const char *value, size_t size, int flags) +{ + Inode &inode = get_inode(ino); + ssize_t ret; + int saverr; - char procname[64]; - sprintf(procname, "/proc/self/fd/%i", inode.fd); + char procname[64]; + sprintf(procname, "/proc/self/fd/%i", inode.fd); - ret = setxattr(procname, name, value, size, flags); - saverr = ret == -1 ? errno : 0; + ret = setxattr(procname, name, value, size, flags); + saverr = ret == -1 ? errno : 0; - fuse_reply_err(req, saverr); + fuse_reply_err(req, saverr); } +static void sfs_removexattr(fuse_req_t req, fuse_ino_t ino, const char *name) +{ + char procname[64]; + Inode &inode = get_inode(ino); + ssize_t ret; + int saverr; -static void sfs_removexattr(fuse_req_t req, fuse_ino_t ino, const char *name) { - char procname[64]; - Inode& inode = get_inode(ino); - ssize_t ret; - int saverr; - - sprintf(procname, "/proc/self/fd/%i", inode.fd); - ret = removexattr(procname, name); - saverr = ret == -1 ? errno : 0; + sprintf(procname, "/proc/self/fd/%i", inode.fd); + ret = removexattr(procname, name); + saverr = ret == -1 ? errno : 0; - fuse_reply_err(req, saverr); + fuse_reply_err(req, saverr); } #endif - -static void assign_operations(fuse_lowlevel_ops &sfs_oper) { - sfs_oper.init = sfs_init; - sfs_oper.lookup = sfs_lookup; - sfs_oper.mkdir = sfs_mkdir; - sfs_oper.mknod = sfs_mknod; - sfs_oper.symlink = sfs_symlink; - sfs_oper.link = sfs_link; - sfs_oper.unlink = sfs_unlink; - sfs_oper.rmdir = sfs_rmdir; - sfs_oper.rename = sfs_rename; - sfs_oper.forget = sfs_forget; - sfs_oper.forget_multi = sfs_forget_multi; - sfs_oper.getattr = sfs_getattr; - sfs_oper.setattr = sfs_setattr; - sfs_oper.readlink = sfs_readlink; - sfs_oper.opendir = sfs_opendir; - sfs_oper.readdir = sfs_readdir; - sfs_oper.readdirplus = sfs_readdirplus; - sfs_oper.releasedir = sfs_releasedir; - sfs_oper.fsyncdir = sfs_fsyncdir; - sfs_oper.create = sfs_create; - sfs_oper.tmpfile = sfs_tmpfile; - sfs_oper.open = sfs_open; - sfs_oper.release = sfs_release; - sfs_oper.flush = sfs_flush; - sfs_oper.fsync = sfs_fsync; - sfs_oper.read = sfs_read; - sfs_oper.write_buf = sfs_write_buf; - sfs_oper.statfs = sfs_statfs; -#ifdef HAVE_POSIX_FALLOCATE - sfs_oper.fallocate = sfs_fallocate; +static void assign_operations(fuse_lowlevel_ops &sfs_oper) +{ + sfs_oper.init = sfs_init; + sfs_oper.lookup = sfs_lookup; + sfs_oper.mkdir = sfs_mkdir; + sfs_oper.mknod = sfs_mknod; + sfs_oper.symlink = sfs_symlink; + sfs_oper.link = sfs_link; + sfs_oper.unlink = sfs_unlink; + sfs_oper.rmdir = sfs_rmdir; + sfs_oper.rename = sfs_rename; + sfs_oper.forget = sfs_forget; + sfs_oper.forget_multi = sfs_forget_multi; + sfs_oper.getattr = sfs_getattr; + sfs_oper.setattr = sfs_setattr; + sfs_oper.readlink = sfs_readlink; + sfs_oper.opendir = sfs_opendir; + sfs_oper.readdir = sfs_readdir; + sfs_oper.readdirplus = sfs_readdirplus; + sfs_oper.releasedir = sfs_releasedir; + sfs_oper.fsyncdir = sfs_fsyncdir; + sfs_oper.create = sfs_create; +#ifdef O_TMPFILE + sfs_oper.tmpfile = sfs_tmpfile; #endif - sfs_oper.flock = sfs_flock; + sfs_oper.open = sfs_open; + sfs_oper.release = sfs_release; + sfs_oper.flush = sfs_flush; + sfs_oper.fsync = sfs_fsync; + sfs_oper.read = sfs_read; + sfs_oper.write_buf = sfs_write_buf; + sfs_oper.statfs = sfs_statfs; + sfs_oper.fallocate = sfs_fallocate; + sfs_oper.flock = sfs_flock; #ifdef HAVE_SETXATTR - sfs_oper.setxattr = sfs_setxattr; - sfs_oper.getxattr = sfs_getxattr; - sfs_oper.listxattr = sfs_listxattr; - sfs_oper.removexattr = sfs_removexattr; + sfs_oper.setxattr = sfs_setxattr; + sfs_oper.getxattr = sfs_getxattr; + sfs_oper.listxattr = sfs_listxattr; + sfs_oper.removexattr = sfs_removexattr; #endif } -static void print_usage(char *prog_name) { - cout << "Usage: " << prog_name << " --help\n" - << " " << prog_name << " [options] <source> <mountpoint>\n"; +static void print_usage(char *prog_name) +{ + cout << "Usage: " << prog_name << " --help\n" + << " " << prog_name << " [options] <source> <mountpoint>\n"; } -static cxxopts::ParseResult parse_wrapper(cxxopts::Options& parser, int& argc, char**& argv) { - try { - return parser.parse(argc, argv); - } catch (cxxopts::option_not_exists_exception& exc) { - std::cout << argv[0] << ": " << exc.what() << std::endl; - print_usage(argv[0]); - exit(2); - } +static cxxopts::ParseResult parse_wrapper(cxxopts::Options &parser, int &argc, + char **&argv) +{ + try { + return parser.parse(argc, argv); + } catch (cxxopts::option_not_exists_exception &exc) { + std::cout << argv[0] << ": " << exc.what() << std::endl; + print_usage(argv[0]); + exit(2); + } } +static void string_split(std::string s, std::vector<std::string> &out, + std::string delimiter) +{ + size_t pos_start = 0, pos_end, delim_len = delimiter.length(); + std::string token; -static void string_split(std::string s, std::vector<std::string>& out, std::string delimiter) { - size_t pos_start = 0, pos_end, delim_len = delimiter.length(); - std::string token; - - while ((pos_end = s.find(delimiter, pos_start)) != std::string::npos) { - token = s.substr(pos_start, pos_end - pos_start); - pos_start = pos_end + delim_len; - out.push_back(token); - } + while ((pos_end = s.find(delimiter, pos_start)) != std::string::npos) { + token = s.substr(pos_start, pos_end - pos_start); + pos_start = pos_end + delim_len; + out.push_back(token); + } - out.push_back(s.substr(pos_start)); + out.push_back(s.substr(pos_start)); } - -static std::string string_join(const std::vector<std::string>& elems, char delim) +static std::string string_join(const std::vector<std::string> &elems, + char delim) { - std::ostringstream out; - for (auto ii = elems.begin(); ii != elems.end(); ++ii) { - out << (*ii); - if (ii + 1 != elems.end()) { - out << delim; - } - } - return out.str(); + std::ostringstream out; + for (auto ii = elems.begin(); ii != elems.end(); ++ii) { + out << (*ii); + if (ii + 1 != elems.end()) { + out << delim; + } + } + return out.str(); } - -static cxxopts::ParseResult parse_options(int argc, char **argv) { - cxxopts::Options opt_parser(argv[0]); - std::vector<std::string> mount_options; - opt_parser.add_options() - ("debug", "Enable filesystem debug messages") - ("debug-fuse", "Enable libfuse debug messages") - ("foreground", "Run in foreground") - ("help", "Print help") - ("nocache", "Disable attribute all caching") - ("nosplice", "Do not use splice(2) to transfer data") - ("nopassthrough", "Do not use pass-through mode for read/write") - ("single", "Run single-threaded") - ("o", "Mount options (see mount.fuse(5) - only use if you know what " - "you are doing)", cxxopts::value(mount_options)) - ("num-threads", "Number of libfuse worker threads", - cxxopts::value<int>()->default_value(SFS_DEFAULT_THREADS)) - ("clone-fd", "use separate fuse device fd for each thread") - ("direct-io", "enable fuse kernel internal direct-io"); - - // FIXME: Find a better way to limit the try clause to just - // opt_parser.parse() (cf. https://github.com/jarro2783/cxxopts/issues/146) - auto options = parse_wrapper(opt_parser, argc, argv); - - if (options.count("help")) { - print_usage(argv[0]); - // Strip everything before the option list from the - // default help string. - auto help = opt_parser.help(); - std::cout << std::endl << "options:" - << help.substr(help.find("\n\n") + 1, string::npos); - exit(0); - - } else if (argc != 3) { - std::cout << argv[0] << ": invalid number of arguments\n"; - print_usage(argv[0]); - exit(2); - } - - fs.debug = options.count("debug") != 0; - fs.debug_fuse = options.count("debug-fuse") != 0; - - fs.foreground = options.count("foreground") != 0; - if (fs.debug || fs.debug_fuse) - fs.foreground = true; - - fs.nosplice = options.count("nosplice") != 0; - fs.passthrough = options.count("nopassthrough") == 0; - fs.num_threads = options["num-threads"].as<int>(); - fs.clone_fd = options.count("clone-fd"); - fs.direct_io = options.count("direct-io"); - char* resolved_path = realpath(argv[1], NULL); - if (resolved_path == NULL) - warn("WARNING: realpath() failed with"); - fs.source = std::string {resolved_path}; - free(resolved_path); - - std::vector<std::string> flattened_mount_opts; - for (auto opt : mount_options) { - string_split(opt, flattened_mount_opts, ","); - } - - bool found_fsname = false; - for (auto opt : flattened_mount_opts) { - if (opt.find("fsname=") == 0) { - found_fsname = true; - continue; - } - - /* Filter out some obviously incorrect options. */ - if (opt == "fd") { - std::cout << argv[0] << ": Unsupported mount option: " << opt << "\n"; - print_usage(argv[0]); - exit(2); - } - } - if (!found_fsname) { - flattened_mount_opts.push_back("fsname=" + fs.source); - } - flattened_mount_opts.push_back("default_permissions"); - fs.fuse_mount_options = string_join(flattened_mount_opts, ','); - return options; +static cxxopts::ParseResult parse_options(int argc, char **argv) +{ + cxxopts::Options opt_parser(argv[0]); + std::vector<std::string> mount_options; + opt_parser.add_options()("debug", "Enable filesystem debug messages")( + "debug-fuse", "Enable libfuse debug messages")( + "foreground", "Run in foreground")("help", "Print help")( + "nocache", "Disable attribute all caching")( + "nosplice", "Do not use splice(2) to transfer data")( + "nopassthrough", "Do not use pass-through mode for read/write")( + "single", "Run single-threaded")( + "o", + "Mount options (see mount.fuse(5) - only use if you know what " + "you are doing)", + cxxopts::value(mount_options))( + "num-threads", "Number of libfuse worker threads", + cxxopts::value<int>()->default_value(SFS_DEFAULT_THREADS))( + "clone-fd", "use separate fuse device fd for each thread")( + "direct-io", "enable fuse kernel internal direct-io"); + + // FIXME: Find a better way to limit the try clause to just + // opt_parser.parse() (cf. https://github.com/jarro2783/cxxopts/issues/146) + auto options = parse_wrapper(opt_parser, argc, argv); + + if (options.count("help")) { + print_usage(argv[0]); + // Strip everything before the option list from the + // default help string. + auto help = opt_parser.help(); + std::cout << std::endl + << "options:" + << help.substr(help.find("\n\n") + 1, string::npos); + std::cout << "\nFuse lowlevel options:\n"; + fuse_lowlevel_help(); + exit(0); + + } else if (argc != 3) { + std::cout << argv[0] << ": invalid number of arguments\n"; + print_usage(argv[0]); + exit(2); + } + + fs.debug = options.count("debug") != 0; + fs.debug_fuse = options.count("debug-fuse") != 0; + + fs.foreground = options.count("foreground") != 0; + if (fs.debug || fs.debug_fuse) + fs.foreground = true; + + fs.nosplice = options.count("nosplice") != 0; + fs.passthrough = options.count("nopassthrough") == 0; + fs.num_threads = options["num-threads"].as<int>(); + fs.clone_fd = options.count("clone-fd"); + fs.direct_io = options.count("direct-io"); + + char *resolved_path = realpath(argv[1], NULL); + if (resolved_path == NULL) + warn("WARNING: realpath() failed with"); + fs.source = std::string{ resolved_path }; + free(resolved_path); + + std::vector<std::string> flattened_mount_opts; + for (auto opt : mount_options) { + string_split(opt, flattened_mount_opts, ","); + } + + bool found_fsname = false; + for (auto opt : flattened_mount_opts) { + if (opt.find("fsname=") == 0) { + found_fsname = true; + continue; + } + + /* Filter out some obviously incorrect options. */ + if (opt == "fd") { + std::cout << argv[0] + << ": Unsupported mount option: " << opt + << "\n"; + print_usage(argv[0]); + exit(2); + } + } + if (!found_fsname) { + flattened_mount_opts.push_back("fsname=" + fs.source); + } + flattened_mount_opts.push_back("default_permissions"); + fs.fuse_mount_options = string_join(flattened_mount_opts, ','); + return options; } - -static void maximize_fd_limit() { - struct rlimit lim {}; - auto res = getrlimit(RLIMIT_NOFILE, &lim); - if (res != 0) { - warn("WARNING: getrlimit() failed with"); - return; - } - lim.rlim_cur = lim.rlim_max; - res = setrlimit(RLIMIT_NOFILE, &lim); - if (res != 0) - warn("WARNING: setrlimit() failed with"); +static void maximize_fd_limit() +{ + struct rlimit lim {}; + auto res = getrlimit(RLIMIT_NOFILE, &lim); + if (res != 0) { + warn("WARNING: getrlimit() failed with"); + return; + } + lim.rlim_cur = lim.rlim_max; + res = setrlimit(RLIMIT_NOFILE, &lim); + if (res != 0) + warn("WARNING: setrlimit() failed with"); } +int main(int argc, char *argv[]) +{ + struct fuse_loop_config *loop_config = NULL; -int main(int argc, char *argv[]) { - - struct fuse_loop_config *loop_config = NULL; + // Parse command line options + auto options{ parse_options(argc, argv) }; - // Parse command line options - auto options {parse_options(argc, argv)}; + // We need an fd for every dentry in our the filesystem that the + // kernel knows about. This is way more than most processes need, + // so try to get rid of any resource softlimit. + maximize_fd_limit(); - // We need an fd for every dentry in our the filesystem that the - // kernel knows about. This is way more than most processes need, - // so try to get rid of any resource softlimit. - maximize_fd_limit(); + // Initialize filesystem root + fs.root.fd = -1; + fs.root.nlookup = 9999; + fs.timeout = options.count("nocache") ? 0 : 86400.0; - // Initialize filesystem root - fs.root.fd = -1; - fs.root.nlookup = 9999; - fs.timeout = options.count("nocache") ? 0 : 86400.0; + struct stat stat; + auto ret = lstat(fs.source.c_str(), &stat); + if (ret == -1) + err(1, "ERROR: failed to stat source (\"%s\")", + fs.source.c_str()); + if (!S_ISDIR(stat.st_mode)) + errx(1, "ERROR: source is not a directory"); + fs.src_dev = stat.st_dev; - struct stat stat; - auto ret = lstat(fs.source.c_str(), &stat); - if (ret == -1) - err(1, "ERROR: failed to stat source (\"%s\")", fs.source.c_str()); - if (!S_ISDIR(stat.st_mode)) - errx(1, "ERROR: source is not a directory"); - fs.src_dev = stat.st_dev; + fs.root.fd = open(fs.source.c_str(), O_PATH); + if (fs.root.fd == -1) + err(1, "ERROR: open(\"%s\", O_PATH)", fs.source.c_str()); - fs.root.fd = open(fs.source.c_str(), O_PATH); - if (fs.root.fd == -1) - err(1, "ERROR: open(\"%s\", O_PATH)", fs.source.c_str()); + // Initialize fuse + fuse_args args = FUSE_ARGS_INIT(0, nullptr); + if (fuse_opt_add_arg(&args, argv[0]) || fuse_opt_add_arg(&args, "-o") || + fuse_opt_add_arg(&args, fs.fuse_mount_options.c_str()) || + (fs.debug_fuse && fuse_opt_add_arg(&args, "-odebug"))) + errx(3, "ERROR: Out of memory adding arguments"); - // Initialize fuse - fuse_args args = FUSE_ARGS_INIT(0, nullptr); - if (fuse_opt_add_arg(&args, argv[0]) || - fuse_opt_add_arg(&args, "-o") || - fuse_opt_add_arg(&args, fs.fuse_mount_options.c_str()) || - (fs.debug_fuse && fuse_opt_add_arg(&args, "-odebug"))) - errx(3, "ERROR: Out of memory"); + ret = -1; + fuse_lowlevel_ops sfs_oper{}; + assign_operations(sfs_oper); + auto se = fuse_session_new(&args, &sfs_oper, sizeof(sfs_oper), &fs); + if (se == nullptr) + goto err_out1; - ret = -1; - fuse_lowlevel_ops sfs_oper {}; - assign_operations(sfs_oper); - auto se = fuse_session_new(&args, &sfs_oper, sizeof(sfs_oper), &fs); - if (se == nullptr) - goto err_out1; + if (fuse_set_signal_handlers(se) != 0) + goto err_out2; - if (fuse_set_signal_handlers(se) != 0) - goto err_out2; + if (fuse_set_fail_signal_handlers(se) != 0) + goto err_out2; - if (fuse_set_fail_signal_handlers(se) != 0) - goto err_out2; + // Don't apply umask, use modes exactly as specified + umask(0); - // Don't apply umask, use modes exactly as specified - umask(0); + // Mount and run main loop + loop_config = fuse_loop_cfg_create(); - // Mount and run main loop - loop_config = fuse_loop_cfg_create(); + if (fs.num_threads != -1) + fuse_loop_cfg_set_max_threads(loop_config, fs.num_threads); - if (fs.num_threads != -1) - fuse_loop_cfg_set_max_threads(loop_config, fs.num_threads); + fuse_loop_cfg_set_clone_fd(loop_config, fs.clone_fd); - fuse_loop_cfg_set_clone_fd(loop_config, fs.clone_fd); - - if (fuse_session_mount(se, argv[2]) != 0) - goto err_out3; + if (fuse_session_mount(se, argv[2]) != 0) + goto err_out3; - fuse_daemonize(fs.foreground); + fuse_daemonize(fs.foreground); - if (!fs.foreground) - fuse_log_enable_syslog("passthrough-hp", LOG_PID | LOG_CONS, LOG_DAEMON); + if (!fs.foreground) + fuse_log_enable_syslog("passthrough-hp", LOG_PID | LOG_CONS, + LOG_DAEMON); - if (options.count("single")) - ret = fuse_session_loop(se); - else - ret = fuse_session_loop_mt(se, loop_config); + if (options.count("single")) + ret = fuse_session_loop(se); + else + ret = fuse_session_loop_mt(se, loop_config); - fuse_session_unmount(se); + fuse_session_unmount(se); err_out3: - fuse_remove_signal_handlers(se); + fuse_remove_signal_handlers(se); err_out2: - fuse_session_destroy(se); + fuse_session_destroy(se); err_out1: - fuse_loop_cfg_destroy(loop_config); - fuse_opt_free_args(&args); + fuse_loop_cfg_destroy(loop_config); + fuse_opt_free_args(&args); - if (!fs.foreground) - fuse_log_close_syslog(); + if (!fs.foreground) + fuse_log_close_syslog(); - return ret ? 1 : 0; + return ret ? 1 : 0; } - diff --git a/example/passthrough_ll.c b/example/passthrough_ll.c index 01a420869..7dfaee370 100644 --- a/example/passthrough_ll.c +++ b/example/passthrough_ll.c @@ -3,7 +3,7 @@ Copyright (C) 2001-2007 Miklos Szeredi <miklos@szeredi.hu> This program can be distributed under the terms of the GNU GPLv2. - See the file COPYING. + See the file GPL2.txt. */ /** @file @@ -13,11 +13,7 @@ * just "passing through" all requests to the corresponding user-space * libc functions. In contrast to passthrough.c and passthrough_fh.c, * this implementation uses the low-level API. Its performance should - * be the least bad among the three, but many operations are not - * implemented. In particular, it is not possible to remove files (or - * directories) because the code necessary to defer actual removal - * until the file is not opened anymore would make the example much - * more complicated. + * be the least bad among the three. * * When writeback caching is enabled (-o writeback mount option), it * is only possible to write to files for which the mounting user has @@ -28,14 +24,14 @@ * * Compile with: * - * gcc -Wall passthrough_ll.c `pkg-config fused --cflags --libs` -o passthrough_ll + * gcc -Wall passthrough_ll.c `pkg-config fuse3 --cflags --libs` -o passthrough_ll * * ## Source code ## * \include passthrough_ll.c */ #define _GNU_SOURCE -#define FUSE_USE_VERSION FUSE_MAKE_VERSION(3, 12) +#define FUSE_USE_VERSION FUSE_MAKE_VERSION(3, 18) #include <fuse_lowlevel.h> #include <unistd.h> @@ -315,7 +311,7 @@ static struct lo_inode *create_new_inode(int fd, struct fuse_entry_param *e, str { struct lo_inode *inode = NULL; struct lo_inode *prev, *next; - + inode = calloc(1, sizeof(struct lo_inode)); if (!inode) return NULL; @@ -352,7 +348,7 @@ static int fill_entry_param_new_inode(fuse_req_t req, fuse_ino_t parent, int fd, e->ino = (uintptr_t) create_new_inode(dup(fd), e, lo); if (lo_debug(req)) - fuse_log(FUSE_LOG_DEBUG, " %lli/%lli -> %lli\n", + fuse_log(FUSE_LOG_DEBUG, " %lli/%d -> %lli\n", (unsigned long long) parent, fd, (unsigned long long) e->ino); return 0; @@ -712,7 +708,7 @@ static void lo_do_readdir(fuse_req_t req, fuse_ino_t ino, size_t size, err = errno; goto error; } else { // End of stream - break; + break; } } } @@ -744,11 +740,11 @@ static void lo_do_readdir(fuse_req_t req, fuse_ino_t ino, size_t size, &st, nextoff); } if (entsize > rem) { - if (entry_ino != 0) + if (entry_ino != 0) lo_forget_one(req, entry_ino, 1); break; } - + p += entsize; rem -= entsize; @@ -816,9 +812,9 @@ static void lo_tmpfile(fuse_req_t req, fuse_ino_t parent, /* parallel_direct_writes feature depends on direct_io features. To make parallel_direct_writes valid, need set fi->direct_io in current function. */ - fi->parallel_direct_writes = 1; - - err = fill_entry_param_new_inode(req, parent, fd, &e); + fi->parallel_direct_writes = 1; + + err = fill_entry_param_new_inode(req, parent, fd, &e); if (err) fuse_reply_err(req, err); else @@ -981,8 +977,8 @@ static void lo_write_buf(fuse_req_t req, fuse_ino_t ino, out_buf.buf[0].pos = off; if (lo_debug(req)) - fuse_log(FUSE_LOG_DEBUG, "lo_write(ino=%" PRIu64 ", size=%zd, off=%lu)\n", - ino, out_buf.buf[0].size, (unsigned long) off); + fuse_log(FUSE_LOG_DEBUG, "lo_write(ino=%" PRIu64 ", size=%zd, off=%jd)\n", + ino, out_buf.buf[0].size, (intmax_t) off); res = fuse_buf_copy(&out_buf, in_buf, 0); if(res < 0) @@ -1006,22 +1002,10 @@ static void lo_statfs(fuse_req_t req, fuse_ino_t ino) static void lo_fallocate(fuse_req_t req, fuse_ino_t ino, int mode, off_t offset, off_t length, struct fuse_file_info *fi) { - int err = EOPNOTSUPP; + int err; (void) ino; -#ifdef HAVE_FALLOCATE - err = fallocate(fi->fh, mode, offset, length); - if (err < 0) - err = errno; - -#elif defined(HAVE_POSIX_FALLOCATE) - if (mode) { - fuse_reply_err(req, EOPNOTSUPP); - return; - } - - err = posix_fallocate(fi->fh, offset, length); -#endif + err = -do_fallocate(fi->fh, mode, offset, length); fuse_reply_err(req, err); } @@ -1199,10 +1183,12 @@ static void lo_copy_file_range(fuse_req_t req, fuse_ino_t ino_in, off_t off_in, ssize_t res; if (lo_debug(req)) - fuse_log(FUSE_LOG_DEBUG, "lo_copy_file_range(ino=%" PRIu64 "/fd=%lu, " - "off=%lu, ino=%" PRIu64 "/fd=%lu, " - "off=%lu, size=%zd, flags=0x%x)\n", - ino_in, fi_in->fh, off_in, ino_out, fi_out->fh, off_out, + fuse_log(FUSE_LOG_DEBUG, + "%s(ino=%lld fd=%lld off=%jd ino=%lld fd=%lld off=%jd, size=%zd, flags=0x%x)\n", + __func__, (unsigned long long)ino_in, + (unsigned long long)fi_in->fh, + (intmax_t) off_in, (unsigned long long)ino_out, + (unsigned long long)fi_out->fh, (intmax_t) off_out, len, flags); res = copy_file_range(fi_in->fh, &off_in, fi_out->fh, &off_out, len, @@ -1227,6 +1213,28 @@ static void lo_lseek(fuse_req_t req, fuse_ino_t ino, off_t off, int whence, fuse_reply_err(req, errno); } +#ifdef HAVE_STATX +static void lo_statx(fuse_req_t req, fuse_ino_t ino, int flags, int mask, + struct fuse_file_info *fi) +{ + struct lo_data *lo = lo_data(req); + struct statx buf; + int res; + int fd; + + if (fi) + fd = fi->fh; + else + fd = lo_fd(req, ino); + + res = statx(fd, "", flags | AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW, mask, &buf); + if (res == -1) + fuse_reply_err(req, errno); + else + fuse_reply_statx(req, 0, &buf, lo->timeout); +} +#endif + static const struct fuse_lowlevel_ops lo_oper = { .init = lo_init, .destroy = lo_destroy, @@ -1267,6 +1275,9 @@ static const struct fuse_lowlevel_ops lo_oper = { .copy_file_range = lo_copy_file_range, #endif .lseek = lo_lseek, +#ifdef HAVE_STATX + .statx = lo_statx, +#endif }; int main(int argc, char *argv[]) diff --git a/example/poll.c b/example/poll.c index e617111fe..cdcdd87de 100644 --- a/example/poll.c +++ b/example/poll.c @@ -4,7 +4,7 @@ Copyright (C) 2008 Tejun Heo <teheo@suse.de> This program can be distributed under the terms of the GNU GPLv2. - See the file COPYING. + See the file GPL2.txt. */ /** @file @@ -15,7 +15,7 @@ * * Compile with: * - * gcc -Wall poll.c `pkg-config fused --cflags --libs` -o poll + * gcc -Wall poll.c `pkg-config fuse3 --cflags --libs` -o poll * * ## Source code ## * \include poll.c diff --git a/example/poll_client.c b/example/poll_client.c index 83c58239c..6dba3e71c 100644 --- a/example/poll_client.c +++ b/example/poll_client.c @@ -4,7 +4,7 @@ Copyright (C) 2008 Tejun Heo <teheo@suse.de> This program can be distributed under the terms of the GNU GPLv2. - See the file COPYING. + See the file GPL2.txt. */ /** @file @@ -31,15 +31,15 @@ #include <stdlib.h> #include <errno.h> -#define FSEL_FILES 16 - int main(void) { - static const char hex_map[FSEL_FILES] = "0123456789ABCDEF"; - int fds[FSEL_FILES]; - int i, nfds, tries; + static const char hex_map[] = "0123456789ABCDEF"; + const size_t fsel_files = sizeof(hex_map) - 1; + int fds[sizeof(hex_map) - 1]; + size_t i, tries; + int nfds; - for (i = 0; i < FSEL_FILES; i++) { + for (i = 0; i < fsel_files; i++) { char name[] = { hex_map[i], '\0' }; fds[i] = open(name, O_RDONLY); if (fds[i] < 0) { @@ -47,15 +47,15 @@ int main(void) return 1; } } - nfds = fds[FSEL_FILES - 1] + 1; + nfds = fds[fsel_files - 1] + 1; - for(tries=0; tries < 16; tries++) { + for (tries = 0; tries < fsel_files; tries++) { static char buf[4096]; fd_set rfds; int rc; FD_ZERO(&rfds); - for (i = 0; i < FSEL_FILES; i++) + for (i = 0; i < fsel_files; i++) FD_SET(fds[i], &rfds); rc = select(nfds, &rfds, NULL, NULL, NULL); @@ -65,12 +65,12 @@ int main(void) return 1; } - for (i = 0; i < FSEL_FILES; i++) { + for (i = 0; i < fsel_files; i++) { if (!FD_ISSET(fds[i], &rfds)) { printf("_: "); continue; } - printf("%X:", i); + printf("%zX:", i); rc = read(fds[i], buf, sizeof(buf)); if (rc < 0) { perror("read"); diff --git a/example/printcap.c b/example/printcap.c index 74d22e1c5..4c59582f9 100644 --- a/example/printcap.c +++ b/example/printcap.c @@ -3,7 +3,7 @@ Copyright (C) 2017 Nikolaus Rath <Nikolaus@rath.org> This program can be distributed under the terms of the GNU GPLv2. - See the file COPYING. + See the file GPL2.txt. */ /** @file @@ -13,7 +13,7 @@ * * Compile with: * - * gcc -Wall printcap.c `pkg-config fused --cflags --libs` -o printcap + * gcc -Wall printcap.c `pkg-config fuse3 --cflags --libs` -o printcap * * ## Source code ## * \include printcap.c diff --git a/example/usdt.bt b/example/usdt.bt new file mode 100644 index 000000000..41cfcdc59 --- /dev/null +++ b/example/usdt.bt @@ -0,0 +1,19 @@ +#!/usr/bin/env bpftrace + +// To run, do `sudo bpftrace usdt.bt` + +usdt:../build/lib/libfuse3.so:libfuse:request_receive +{ + printf("libfuse:request_receive hit, err=%d\n", arg0); +} + +usdt:../build/lib/libfuse3.so:libfuse:request_process +{ + printf("libfuse:request_process hit, opcode=%u, unique=%u\n", arg0, arg1); +} + +usdt:../build/lib/libfuse3.so:libfuse:request_reply +{ + printf("libfuse:request_reply hit, unique=%lu, len=%u, err=%u, reply_err=%d\n", + arg0, arg1, arg2, arg3); +} diff --git a/include/cuse_lowlevel.h b/include/cuse_lowlevel.h index 80476c20b..fe0b6f2da 100644 --- a/include/cuse_lowlevel.h +++ b/include/cuse_lowlevel.h @@ -4,7 +4,7 @@ Copyright (C) 2008-2009 Tejun Heo <tj@kernel.org> This program can be distributed under the terms of the GNU LGPLv2. - See the file COPYING.LIB. + See the file LGPL2.txt. Read example/cusexmp.c for usages. */ diff --git a/include/fuse.h b/include/fuse.h index 4582cc7ac..9dfd79e39 100644 --- a/include/fuse.h +++ b/include/fuse.h @@ -3,7 +3,7 @@ Copyright (C) 2001-2007 Miklos Szeredi <miklos@szeredi.hu> This program can be distributed under the terms of the GNU LGPLv2. - See the file COPYING.LIB. + See the file LGPL2.txt. */ #ifndef FUSE_H_ @@ -33,6 +33,9 @@ extern "C" { * Basic FUSE API * * ----------------------------------------------------------- */ +/* Forward declaration */ +struct statx; + /** Handle for a FUSE filesystem */ struct fuse; @@ -57,13 +60,16 @@ enum fuse_readdir_flags { */ enum fuse_fill_dir_flags { /** - * "Plus" mode: all file attributes are valid + * "Plus" mode: file attributes are valid * * The attributes are used by the kernel to prefill the inode cache * during a readdir. * * It is okay to set FUSE_FILL_DIR_PLUS if FUSE_READDIR_PLUS is not set * and vice versa. + * + * This does not make libfuse honor the 'st_ino' field. That is + * controlled by the 'use_ino' option instead. */ FUSE_FILL_DIR_DEFAULTS = 0, FUSE_FILL_DIR_PLUS = (1 << 1) @@ -850,6 +856,17 @@ struct fuse_operations { * Find next data or hole after the specified offset */ off_t (*lseek) (const char *, off_t off, int whence, struct fuse_file_info *); + + /** + * Get extended file attributes. + * + * fi may be NULL. + * + * If path is NULL, then the AT_EMPTY_PATH bit in flags will be + * already set. + */ + int (*statx)(const char *path, int flags, int mask, struct statx *stxbuf, + struct fuse_file_info *fi); }; /** Extra context that may be needed by some filesystems @@ -1153,6 +1170,7 @@ int fuse_loop_mt_32(struct fuse *f, struct fuse_loop_config *config); #if (defined(LIBFUSE_BUILT_WITH_VERSIONED_SYMBOLS)) int fuse_loop_mt(struct fuse *f, struct fuse_loop_config *config); #else +int fuse_loop_mt_312(struct fuse *f, struct fuse_loop_config *config); #define fuse_loop_mt(f, config) fuse_loop_mt_312(f, config) #endif /* LIBFUSE_BUILT_WITH_VERSIONED_SYMBOLS */ #endif @@ -1344,6 +1362,8 @@ ssize_t fuse_fs_copy_file_range(struct fuse_fs *fs, const char *path_in, size_t len, int flags); off_t fuse_fs_lseek(struct fuse_fs *fs, const char *path, off_t off, int whence, struct fuse_file_info *fi); +int fuse_fs_statx(struct fuse_fs *fs, const char *path, int flags, int mask, + struct statx *stxbuf, struct fuse_file_info *fi); void fuse_fs_init(struct fuse_fs *fs, struct fuse_conn_info *conn, struct fuse_config *cfg); void fuse_fs_destroy(struct fuse_fs *fs); diff --git a/include/fuse_common.h b/include/fuse_common.h index f96899075..b6934599e 100644 --- a/include/fuse_common.h +++ b/include/fuse_common.h @@ -2,12 +2,11 @@ Copyright (C) 2001-2007 Miklos Szeredi <miklos@szeredi.hu> This program can be distributed under the terms of the GNU LGPLv2. - See the file COPYING.LIB. + See the file LGPL2.txt. */ /** @file */ -#include <stdbool.h> #if !defined(FUSE_H_) && !defined(FUSE_LOWLEVEL_H_) #error "Never include <fuse_common.h> directly; use <fuse.h> or <fuse_lowlevel.h> instead." #endif @@ -24,6 +23,7 @@ #include "fuse_opt.h" #include "fuse_log.h" #include <stdint.h> +#include <stdbool.h> #include <sys/types.h> #include <assert.h> @@ -96,8 +96,16 @@ struct fuse_file_info { file */ uint32_t parallel_direct_writes : 1; + /** + * Set in open, create and write when KILLPRIV_V2 was negotiated and + * the application issuing the syscall lacks CAP_FSETID; the + * filesystem's own privileges do not matter. The filesystem then + * has to remove setuid/setgid bits. + */ + uint32_t kill_suidgid : 1; + /** Padding. Reserved for future use*/ - uint32_t padding : 23; + uint32_t padding : 22; uint32_t padding2 : 32; uint32_t padding3 : 32; @@ -174,7 +182,7 @@ struct fuse_loop_config_v1 { * * This feature is enabled by default when supported by the kernel. */ -#define FUSE_CAP_ASYNC_READ (1 << 0) +#define FUSE_CAP_ASYNC_READ (1UL << 0) /** * Indicates that the filesystem supports "remote" locking. @@ -182,7 +190,7 @@ struct fuse_loop_config_v1 { * This feature is enabled by default when supported by the kernel, * and if getlk() and setlk() handlers are implemented. */ -#define FUSE_CAP_POSIX_LOCKS (1 << 1) +#define FUSE_CAP_POSIX_LOCKS (1UL << 1) /** * Indicates that the filesystem supports the O_TRUNC open flag. If @@ -191,7 +199,7 @@ struct fuse_loop_config_v1 { * * This feature is enabled by default when supported by the kernel. */ -#define FUSE_CAP_ATOMIC_O_TRUNC (1 << 3) +#define FUSE_CAP_ATOMIC_O_TRUNC (1UL << 3) /** * Indicates that the filesystem supports lookups of "." and "..". @@ -203,7 +211,7 @@ struct fuse_loop_config_v1 { * * This feature is disabled by default. */ -#define FUSE_CAP_EXPORT_SUPPORT (1 << 4) +#define FUSE_CAP_EXPORT_SUPPORT (1UL << 4) /** * Indicates that the kernel should not apply the umask to the @@ -211,7 +219,7 @@ struct fuse_loop_config_v1 { * * This feature is disabled by default. */ -#define FUSE_CAP_DONT_MASK (1 << 6) +#define FUSE_CAP_DONT_MASK (1UL << 6) /** * Indicates that libfuse should try to use splice() when writing to @@ -219,7 +227,7 @@ struct fuse_loop_config_v1 { * * This feature is disabled by default. */ -#define FUSE_CAP_SPLICE_WRITE (1 << 7) +#define FUSE_CAP_SPLICE_WRITE (1UL << 7) /** * Indicates that libfuse should try to move pages instead of copying when @@ -227,7 +235,7 @@ struct fuse_loop_config_v1 { * * This feature is disabled by default. */ -#define FUSE_CAP_SPLICE_MOVE (1 << 8) +#define FUSE_CAP_SPLICE_MOVE (1UL << 8) /** * Indicates that libfuse should try to use splice() when reading from @@ -236,7 +244,7 @@ struct fuse_loop_config_v1 { * This feature is enabled by default when supported by the kernel and * if the filesystem implements a write_buf() handler. */ -#define FUSE_CAP_SPLICE_READ (1 << 9) +#define FUSE_CAP_SPLICE_READ (1UL << 9) /** * If set, the calls to flock(2) will be emulated using POSIX locks and must @@ -249,14 +257,14 @@ struct fuse_loop_config_v1 { * This feature is enabled by default when supported by the kernel and * if the filesystem implements a flock() handler. */ -#define FUSE_CAP_FLOCK_LOCKS (1 << 10) +#define FUSE_CAP_FLOCK_LOCKS (1UL << 10) /** * Indicates that the filesystem supports ioctl's on directories. * * This feature is enabled by default when supported by the kernel. */ -#define FUSE_CAP_IOCTL_DIR (1 << 11) +#define FUSE_CAP_IOCTL_DIR (1UL << 11) /** * Traditionally, while a file is open the FUSE kernel module only @@ -278,7 +286,7 @@ struct fuse_loop_config_v1 { * * This feature is enabled by default when supported by the kernel. */ -#define FUSE_CAP_AUTO_INVAL_DATA (1 << 12) +#define FUSE_CAP_AUTO_INVAL_DATA (1UL << 12) /** * Indicates that the filesystem supports readdirplus. @@ -286,7 +294,7 @@ struct fuse_loop_config_v1 { * This feature is enabled by default when supported by the kernel and if the * filesystem implements a readdirplus() handler. */ -#define FUSE_CAP_READDIRPLUS (1 << 13) +#define FUSE_CAP_READDIRPLUS (1UL << 13) /** * Indicates that the filesystem supports adaptive readdirplus. @@ -314,7 +322,7 @@ struct fuse_loop_config_v1 { * if the filesystem implements both a readdirplus() and a readdir() * handler. */ -#define FUSE_CAP_READDIRPLUS_AUTO (1 << 14) +#define FUSE_CAP_READDIRPLUS_AUTO (1UL << 14) /** * Indicates that the filesystem supports asynchronous direct I/O submission. @@ -325,7 +333,7 @@ struct fuse_loop_config_v1 { * * This feature is enabled by default when supported by the kernel. */ -#define FUSE_CAP_ASYNC_DIO (1 << 15) +#define FUSE_CAP_ASYNC_DIO (1UL << 15) /** * Indicates that writeback caching should be enabled. This means that @@ -334,7 +342,7 @@ struct fuse_loop_config_v1 { * * This feature is disabled by default. */ -#define FUSE_CAP_WRITEBACK_CACHE (1 << 16) +#define FUSE_CAP_WRITEBACK_CACHE (1UL << 16) /** * Indicates support for zero-message opens. If this flag is set in @@ -349,7 +357,7 @@ struct fuse_loop_config_v1 { * this behavior you must return `ENOSYS` from the open() handler on supporting * kernels. */ -#define FUSE_CAP_NO_OPEN_SUPPORT (1 << 17) +#define FUSE_CAP_NO_OPEN_SUPPORT (1UL << 17) /** * Indicates support for parallel directory operations. If this flag @@ -357,7 +365,7 @@ struct fuse_loop_config_v1 { * readdir() requests are never issued concurrently for the same * directory. */ -#define FUSE_CAP_PARALLEL_DIROPS (1 << 18) +#define FUSE_CAP_PARALLEL_DIROPS (1UL << 18) /** * Indicates support for POSIX ACLs. @@ -376,7 +384,7 @@ struct fuse_loop_config_v1 { * * This feature is disabled by default. */ -#define FUSE_CAP_POSIX_ACL (1 << 19) +#define FUSE_CAP_POSIX_ACL (1UL << 19) /** * Indicates that the filesystem is responsible for unsetting @@ -385,7 +393,7 @@ struct fuse_loop_config_v1 { * * This feature is disabled by default. */ -#define FUSE_CAP_HANDLE_KILLPRIV (1 << 20) +#define FUSE_CAP_HANDLE_KILLPRIV (1UL << 20) /** * Indicates that the filesystem is responsible for unsetting @@ -402,7 +410,7 @@ struct fuse_loop_config_v1 { * * This feature is disabled by default. */ -#define FUSE_CAP_HANDLE_KILLPRIV_V2 (1 << 21) +#define FUSE_CAP_HANDLE_KILLPRIV_V2 (1UL << 21) /** * Indicates that the kernel supports caching symlinks in its page cache. @@ -415,7 +423,7 @@ struct fuse_loop_config_v1 { * If the kernel supports it (>= 4.20), you can enable this feature by * setting this flag in the `want` field of the `fuse_conn_info` structure. */ -#define FUSE_CAP_CACHE_SYMLINKS (1 << 23) +#define FUSE_CAP_CACHE_SYMLINKS (1UL << 23) /** * Indicates support for zero-message opendirs. If this flag is set in @@ -430,7 +438,7 @@ struct fuse_loop_config_v1 { * this behavior you must return `ENOSYS` from the opendir() handler on * supporting kernels. */ -#define FUSE_CAP_NO_OPENDIR_SUPPORT (1 << 24) +#define FUSE_CAP_NO_OPENDIR_SUPPORT (1UL << 24) /** * Indicates support for invalidating cached pages only on explicit request. @@ -453,7 +461,7 @@ struct fuse_loop_config_v1 { * * This feature is disabled by default. */ -#define FUSE_CAP_EXPLICIT_INVAL_DATA (1 << 25) +#define FUSE_CAP_EXPLICIT_INVAL_DATA (1UL << 25) /** * Indicates support that dentries can be expired. @@ -469,14 +477,14 @@ struct fuse_loop_config_v1 { * The dentry could also be mounted in a different mount instance, in which case * any submounts will still be detached. */ -#define FUSE_CAP_EXPIRE_ONLY (1 << 26) +#define FUSE_CAP_EXPIRE_ONLY (1UL << 26) /** * Indicates that an extended 'struct fuse_setxattr' is used by the kernel * side - extra_flags are passed, which are used (as of now by acl) processing. * For example FUSE_SETXATTR_ACL_KILL_SGID might be set. */ -#define FUSE_CAP_SETXATTR_EXT (1 << 27) +#define FUSE_CAP_SETXATTR_EXT (1UL << 27) /** * Files opened with FUSE_DIRECT_IO do not support MAP_SHARED mmap. This restriction @@ -485,7 +493,7 @@ struct fuse_loop_config_v1 { * ensure coherency between mount points (or network clients) and with kernel page * cache as enforced by mmap that cannot be guaranteed anymore. */ -#define FUSE_CAP_DIRECT_IO_ALLOW_MMAP (1 << 28) +#define FUSE_CAP_DIRECT_IO_ALLOW_MMAP (1UL << 28) /** * Indicates support for passthrough mode access for read/write operations. @@ -497,7 +505,7 @@ struct fuse_loop_config_v1 { * * This feature is disabled by default. */ -#define FUSE_CAP_PASSTHROUGH (1 << 29) +#define FUSE_CAP_PASSTHROUGH (1UL << 29) /** * Indicates that the file system cannot handle NFS export @@ -505,7 +513,12 @@ struct fuse_loop_config_v1 { * If this flag is set NFS export and name_to_handle_at * is not going to work at all and will fail with EOPNOTSUPP. */ -#define FUSE_CAP_NO_EXPORT_SUPPORT (1 << 30) +#define FUSE_CAP_NO_EXPORT_SUPPORT (1UL << 30) + +/** + * Indicates support for io-uring between fuse-server and fuse-client + */ +#define FUSE_CAP_OVER_IO_URING (1UL << 31) /** * Ioctl flags @@ -695,10 +708,16 @@ struct fuse_conn_info { */ uint64_t want_ext; + /** + * Request timeout (in seconds). If the request is not answered by + * this timeout, the connection will be aborted by the kernel. + */ + uint16_t request_timeout; + /** * For future use. */ - uint32_t reserved[16]; + uint16_t reserved[31]; }; struct fuse_session; diff --git a/include/fuse_kernel.h b/include/fuse_kernel.h index d08b99d60..94621f68a 100644 --- a/include/fuse_kernel.h +++ b/include/fuse_kernel.h @@ -217,6 +217,28 @@ * - add backing_id to fuse_open_out, add FOPEN_PASSTHROUGH open flag * - add FUSE_NO_EXPORT_SUPPORT init flag * - add FUSE_NOTIFY_RESEND, add FUSE_HAS_RESEND init flag + * + * 7.41 + * - add FUSE_ALLOW_IDMAP + * 7.42 + * - Add FUSE_OVER_IO_URING and all other io-uring related flags and data + * structures: + * - struct fuse_uring_ent_in_out + * - struct fuse_uring_req_header + * - struct fuse_uring_cmd_req + * - FUSE_URING_IN_OUT_HEADER_SZ + * - FUSE_URING_OP_IN_OUT_SZ + * - enum fuse_uring_cmd + * + * 7.43 + * - add FUSE_REQUEST_TIMEOUT + * + * 7.44 + * - add FUSE_NOTIFY_INC_EPOCH + * + * 7.45 + * - add FUSE_COPY_FILE_RANGE_64 + * - add struct fuse_copy_file_range_out */ #ifndef _LINUX_FUSE_H @@ -252,7 +274,7 @@ #define FUSE_KERNEL_VERSION 7 /** Minor version number of this interface */ -#define FUSE_KERNEL_MINOR_VERSION 40 +#define FUSE_KERNEL_MINOR_VERSION 45 /** The node ID of the root inode */ #define FUSE_ROOT_ID 1 @@ -421,6 +443,10 @@ struct fuse_file_lock { * FUSE_NO_EXPORT_SUPPORT: explicitly disable export support * FUSE_HAS_RESEND: kernel supports resending pending requests, and the high bit * of the request ID indicates resend requests + * FUSE_ALLOW_IDMAP: allow creation of idmapped mounts + * FUSE_OVER_IO_URING: Indicate that client supports io-uring + * FUSE_REQUEST_TIMEOUT: kernel supports timing out requests. + * init_out.request_timeout contains the timeout (in secs) */ #define FUSE_ASYNC_READ (1 << 0) #define FUSE_POSIX_LOCKS (1 << 1) @@ -463,9 +489,11 @@ struct fuse_file_lock { #define FUSE_PASSTHROUGH (1ULL << 37) #define FUSE_NO_EXPORT_SUPPORT (1ULL << 38) #define FUSE_HAS_RESEND (1ULL << 39) - /* Obsolete alias for FUSE_DIRECT_IO_ALLOW_MMAP */ #define FUSE_DIRECT_IO_RELAX FUSE_DIRECT_IO_ALLOW_MMAP +#define FUSE_ALLOW_IDMAP (1ULL << 40) +#define FUSE_OVER_IO_URING (1ULL << 41) +#define FUSE_REQUEST_TIMEOUT (1ULL << 42) /** * CUSE INIT request/reply flags @@ -633,6 +661,7 @@ enum fuse_opcode { FUSE_SYNCFS = 50, FUSE_TMPFILE = 51, FUSE_STATX = 52, + FUSE_COPY_FILE_RANGE_64 = 53, /* CUSE specific operations */ CUSE_INIT = 4096, @@ -650,6 +679,7 @@ enum fuse_notify_code { FUSE_NOTIFY_RETRIEVE = 5, FUSE_NOTIFY_DELETE = 6, FUSE_NOTIFY_RESEND = 7, + FUSE_NOTIFY_INC_EPOCH = 8, FUSE_NOTIFY_CODE_MAX, }; @@ -893,7 +923,8 @@ struct fuse_init_out { uint16_t map_alignment; uint32_t flags2; uint32_t max_stack_depth; - uint32_t unused[6]; + uint16_t request_timeout; + uint16_t unused[11]; }; #define CUSE_INIT_INFO_MAX 4096 @@ -984,6 +1015,21 @@ struct fuse_fallocate_in { */ #define FUSE_UNIQUE_RESEND (1ULL << 63) +/** + * This value will be set by the kernel to + * (struct fuse_in_header).{uid,gid} fields in + * case when: + * - fuse daemon enabled FUSE_ALLOW_IDMAP + * - idmapping information is not available and uid/gid + * can not be mapped in accordance with an idmapping. + * + * Note: an idmapping information always available + * for inode creation operations like: + * FUSE_MKNOD, FUSE_SYMLINK, FUSE_MKDIR, FUSE_TMPFILE, + * FUSE_CREATE and FUSE_RENAME2 (with RENAME_WHITEOUT). + */ +#define FUSE_INVALID_UIDGID ((uint32_t)(-1)) + struct fuse_in_header { uint32_t len; uint32_t opcode; @@ -1107,6 +1153,11 @@ struct fuse_copy_file_range_in { uint64_t flags; }; +/* For FUSE_COPY_FILE_RANGE_64 */ +struct fuse_copy_file_range_out { + uint64_t bytes_copied; +}; + #define FUSE_SETUPMAPPING_FLAG_WRITE (1ull << 0) #define FUSE_SETUPMAPPING_FLAG_READ (1ull << 1) struct fuse_setupmapping_in { @@ -1186,4 +1237,67 @@ struct fuse_supp_groups { uint32_t groups[]; }; +/** + * Size of the ring buffer header + */ +#define FUSE_URING_IN_OUT_HEADER_SZ 128 +#define FUSE_URING_OP_IN_OUT_SZ 128 + +/* Used as part of the fuse_uring_req_header */ +struct fuse_uring_ent_in_out { + uint64_t flags; + + /* + * commit ID to be used in a reply to a ring request (see also + * struct fuse_uring_cmd_req) + */ + uint64_t commit_id; + + /* size of user payload buffer */ + uint32_t payload_sz; + uint32_t padding; + + uint64_t reserved; +}; + +/** + * Header for all fuse-io-uring requests + */ +struct fuse_uring_req_header { + /* struct fuse_in_header / struct fuse_out_header */ + char in_out[FUSE_URING_IN_OUT_HEADER_SZ]; + + /* per op code header */ + char op_in[FUSE_URING_OP_IN_OUT_SZ]; + + struct fuse_uring_ent_in_out ring_ent_in_out; +}; + +/** + * sqe commands to the kernel + */ +enum fuse_uring_cmd { + FUSE_IO_URING_CMD_INVALID = 0, + + /* register the request buffer and fetch a fuse request */ + FUSE_IO_URING_CMD_REGISTER = 1, + + /* commit fuse request result and fetch next request */ + FUSE_IO_URING_CMD_COMMIT_AND_FETCH = 2, +}; + +/** + * In the 80B command area of the SQE. + */ +struct fuse_uring_cmd_req { + uint64_t flags; + + /* entry identifier for commits */ + uint64_t commit_id; + + /* queue the command is for (queue index) */ + uint16_t qid; + uint8_t padding[6]; +}; + #endif /* _LINUX_FUSE_H */ diff --git a/include/fuse_log.h b/include/fuse_log.h index c8559572a..7948bab14 100644 --- a/include/fuse_log.h +++ b/include/fuse_log.h @@ -3,7 +3,7 @@ Copyright (C) 2019 Red Hat, Inc. This program can be distributed under the terms of the GNU LGPLv2. - See the file COPYING.LIB. + See the file LGPL2.txt. */ #ifndef FUSE_LOG_H_ @@ -73,7 +73,8 @@ void fuse_set_log_func(fuse_log_func_t func); * @param level severity level (FUSE_LOG_ERR, FUSE_LOG_DEBUG, etc) * @param fmt sprintf-style format string including newline */ -void fuse_log(enum fuse_log_level level, const char *fmt, ...); +void fuse_log(enum fuse_log_level level, const char *fmt, ...) + __attribute__((format(printf, 2, 3))); /** * Switch default log handler from stderr to syslog diff --git a/include/fuse_lowlevel.h b/include/fuse_lowlevel.h index c7b44d963..c8009b89e 100644 --- a/include/fuse_lowlevel.h +++ b/include/fuse_lowlevel.h @@ -3,7 +3,7 @@ Copyright (C) 2001-2007 Miklos Szeredi <miklos@szeredi.hu> This program can be distributed under the terms of the GNU LGPLv2. - See the file COPYING.LIB. + See the file LGPL2.txt. */ #ifndef FUSE_LOWLEVEL_H_ @@ -49,6 +49,9 @@ typedef uint64_t fuse_ino_t; /** Request pointer type */ typedef struct fuse_req *fuse_req_t; +/* Forward declaration */ +struct statx; + /** * Session * @@ -194,11 +197,10 @@ enum fuse_notify_entry_flags { * `fuse_session_new()`. In this case, methods will only be called if * the kernel's permission check has succeeded. * - * The filesystem sometimes needs to handle a return value of -ENOENT - * from the reply function, which means, that the request was - * interrupted, and the reply discarded. For example if - * fuse_reply_open() return -ENOENT means, that the release method for - * this file will not be called. + * It is generally not really necessary to check the fuse_reply_* return + * values for errors, as any error in sending a reply indicates an + * unrecoverable problem with the kernel fuse connection, which will also + * terminate the session loop anyway. * * This data structure is ABI sensitive, on adding new functions these need to * be appended at the end of the struct @@ -450,7 +452,7 @@ struct fuse_lowlevel_ops { * * If this request is answered with an error code of ENOSYS, this is * treated as a permanent failure with error code EINVAL, i.e. all - * future bmap requests will fail with EINVAL without being + * future rename requests will fail with EINVAL without being * send to the filesystem process. * * *flags* may be `RENAME_EXCHANGE` or `RENAME_NOREPLACE`. If @@ -1303,7 +1305,6 @@ struct fuse_lowlevel_ops { void (*lseek) (fuse_req_t req, fuse_ino_t ino, off_t off, int whence, struct fuse_file_info *fi); - /** * Create a tempfile * @@ -1325,6 +1326,21 @@ struct fuse_lowlevel_ops { void (*tmpfile) (fuse_req_t req, fuse_ino_t parent, mode_t mode, struct fuse_file_info *fi); + /** + * Get extended file attributes. + * + * Valid replies: + * fuse_reply_statx + * fuse_reply_err + * + * @param req request handle + * @param ino the inode number + * @param flags bitmask of requested flags + * @param mask bitmask of requested fields + * @param fi file information (may be NULL) + */ + void (*statx)(fuse_req_t req, fuse_ino_t ino, int flags, int mask, + struct fuse_file_info *fi); }; /** @@ -1705,6 +1721,20 @@ int fuse_reply_poll(fuse_req_t req, unsigned revents); */ int fuse_reply_lseek(fuse_req_t req, off_t off); +/** + * Reply with extended file attributes. + * + * Possible requests: + * statx + * + * @param req request handle + * @param flags statx flags + * @param statx the attributes + * @param attr_timeout validity timeout (in seconds) for the attributes + * @return zero for success, -errno for failure to send reply + */ +int fuse_reply_statx(fuse_req_t req, int flags, struct statx *statx, double attr_timeout); + /* ----------------------------------------------------------- * * Notification * * ----------------------------------------------------------- */ @@ -1744,6 +1774,20 @@ int fuse_lowlevel_notify_poll(struct fuse_pollhandle *ph); int fuse_lowlevel_notify_inval_inode(struct fuse_session *se, fuse_ino_t ino, off_t off, off_t len); +/** + * Notify to increment the epoch for the current + * + * Each fuse connection has an 'epoch', which is initialized during INIT. + * Caching will then be validated against the epoch value: if the current epoch + * is higher than an object being revalidated, the object is invalid. + * + * This function simply increment the current epoch value. + * + * @param se the session object + * @return zero for success, -errno for failure + */ +int fuse_lowlevel_notify_increment_epoch(struct fuse_session *se); + /** * Notify to invalidate parent attributes and the dentry matching parent/name * @@ -2070,6 +2114,8 @@ fuse_session_new_versioned(struct fuse_args *args, * If not all options are known, an error message is written to stderr * and the function returns NULL. * + * To create a no-op session just for mounting pass op as NULL. + * * Option parsing skips argv[0], which is assumed to contain the * program name. To prevent accidentally passing an option in * argv[0], this element must always be present (even if no options @@ -2124,6 +2170,13 @@ int fuse_session_custom_io_317(struct fuse_session *se, * The provided file descriptor `fd` will be closed when fuse_session_destroy() * is called. * + * Requires a library built with -Denable-custom-io=true. The peer at the other + * end of `fd` takes the place of the kernel as the sender of requests, so it + * owes the session the validation the kernel would have done -- libfuse does + * not repeat it. A request whose length or count field exceeds the bytes that + * actually arrived makes the handlers read past the receive buffer and crash + * the filesystem process. + * * @param se session object * @param io Custom io to use when retrieving/sending requests/responses * @param fd file descriptor for the session @@ -2132,6 +2185,7 @@ int fuse_session_custom_io_317(struct fuse_session *se, * @return -EINVAL if `io`, `io->read` or `ìo->writev` are NULL * @return -EBADF if `fd` was smaller than 0 * @return -errno if failed to allocate memory to store `io` + * @return -ENOTSUP if the library was built without custom io * **/ #if FUSE_MAKE_VERSION(3, 17) <= FUSE_USE_VERSION @@ -2212,12 +2266,17 @@ int fuse_session_loop(struct fuse_session *se); /** * Flag a session as terminated. * - * This will cause any running event loops to terminate on the next opportunity. If this function is - * called by a thread that is not a FUSE worker thread, the next - * opportunity will be when FUSE a request is received (which may be far in the future if the - * filesystem is not currently being used by any clients). One way to avoid this delay is to - * afterwards sent a signal to the main thread (if fuse_set_signal_handlers() is used, SIGPIPE - * will cause the main thread to wake-up but otherwise be ignored). + * This will cause any running event loop to terminate on the next opportunity. + * fuse_session_loop_mt(), and fuse_session_loop() from FUSE_USE_VERSION 3.19 + * on, are woken up directly and return without waiting for a request, no + * matter which thread calls this. + * + * The deprecated fuse_session_loop() below FUSE_USE_VERSION 3.19 is not woken + * up and only notices the flag once the next request is received, which may be + * far in the future if the filesystem is not currently being used by any + * clients. Sending a signal to its thread afterwards avoids that delay - if + * fuse_set_signal_handlers() is used, SIGPIPE will cause the thread to wake-up + * but otherwise be ignored. * * @param se the session */ @@ -2315,6 +2374,29 @@ void fuse_session_process_buf(struct fuse_session *se, */ int fuse_session_receive_buf(struct fuse_session *se, struct fuse_buf *buf); +/** + * Check if the request is submitted through fuse-io-uring + */ +bool fuse_req_is_uring(fuse_req_t req); + +/** + * Get the payload of a request + * (for requests submitted through fuse-io-uring only) + * + * This is useful for a file system that wants to write data directly + * to the request buffer. With io-uring the req is the buffer owner + * and the file system can write directly to the buffer and avoid + * extra copying. For example useful for network file systems. + * + * @param req the request + * @param payload pointer to the payload + * @param payload_sz size of the payload + * @param mr memory registration handle, currently unused + * @return 0 on success, -errno on failure + */ +int fuse_req_get_payload(fuse_req_t req, char **payload, size_t *payload_sz, + void **mr); + #ifdef __cplusplus } #endif diff --git a/include/fuse_opt.h b/include/fuse_opt.h index d8573e74f..a9a41fe80 100644 --- a/include/fuse_opt.h +++ b/include/fuse_opt.h @@ -3,7 +3,7 @@ Copyright (C) 2001-2007 Miklos Szeredi <miklos@szeredi.hu> This program can be distributed under the terms of the GNU LGPLv2. - See the file COPYING.LIB. + See the file LGPL2.txt. */ #ifndef FUSE_OPT_H_ diff --git a/lib/buffer.c b/lib/buffer.c index 6375433ee..2b72d95a0 100644 --- a/lib/buffer.c +++ b/lib/buffer.c @@ -6,7 +6,7 @@ fuse_bufvec`. This program can be distributed under the terms of the GNU LGPLv2. - See the file COPYING.LIB + See the file LGPL2.txt */ #define _GNU_SOURCE diff --git a/lib/compat.c b/lib/compat.c index b98ca4b04..5fac4155c 100644 --- a/lib/compat.c +++ b/lib/compat.c @@ -7,7 +7,7 @@ file system by implementing nothing but the request handlers. This program can be distributed under the terms of the GNU LGPLv2. - See the file COPYING.LIB. + See the file LGPL2.txt. */ /* Description: diff --git a/lib/cuse_lowlevel.c b/lib/cuse_lowlevel.c index 5387f8430..0bc2e66be 100644 --- a/lib/cuse_lowlevel.c +++ b/lib/cuse_lowlevel.c @@ -4,7 +4,7 @@ Copyright (C) 2008 Tejun Heo <teheo@suse.de> This program can be distributed under the terms of the GNU LGPLv2. - See the file COPYING.LIB. + See the file LGPL2.txt. */ #include "fuse_config.h" @@ -192,9 +192,11 @@ static int cuse_reply_init(fuse_req_t req, struct cuse_init_out *arg, return fuse_send_reply_iov_nofree(req, 0, iov, 3); } -void cuse_lowlevel_init(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) +void _cuse_lowlevel_init(fuse_req_t req, const fuse_ino_t nodeid, + const void *op_in, const void *req_payload) { - struct fuse_init_in *arg = (struct fuse_init_in *) inarg; + const struct fuse_init_in *arg = op_in; + (void)req_payload; struct cuse_init_out outarg; struct fuse_session *se = req->se; struct cuse_data *cd = se->cuse_data; @@ -263,6 +265,11 @@ void cuse_lowlevel_init(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) fuse_free_req(req); } +void cuse_lowlevel_init(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) +{ + _cuse_lowlevel_init(req, nodeid, inarg, NULL); +} + struct fuse_session *cuse_lowlevel_setup(int argc, char *argv[], const struct cuse_info *ci, const struct cuse_lowlevel_ops *clop, diff --git a/lib/fuse.c b/lib/fuse.c index 85914546e..a00098173 100644 --- a/lib/fuse.c +++ b/lib/fuse.c @@ -6,7 +6,7 @@ API. This program can be distributed under the terms of the GNU LGPLv2. - See the file COPYING.LIB + See the file LGPL2.txt */ #define _GNU_SOURCE @@ -1019,6 +1019,7 @@ static int try_get_path(struct fuse *f, fuse_ino_t nodeid, const char *name, if (wnodep) { assert(need_lock); + assert(name != NULL); wnode = lookup_node(f, nodeid, name); if (wnode) { if (wnode->treelock != 0) { @@ -1507,6 +1508,29 @@ static void set_stat(struct fuse *f, fuse_ino_t nodeid, struct stat *stbuf) stbuf->st_gid = f->conf.gid; } +#ifdef HAVE_STATX +static void set_statx(struct fuse *f, fuse_ino_t nodeid, struct statx *stxbuf) +{ + if (!f->conf.use_ino) + stxbuf->stx_ino = nodeid; + if (f->conf.set_mode) { + if (f->conf.dmask && S_ISDIR(stxbuf->stx_mode)) + stxbuf->stx_mode = (stxbuf->stx_mode & S_IFMT) | + (0777 & ~f->conf.dmask); + else if (f->conf.fmask) + stxbuf->stx_mode = (stxbuf->stx_mode & S_IFMT) | + (0777 & ~f->conf.fmask); + else + stxbuf->stx_mode = (stxbuf->stx_mode & S_IFMT) | + (0777 & ~f->conf.umask); + } + if (f->conf.set_uid) + stxbuf->stx_uid = f->conf.uid; + if (f->conf.set_gid) + stxbuf->stx_gid = f->conf.gid; +} +#endif + static struct fuse *req_fuse(fuse_req_t req) { return (struct fuse *) fuse_req_userdata(req); @@ -1592,146 +1616,133 @@ int fuse_fs_getattr(struct fuse_fs *fs, const char *path, struct stat *buf, struct fuse_file_info *fi) { fuse_get_context()->private_data = fs->user_data; - if (fs->op.getattr) { - if (fs->debug) { - char buf[10]; - fuse_log(FUSE_LOG_DEBUG, "getattr[%s] %s\n", - file_info_string(fi, buf, sizeof(buf)), - path); - } - return fs->op.getattr(path, buf, fi); - } else { + if (!fs->op.getattr) return -ENOSYS; + + if (fs->debug) { + char buf[10]; + + fuse_log(FUSE_LOG_DEBUG, "getattr[%s] %s\n", + file_info_string(fi, buf, sizeof(buf)), + path); } + return fs->op.getattr(path, buf, fi); } int fuse_fs_rename(struct fuse_fs *fs, const char *oldpath, const char *newpath, unsigned int flags) { fuse_get_context()->private_data = fs->user_data; - if (fs->op.rename) { - if (fs->debug) - fuse_log(FUSE_LOG_DEBUG, "rename %s %s 0x%x\n", oldpath, newpath, - flags); - - return fs->op.rename(oldpath, newpath, flags); - } else { + if (!fs->op.rename) return -ENOSYS; - } + if (fs->debug) + fuse_log(FUSE_LOG_DEBUG, "rename %s %s 0x%x\n", oldpath, newpath, + flags); + + return fs->op.rename(oldpath, newpath, flags); } int fuse_fs_unlink(struct fuse_fs *fs, const char *path) { fuse_get_context()->private_data = fs->user_data; - if (fs->op.unlink) { - if (fs->debug) - fuse_log(FUSE_LOG_DEBUG, "unlink %s\n", path); - - return fs->op.unlink(path); - } else { + if (!fs->op.unlink) return -ENOSYS; - } + if (fs->debug) + fuse_log(FUSE_LOG_DEBUG, "unlink %s\n", path); + + return fs->op.unlink(path); } int fuse_fs_rmdir(struct fuse_fs *fs, const char *path) { fuse_get_context()->private_data = fs->user_data; - if (fs->op.rmdir) { - if (fs->debug) - fuse_log(FUSE_LOG_DEBUG, "rmdir %s\n", path); - - return fs->op.rmdir(path); - } else { + if (!fs->op.rmdir) return -ENOSYS; - } + if (fs->debug) + fuse_log(FUSE_LOG_DEBUG, "rmdir %s\n", path); + + return fs->op.rmdir(path); } int fuse_fs_symlink(struct fuse_fs *fs, const char *linkname, const char *path) { fuse_get_context()->private_data = fs->user_data; - if (fs->op.symlink) { - if (fs->debug) - fuse_log(FUSE_LOG_DEBUG, "symlink %s %s\n", linkname, path); - - return fs->op.symlink(linkname, path); - } else { + if (!fs->op.symlink) return -ENOSYS; - } + if (fs->debug) + fuse_log(FUSE_LOG_DEBUG, "symlink %s %s\n", linkname, path); + + return fs->op.symlink(linkname, path); } int fuse_fs_link(struct fuse_fs *fs, const char *oldpath, const char *newpath) { fuse_get_context()->private_data = fs->user_data; - if (fs->op.link) { - if (fs->debug) - fuse_log(FUSE_LOG_DEBUG, "link %s %s\n", oldpath, newpath); - - return fs->op.link(oldpath, newpath); - } else { + if (!fs->op.link) return -ENOSYS; - } + if (fs->debug) + fuse_log(FUSE_LOG_DEBUG, "link %s %s\n", oldpath, newpath); + + return fs->op.link(oldpath, newpath); } int fuse_fs_release(struct fuse_fs *fs, const char *path, struct fuse_file_info *fi) { fuse_get_context()->private_data = fs->user_data; - if (fs->op.release) { - if (fs->debug) - fuse_log(FUSE_LOG_DEBUG, "release%s[%llu] flags: 0x%x\n", - fi->flush ? "+flush" : "", - (unsigned long long) fi->fh, fi->flags); - - return fs->op.release(path, fi); - } else { + if (!fs->op.release) return 0; - } + + if (fs->debug) + fuse_log(FUSE_LOG_DEBUG, "release%s[%llu] flags: 0x%x\n", + fi->flush ? "+flush" : "", + (unsigned long long) fi->fh, fi->flags); + + return fs->op.release(path, fi); } int fuse_fs_opendir(struct fuse_fs *fs, const char *path, struct fuse_file_info *fi) { + int err; + fuse_get_context()->private_data = fs->user_data; - if (fs->op.opendir) { - int err; + if (!fs->op.opendir) + return 0; - if (fs->debug) - fuse_log(FUSE_LOG_DEBUG, "opendir flags: 0x%x %s\n", fi->flags, - path); + if (fs->debug) + fuse_log(FUSE_LOG_DEBUG, "opendir flags: 0x%x %s\n", fi->flags, + path); - err = fs->op.opendir(path, fi); + err = fs->op.opendir(path, fi); - if (fs->debug && !err) - fuse_log(FUSE_LOG_DEBUG, " opendir[%llu] flags: 0x%x %s\n", - (unsigned long long) fi->fh, fi->flags, path); + if (fs->debug && !err) + fuse_log(FUSE_LOG_DEBUG, " opendir[%llu] flags: 0x%x %s\n", + (unsigned long long) fi->fh, fi->flags, path); - return err; - } else { - return 0; - } + return err; } int fuse_fs_open(struct fuse_fs *fs, const char *path, struct fuse_file_info *fi) { + int err; + fuse_get_context()->private_data = fs->user_data; - if (fs->op.open) { - int err; + if (!fs->op.open) + return 0; - if (fs->debug) - fuse_log(FUSE_LOG_DEBUG, "open flags: 0x%x %s\n", fi->flags, - path); + if (fs->debug) + fuse_log(FUSE_LOG_DEBUG, "open flags: 0x%x %s\n", fi->flags, + path); - err = fs->op.open(path, fi); + err = fs->op.open(path, fi); - if (fs->debug && !err) - fuse_log(FUSE_LOG_DEBUG, " open[%llu] flags: 0x%x %s\n", - (unsigned long long) fi->fh, fi->flags, path); + if (fs->debug && !err) + fuse_log(FUSE_LOG_DEBUG, " open[%llu] flags: 0x%x %s\n", + (unsigned long long) fi->fh, fi->flags, path); - return err; - } else { - return 0; - } + return err; } static void fuse_free_buf(struct fuse_bufvec *buf) @@ -1750,161 +1761,159 @@ int fuse_fs_read_buf(struct fuse_fs *fs, const char *path, struct fuse_bufvec **bufp, size_t size, off_t off, struct fuse_file_info *fi) { - fuse_get_context()->private_data = fs->user_data; - if (fs->op.read || fs->op.read_buf) { - int res; + int res; - if (fs->debug) - fuse_log(FUSE_LOG_DEBUG, - "read[%llu] %zu bytes from %llu flags: 0x%x\n", - (unsigned long long) fi->fh, - size, (unsigned long long) off, fi->flags); + fuse_get_context()->private_data = fs->user_data; + if (!fs->op.read && !fs->op.read_buf) + return -ENOSYS; - if (fs->op.read_buf) { - res = fs->op.read_buf(path, bufp, size, off, fi); - } else { - struct fuse_bufvec *buf; - void *mem; + if (fs->debug) + fuse_log(FUSE_LOG_DEBUG, + "read[%llu] %zu bytes from %llu flags: 0x%x\n", + (unsigned long long) fi->fh, + size, (unsigned long long) off, fi->flags); - buf = malloc(sizeof(struct fuse_bufvec)); - if (buf == NULL) - return -ENOMEM; + if (fs->op.read_buf) { + res = fs->op.read_buf(path, bufp, size, off, fi); + } else { + struct fuse_bufvec *buf; + void *mem; - mem = malloc(size); - if (mem == NULL) { - free(buf); - return -ENOMEM; - } - *buf = FUSE_BUFVEC_INIT(size); - buf->buf[0].mem = mem; - *bufp = buf; + buf = malloc(sizeof(struct fuse_bufvec)); + if (buf == NULL) + return -ENOMEM; - res = fs->op.read(path, mem, size, off, fi); - if (res >= 0) - buf->buf[0].size = res; + mem = malloc(size); + if (mem == NULL) { + free(buf); + return -ENOMEM; } + *buf = FUSE_BUFVEC_INIT(size); + buf->buf[0].mem = mem; + *bufp = buf; - if (fs->debug && res >= 0) - fuse_log(FUSE_LOG_DEBUG, " read[%llu] %zu bytes from %llu\n", - (unsigned long long) fi->fh, - fuse_buf_size(*bufp), - (unsigned long long) off); - if (res >= 0 && fuse_buf_size(*bufp) > size) - fuse_log(FUSE_LOG_ERR, "fuse: read too many bytes\n"); + res = fs->op.read(path, mem, size, off, fi); + if (res >= 0) + buf->buf[0].size = res; + } - if (res < 0) - return res; + if (fs->debug && res >= 0) + fuse_log(FUSE_LOG_DEBUG, " read[%llu] %zu bytes from %llu\n", + (unsigned long long) fi->fh, + fuse_buf_size(*bufp), + (unsigned long long) off); + if (res >= 0 && fuse_buf_size(*bufp) > size) + fuse_log(FUSE_LOG_ERR, "fuse: read too many bytes\n"); - return 0; - } else { - return -ENOSYS; - } + if (res < 0) + return res; + + return 0; } int fuse_fs_read(struct fuse_fs *fs, const char *path, char *mem, size_t size, off_t off, struct fuse_file_info *fi) { + int res; + fuse_get_context()->private_data = fs->user_data; - if (fs->op.read || fs->op.read_buf) { - int res; + if (!fs->op.read && !fs->op.read_buf) + return -ENOSYS; - if (fs->debug) - fuse_log(FUSE_LOG_DEBUG, - "read[%llu] %zu bytes from %llu flags: 0x%x\n", - (unsigned long long) fi->fh, - size, (unsigned long long) off, fi->flags); + if (fs->debug) + fuse_log(FUSE_LOG_DEBUG, + "read[%llu] %zu bytes from %llu flags: 0x%x\n", + (unsigned long long) fi->fh, + size, (unsigned long long) off, fi->flags); - if (fs->op.read_buf) { - struct fuse_bufvec *buf = NULL; + if (fs->op.read_buf) { + struct fuse_bufvec *buf = NULL; - res = fs->op.read_buf(path, &buf, size, off, fi); - if (res == 0) { - struct fuse_bufvec dst = FUSE_BUFVEC_INIT(size); + res = fs->op.read_buf(path, &buf, size, off, fi); + if (res == 0) { + struct fuse_bufvec dst = FUSE_BUFVEC_INIT(size); - dst.buf[0].mem = mem; - res = fuse_buf_copy(&dst, buf, 0); - } - fuse_free_buf(buf); - } else { - res = fs->op.read(path, mem, size, off, fi); + dst.buf[0].mem = mem; + res = fuse_buf_copy(&dst, buf, 0); } - - if (fs->debug && res >= 0) - fuse_log(FUSE_LOG_DEBUG, " read[%llu] %u bytes from %llu\n", - (unsigned long long) fi->fh, - res, - (unsigned long long) off); - if (res >= 0 && res > (int) size) - fuse_log(FUSE_LOG_ERR, "fuse: read too many bytes\n"); - - return res; + fuse_free_buf(buf); } else { - return -ENOSYS; + res = fs->op.read(path, mem, size, off, fi); } + + if (fs->debug && res >= 0) + fuse_log(FUSE_LOG_DEBUG, " read[%llu] %u bytes from %llu\n", + (unsigned long long) fi->fh, + res, + (unsigned long long) off); + if (res >= 0 && res > (int) size) + fuse_log(FUSE_LOG_ERR, "fuse: read too many bytes\n"); + + return res; } int fuse_fs_write_buf(struct fuse_fs *fs, const char *path, struct fuse_bufvec *buf, off_t off, struct fuse_file_info *fi) { + int res; + size_t size; + fuse_get_context()->private_data = fs->user_data; - if (fs->op.write_buf || fs->op.write) { - int res; - size_t size = fuse_buf_size(buf); + if (!fs->op.write_buf && !fs->op.write) + return -ENOSYS; - assert(buf->idx == 0 && buf->off == 0); - if (fs->debug) - fuse_log(FUSE_LOG_DEBUG, - "write%s[%llu] %zu bytes to %llu flags: 0x%x\n", - fi->writepage ? "page" : "", - (unsigned long long) fi->fh, - size, - (unsigned long long) off, - fi->flags); - - if (fs->op.write_buf) { - res = fs->op.write_buf(path, buf, off, fi); + size = fuse_buf_size(buf); + assert(buf->idx == 0 && buf->off == 0); + if (fs->debug) + fuse_log(FUSE_LOG_DEBUG, + "write%s[%llu] %zu bytes to %llu flags: 0x%x\n", + fi->writepage ? "page" : "", + (unsigned long long) fi->fh, + size, + (unsigned long long) off, + fi->flags); + + if (fs->op.write_buf) { + res = fs->op.write_buf(path, buf, off, fi); + } else { + void *mem = NULL; + struct fuse_buf *flatbuf; + struct fuse_bufvec tmp = FUSE_BUFVEC_INIT(size); + + if (buf->count == 1 && + !(buf->buf[0].flags & FUSE_BUF_IS_FD)) { + flatbuf = &buf->buf[0]; } else { - void *mem = NULL; - struct fuse_buf *flatbuf; - struct fuse_bufvec tmp = FUSE_BUFVEC_INIT(size); + res = -ENOMEM; + mem = malloc(size); + if (mem == NULL) + goto out; - if (buf->count == 1 && - !(buf->buf[0].flags & FUSE_BUF_IS_FD)) { - flatbuf = &buf->buf[0]; - } else { - res = -ENOMEM; - mem = malloc(size); - if (mem == NULL) - goto out; - - tmp.buf[0].mem = mem; - res = fuse_buf_copy(&tmp, buf, 0); - if (res <= 0) - goto out_free; - - tmp.buf[0].size = res; - flatbuf = &tmp.buf[0]; - } + tmp.buf[0].mem = mem; + res = fuse_buf_copy(&tmp, buf, 0); + if (res <= 0) + goto out_free; - res = fs->op.write(path, flatbuf->mem, flatbuf->size, - off, fi); -out_free: - free(mem); + tmp.buf[0].size = res; + flatbuf = &tmp.buf[0]; } -out: - if (fs->debug && res >= 0) - fuse_log(FUSE_LOG_DEBUG, " write%s[%llu] %u bytes to %llu\n", - fi->writepage ? "page" : "", - (unsigned long long) fi->fh, res, - (unsigned long long) off); - if (res > (int) size) - fuse_log(FUSE_LOG_ERR, "fuse: wrote too many bytes\n"); - return res; - } else { - return -ENOSYS; + res = fs->op.write(path, flatbuf->mem, flatbuf->size, + off, fi); +out_free: + free(mem); } +out: + if (fs->debug && res >= 0) + fuse_log(FUSE_LOG_DEBUG, " write%s[%llu] %u bytes to %llu\n", + fi->writepage ? "page" : "", + (unsigned long long) fi->fh, res, + (unsigned long long) off); + if (res > (int) size) + fuse_log(FUSE_LOG_ERR, "fuse: wrote too many bytes\n"); + + return res; } int fuse_fs_write(struct fuse_fs *fs, const char *path, const char *mem, @@ -1921,45 +1930,41 @@ int fuse_fs_fsync(struct fuse_fs *fs, const char *path, int datasync, struct fuse_file_info *fi) { fuse_get_context()->private_data = fs->user_data; - if (fs->op.fsync) { - if (fs->debug) - fuse_log(FUSE_LOG_DEBUG, "fsync[%llu] datasync: %i\n", - (unsigned long long) fi->fh, datasync); - - return fs->op.fsync(path, datasync, fi); - } else { + if (!fs->op.fsync) return -ENOSYS; - } + + if (fs->debug) + fuse_log(FUSE_LOG_DEBUG, "fsync[%llu] datasync: %i\n", + (unsigned long long) fi->fh, datasync); + + return fs->op.fsync(path, datasync, fi); } int fuse_fs_fsyncdir(struct fuse_fs *fs, const char *path, int datasync, struct fuse_file_info *fi) { fuse_get_context()->private_data = fs->user_data; - if (fs->op.fsyncdir) { - if (fs->debug) - fuse_log(FUSE_LOG_DEBUG, "fsyncdir[%llu] datasync: %i\n", - (unsigned long long) fi->fh, datasync); - - return fs->op.fsyncdir(path, datasync, fi); - } else { + if (!fs->op.fsyncdir) return -ENOSYS; - } + + if (fs->debug) + fuse_log(FUSE_LOG_DEBUG, "fsyncdir[%llu] datasync: %i\n", + (unsigned long long) fi->fh, datasync); + + return fs->op.fsyncdir(path, datasync, fi); } int fuse_fs_flush(struct fuse_fs *fs, const char *path, struct fuse_file_info *fi) { fuse_get_context()->private_data = fs->user_data; - if (fs->op.flush) { - if (fs->debug) - fuse_log(FUSE_LOG_DEBUG, "flush[%llu]\n", - (unsigned long long) fi->fh); - - return fs->op.flush(path, fi); - } else { + if (!fs->op.flush) return -ENOSYS; - } + if (fs->debug) + fuse_log(FUSE_LOG_DEBUG, "flush[%llu]\n", + (unsigned long long) fi->fh); + + return fs->op.flush(path, fi); } int fuse_fs_statfs(struct fuse_fs *fs, const char *path, struct statvfs *buf) @@ -1981,15 +1986,14 @@ int fuse_fs_releasedir(struct fuse_fs *fs, const char *path, struct fuse_file_info *fi) { fuse_get_context()->private_data = fs->user_data; - if (fs->op.releasedir) { - if (fs->debug) - fuse_log(FUSE_LOG_DEBUG, "releasedir[%llu] flags: 0x%x\n", - (unsigned long long) fi->fh, fi->flags); + if (!fs->op.releasedir) + return 0; - return fs->op.releasedir(path, fi); - } else { - return 0; - } + if (fs->debug) + fuse_log(FUSE_LOG_DEBUG, "releasedir[%llu] flags: 0x%x\n", + (unsigned long long) fi->fh, fi->flags); + + return fs->op.releasedir(path, fi); } int fuse_fs_readdir(struct fuse_fs *fs, const char *path, void *buf, @@ -1998,273 +2002,247 @@ int fuse_fs_readdir(struct fuse_fs *fs, const char *path, void *buf, enum fuse_readdir_flags flags) { fuse_get_context()->private_data = fs->user_data; - if (fs->op.readdir) { - if (fs->debug) { - fuse_log(FUSE_LOG_DEBUG, "readdir%s[%llu] from %llu\n", - (flags & FUSE_READDIR_PLUS) ? "plus" : "", - (unsigned long long) fi->fh, - (unsigned long long) off); - } - - return fs->op.readdir(path, buf, filler, off, fi, flags); - } else { + if (!fs->op.readdir) return -ENOSYS; + if (fs->debug) { + fuse_log(FUSE_LOG_DEBUG, "readdir%s[%llu] from %llu\n", + (flags & FUSE_READDIR_PLUS) ? "plus" : "", + (unsigned long long) fi->fh, + (unsigned long long) off); } + + return fs->op.readdir(path, buf, filler, off, fi, flags); } int fuse_fs_create(struct fuse_fs *fs, const char *path, mode_t mode, struct fuse_file_info *fi) { + int err; + fuse_get_context()->private_data = fs->user_data; - if (fs->op.create) { - int err; + if (!fs->op.create) + return -ENOSYS; - if (fs->debug) - fuse_log(FUSE_LOG_DEBUG, - "create flags: 0x%x %s 0%o umask=0%03o\n", - fi->flags, path, mode, - fuse_get_context()->umask); + if (fs->debug) + fuse_log(FUSE_LOG_DEBUG, + "create flags: 0x%x %s 0%o umask=0%03o\n", + fi->flags, path, mode, + fuse_get_context()->umask); - err = fs->op.create(path, mode, fi); + err = fs->op.create(path, mode, fi); - if (fs->debug && !err) - fuse_log(FUSE_LOG_DEBUG, " create[%llu] flags: 0x%x %s\n", - (unsigned long long) fi->fh, fi->flags, path); + if (fs->debug && !err) + fuse_log(FUSE_LOG_DEBUG, " create[%llu] flags: 0x%x %s\n", + (unsigned long long) fi->fh, fi->flags, path); - return err; - } else { - return -ENOSYS; - } + return err; } int fuse_fs_lock(struct fuse_fs *fs, const char *path, struct fuse_file_info *fi, int cmd, struct flock *lock) { fuse_get_context()->private_data = fs->user_data; - if (fs->op.lock) { - if (fs->debug) - fuse_log(FUSE_LOG_DEBUG, "lock[%llu] %s %s start: %llu len: %llu pid: %llu\n", - (unsigned long long) fi->fh, - (cmd == F_GETLK ? "F_GETLK" : - (cmd == F_SETLK ? "F_SETLK" : - (cmd == F_SETLKW ? "F_SETLKW" : "???"))), - (lock->l_type == F_RDLCK ? "F_RDLCK" : - (lock->l_type == F_WRLCK ? "F_WRLCK" : - (lock->l_type == F_UNLCK ? "F_UNLCK" : - "???"))), - (unsigned long long) lock->l_start, - (unsigned long long) lock->l_len, - (unsigned long long) lock->l_pid); - - return fs->op.lock(path, fi, cmd, lock); - } else { + if (!fs->op.lock) return -ENOSYS; - } + + if (fs->debug) + fuse_log(FUSE_LOG_DEBUG, "lock[%llu] %s %s start: %llu len: %llu pid: %llu\n", + (unsigned long long) fi->fh, + (cmd == F_GETLK ? "F_GETLK" : + (cmd == F_SETLK ? "F_SETLK" : + (cmd == F_SETLKW ? "F_SETLKW" : "???"))), + (lock->l_type == F_RDLCK ? "F_RDLCK" : + (lock->l_type == F_WRLCK ? "F_WRLCK" : + (lock->l_type == F_UNLCK ? "F_UNLCK" : + "???"))), + (unsigned long long) lock->l_start, + (unsigned long long) lock->l_len, + (unsigned long long) lock->l_pid); + + return fs->op.lock(path, fi, cmd, lock); } int fuse_fs_flock(struct fuse_fs *fs, const char *path, struct fuse_file_info *fi, int op) { fuse_get_context()->private_data = fs->user_data; - if (fs->op.flock) { - if (fs->debug) { - int xop = op & ~LOCK_NB; - - fuse_log(FUSE_LOG_DEBUG, "lock[%llu] %s%s\n", - (unsigned long long) fi->fh, - xop == LOCK_SH ? "LOCK_SH" : - (xop == LOCK_EX ? "LOCK_EX" : - (xop == LOCK_UN ? "LOCK_UN" : "???")), - (op & LOCK_NB) ? "|LOCK_NB" : ""); - } - return fs->op.flock(path, fi, op); - } else { + if (!fs->op.flock) return -ENOSYS; + + if (fs->debug) { + int xop = op & ~LOCK_NB; + + fuse_log(FUSE_LOG_DEBUG, "lock[%llu] %s%s\n", + (unsigned long long) fi->fh, + xop == LOCK_SH ? "LOCK_SH" : + (xop == LOCK_EX ? "LOCK_EX" : + (xop == LOCK_UN ? "LOCK_UN" : "???")), + (op & LOCK_NB) ? "|LOCK_NB" : ""); } + return fs->op.flock(path, fi, op); } int fuse_fs_chown(struct fuse_fs *fs, const char *path, uid_t uid, gid_t gid, struct fuse_file_info *fi) { fuse_get_context()->private_data = fs->user_data; - if (fs->op.chown) { - if (fs->debug) { - char buf[10]; - fuse_log(FUSE_LOG_DEBUG, "chown[%s] %s %lu %lu\n", - file_info_string(fi, buf, sizeof(buf)), - path, (unsigned long) uid, (unsigned long) gid); - } - return fs->op.chown(path, uid, gid, fi); - } else { + if (!fs->op.chown) return -ENOSYS; + if (fs->debug) { + char buf[10]; + + fuse_log(FUSE_LOG_DEBUG, "chown[%s] %s %lu %lu\n", + file_info_string(fi, buf, sizeof(buf)), + path, (unsigned long) uid, (unsigned long) gid); } + return fs->op.chown(path, uid, gid, fi); } int fuse_fs_truncate(struct fuse_fs *fs, const char *path, off_t size, struct fuse_file_info *fi) { fuse_get_context()->private_data = fs->user_data; - if (fs->op.truncate) { - if (fs->debug) { - char buf[10]; - fuse_log(FUSE_LOG_DEBUG, "truncate[%s] %llu\n", - file_info_string(fi, buf, sizeof(buf)), - (unsigned long long) size); - } - return fs->op.truncate(path, size, fi); - } else { + if (!fs->op.truncate) return -ENOSYS; + if (fs->debug) { + char buf[10]; + + fuse_log(FUSE_LOG_DEBUG, "truncate[%s] %llu\n", + file_info_string(fi, buf, sizeof(buf)), + (unsigned long long) size); } + return fs->op.truncate(path, size, fi); } int fuse_fs_utimens(struct fuse_fs *fs, const char *path, const struct timespec tv[2], struct fuse_file_info *fi) { fuse_get_context()->private_data = fs->user_data; - if (fs->op.utimens) { - if (fs->debug) { - char buf[10]; - fuse_log(FUSE_LOG_DEBUG, "utimens[%s] %s %li.%09lu %li.%09lu\n", - file_info_string(fi, buf, sizeof(buf)), - path, tv[0].tv_sec, tv[0].tv_nsec, - tv[1].tv_sec, tv[1].tv_nsec); - } - return fs->op.utimens(path, tv, fi); - } else { + if (!fs->op.utimens) return -ENOSYS; + if (fs->debug) { + char buf[10]; + + fuse_log(FUSE_LOG_DEBUG, "utimens[%s] %s %jd.%09ld %jd.%09ld\n", + file_info_string(fi, buf, sizeof(buf)), + path, (intmax_t)tv[0].tv_sec, tv[0].tv_nsec, + (intmax_t)tv[1].tv_sec, tv[1].tv_nsec); } + return fs->op.utimens(path, tv, fi); } int fuse_fs_access(struct fuse_fs *fs, const char *path, int mask) { fuse_get_context()->private_data = fs->user_data; - if (fs->op.access) { - if (fs->debug) - fuse_log(FUSE_LOG_DEBUG, "access %s 0%o\n", path, mask); - - return fs->op.access(path, mask); - } else { + if (!fs->op.access) return -ENOSYS; - } + if (fs->debug) + fuse_log(FUSE_LOG_DEBUG, "access %s 0%o\n", path, mask); + + return fs->op.access(path, mask); } int fuse_fs_readlink(struct fuse_fs *fs, const char *path, char *buf, size_t len) { fuse_get_context()->private_data = fs->user_data; - if (fs->op.readlink) { - if (fs->debug) - fuse_log(FUSE_LOG_DEBUG, "readlink %s %lu\n", path, - (unsigned long) len); - - return fs->op.readlink(path, buf, len); - } else { + if (!fs->op.readlink) return -ENOSYS; - } + if (fs->debug) + fuse_log(FUSE_LOG_DEBUG, "readlink %s %lu\n", path, + (unsigned long) len); + + return fs->op.readlink(path, buf, len); } int fuse_fs_mknod(struct fuse_fs *fs, const char *path, mode_t mode, dev_t rdev) { fuse_get_context()->private_data = fs->user_data; - if (fs->op.mknod) { - if (fs->debug) - fuse_log(FUSE_LOG_DEBUG, "mknod %s 0%o 0x%llx umask=0%03o\n", - path, mode, (unsigned long long) rdev, - fuse_get_context()->umask); - - return fs->op.mknod(path, mode, rdev); - } else { + if (!fs->op.mknod) return -ENOSYS; - } + if (fs->debug) + fuse_log(FUSE_LOG_DEBUG, "mknod %s 0%o 0x%llx umask=0%03o\n", + path, mode, (unsigned long long) rdev, + fuse_get_context()->umask); + + return fs->op.mknod(path, mode, rdev); } int fuse_fs_mkdir(struct fuse_fs *fs, const char *path, mode_t mode) { fuse_get_context()->private_data = fs->user_data; - if (fs->op.mkdir) { - if (fs->debug) - fuse_log(FUSE_LOG_DEBUG, "mkdir %s 0%o umask=0%03o\n", - path, mode, fuse_get_context()->umask); - - return fs->op.mkdir(path, mode); - } else { + if (!fs->op.mkdir) return -ENOSYS; - } + if (fs->debug) + fuse_log(FUSE_LOG_DEBUG, "mkdir %s 0%o umask=0%03o\n", + path, mode, fuse_get_context()->umask); + + return fs->op.mkdir(path, mode); } int fuse_fs_setxattr(struct fuse_fs *fs, const char *path, const char *name, const char *value, size_t size, int flags) { fuse_get_context()->private_data = fs->user_data; - if (fs->op.setxattr) { - if (fs->debug) - fuse_log(FUSE_LOG_DEBUG, "setxattr %s %s %lu 0x%x\n", - path, name, (unsigned long) size, flags); - - return fs->op.setxattr(path, name, value, size, flags); - } else { + if (!fs->op.setxattr) return -ENOSYS; - } + if (fs->debug) + fuse_log(FUSE_LOG_DEBUG, "setxattr %s %s %lu 0x%x\n", + path, name, (unsigned long) size, flags); + + return fs->op.setxattr(path, name, value, size, flags); } int fuse_fs_getxattr(struct fuse_fs *fs, const char *path, const char *name, char *value, size_t size) { fuse_get_context()->private_data = fs->user_data; - if (fs->op.getxattr) { - if (fs->debug) - fuse_log(FUSE_LOG_DEBUG, "getxattr %s %s %lu\n", - path, name, (unsigned long) size); - - return fs->op.getxattr(path, name, value, size); - } else { + if (!fs->op.getxattr) return -ENOSYS; - } + if (fs->debug) + fuse_log(FUSE_LOG_DEBUG, "getxattr %s %s %lu\n", + path, name, (unsigned long) size); + + return fs->op.getxattr(path, name, value, size); } int fuse_fs_listxattr(struct fuse_fs *fs, const char *path, char *list, size_t size) { fuse_get_context()->private_data = fs->user_data; - if (fs->op.listxattr) { - if (fs->debug) - fuse_log(FUSE_LOG_DEBUG, "listxattr %s %lu\n", - path, (unsigned long) size); - - return fs->op.listxattr(path, list, size); - } else { + if (!fs->op.listxattr) return -ENOSYS; - } + if (fs->debug) + fuse_log(FUSE_LOG_DEBUG, "listxattr %s %lu\n", + path, (unsigned long) size); + + return fs->op.listxattr(path, list, size); } int fuse_fs_bmap(struct fuse_fs *fs, const char *path, size_t blocksize, uint64_t *idx) { fuse_get_context()->private_data = fs->user_data; - if (fs->op.bmap) { - if (fs->debug) - fuse_log(FUSE_LOG_DEBUG, "bmap %s blocksize: %lu index: %llu\n", - path, (unsigned long) blocksize, - (unsigned long long) *idx); - - return fs->op.bmap(path, blocksize, idx); - } else { + if (!fs->op.bmap) return -ENOSYS; - } + if (fs->debug) + fuse_log(FUSE_LOG_DEBUG, "bmap %s blocksize: %lu index: %llu\n", + path, (unsigned long) blocksize, + (unsigned long long) *idx); + + return fs->op.bmap(path, blocksize, idx); } int fuse_fs_removexattr(struct fuse_fs *fs, const char *path, const char *name) { fuse_get_context()->private_data = fs->user_data; - if (fs->op.removexattr) { - if (fs->debug) - fuse_log(FUSE_LOG_DEBUG, "removexattr %s %s\n", path, name); - - return fs->op.removexattr(path, name); - } else { + if (!fs->op.removexattr) return -ENOSYS; - } + if (fs->debug) + fuse_log(FUSE_LOG_DEBUG, "removexattr %s %s\n", path, name); + + return fs->op.removexattr(path, name); } int fuse_fs_ioctl(struct fuse_fs *fs, const char *path, unsigned int cmd, @@ -2272,55 +2250,56 @@ int fuse_fs_ioctl(struct fuse_fs *fs, const char *path, unsigned int cmd, void *data) { fuse_get_context()->private_data = fs->user_data; - if (fs->op.ioctl) { - if (fs->debug) - fuse_log(FUSE_LOG_DEBUG, "ioctl[%llu] 0x%x flags: 0x%x\n", - (unsigned long long) fi->fh, cmd, flags); - - return fs->op.ioctl(path, cmd, arg, fi, flags, data); - } else + if (!fs->op.ioctl) return -ENOSYS; + if (fs->debug) + fuse_log(FUSE_LOG_DEBUG, "ioctl[%llu] 0x%x flags: 0x%x\n", + (unsigned long long) fi->fh, cmd, flags); + + return fs->op.ioctl(path, cmd, arg, fi, flags, data); } int fuse_fs_poll(struct fuse_fs *fs, const char *path, struct fuse_file_info *fi, struct fuse_pollhandle *ph, unsigned *reventsp) { + int res; + fuse_get_context()->private_data = fs->user_data; - if (fs->op.poll) { - int res; - if (fs->debug) - fuse_log(FUSE_LOG_DEBUG, "poll[%llu] ph: %p, events 0x%x\n", - (unsigned long long) fi->fh, ph, - fi->poll_events); + if (!fs->op.poll) { + fuse_pollhandle_destroy(ph); + return -ENOSYS; + } - res = fs->op.poll(path, fi, ph, reventsp); + if (fs->debug) + fuse_log(FUSE_LOG_DEBUG, "poll[%llu] ph: %p, events 0x%x\n", + (unsigned long long) fi->fh, ph, + fi->poll_events); - if (fs->debug && !res) - fuse_log(FUSE_LOG_DEBUG, " poll[%llu] revents: 0x%x\n", - (unsigned long long) fi->fh, *reventsp); + res = fs->op.poll(path, fi, ph, reventsp); - return res; - } else - return -ENOSYS; + if (fs->debug && !res) + fuse_log(FUSE_LOG_DEBUG, " poll[%llu] revents: 0x%x\n", + (unsigned long long) fi->fh, *reventsp); + + return res; } int fuse_fs_fallocate(struct fuse_fs *fs, const char *path, int mode, off_t offset, off_t length, struct fuse_file_info *fi) { fuse_get_context()->private_data = fs->user_data; - if (fs->op.fallocate) { - if (fs->debug) - fuse_log(FUSE_LOG_DEBUG, "fallocate %s mode %x, offset: %llu, length: %llu\n", - path, - mode, - (unsigned long long) offset, - (unsigned long long) length); - - return fs->op.fallocate(path, mode, offset, length, fi); - } else + if (!fs->op.fallocate) return -ENOSYS; + if (fs->debug) + fuse_log(FUSE_LOG_DEBUG, "fallocate %s mode %x, offset: %llu, length: %llu\n", + path, + mode, + (unsigned long long) offset, + (unsigned long long) length); + + return fs->op.fallocate(path, mode, offset, length, fi); } ssize_t fuse_fs_copy_file_range(struct fuse_fs *fs, const char *path_in, @@ -2330,38 +2309,69 @@ ssize_t fuse_fs_copy_file_range(struct fuse_fs *fs, const char *path_in, size_t len, int flags) { fuse_get_context()->private_data = fs->user_data; - if (fs->op.copy_file_range) { - if (fs->debug) - fuse_log(FUSE_LOG_DEBUG, "copy_file_range from %s:%llu to " - "%s:%llu, length: %llu\n", - path_in, - (unsigned long long) off_in, - path_out, - (unsigned long long) off_out, - (unsigned long long) len); - - return fs->op.copy_file_range(path_in, fi_in, off_in, path_out, - fi_out, off_out, len, flags); - } else + if (!fs->op.copy_file_range) return -ENOSYS; + if (fs->debug) + fuse_log(FUSE_LOG_DEBUG, "copy_file_range from %s:%llu to " + "%s:%llu, length: %llu\n", + path_in, + (unsigned long long) off_in, + path_out, + (unsigned long long) off_out, + (unsigned long long) len); + + return fs->op.copy_file_range(path_in, fi_in, off_in, path_out, + fi_out, off_out, len, flags); } off_t fuse_fs_lseek(struct fuse_fs *fs, const char *path, off_t off, int whence, struct fuse_file_info *fi) { fuse_get_context()->private_data = fs->user_data; - if (fs->op.lseek) { + if (!fs->op.lseek) + return -ENOSYS; + if (fs->debug) { + char buf[10]; + + fuse_log(FUSE_LOG_DEBUG, "lseek[%s] %llu %d\n", + file_info_string(fi, buf, sizeof(buf)), + (unsigned long long) off, whence); + } + return fs->op.lseek(path, off, whence, fi); +} + +#ifdef HAVE_STATX +int fuse_fs_statx(struct fuse_fs *fs, const char *path, int flags, int mask, + struct statx *stxbuf, struct fuse_file_info *fi) +{ + fuse_get_context()->private_data = fs->user_data; + if (fs->op.statx) { if (fs->debug) { char buf[10]; - fuse_log(FUSE_LOG_DEBUG, "lseek[%s] %llu %d\n", - file_info_string(fi, buf, sizeof(buf)), - (unsigned long long) off, whence); + + fuse_log(FUSE_LOG_DEBUG, "statx[%s] %s %d %d\n", + file_info_string(fi, buf, sizeof(buf)), path, + flags, mask); } - return fs->op.lseek(path, off, whence, fi); - } else { - return -ENOSYS; + return fs->op.statx(path, flags, mask, stxbuf, fi); } + + return -ENOSYS; } +#else +int fuse_fs_statx(struct fuse_fs *fs, const char *path, int flags, int mask, + struct statx *stxbuf, struct fuse_file_info *fi) +{ + (void)fs; + (void)path; + (void)flags; + (void)mask; + (void)stxbuf; + (void)fi; + + return -ENOSYS; +} +#endif static int is_open(struct fuse *f, fuse_ino_t dir, const char *name) { @@ -2526,7 +2536,11 @@ static struct fuse_context_i *fuse_create_context(struct fuse *f) fuse_log(FUSE_LOG_ERR, "fuse: failed to allocate thread specific data\n"); abort(); } - pthread_setspecific(fuse_context_key, c); + if (pthread_setspecific(fuse_context_key, c) != 0) { + fuse_log(FUSE_LOG_ERR, "fuse: failed to set thread specific data\n"); + free(c); + abort(); + } } else { memset(c, 0, sizeof(*c)); } @@ -2654,7 +2668,7 @@ static void fuse_lib_lookup(fuse_req_t req, fuse_ino_t parent, const char *name) { struct fuse *f = req_fuse_prepare(req); - struct fuse_entry_param e; + struct fuse_entry_param e = { .ino = 0 }; /* invalid ino */ char *path; int err; struct node *dot = NULL; @@ -2775,17 +2789,17 @@ int fuse_fs_chmod(struct fuse_fs *fs, const char *path, mode_t mode, struct fuse_file_info *fi) { fuse_get_context()->private_data = fs->user_data; - if (fs->op.chmod) { - if (fs->debug) { - char buf[10]; - fuse_log(FUSE_LOG_DEBUG, "chmod[%s] %s %llo\n", - file_info_string(fi, buf, sizeof(buf)), - path, (unsigned long long) mode); - } - return fs->op.chmod(path, mode, fi); - } - else + if (!fs->op.chmod) return -ENOSYS; + + if (fs->debug) { + char buf[10]; + + fuse_log(FUSE_LOG_DEBUG, "chmod[%s] %s %llo\n", + file_info_string(fi, buf, sizeof(buf)), + path, (unsigned long long) mode); + } + return fs->op.chmod(path, mode, fi); } static void fuse_lib_setattr(fuse_req_t req, fuse_ino_t ino, struct stat *attr, @@ -3563,17 +3577,17 @@ static int fill_dir_plus(void *dh_, const char *name, const struct stat *statp, if (statp && (flags & FUSE_FILL_DIR_PLUS)) { e.attr = *statp; - } else { - e.attr.st_ino = FUSE_UNKNOWN_INO; - if (statp) { - e.attr.st_mode = statp->st_mode; - if (f->conf.use_ino) - e.attr.st_ino = statp->st_ino; - } - if (!f->conf.use_ino && f->conf.readdir_ino) { - e.attr.st_ino = (ino_t) - lookup_nodeid(f, dh->nodeid, name); - } + } + + e.attr.st_ino = FUSE_UNKNOWN_INO; + if (statp) { + e.attr.st_mode = statp->st_mode; + if (f->conf.use_ino) + e.attr.st_ino = statp->st_ino; + } + if (!f->conf.use_ino && f->conf.readdir_ino) { + e.attr.st_ino = (ino_t) + lookup_nodeid(f, dh->nodeid, name); } if (off) { @@ -4323,6 +4337,8 @@ static void fuse_lib_poll(fuse_req_t req, fuse_ino_t ino, err = fuse_fs_poll(f->fs, path, fi, ph, &revents); fuse_finish_interrupt(f, req, &d); free_path(f, ino, path); + } else { + fuse_pollhandle_destroy(ph); } if (!err) fuse_reply_poll(req, revents); @@ -4412,6 +4428,55 @@ static void fuse_lib_lseek(fuse_req_t req, fuse_ino_t ino, off_t off, int whence reply_err(req, res); } +#ifdef HAVE_STATX +static void fuse_lib_statx(fuse_req_t req, fuse_ino_t ino, int flags, int mask, + struct fuse_file_info *fi) +{ + struct fuse *f = req_fuse_prepare(req); + struct statx stxbuf; + char *path; + int err; + + memset(&stxbuf, 0, sizeof(stxbuf)); + + if (fi != NULL) + err = get_path_nullok(f, ino, &path); + else + err = get_path(f, ino, &path); + + if (!err) { + struct fuse_intr_data d; + + if (!path) + flags |= AT_EMPTY_PATH; + fuse_prepare_interrupt(f, req, &d); + err = fuse_fs_statx(f->fs, path, flags, mask, &stxbuf, fi); + fuse_finish_interrupt(f, req, &d); + free_path(f, ino, path); + } + if (!err) { + struct node *node; + + pthread_mutex_lock(&f->lock); + node = get_node(f, ino); + if (node->is_hidden && stxbuf.stx_nlink > 0) + stxbuf.stx_nlink--; + if (f->conf.auto_cache) { + struct stat stbuf; + + stbuf.st_mtime = stxbuf.stx_mtime.tv_sec; + ST_MTIM_NSEC_SET(&stbuf, stxbuf.stx_mtime.tv_nsec); + stbuf.st_size = stxbuf.stx_size; + update_stat(node, &stbuf); + } + pthread_mutex_unlock(&f->lock); + set_statx(f, ino, &stxbuf); + fuse_reply_statx(req, 0, &stxbuf, f->conf.attr_timeout); + } else + reply_err(req, err); +} +#endif + static int clean_delay(struct fuse *f) { /* @@ -4510,6 +4575,9 @@ static struct fuse_lowlevel_ops fuse_path_ops = { .fallocate = fuse_lib_fallocate, .copy_file_range = fuse_lib_copy_file_range, .lseek = fuse_lib_lseek, +#ifdef HAVE_STATX + .statx = fuse_lib_statx, +#endif }; int fuse_notify_poll(struct fuse_pollhandle *ph) @@ -4569,8 +4637,7 @@ static int fuse_session_loop_remember(struct fuse *f) } } - free(fbuf.mem); - fuse_session_reset(se); + fuse_buf_free(&fbuf); return res < 0 ? -1 : 0; } @@ -4734,8 +4801,10 @@ static void print_module_help(const char *name, { struct fuse_args a = FUSE_ARGS_INIT(0, NULL); if (fuse_opt_add_arg(&a, "") == -1 || - fuse_opt_add_arg(&a, "-h") == -1) + fuse_opt_add_arg(&a, "-h") == -1) { + fuse_opt_free_args(&a); return; + } printf("\nOptions for %s module:\n", name); (*fac)(&a, NULL); fuse_opt_free_args(&a); @@ -4892,9 +4961,7 @@ static void *fuse_prune_nodes(void *fuse) struct fuse *f = fuse; int sleep_time; -#ifdef HAVE_PTHREAD_SETNAME_NP - pthread_setname_np(pthread_self(), "fuse_prune_nodes"); -#endif + fuse_set_thread_name("fuse_prune_nodes"); while(1) { sleep_time = fuse_clean_cache(f); @@ -5019,10 +5086,6 @@ struct fuse *_fuse_new_31(struct fuse_args *args, if (f->se == NULL) goto out_free_fs; - if (f->conf.debug) { - fuse_log(FUSE_LOG_DEBUG, "nullpath_ok: %i\n", f->conf.nullpath_ok); - } - /* Trace topmost layer by default */ f->fs->debug = f->conf.debug; f->ctr = 0; diff --git a/lib/fuse_i.h b/lib/fuse_i.h index acf9d5ae2..fa8d26b42 100644 --- a/lib/fuse_i.h +++ b/lib/fuse_i.h @@ -3,13 +3,18 @@ Copyright (C) 2001-2007 Miklos Szeredi <miklos@szeredi.hu> This program can be distributed under the terms of the GNU LGPLv2. - See the file COPYING.LIB + See the file LGPL2.txt */ +#ifndef LIB_FUSE_I_H_ +#define LIB_FUSE_I_H_ + #include "fuse.h" #include "fuse_lowlevel.h" #include "util.h" +#include <pthread.h> +#include <semaphore.h> #include <stdint.h> #include <stdbool.h> #include <errno.h> @@ -23,6 +28,7 @@ }) struct mount_opts; +struct fuse_ring_pool; struct fuse_req { struct fuse_session *se; @@ -32,7 +38,11 @@ struct fuse_req { struct fuse_ctx ctx; struct fuse_chan *ch; int interrupted; - unsigned int ioctl_64bit : 1; + struct { + unsigned int ioctl_64bit : 1; + unsigned int is_uring : 1; + unsigned int is_copy_file_range_64 : 1; + } flags; union { struct { uint64_t unique; @@ -54,9 +64,14 @@ struct fuse_notify_req { struct fuse_notify_req *prev; }; +struct fuse_session_uring { + bool enable; + unsigned int q_depth; + struct fuse_ring_pool *pool; +}; + struct fuse_session { _Atomic(char *)mountpoint; - volatile int exited; int fd; struct fuse_custom_io *io; struct mount_opts *mo; @@ -79,14 +94,23 @@ struct fuse_session { _Atomic size_t bufsize; int error; - /* This is useful if any kind of ABI incompatibility is found at + /* + * This is useful if any kind of ABI incompatibility is found at * a later version, to 'fix' it at run time. */ struct libfuse_version version; + /* thread synchronization */ + _Atomic bool mt_exited; + pthread_mutex_t mt_lock; + sem_t mt_finish; + /* true if reading requests from /dev/fuse are handled internally */ bool buf_reallocable; + /* io_uring */ + struct fuse_session_uring uring; + /* * conn->want and conn_want_ext options set by libfuse , needed * to correctly convert want to want_ext @@ -148,7 +172,7 @@ struct fuse_loop_config /** * The maximum number of available worker threads before they * start to get deleted when they become idle. If not - * specified, the default is 10. + * specified, the default is -1. * * Adjusting this has performance implications; a very small number * of threads in the pool will cause a lot of thread creation and @@ -196,7 +220,10 @@ int fuse_kern_mount(const char *mountpoint, struct mount_opts *mo); int fuse_send_reply_iov_nofree(fuse_req_t req, int error, struct iovec *iov, int count); void fuse_free_req(fuse_req_t req); +void list_init_req(struct fuse_req *req); +void _cuse_lowlevel_init(fuse_req_t req, const fuse_ino_t nodeid, + const void *req_header, const void *req_payload); void cuse_lowlevel_init(fuse_req_t req, fuse_ino_t nodeide, const void *inarg); int fuse_start_thread(pthread_t *thread_id, void *(*func)(void *), void *arg); @@ -235,3 +262,5 @@ int fuse_loop_cfg_verify(struct fuse_loop_config *config); /* room needed in buffer to accommodate header */ #define FUSE_BUFFER_HEADER_SIZE 0x1000 + +#endif /* LIB_FUSE_I_H_*/ diff --git a/lib/fuse_log.c b/lib/fuse_log.c index c1d16c148..66dcf8a06 100644 --- a/lib/fuse_log.c +++ b/lib/fuse_log.c @@ -5,63 +5,26 @@ Logging API. This program can be distributed under the terms of the GNU LGPLv2. - See the file COPYING.LIB + See the file LGPL2.txt */ #include "fuse_log.h" -#include <stdarg.h> #include <stdio.h> #include <stdbool.h> #include <syslog.h> +#include <stdarg.h> #define MAX_SYSLOG_LINE_LEN 512 static bool to_syslog = false; -static void default_log_func(__attribute__((unused)) enum fuse_log_level level, - const char *fmt, va_list ap) +static void default_log_func(enum fuse_log_level level, const char *fmt, va_list ap) { - if (to_syslog) { - int sys_log_level = LOG_ERR; - - /* - * with glibc fuse_log_level has identical values as - * syslog levels, but we also support BSD - better we convert to - * be sure. - */ - switch (level) { - case FUSE_LOG_DEBUG: - sys_log_level = LOG_DEBUG; - break; - case FUSE_LOG_INFO: - sys_log_level = LOG_INFO; - break; - case FUSE_LOG_NOTICE: - sys_log_level = LOG_NOTICE; - break; - case FUSE_LOG_WARNING: - sys_log_level = LOG_WARNING; - break; - case FUSE_LOG_ERR: - sys_log_level = LOG_ERR; - break; - case FUSE_LOG_CRIT: - sys_log_level = LOG_CRIT; - break; - case FUSE_LOG_ALERT: - sys_log_level = LOG_ALERT; - break; - case FUSE_LOG_EMERG: - sys_log_level = LOG_EMERG; - } - - char log[MAX_SYSLOG_LINE_LEN]; - vsnprintf(log, MAX_SYSLOG_LINE_LEN, fmt, ap); - syslog(sys_log_level, "%s", log); - } else { + if (to_syslog) + vsyslog(level, fmt, ap); + else vfprintf(stderr, fmt, ap); - } } static fuse_log_func_t log_func = default_log_func; diff --git a/lib/fuse_loop.c b/lib/fuse_loop.c index 410f43fbb..1ff075cbf 100644 --- a/lib/fuse_loop.c +++ b/lib/fuse_loop.c @@ -5,13 +5,13 @@ Implementation of the single-threaded FUSE session loop. This program can be distributed under the terms of the GNU LGPLv2. - See the file COPYING.LIB + See the file LGPL2.txt */ #include "fuse_config.h" #include "fuse_lowlevel.h" #include "fuse_i.h" - +#include "fuse_uring_i.h" #include <stdio.h> #include <stdlib.h> #include <errno.h> @@ -41,6 +41,8 @@ int fuse_session_loop(struct fuse_session *se) res = 0; if(se->error != 0) res = se->error; - fuse_session_reset(se); + + if (se->uring.pool) + fuse_uring_stop(se); return res; } diff --git a/lib/fuse_loop_mt.c b/lib/fuse_loop_mt.c index 01c51ccac..7bdb1c76f 100644 --- a/lib/fuse_loop_mt.c +++ b/lib/fuse_loop_mt.c @@ -5,7 +5,7 @@ Implementation of the multi-threaded FUSE session loop. This program can be distributed under the terms of the GNU LGPLv2. - See the file COPYING.LIB. + See the file LGPL2.txt. */ #define _GNU_SOURCE @@ -15,6 +15,7 @@ #include "fuse_misc.h" #include "fuse_kernel.h" #include "fuse_i.h" +#include "fuse_uring_i.h" #include "util.h" #include <stdio.h> @@ -53,14 +54,12 @@ struct fuse_worker { struct fuse_mt *mt; }; +/* synchronization via se->mt_lock */ struct fuse_mt { - pthread_mutex_t lock; int numworker; int numavail; struct fuse_session *se; struct fuse_worker main; - sem_t finish; - int exit; int error; int clone_fd; int max_idle; @@ -131,32 +130,30 @@ static void *fuse_do_work(void *data) { struct fuse_worker *w = (struct fuse_worker *) data; struct fuse_mt *mt = w->mt; + struct fuse_session *se = mt->se; -#ifdef HAVE_PTHREAD_SETNAME_NP - pthread_setname_np(pthread_self(), "fuse_worker"); -#endif + fuse_set_thread_name("fuse_worker"); - while (!fuse_session_exited(mt->se)) { + while (!fuse_session_exited(se)) { int isforget = 0; int res; pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); - res = fuse_session_receive_buf_internal(mt->se, &w->fbuf, - w->ch); + res = fuse_session_receive_buf_internal(se, &w->fbuf, w->ch); pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL); if (res == -EINTR) continue; if (res <= 0) { if (res < 0) { - fuse_session_exit(mt->se); + fuse_session_exit(se); mt->error = res; } break; } - pthread_mutex_lock(&mt->lock); - if (mt->exit) { - pthread_mutex_unlock(&mt->lock); + pthread_mutex_lock(&se->mt_lock); + if (fuse_session_exited(se)) { + pthread_mutex_unlock(&se->mt_lock); return NULL; } @@ -174,13 +171,14 @@ static void *fuse_do_work(void *data) if (!isforget) mt->numavail--; - if (mt->numavail == 0 && mt->numworker < mt->max_threads) + if (mt->numavail == 0 && mt->numworker < mt->max_threads && + likely(se->got_init)) fuse_loop_start_thread(mt); - pthread_mutex_unlock(&mt->lock); + pthread_mutex_unlock(&se->mt_lock); - fuse_session_process_buf_internal(mt->se, &w->fbuf, w->ch); + fuse_session_process_buf_internal(se, &w->fbuf, w->ch); - pthread_mutex_lock(&mt->lock); + pthread_mutex_lock(&se->mt_lock); if (!isforget) mt->numavail++; @@ -191,14 +189,14 @@ static void *fuse_do_work(void *data) * delayed, a moving average might be useful for that. */ if (mt->max_idle != -1 && mt->numavail > mt->max_idle && mt->numworker > 1) { - if (mt->exit) { - pthread_mutex_unlock(&mt->lock); + if (fuse_session_exited(se)) { + pthread_mutex_unlock(&se->mt_lock); return NULL; } list_del_worker(w); mt->numavail--; mt->numworker--; - pthread_mutex_unlock(&mt->lock); + pthread_mutex_unlock(&se->mt_lock); pthread_detach(w->thread_id); fuse_buf_free(&w->fbuf); @@ -206,11 +204,10 @@ static void *fuse_do_work(void *data) free(w); return NULL; } - pthread_mutex_unlock(&mt->lock); + pthread_mutex_unlock(&se->mt_lock); } - sem_post(&mt->finish); - + sem_post(&se->mt_finish); return NULL; } @@ -360,9 +357,9 @@ static int fuse_loop_start_thread(struct fuse_mt *mt) static void fuse_join_worker(struct fuse_mt *mt, struct fuse_worker *w) { pthread_join(w->thread_id, NULL); - pthread_mutex_lock(&mt->lock); + pthread_mutex_lock(&mt->se->mt_lock); list_del_worker(w); - pthread_mutex_unlock(&mt->lock); + pthread_mutex_unlock(&mt->se->mt_lock); fuse_buf_free(&w->fbuf); fuse_chan_put(w->ch); free(w); @@ -384,6 +381,8 @@ int err; } else { /* The caller does not care about parameters - use the default */ config = fuse_loop_cfg_create(); + if (!config) + return -ENOMEM; created_config = 1; } @@ -398,34 +397,34 @@ int err; mt.max_threads = config->max_threads; mt.main.thread_id = pthread_self(); mt.main.prev = mt.main.next = &mt.main; - sem_init(&mt.finish, 0, 0); - pthread_mutex_init(&mt.lock, NULL); - pthread_mutex_lock(&mt.lock); + pthread_mutex_lock(&se->mt_lock); err = fuse_loop_start_thread(&mt); - pthread_mutex_unlock(&mt.lock); + pthread_mutex_unlock(&se->mt_lock); if (!err) { - /* sem_wait() is interruptible */ while (!fuse_session_exited(se)) - sem_wait(&mt.finish); + sem_wait(&se->mt_finish); + if (se->debug) + fuse_log(FUSE_LOG_DEBUG, + "fuse: session exited, terminating workers\n"); - pthread_mutex_lock(&mt.lock); + pthread_mutex_lock(&se->mt_lock); for (w = mt.main.next; w != &mt.main; w = w->next) pthread_cancel(w->thread_id); - mt.exit = 1; - pthread_mutex_unlock(&mt.lock); + pthread_mutex_unlock(&se->mt_lock); while (mt.main.next != &mt.main) fuse_join_worker(&mt, mt.main.next); err = mt.error; + + if (se->uring.pool) + fuse_uring_stop(se); } - pthread_mutex_destroy(&mt.lock); - sem_destroy(&mt.finish); if(se->error != 0) err = se->error; - fuse_session_reset(se); + if (created_config) { fuse_loop_cfg_destroy(config); @@ -465,6 +464,8 @@ int fuse_session_loop_mt_31(struct fuse_session *se, int clone_fd) { int err; struct fuse_loop_config *config = fuse_loop_cfg_create(); + if (!config) + return -ENOMEM; if (clone_fd > 0) fuse_loop_cfg_set_clone_fd(config, clone_fd); err = fuse_session_loop_mt_312(se, config); diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index 2164ba5bf..00c281502 100644 --- a/lib/fuse_lowlevel.c +++ b/lib/fuse_lowlevel.c @@ -6,7 +6,7 @@ functions are implemented in separate files. This program can be distributed under the terms of the GNU LGPLv2. - See the file COPYING.LIB + See the file LGPL2.txt */ #define _GNU_SOURCE @@ -18,8 +18,12 @@ #include "fuse_misc.h" #include "mount_util.h" #include "util.h" +#include "fuse_uring_i.h" +#include <pthread.h> +#include <stdatomic.h> #include <stdint.h> +#include <inttypes.h> #include <stdbool.h> #include <stdio.h> #include <stdlib.h> @@ -32,6 +36,11 @@ #include <assert.h> #include <sys/file.h> #include <sys/ioctl.h> +#include <stdalign.h> + +#ifdef USDT_ENABLED +#include "usdt.h" +#endif #ifndef F_LINUX_SPECIFIC_BASE #define F_LINUX_SPECIFIC_BASE 1024 @@ -40,6 +49,9 @@ #define F_SETPIPE_SZ (F_LINUX_SPECIFIC_BASE + 7) #endif +#ifndef O_CLOEXEC +#define O_CLOEXEC 0 +#endif #define PARAM(inarg) (((char *)(inarg)) + sizeof(*(inarg))) #define OFFSET_MAX 0x7fffffffffffffffLL @@ -56,6 +68,45 @@ static __attribute__((constructor)) void fuse_ll_init_pagesize(void) pagesize = getpagesize(); } +#ifdef USDT_ENABLED +/* tracepoints */ +static void trace_request_receive(int err) +{ + USDT(libfuse, request_receive, err); +} + +static void trace_request_process(unsigned int opcode, unsigned int unique) +{ + USDT(libfuse, request_process, opcode, unique); +} + +static void trace_request_reply(uint64_t unique, unsigned int len, + int error, int reply_err) +{ + USDT(libfuse, request_reply, unique, len, error, reply_err); +} +#else +static void trace_request_receive(int err) +{ + (void)err; +} + +static void trace_request_process(unsigned int opcode, unsigned int unique) +{ + (void)opcode; + (void)unique; +} + +static void trace_request_reply(uint64_t unique, unsigned int len, + int error, int reply_err) +{ + (void)unique; + (void)len; + (void)error; + (void)reply_err; +} +#endif + static void convert_stat(const struct stat *stbuf, struct fuse_attr *attr) { attr->ino = stbuf->st_ino; @@ -99,7 +150,7 @@ static size_t iov_length(const struct iovec *iov, size_t count) return ret; } -static void list_init_req(struct fuse_req *req) +void list_init_req(struct fuse_req *req) { req->next = req; req->prev = req; @@ -124,6 +175,10 @@ static void list_add_req(struct fuse_req *req, struct fuse_req *next) static void destroy_req(fuse_req_t req) { + if (req->flags.is_uring) { + fuse_log(FUSE_LOG_ERR, "Refusing to destruct uring req\n"); + return; + } assert(req->ch == NULL); pthread_mutex_destroy(&req->lock); free(req); @@ -134,7 +189,11 @@ void fuse_free_req(fuse_req_t req) int ctr; struct fuse_session *se = req->se; - if (se->conn.no_interrupt) { + /* XXX: for now no support for interrupts with io-uring + * It actually might work already, though. But then would add + * a lock across ring queues. + */ + if (se->conn.no_interrupt || req->flags.is_uring) { ctr = --req->ref_cnt; fuse_chan_put(req->ch); req->ch = NULL; @@ -169,14 +228,47 @@ static struct fuse_req *fuse_ll_alloc_req(struct fuse_session *se) return req; } -/* Send data. If *ch* is NULL, send via session master fd */ +/* + * Send data to fuse-kernel using an fd of the fuse device. + */ +static int fuse_write_msg_dev(struct fuse_session *se, struct fuse_chan *ch, + struct iovec *iov, int count) +{ + ssize_t res; + int err; + + if (se->io != NULL) + + /* se->io->writev is never NULL if se->io is not NULL as + * specified by fuse_session_custom_io() + */ + res = se->io->writev(ch ? ch->fd : se->fd, iov, count, + se->userdata); + else + res = writev(ch ? ch->fd : se->fd, iov, count); + + if (res == -1) { + /* ENOENT means the operation was interrupted */ + err = errno; + if (!fuse_session_exited(se) && err != ENOENT) + perror("fuse: writing device"); + return -err; + } + + return 0; +} + static int fuse_send_msg(struct fuse_session *se, struct fuse_chan *ch, - struct iovec *iov, int count) + struct iovec *iov, int count, fuse_req_t req) { struct fuse_out_header *out = iov[0].iov_base; + int err; + bool is_uring = req && req->flags.is_uring ? true : false; - assert(se != NULL); + if (!is_uring) + assert(se != NULL); out->len = iov_length(iov, count); + if (se->debug) { if (out->unique == 0) { fuse_log(FUSE_LOG_DEBUG, "NOTIFY: code=%d length=%u\n", @@ -193,28 +285,15 @@ static int fuse_send_msg(struct fuse_session *se, struct fuse_chan *ch, } } - ssize_t res; - if (se->io != NULL) - /* se->io->writev is never NULL if se->io is not NULL as - specified by fuse_session_custom_io()*/ - res = se->io->writev(ch ? ch->fd : se->fd, iov, count, - se->userdata); + if (is_uring) + err = fuse_send_msg_uring(req, iov, count); else - res = writev(ch ? ch->fd : se->fd, iov, count); - - int err = errno; + err = fuse_write_msg_dev(se, ch, iov, count); - if (res == -1) { - /* ENOENT means the operation was interrupted */ - if (!fuse_session_exited(se) && err != ENOENT) - perror("fuse: writing device"); - return -err; - } - - return 0; + trace_request_reply(out->unique, out->len, out->error, err); + return err; } - int fuse_send_reply_iov_nofree(fuse_req_t req, int error, struct iovec *iov, int count) { @@ -236,7 +315,7 @@ int fuse_send_reply_iov_nofree(fuse_req_t req, int error, struct iovec *iov, iov[0].iov_base = &out; iov[0].iov_len = sizeof(struct fuse_out_header); - return fuse_send_msg(req->se, req->ch, iov, count); + return fuse_send_msg(req->se, req->ch, iov, count, req); } static int send_reply_iov(fuse_req_t req, int error, struct iovec *iov, @@ -252,6 +331,9 @@ static int send_reply_iov(fuse_req_t req, int error, struct iovec *iov, static int send_reply(fuse_req_t req, int error, const void *arg, size_t argsize) { + if (req->flags.is_uring) + return send_reply_uring(req, error, arg, argsize); + struct iovec iov[2]; int count = 1; if (argsize) { @@ -335,6 +417,10 @@ int fuse_reply_err(fuse_req_t req, int err) void fuse_reply_none(fuse_req_t req) { + if (req->flags.is_uring) { + send_reply_uring(req, 0, NULL, 0); + return; + } fuse_free_req(req); } @@ -511,7 +597,7 @@ int fuse_reply_open(fuse_req_t req, const struct fuse_file_info *f) return send_reply_ok(req, &arg, sizeof(arg)); } -int fuse_reply_write(fuse_req_t req, size_t count) +static int do_fuse_reply_write(fuse_req_t req, size_t count) { struct fuse_write_out arg; @@ -521,6 +607,28 @@ int fuse_reply_write(fuse_req_t req, size_t count) return send_reply_ok(req, &arg, sizeof(arg)); } +static int do_fuse_reply_copy(fuse_req_t req, size_t count) +{ + struct fuse_copy_file_range_out arg; + + memset(&arg, 0, sizeof(arg)); + arg.bytes_copied = count; + + return send_reply_ok(req, &arg, sizeof(arg)); +} + +int fuse_reply_write(fuse_req_t req, size_t count) +{ + /* + * This function is also used by FUSE_COPY_FILE_RANGE and its 64-bit + * variant. + */ + if (req->flags.is_copy_file_range_64) + return do_fuse_reply_copy(req, count); + else + return do_fuse_reply_write(req, count); +} + int fuse_reply_buf(fuse_req_t req, const char *buf, size_t size) { return send_reply_ok(req, buf, size); @@ -530,7 +638,7 @@ static int fuse_send_data_iov_fallback(struct fuse_session *se, struct fuse_chan *ch, struct iovec *iov, int iov_count, struct fuse_bufvec *buf, - size_t len) + size_t len, fuse_req_t req) { struct fuse_bufvec mem_buf = FUSE_BUFVEC_INIT(len); void *mbuf; @@ -545,7 +653,7 @@ static int fuse_send_data_iov_fallback(struct fuse_session *se, iov[iov_count].iov_base = buf->buf[0].mem; iov[iov_count].iov_len = len; iov_count++; - return fuse_send_msg(se, ch, iov, iov_count); + return fuse_send_msg(se, ch, iov, iov_count, req); } res = posix_memalign(&mbuf, pagesize, len); @@ -563,7 +671,7 @@ static int fuse_send_data_iov_fallback(struct fuse_session *se, iov[iov_count].iov_base = mbuf; iov[iov_count].iov_len = len; iov_count++; - res = fuse_send_msg(se, ch, iov, iov_count); + res = fuse_send_msg(se, ch, iov, iov_count, req); free(mbuf); return res; @@ -653,11 +761,15 @@ static int read_back(int fd, char *buf, size_t len) res = read(fd, buf, len); if (res == -1) { - fuse_log(FUSE_LOG_ERR, "fuse: internal error: failed to read back from pipe: %s\n", strerror(errno)); + fuse_log(FUSE_LOG_ERR, + "fuse: internal error: failed to read back from pipe: %s\n", + strerror(errno)); return -EIO; } if (res != len) { - fuse_log(FUSE_LOG_ERR, "fuse: internal error: short read back from pipe: %i from %zi\n", res, len); + fuse_log(FUSE_LOG_ERR, + "fuse: internal error: short read back from pipe: %i from %zd\n", + res, len); return -EIO; } return 0; @@ -670,7 +782,7 @@ static int grow_pipe_to_max(int pipefd) long maxfd; char buf[32]; - maxfd = open("/proc/sys/fs/pipe-max-size", O_RDONLY); + maxfd = open("/proc/sys/fs/pipe-max-size", O_RDONLY | O_CLOEXEC); if (maxfd < 0) return -errno; @@ -695,8 +807,9 @@ static int grow_pipe_to_max(int pipefd) } static int fuse_send_data_iov(struct fuse_session *se, struct fuse_chan *ch, - struct iovec *iov, int iov_count, - struct fuse_bufvec *buf, unsigned int flags) + struct iovec *iov, int iov_count, + struct fuse_bufvec *buf, unsigned int flags, + fuse_req_t req) { int res; size_t len = fuse_buf_size(buf); @@ -848,7 +961,7 @@ static int fuse_send_data_iov(struct fuse_session *se, struct fuse_chan *ch, iov[iov_count].iov_base = mbuf; iov[iov_count].iov_len = len; iov_count++; - res = fuse_send_msg(se, ch, iov, iov_count); + res = fuse_send_msg(se, ch, iov, iov_count, req); free(mbuf); return res; } @@ -895,17 +1008,18 @@ static int fuse_send_data_iov(struct fuse_session *se, struct fuse_chan *ch, return res; fallback: - return fuse_send_data_iov_fallback(se, ch, iov, iov_count, buf, len); + return fuse_send_data_iov_fallback(se, ch, iov, iov_count, buf, len, req); } #else static int fuse_send_data_iov(struct fuse_session *se, struct fuse_chan *ch, struct iovec *iov, int iov_count, - struct fuse_bufvec *buf, unsigned int flags) + struct fuse_bufvec *req_data, unsigned int flags, + fuse_req_t req) { - size_t len = fuse_buf_size(buf); + size_t len = fuse_buf_size(req_data); (void) flags; - return fuse_send_data_iov_fallback(se, ch, iov, iov_count, buf, len); + return fuse_send_data_iov_fallback(se, ch, iov, iov_count, req_data, len, req); } #endif @@ -916,13 +1030,16 @@ int fuse_reply_data(fuse_req_t req, struct fuse_bufvec *bufv, struct fuse_out_header out; int res; + if (req->flags.is_uring) + return fuse_reply_data_uring(req, bufv, flags); + iov[0].iov_base = &out; iov[0].iov_len = sizeof(struct fuse_out_header); out.unique = req->unique; out.error = 0; - res = fuse_send_data_iov(req->se, req->ch, iov, 1, bufv, flags); + res = fuse_send_data_iov(req->se, req->ch, iov, 1, bufv, flags, req); if (res <= 0) { fuse_free_req(req); return res; @@ -1031,7 +1148,7 @@ int fuse_reply_ioctl_retry(fuse_req_t req, } } else { /* Can't handle non-compat 64bit ioctls on 32bit */ - if (sizeof(void *) == 4 && req->ioctl_64bit) { + if (sizeof(void *) == 4 && req->flags.ioctl_64bit) { res = fuse_reply_err(req, EINVAL); goto out; } @@ -1133,9 +1250,39 @@ int fuse_reply_lseek(fuse_req_t req, off_t off) return send_reply_ok(req, &arg, sizeof(arg)); } -static void do_lookup(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) +#ifdef HAVE_STATX +int fuse_reply_statx(fuse_req_t req, int flags, struct statx *statx, + double attr_timeout) { - char *name = (char *) inarg; + struct fuse_statx_out arg; + + memset(&arg, 0, sizeof(arg)); + arg.flags = flags; + arg.attr_valid = calc_timeout_sec(attr_timeout); + arg.attr_valid_nsec = calc_timeout_nsec(attr_timeout); + memcpy(&arg.stat, statx, sizeof(arg.stat)); + + return send_reply_ok(req, &arg, sizeof(arg)); +} +#else +int fuse_reply_statx(fuse_req_t req, int flags, struct statx *statx, + double attr_timeout) +{ + (void)req; + (void)flags; + (void)statx; + (void)attr_timeout; + + return -ENOSYS; +} +#endif + +static void _do_lookup(fuse_req_t req, const fuse_ino_t nodeid, + const void *op_in, const void *in_payload) +{ + (void)op_in; + + char *name = (char *)in_payload; if (req->se->op.lookup) req->se->op.lookup(req, nodeid, name); @@ -1143,9 +1290,18 @@ static void do_lookup(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) fuse_reply_err(req, ENOSYS); } -static void do_forget(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) +static void do_lookup(fuse_req_t req, const fuse_ino_t nodeid, + const void *inarg) +{ + _do_lookup(req, nodeid, NULL, inarg); +} + +static void _do_forget(fuse_req_t req, const fuse_ino_t nodeid, + const void *op_in, const void *in_payload) { - struct fuse_forget_in *arg = (struct fuse_forget_in *) inarg; + (void)in_payload; + + struct fuse_forget_in *arg = (struct fuse_forget_in *)op_in; if (req->se->op.forget) req->se->op.forget(req, nodeid, arg->nlookup); @@ -1153,21 +1309,27 @@ static void do_forget(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) fuse_reply_none(req); } -static void do_batch_forget(fuse_req_t req, fuse_ino_t nodeid, - const void *inarg) +static void do_forget(fuse_req_t req, const fuse_ino_t nodeid, + const void *inarg) { - struct fuse_batch_forget_in *arg = (void *) inarg; - struct fuse_forget_one *param = (void *) PARAM(arg); + _do_forget(req, nodeid, inarg, NULL); +} + +static void _do_batch_forget(fuse_req_t req, const fuse_ino_t nodeid, + const void *op_in, const void *in_payload) +{ + (void)nodeid; unsigned int i; - (void) nodeid; + const struct fuse_batch_forget_in *arg = op_in; + const struct fuse_forget_one *forgets = in_payload; if (req->se->op.forget_multi) { req->se->op.forget_multi(req, arg->count, - (struct fuse_forget_data *) param); + (struct fuse_forget_data *)in_payload); } else if (req->se->op.forget) { for (i = 0; i < arg->count; i++) { - struct fuse_forget_one *forget = ¶m[i]; + const struct fuse_forget_one *forget = &forgets[i]; struct fuse_req *dummy_req; dummy_req = fuse_ll_alloc_req(req->se); @@ -1187,14 +1349,25 @@ static void do_batch_forget(fuse_req_t req, fuse_ino_t nodeid, } } -static void do_getattr(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) +static void do_batch_forget(fuse_req_t req, const fuse_ino_t nodeid, + const void *inarg) +{ + struct fuse_batch_forget_in *arg = (void *)inarg; + struct fuse_forget_one *param = (void *)PARAM(arg); + + _do_batch_forget(req, nodeid, inarg, param); +} + +static void _do_getattr(fuse_req_t req, const fuse_ino_t nodeid, + const void *op_in, const void *in_payload) { + struct fuse_getattr_in *arg = (struct fuse_getattr_in *)op_in; + (void)in_payload; + struct fuse_file_info *fip = NULL; struct fuse_file_info fi; if (req->se->conn.proto_minor >= 9) { - struct fuse_getattr_in *arg = (struct fuse_getattr_in *) inarg; - if (arg->getattr_flags & FUSE_GETATTR_FH) { memset(&fi, 0, sizeof(fi)); fi.fh = arg->fh; @@ -1208,9 +1381,18 @@ static void do_getattr(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) fuse_reply_err(req, ENOSYS); } -static void do_setattr(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) +static void do_getattr(fuse_req_t req, const fuse_ino_t nodeid, + const void *inarg) +{ + _do_getattr(req, nodeid, inarg, NULL); +} + +static void _do_setattr(fuse_req_t req, const fuse_ino_t nodeid, + const void *op_in, const void *in_payload) { - struct fuse_setattr_in *arg = (struct fuse_setattr_in *) inarg; + (void)in_payload; + const struct fuse_setattr_in *arg = op_in; + uint32_t valid = arg->valid; if (req->se->op.setattr) { struct fuse_file_info *fi = NULL; @@ -1219,32 +1401,34 @@ static void do_setattr(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) memset(&stbuf, 0, sizeof(stbuf)); convert_attr(arg, &stbuf); if (arg->valid & FATTR_FH) { - arg->valid &= ~FATTR_FH; + valid &= ~FATTR_FH; memset(&fi_store, 0, sizeof(fi_store)); fi = &fi_store; fi->fh = arg->fh; } - arg->valid &= - FUSE_SET_ATTR_MODE | - FUSE_SET_ATTR_UID | - FUSE_SET_ATTR_GID | - FUSE_SET_ATTR_SIZE | - FUSE_SET_ATTR_ATIME | - FUSE_SET_ATTR_MTIME | - FUSE_SET_ATTR_KILL_SUID | - FUSE_SET_ATTR_KILL_SGID | - FUSE_SET_ATTR_ATIME_NOW | - FUSE_SET_ATTR_MTIME_NOW | - FUSE_SET_ATTR_CTIME; - - req->se->op.setattr(req, nodeid, &stbuf, arg->valid, fi); + valid &= FUSE_SET_ATTR_MODE | FUSE_SET_ATTR_UID | + FUSE_SET_ATTR_GID | FUSE_SET_ATTR_SIZE | + FUSE_SET_ATTR_ATIME | FUSE_SET_ATTR_MTIME | + FUSE_SET_ATTR_KILL_SUID | FUSE_SET_ATTR_KILL_SGID | + FUSE_SET_ATTR_ATIME_NOW | FUSE_SET_ATTR_MTIME_NOW | + FUSE_SET_ATTR_CTIME; + + req->se->op.setattr(req, nodeid, &stbuf, valid, fi); } else fuse_reply_err(req, ENOSYS); } -static void do_access(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) +static void do_setattr(fuse_req_t req, const fuse_ino_t nodeid, + const void *inarg) +{ + _do_setattr(req, nodeid, inarg, NULL); +} + +static void _do_access(fuse_req_t req, const fuse_ino_t nodeid, + const void *op_in, const void *in_payload) { - struct fuse_access_in *arg = (struct fuse_access_in *) inarg; + (void)in_payload; + const struct fuse_access_in *arg = op_in; if (req->se->op.access) req->se->op.access(req, nodeid, arg->mask); @@ -1252,9 +1436,17 @@ static void do_access(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) fuse_reply_err(req, ENOSYS); } -static void do_readlink(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) +static void do_access(fuse_req_t req, const fuse_ino_t nodeid, + const void *inarg) { - (void) inarg; + _do_access(req, nodeid, inarg, NULL); +} + +static void _do_readlink(fuse_req_t req, const fuse_ino_t nodeid, + const void *op_in, const void *in_payload) +{ + (void)op_in; + (void)in_payload; if (req->se->op.readlink) req->se->op.readlink(req, nodeid); @@ -1262,15 +1454,20 @@ static void do_readlink(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) fuse_reply_err(req, ENOSYS); } -static void do_mknod(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) +static void do_readlink(fuse_req_t req, const fuse_ino_t nodeid, + const void *inarg) { - struct fuse_mknod_in *arg = (struct fuse_mknod_in *) inarg; - char *name = PARAM(arg); + _do_readlink(req, nodeid, inarg, NULL); +} + +static void _do_mknod(fuse_req_t req, const fuse_ino_t nodeid, + const void *op_in, const void *in_payload) +{ + const struct fuse_mknod_in *arg = (struct fuse_mknod_in *)op_in; + const char *name = in_payload; if (req->se->conn.proto_minor >= 12) req->ctx.umask = arg->umask; - else - name = (char *) inarg + FUSE_COMPAT_MKNOD_IN_SIZE; if (req->se->op.mknod) req->se->op.mknod(req, nodeid, name, arg->mode, arg->rdev); @@ -1278,22 +1475,45 @@ static void do_mknod(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) fuse_reply_err(req, ENOSYS); } -static void do_mkdir(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) +static void do_mknod(fuse_req_t req, const fuse_ino_t nodeid, const void *inarg) { - struct fuse_mkdir_in *arg = (struct fuse_mkdir_in *) inarg; + struct fuse_mknod_in *arg = (struct fuse_mknod_in *)inarg; + char *name = PARAM(arg); + + if (req->se->conn.proto_minor < 12) + name = (char *)inarg + FUSE_COMPAT_MKNOD_IN_SIZE; + + _do_mknod(req, nodeid, inarg, name); +} + +static void _do_mkdir(fuse_req_t req, const fuse_ino_t nodeid, + const void *op_in, const void *in_payload) +{ + const char *name = in_payload; + const struct fuse_mkdir_in *arg = op_in; if (req->se->conn.proto_minor >= 12) req->ctx.umask = arg->umask; if (req->se->op.mkdir) - req->se->op.mkdir(req, nodeid, PARAM(arg), arg->mode); + req->se->op.mkdir(req, nodeid, name, arg->mode); else fuse_reply_err(req, ENOSYS); } -static void do_unlink(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) +static void do_mkdir(fuse_req_t req, const fuse_ino_t nodeid, const void *inarg) { - char *name = (char *) inarg; + const struct fuse_mkdir_in *arg = inarg; + const char *name = PARAM(arg); + + _do_mkdir(req, nodeid, inarg, name); +} + +static void _do_unlink(fuse_req_t req, const fuse_ino_t nodeid, + const void *op_in, const void *in_payload) +{ + (void)op_in; + const char *name = in_payload; if (req->se->op.unlink) req->se->op.unlink(req, nodeid, name); @@ -1301,9 +1521,17 @@ static void do_unlink(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) fuse_reply_err(req, ENOSYS); } -static void do_rmdir(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) +static void do_unlink(fuse_req_t req, const fuse_ino_t nodeid, + const void *inarg) { - char *name = (char *) inarg; + _do_unlink(req, nodeid, NULL, inarg); +} + +static void _do_rmdir(fuse_req_t req, const fuse_ino_t nodeid, + const void *op_in, const void *in_payload) +{ + (void)op_in; + const char *name = in_payload; if (req->se->op.rmdir) req->se->op.rmdir(req, nodeid, name); @@ -1311,10 +1539,17 @@ static void do_rmdir(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) fuse_reply_err(req, ENOSYS); } -static void do_symlink(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) +static void do_rmdir(fuse_req_t req, const fuse_ino_t nodeid, const void *inarg) { - char *name = (char *) inarg; - char *linkname = ((char *) inarg) + strlen((char *) inarg) + 1; + _do_rmdir(req, nodeid, NULL, inarg); +} + +static void _do_symlink(fuse_req_t req, const fuse_ino_t nodeid, + const void *op_in, const void *in_payload) +{ + (void)op_in; + const char *name = (char *)in_payload; + const char *linkname = name + strlen(name) + 1; if (req->se->op.symlink) req->se->op.symlink(req, linkname, nodeid, name); @@ -1322,45 +1557,63 @@ static void do_symlink(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) fuse_reply_err(req, ENOSYS); } -static void do_rename(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) +static void do_symlink(fuse_req_t req, const fuse_ino_t nodeid, + const void *inarg) +{ + _do_symlink(req, nodeid, NULL, inarg); +} + +static void _do_rename(fuse_req_t req, const fuse_ino_t nodeid, + const void *op_in, const void *in_payload) { - struct fuse_rename_in *arg = (struct fuse_rename_in *) inarg; - char *oldname = PARAM(arg); - char *newname = oldname + strlen(oldname) + 1; + const struct fuse_rename_in *arg = (struct fuse_rename_in *)op_in; + const char *oldname = in_payload; + const char *newname = oldname + strlen(oldname) + 1; if (req->se->op.rename) req->se->op.rename(req, nodeid, oldname, arg->newdir, newname, - 0); + 0); else fuse_reply_err(req, ENOSYS); } -static void do_rename2(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) +static void do_rename(fuse_req_t req, const fuse_ino_t nodeid, + const void *inarg) +{ + const struct fuse_rename_in *arg = inarg; + const void *payload = PARAM(arg); + + _do_rename(req, nodeid, arg, payload); +} + +static void _do_rename2(fuse_req_t req, const fuse_ino_t nodeid, + const void *op_in, const void *in_payload) { - struct fuse_rename2_in *arg = (struct fuse_rename2_in *) inarg; - char *oldname = PARAM(arg); - char *newname = oldname + strlen(oldname) + 1; + const struct fuse_rename2_in *arg = op_in; + const char *oldname = in_payload; + const char *newname = oldname + strlen(oldname) + 1; if (req->se->op.rename) req->se->op.rename(req, nodeid, oldname, arg->newdir, newname, - arg->flags); + arg->flags); else fuse_reply_err(req, ENOSYS); } -static void do_link(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) +static void do_rename2(fuse_req_t req, const fuse_ino_t nodeid, + const void *inarg) { - struct fuse_link_in *arg = (struct fuse_link_in *) inarg; + const struct fuse_rename2_in *arg = inarg; + const void *payload = PARAM(arg); - if (req->se->op.link) - req->se->op.link(req, arg->oldnodeid, nodeid, PARAM(arg)); - else - fuse_reply_err(req, ENOSYS); + _do_rename2(req, nodeid, arg, payload); } -static void do_tmpfile(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) +static void _do_tmpfile(fuse_req_t req, fuse_ino_t nodeid, const void *op_in, + const void *in_payload) { - struct fuse_create_in *arg = (struct fuse_create_in *) inarg; + (void)in_payload; + const struct fuse_create_in *arg = op_in; if (req->se->op.tmpfile) { struct fuse_file_info fi; @@ -1376,35 +1629,83 @@ static void do_tmpfile(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) fuse_reply_err(req, ENOSYS); } -static void do_create(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) +static void do_tmpfile(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) { struct fuse_create_in *arg = (struct fuse_create_in *) inarg; + _do_tmpfile(req, nodeid, arg, NULL); +} + +static void _do_link(fuse_req_t req, const fuse_ino_t nodeid, const void *op_in, + const void *in_payload) +{ + struct fuse_link_in *arg = (struct fuse_link_in *)op_in; + + if (req->se->op.link) + req->se->op.link(req, arg->oldnodeid, nodeid, in_payload); + else + fuse_reply_err(req, ENOSYS); +} + +static void do_link(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) +{ + const struct fuse_link_in *arg = inarg; + const void *name = PARAM(arg); + + _do_link(req, nodeid, inarg, name); +} + +static void _do_create(fuse_req_t req, const fuse_ino_t nodeid, + const void *op_in, const void *in_payload) +{ + const struct fuse_create_in *arg = op_in; + const char *name = in_payload; + if (req->se->op.create) { struct fuse_file_info fi; - char *name = PARAM(arg); memset(&fi, 0, sizeof(fi)); fi.flags = arg->flags; if (req->se->conn.proto_minor >= 12) req->ctx.umask = arg->umask; - else - name = (char *) inarg + sizeof(struct fuse_open_in); + + if (req->se->conn.want_ext & FUSE_CAP_HANDLE_KILLPRIV_V2) + fi.kill_suidgid = + (arg->open_flags & FUSE_OPEN_KILL_SUIDGID) != 0; req->se->op.create(req, nodeid, name, arg->mode, &fi); - } else + } else { fuse_reply_err(req, ENOSYS); + } +} + +static void do_create(fuse_req_t req, const fuse_ino_t nodeid, + const void *inarg) +{ + const struct fuse_create_in *arg = (struct fuse_create_in *)inarg; + void *payload = PARAM(arg); + + if (req->se->conn.proto_minor < 12) + payload = (char *)inarg + sizeof(struct fuse_open_in); + + _do_create(req, nodeid, arg, payload); } -static void do_open(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) +static void _do_open(fuse_req_t req, const fuse_ino_t nodeid, const void *op_in, + const void *in_payload) { - struct fuse_open_in *arg = (struct fuse_open_in *) inarg; + (void)in_payload; + struct fuse_open_in *arg = (struct fuse_open_in *)op_in; struct fuse_file_info fi; memset(&fi, 0, sizeof(fi)); fi.flags = arg->flags; + if (req->se->conn.want_ext & FUSE_CAP_HANDLE_KILLPRIV_V2) + fi.kill_suidgid = + (arg->open_flags & FUSE_OPEN_KILL_SUIDGID) != 0; + if (req->se->op.open) req->se->op.open(req, nodeid, &fi); else if (req->se->conn.want_ext & FUSE_CAP_NO_OPEN_SUPPORT) @@ -1413,9 +1714,16 @@ static void do_open(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) fuse_reply_open(req, &fi); } -static void do_read(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) +static void do_open(fuse_req_t req, const fuse_ino_t nodeid, const void *inarg) { - struct fuse_read_in *arg = (struct fuse_read_in *) inarg; + _do_open(req, nodeid, inarg, NULL); +} + +static void _do_read(fuse_req_t req, const fuse_ino_t nodeid, const void *op_in, + const void *in_payload) +{ + (void)in_payload; + struct fuse_read_in *arg = (struct fuse_read_in *)op_in; if (req->se->op.read) { struct fuse_file_info fi; @@ -1431,68 +1739,105 @@ static void do_read(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) fuse_reply_err(req, ENOSYS); } -static void do_write(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) +static void do_read(fuse_req_t req, const fuse_ino_t nodeid, const void *inarg) { - struct fuse_write_in *arg = (struct fuse_write_in *) inarg; + _do_read(req, nodeid, inarg, NULL); +} + +static void _do_write(fuse_req_t req, const fuse_ino_t nodeid, + const void *op_in, const void *in_payload) +{ + struct fuse_write_in *arg = (struct fuse_write_in *)op_in; + const char *buf = in_payload; struct fuse_file_info fi; - char *param; memset(&fi, 0, sizeof(fi)); fi.fh = arg->fh; fi.writepage = (arg->write_flags & FUSE_WRITE_CACHE) != 0; - if (req->se->conn.proto_minor < 9) { - param = ((char *) arg) + FUSE_COMPAT_WRITE_IN_SIZE; - } else { + if (req->se->conn.want_ext & FUSE_CAP_HANDLE_KILLPRIV_V2) + fi.kill_suidgid = + (arg->write_flags & FUSE_WRITE_KILL_SUIDGID) != 0; + + if (req->se->conn.proto_minor >= 9) { fi.lock_owner = arg->lock_owner; fi.flags = arg->flags; - param = PARAM(arg); } if (req->se->op.write) - req->se->op.write(req, nodeid, param, arg->size, - arg->offset, &fi); + req->se->op.write(req, nodeid, buf, arg->size, arg->offset, + &fi); else fuse_reply_err(req, ENOSYS); } -static void do_write_buf(fuse_req_t req, fuse_ino_t nodeid, const void *inarg, - const struct fuse_buf *ibuf) +static void do_write(fuse_req_t req, const fuse_ino_t nodeid, const void *inarg) +{ + struct fuse_write_in *arg = (struct fuse_write_in *)inarg; + const void *payload; + + if (req->se->conn.proto_minor < 9) + payload = ((char *)arg) + FUSE_COMPAT_WRITE_IN_SIZE; + else + payload = PARAM(arg); + + _do_write(req, nodeid, arg, payload); +} + +static void _do_write_buf(fuse_req_t req, const fuse_ino_t nodeid, + const void *op_in, struct fuse_bufvec *bufv) { struct fuse_session *se = req->se; - struct fuse_bufvec bufv = { - .buf[0] = *ibuf, - .count = 1, - }; - struct fuse_write_in *arg = (struct fuse_write_in *) inarg; + struct fuse_write_in *arg = (struct fuse_write_in *)op_in; struct fuse_file_info fi; memset(&fi, 0, sizeof(fi)); fi.fh = arg->fh; fi.writepage = arg->write_flags & FUSE_WRITE_CACHE; + if (se->conn.want_ext & FUSE_CAP_HANDLE_KILLPRIV_V2) + fi.kill_suidgid = + (arg->write_flags & FUSE_WRITE_KILL_SUIDGID) != 0; + + if (se->conn.proto_minor >= 9) { + fi.lock_owner = arg->lock_owner; + fi.flags = arg->flags; + } + + se->op.write_buf(req, nodeid, bufv, arg->offset, &fi); +} + +static void do_write_buf(fuse_req_t req, const fuse_ino_t nodeid, + const void *inarg, const struct fuse_buf *ibuf) +{ + struct fuse_session *se = req->se; + struct fuse_bufvec bufv = { + .buf[0] = *ibuf, + .count = 1, + }; + struct fuse_write_in *arg = (struct fuse_write_in *)inarg; + if (se->conn.proto_minor < 9) { - bufv.buf[0].mem = ((char *) arg) + FUSE_COMPAT_WRITE_IN_SIZE; + bufv.buf[0].mem = ((char *)arg) + FUSE_COMPAT_WRITE_IN_SIZE; bufv.buf[0].size -= sizeof(struct fuse_in_header) + - FUSE_COMPAT_WRITE_IN_SIZE; + FUSE_COMPAT_WRITE_IN_SIZE; assert(!(bufv.buf[0].flags & FUSE_BUF_IS_FD)); } else { - fi.lock_owner = arg->lock_owner; - fi.flags = arg->flags; if (!(bufv.buf[0].flags & FUSE_BUF_IS_FD)) bufv.buf[0].mem = PARAM(arg); bufv.buf[0].size -= sizeof(struct fuse_in_header) + - sizeof(struct fuse_write_in); + sizeof(struct fuse_write_in); } if (bufv.buf[0].size < arg->size) { - fuse_log(FUSE_LOG_ERR, "fuse: do_write_buf: buffer size too small\n"); + fuse_log(FUSE_LOG_ERR, + "fuse: %s: buffer size too small\n", __func__); fuse_reply_err(req, EIO); goto out; } bufv.buf[0].size = arg->size; - se->op.write_buf(req, nodeid, &bufv, arg->offset, &fi); + _do_write_buf(req, nodeid, inarg, &bufv); out: /* Need to reset the pipe if ->write_buf() didn't consume all data */ @@ -1500,9 +1845,11 @@ static void do_write_buf(fuse_req_t req, fuse_ino_t nodeid, const void *inarg, fuse_ll_clear_pipe(se); } -static void do_flush(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) +static void _do_flush(fuse_req_t req, const fuse_ino_t nodeid, + const void *op_in, const void *in_payload) { - struct fuse_flush_in *arg = (struct fuse_flush_in *) inarg; + (void)in_payload; + struct fuse_flush_in *arg = (struct fuse_flush_in *)op_in; struct fuse_file_info fi; memset(&fi, 0, sizeof(fi)); @@ -1517,9 +1864,16 @@ static void do_flush(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) fuse_reply_err(req, ENOSYS); } -static void do_release(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) +static void do_flush(fuse_req_t req, const fuse_ino_t nodeid, const void *inarg) +{ + _do_flush(req, nodeid, inarg, NULL); +} + +static void _do_release(fuse_req_t req, const fuse_ino_t nodeid, + const void *op_in, const void *in_payload) { - struct fuse_release_in *arg = (struct fuse_release_in *) inarg; + (void)in_payload; + const struct fuse_release_in *arg = op_in; struct fuse_file_info fi; memset(&fi, 0, sizeof(fi)); @@ -1540,9 +1894,17 @@ static void do_release(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) fuse_reply_err(req, 0); } -static void do_fsync(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) +static void do_release(fuse_req_t req, const fuse_ino_t nodeid, + const void *inarg) { - struct fuse_fsync_in *arg = (struct fuse_fsync_in *) inarg; + _do_release(req, nodeid, inarg, NULL); +} + +static void _do_fsync(fuse_req_t req, const fuse_ino_t nodeid, + const void *op_in, const void *in_payload) +{ + (void)in_payload; + const struct fuse_fsync_in *arg = op_in; struct fuse_file_info fi; int datasync = arg->fsync_flags & 1; @@ -1555,13 +1917,21 @@ static void do_fsync(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) fuse_reply_err(req, ENOSYS); } -static void do_opendir(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) +static void do_fsync(fuse_req_t req, const fuse_ino_t nodeid, const void *inarg) +{ + _do_fsync(req, nodeid, inarg, NULL); +} + +static void _do_opendir(fuse_req_t req, const fuse_ino_t nodeid, + const void *op_in, const void *in_payload) { - struct fuse_open_in *arg = (struct fuse_open_in *) inarg; + (void)in_payload; + const struct fuse_open_in *arg = op_in; struct fuse_file_info fi; memset(&fi, 0, sizeof(fi)); fi.flags = arg->flags; + /* XXX: fuse_open_in::open_flags */ if (req->se->op.opendir) req->se->op.opendir(req, nodeid, &fi); @@ -1571,9 +1941,17 @@ static void do_opendir(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) fuse_reply_open(req, &fi); } -static void do_readdir(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) +static void do_opendir(fuse_req_t req, const fuse_ino_t nodeid, + const void *inarg) +{ + _do_opendir(req, nodeid, inarg, NULL); +} + +static void _do_readdir(fuse_req_t req, const fuse_ino_t nodeid, + const void *op_in, const void *in_payload) { - struct fuse_read_in *arg = (struct fuse_read_in *) inarg; + (void)in_payload; + struct fuse_read_in *arg = (struct fuse_read_in *)op_in; struct fuse_file_info fi; memset(&fi, 0, sizeof(fi)); @@ -1585,9 +1963,17 @@ static void do_readdir(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) fuse_reply_err(req, ENOSYS); } -static void do_readdirplus(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) +static void do_readdir(fuse_req_t req, const fuse_ino_t nodeid, + const void *inarg) { - struct fuse_read_in *arg = (struct fuse_read_in *) inarg; + _do_readdir(req, nodeid, inarg, NULL); +} + +static void _do_readdirplus(fuse_req_t req, const fuse_ino_t nodeid, + const void *op_in, const void *in_payload) +{ + (void)in_payload; + struct fuse_read_in *arg = (struct fuse_read_in *)op_in; struct fuse_file_info fi; memset(&fi, 0, sizeof(fi)); @@ -1599,9 +1985,17 @@ static void do_readdirplus(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) fuse_reply_err(req, ENOSYS); } -static void do_releasedir(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) +static void do_readdirplus(fuse_req_t req, const fuse_ino_t nodeid, + const void *inarg) +{ + _do_readdirplus(req, nodeid, inarg, NULL); +} + +static void _do_releasedir(fuse_req_t req, const fuse_ino_t nodeid, + const void *op_in, const void *in_payload) { - struct fuse_release_in *arg = (struct fuse_release_in *) inarg; + (void)in_payload; + struct fuse_release_in *arg = (struct fuse_release_in *)op_in; struct fuse_file_info fi; memset(&fi, 0, sizeof(fi)); @@ -1614,9 +2008,17 @@ static void do_releasedir(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) fuse_reply_err(req, 0); } -static void do_fsyncdir(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) +static void do_releasedir(fuse_req_t req, const fuse_ino_t nodeid, + const void *inarg) +{ + _do_releasedir(req, nodeid, inarg, NULL); +} + +static void _do_fsyncdir(fuse_req_t req, const fuse_ino_t nodeid, + const void *op_in, const void *in_payload) { - struct fuse_fsync_in *arg = (struct fuse_fsync_in *) inarg; + (void)in_payload; + struct fuse_fsync_in *arg = (struct fuse_fsync_in *)op_in; struct fuse_file_info fi; int datasync = arg->fsync_flags & 1; @@ -1628,11 +2030,18 @@ static void do_fsyncdir(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) else fuse_reply_err(req, ENOSYS); } +static void do_fsyncdir(fuse_req_t req, const fuse_ino_t nodeid, + const void *inarg) +{ + _do_fsyncdir(req, nodeid, inarg, NULL); +} -static void do_statfs(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) +static void _do_statfs(fuse_req_t req, const fuse_ino_t nodeid, + const void *op_in, const void *in_payload) { (void) nodeid; - (void) inarg; + (void)op_in; + (void)in_payload; if (req->se->op.statfs) req->se->op.statfs(req, nodeid); @@ -1644,55 +2053,93 @@ static void do_statfs(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) fuse_reply_statfs(req, &buf); } } +static void do_statfs(fuse_req_t req, const fuse_ino_t nodeid, + const void *inarg) +{ + _do_statfs(req, nodeid, inarg, NULL); +} -static void do_setxattr(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) +static void _do_setxattr(fuse_req_t req, const fuse_ino_t nodeid, + const void *op_in, const void *in_payload) { - struct fuse_session *se = req->se; - unsigned int xattr_ext = !!(se->conn.want_ext & FUSE_CAP_SETXATTR_EXT); - struct fuse_setxattr_in *arg = (struct fuse_setxattr_in *) inarg; - char *name = xattr_ext ? PARAM(arg) : - (char *)arg + FUSE_COMPAT_SETXATTR_IN_SIZE; - char *value = name + strlen(name) + 1; + struct fuse_setxattr_in *arg = (struct fuse_setxattr_in *)op_in; + const char *name = in_payload; + const char *value = name + strlen(name) + 1; /* XXX:The API should be extended to support extra_flags/setxattr_flags */ + if (req->se->op.setxattr) req->se->op.setxattr(req, nodeid, name, value, arg->size, - arg->flags); + arg->flags); else fuse_reply_err(req, ENOSYS); } +static void do_setxattr(fuse_req_t req, const fuse_ino_t nodeid, + const void *inarg) +{ + struct fuse_session *se = req->se; + unsigned int xattr_ext = !!(se->conn.want & FUSE_CAP_SETXATTR_EXT); + const struct fuse_setxattr_in *arg = inarg; + char *payload = xattr_ext ? PARAM(arg) : + (char *)arg + FUSE_COMPAT_SETXATTR_IN_SIZE; + + _do_setxattr(req, nodeid, arg, payload); +} -static void do_getxattr(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) +static void _do_getxattr(fuse_req_t req, const fuse_ino_t nodeid, + const void *op_in, const void *in_payload) { - struct fuse_getxattr_in *arg = (struct fuse_getxattr_in *) inarg; + const struct fuse_getxattr_in *arg = op_in; if (req->se->op.getxattr) - req->se->op.getxattr(req, nodeid, PARAM(arg), arg->size); + req->se->op.getxattr(req, nodeid, in_payload, arg->size); else fuse_reply_err(req, ENOSYS); } -static void do_listxattr(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) +static void do_getxattr(fuse_req_t req, const fuse_ino_t nodeid, + const void *inarg) +{ + const struct fuse_getxattr_in *arg = inarg; + const void *payload = PARAM(arg); + + _do_getxattr(req, nodeid, arg, payload); +} + +static void _do_listxattr(fuse_req_t req, const fuse_ino_t nodeid, + const void *inarg, const void *in_payload) { - struct fuse_getxattr_in *arg = (struct fuse_getxattr_in *) inarg; + (void)in_payload; + const struct fuse_getxattr_in *arg = inarg; if (req->se->op.listxattr) req->se->op.listxattr(req, nodeid, arg->size); else fuse_reply_err(req, ENOSYS); } +static void do_listxattr(fuse_req_t req, const fuse_ino_t nodeid, + const void *inarg) +{ + _do_listxattr(req, nodeid, inarg, NULL); +} -static void do_removexattr(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) +static void _do_removexattr(fuse_req_t req, const fuse_ino_t nodeid, + const void *inarg, const void *in_payload) { - char *name = (char *) inarg; + (void)inarg; + const char *name = in_payload; if (req->se->op.removexattr) req->se->op.removexattr(req, nodeid, name); else fuse_reply_err(req, ENOSYS); } +static void do_removexattr(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) +{ + _do_removexattr(req, nodeid, NULL, inarg); +} -static void convert_fuse_file_lock(struct fuse_file_lock *fl, +static void convert_fuse_file_lock(const struct fuse_file_lock *fl, struct flock *flock) { memset(flock, 0, sizeof(struct flock)); @@ -1706,9 +2153,11 @@ static void convert_fuse_file_lock(struct fuse_file_lock *fl, flock->l_pid = fl->pid; } -static void do_getlk(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) +static void _do_getlk(fuse_req_t req, const fuse_ino_t nodeid, + const void *op_in, const void *in_payload) { - struct fuse_lk_in *arg = (struct fuse_lk_in *) inarg; + (void)in_payload; + const struct fuse_lk_in *arg = op_in; struct fuse_file_info fi; struct flock flock; @@ -1722,11 +2171,15 @@ static void do_getlk(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) else fuse_reply_err(req, ENOSYS); } +static void do_getlk(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) +{ + _do_getlk(req, nodeid, inarg, NULL); +} -static void do_setlk_common(fuse_req_t req, fuse_ino_t nodeid, - const void *inarg, int sleep) +static void do_setlk_common(fuse_req_t req, const fuse_ino_t nodeid, + const void *op_in, int sleep) { - struct fuse_lk_in *arg = (struct fuse_lk_in *) inarg; + const struct fuse_lk_in *arg = op_in; struct fuse_file_info fi; struct flock flock; @@ -1764,14 +2217,27 @@ static void do_setlk_common(fuse_req_t req, fuse_ino_t nodeid, } } -static void do_setlk(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) +static void _do_setlk(fuse_req_t req, const fuse_ino_t nodeid, + const void *op_in, const void *in_payload) +{ + (void)in_payload; + do_setlk_common(req, nodeid, op_in, 0); +} + +static void do_setlk(fuse_req_t req, const fuse_ino_t nodeid, const void *inarg) { - do_setlk_common(req, nodeid, inarg, 0); + _do_setlk(req, nodeid, inarg, NULL); } +static void _do_setlkw(fuse_req_t req, const fuse_ino_t nodeid, + const void *op_in, const void *in_payload) +{ + (void)in_payload; + do_setlk_common(req, nodeid, op_in, 1); +} static void do_setlkw(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) { - do_setlk_common(req, nodeid, inarg, 1); + _do_setlkw(req, nodeid, inarg, NULL); } static int find_interrupted(struct fuse_session *se, struct fuse_req *req) @@ -1814,9 +2280,11 @@ static int find_interrupted(struct fuse_session *se, struct fuse_req *req) return 0; } -static void do_interrupt(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) +static void _do_interrupt(fuse_req_t req, const fuse_ino_t nodeid, + const void *op_in, const void *in_payload) { - struct fuse_interrupt_in *arg = (struct fuse_interrupt_in *) inarg; + (void)in_payload; + const struct fuse_interrupt_in *arg = op_in; struct fuse_session *se = req->se; (void) nodeid; @@ -1835,6 +2303,10 @@ static void do_interrupt(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) list_add_req(req, &se->interrupts); pthread_mutex_unlock(&se->lock); } +static void do_interrupt(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) +{ + _do_interrupt(req, nodeid, inarg, NULL); +} static struct fuse_req *check_interrupt(struct fuse_session *se, struct fuse_req *req) @@ -1861,21 +2333,28 @@ static struct fuse_req *check_interrupt(struct fuse_session *se, return NULL; } -static void do_bmap(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) +static void _do_bmap(fuse_req_t req, const fuse_ino_t nodeid, const void *op_in, + const void *in_payload) { - struct fuse_bmap_in *arg = (struct fuse_bmap_in *) inarg; + (void)in_payload; + const struct fuse_bmap_in *arg = op_in; if (req->se->op.bmap) req->se->op.bmap(req, nodeid, arg->blocksize, arg->block); else fuse_reply_err(req, ENOSYS); } +static void do_bmap(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) +{ + _do_bmap(req, nodeid, inarg, NULL); +} -static void do_ioctl(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) +static void _do_ioctl(fuse_req_t req, const fuse_ino_t nodeid, + const void *op_in, const void *in_payload) { - struct fuse_ioctl_in *arg = (struct fuse_ioctl_in *) inarg; + struct fuse_ioctl_in *arg = (struct fuse_ioctl_in *)op_in; unsigned int flags = arg->flags; - void *in_buf = arg->in_size ? PARAM(arg) : NULL; + const void *in_buf = in_payload; struct fuse_file_info fi; if (flags & FUSE_IOCTL_DIR && @@ -1889,7 +2368,7 @@ static void do_ioctl(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) if (sizeof(void *) == 4 && req->se->conn.proto_minor >= 16 && !(flags & FUSE_IOCTL_32BIT)) { - req->ioctl_64bit = 1; + req->flags.ioctl_64bit = 1; } if (req->se->op.ioctl) @@ -1899,15 +2378,24 @@ static void do_ioctl(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) else fuse_reply_err(req, ENOSYS); } +static void do_ioctl(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) +{ + const struct fuse_ioctl_in *arg = inarg; + void *in_buf = arg->in_size ? PARAM(arg) : NULL; + + _do_ioctl(req, nodeid, arg, in_buf); +} void fuse_pollhandle_destroy(struct fuse_pollhandle *ph) { free(ph); } -static void do_poll(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) +static void _do_poll(fuse_req_t req, const fuse_ino_t nodeid, const void *op_in, + const void *in_payload) { - struct fuse_poll_in *arg = (struct fuse_poll_in *) inarg; + (void)in_payload; + struct fuse_poll_in *arg = (struct fuse_poll_in *)op_in; struct fuse_file_info fi; memset(&fi, 0, sizeof(fi)); @@ -1933,23 +2421,37 @@ static void do_poll(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) } } -static void do_fallocate(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) +static void do_poll(fuse_req_t req, const fuse_ino_t nodeid, const void *inarg) { - struct fuse_fallocate_in *arg = (struct fuse_fallocate_in *) inarg; + _do_poll(req, nodeid, inarg, NULL); +} + +static void _do_fallocate(fuse_req_t req, const fuse_ino_t nodeid, + const void *op_in, const void *in_payload) +{ + (void)in_payload; + const struct fuse_fallocate_in *arg = op_in; struct fuse_file_info fi; memset(&fi, 0, sizeof(fi)); fi.fh = arg->fh; if (req->se->op.fallocate) - req->se->op.fallocate(req, nodeid, arg->mode, arg->offset, arg->length, &fi); + req->se->op.fallocate(req, nodeid, arg->mode, arg->offset, + arg->length, &fi); else fuse_reply_err(req, ENOSYS); } -static void do_copy_file_range(fuse_req_t req, fuse_ino_t nodeid_in, const void *inarg) +static void do_fallocate(fuse_req_t req, const fuse_ino_t nodeid, + const void *inarg) +{ + _do_fallocate(req, nodeid, inarg, NULL); +} + +static void copy_file_range_common(fuse_req_t req, const fuse_ino_t nodeid_in, + const struct fuse_copy_file_range_in *arg) { - struct fuse_copy_file_range_in *arg = (struct fuse_copy_file_range_in *) inarg; struct fuse_file_info fi_in, fi_out; memset(&fi_in, 0, sizeof(fi_in)); @@ -1958,19 +2460,63 @@ static void do_copy_file_range(fuse_req_t req, fuse_ino_t nodeid_in, const void memset(&fi_out, 0, sizeof(fi_out)); fi_out.fh = arg->fh_out; - if (req->se->op.copy_file_range) - req->se->op.copy_file_range(req, nodeid_in, arg->off_in, - &fi_in, arg->nodeid_out, - arg->off_out, &fi_out, arg->len, - arg->flags); + req->se->op.copy_file_range(req, nodeid_in, arg->off_in, &fi_in, + arg->nodeid_out, arg->off_out, + &fi_out, arg->len, arg->flags); else fuse_reply_err(req, ENOSYS); } -static void do_lseek(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) +static void _do_copy_file_range(fuse_req_t req, const fuse_ino_t nodeid_in, + const void *op_in, const void *in_payload) { - struct fuse_lseek_in *arg = (struct fuse_lseek_in *) inarg; + const struct fuse_copy_file_range_in *arg = op_in; + struct fuse_copy_file_range_in arg_tmp; + + (void) in_payload; + /* fuse_write_out can only handle 32bit copy size */ + if (arg->len > 0xfffff000) { + arg_tmp = *arg; + arg_tmp.len = 0xfffff000; + arg = &arg_tmp; + } + copy_file_range_common(req, nodeid_in, arg); +} + +static void do_copy_file_range(fuse_req_t req, const fuse_ino_t nodeid_in, + const void *inarg) +{ + _do_copy_file_range(req, nodeid_in, inarg, NULL); +} + +static void _do_copy_file_range_64(fuse_req_t req, const fuse_ino_t nodeid_in, + const void *op_in, const void *in_payload) +{ + (void) in_payload; + req->flags.is_copy_file_range_64 = 1; + /* Limit size on 32bit userspace to avoid conversion overflow */ + if (sizeof(size_t) == 4) + _do_copy_file_range(req, nodeid_in, op_in, NULL); + else + copy_file_range_common(req, nodeid_in, op_in); +} + +static void do_copy_file_range_64(fuse_req_t req, const fuse_ino_t nodeid_in, + const void *inarg) +{ + _do_copy_file_range_64(req, nodeid_in, inarg, NULL); +} + +/* + * Note that the uint64_t offset in struct fuse_lseek_in is derived from + * linux kernel loff_t and is therefore signed. + */ +static void _do_lseek(fuse_req_t req, const fuse_ino_t nodeid, + const void *op_in, const void *in_payload) +{ + (void)in_payload; + const struct fuse_lseek_in *arg = op_in; struct fuse_file_info fi; memset(&fi, 0, sizeof(fi)); @@ -1982,13 +2528,54 @@ static void do_lseek(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) fuse_reply_err(req, ENOSYS); } +static void do_lseek(fuse_req_t req, const fuse_ino_t nodeid, const void *inarg) +{ + _do_lseek(req, nodeid, inarg, NULL); +} + +#ifdef HAVE_STATX +static void _do_statx(fuse_req_t req, const fuse_ino_t nodeid, + const void *op_in, const void *in_payload) +{ + (void)in_payload; + const struct fuse_statx_in *arg = op_in; + struct fuse_file_info *fip = NULL; + struct fuse_file_info fi; + + if (arg->getattr_flags & FUSE_GETATTR_FH) { + memset(&fi, 0, sizeof(fi)); + fi.fh = arg->fh; + fip = &fi; + } + + if (req->se->op.statx) + req->se->op.statx(req, nodeid, arg->sx_flags, arg->sx_mask, fip); + else + fuse_reply_err(req, ENOSYS); +} +#else +static void _do_statx(fuse_req_t req, const fuse_ino_t nodeid, + const void *op_in, const void *in_payload) +{ + (void)in_payload; + (void)nodeid; + (void)op_in; + fuse_reply_err(req, ENOSYS); +} +#endif + +static void do_statx(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) +{ + _do_statx(req, nodeid, inarg, NULL); +} + static bool want_flags_valid(uint64_t capable, uint64_t want) { uint64_t unknown_flags = want & (~capable); if (unknown_flags != 0) { fuse_log(FUSE_LOG_ERR, - "fuse: unknown connection 'want' flags: 0x%08lx\n", - unknown_flags); + "fuse: unknown connection 'want' flags: 0x%08llx\n", + (unsigned long long)unknown_flags); return false; } return true; @@ -2014,10 +2601,12 @@ int fuse_convert_to_conn_want_ext(struct fuse_conn_info *conn) fuse_lower_32_bits(conn->want_ext) != conn->want) { if (conn->want_ext != se->conn_want_ext) { fuse_log(FUSE_LOG_ERR, - "%s: Both conn->want_ext and conn->want are set.\n" - "want=%x, want_ext=%lx, se->want=%lx se->want_ext=%lx\n", - __func__, conn->want, conn->want_ext, - se->conn_want, se->conn_want_ext); + "%s: Both conn->want_ext and conn->want are set.\n" + "want=%x want_ext=%llx, se->want=%x se->want_ext=%llx\n", + __func__, conn->want, + (unsigned long long)conn->want_ext, + se->conn_want, + (unsigned long long)se->conn_want_ext); return -EINVAL; } @@ -2064,13 +2653,14 @@ bool fuse_get_feature_flag(struct fuse_conn_info *conn, return conn->capable_ext & flag ? true : false; } - /* Prevent bogus data races (bogus since "init" is called before * multi-threading becomes relevant */ -static __attribute__((no_sanitize("thread"))) -void do_init(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) +static __attribute__((no_sanitize("thread"))) void +_do_init(fuse_req_t req, const fuse_ino_t nodeid, const void *op_in, + const void *in_payload) { - struct fuse_init_in *arg = (struct fuse_init_in *) inarg; + (void)in_payload; + const struct fuse_init_in *arg = op_in; struct fuse_init_out outarg; struct fuse_session *se = req->se; size_t bufsize = se->bufsize; @@ -2079,6 +2669,8 @@ void do_init(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) uint64_t outargflags = 0; bool buf_reallocable = se->buf_reallocable; (void) nodeid; + bool enable_io_uring = false; + if (se->debug) { fuse_log(FUSE_LOG_DEBUG, "INIT: %u.%u\n", arg->major, arg->minor); if (arg->major == 7 && arg->minor >= 6) { @@ -2172,6 +2764,9 @@ void do_init(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) se->conn.capable_ext |= FUSE_CAP_PASSTHROUGH; if (inargflags & FUSE_NO_EXPORT_SUPPORT) se->conn.capable_ext |= FUSE_CAP_NO_EXPORT_SUPPORT; + if (inargflags & FUSE_OVER_IO_URING) + se->conn.capable_ext |= FUSE_CAP_OVER_IO_URING; + } else { se->conn.max_readahead = 0; } @@ -2195,7 +2790,7 @@ void do_init(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) /* Default settings for modern filesystems. * * Most of these capabilities were disabled by default in - * libfuse2 for backwards compatibility reasons. In libfused, + * libfuse2 for backwards compatibility reasons. In libfuse3, * we can finally enable them by default (as long as they're * supported by the kernel). */ @@ -2215,6 +2810,7 @@ void do_init(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) LL_SET_DEFAULT(se->op.readdirplus, FUSE_CAP_READDIRPLUS); LL_SET_DEFAULT(se->op.readdirplus && se->op.readdir, FUSE_CAP_READDIRPLUS_AUTO); + LL_SET_DEFAULT(1, FUSE_CAP_OVER_IO_URING); /* This could safely become default, but libfuse needs an API extension * to support it @@ -2223,7 +2819,6 @@ void do_init(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) se->conn.time_gran = 1; - se->got_init = 1; if (se->op.init) { // Apply the first 32 bits of capable_ext to capable se->conn.capable = fuse_lower_32_bits(se->conn.capable_ext); @@ -2325,13 +2920,15 @@ void do_init(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) } if (se->conn.want_ext & FUSE_CAP_NO_EXPORT_SUPPORT) outargflags |= FUSE_NO_EXPORT_SUPPORT; - - if (inargflags & FUSE_INIT_EXT) { - outargflags |= FUSE_INIT_EXT; - outarg.flags2 = outargflags >> 32; + if (se->uring.enable && se->conn.want_ext & FUSE_CAP_OVER_IO_URING) { + outargflags |= FUSE_OVER_IO_URING; + enable_io_uring = true; } - outarg.flags = outargflags; + if ((inargflags & FUSE_REQUEST_TIMEOUT) && se->conn.request_timeout) { + outargflags |= FUSE_REQUEST_TIMEOUT; + outarg.request_timeout = se->conn.request_timeout; + } outarg.max_readahead = se->conn.max_readahead; outarg.max_write = se->conn.max_write; @@ -2372,16 +2969,52 @@ void do_init(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) else if (arg->minor < 23) outargsize = FUSE_COMPAT_22_INIT_OUT_SIZE; + /* XXX: Add an option to make non-available io-uring fatal */ + if (enable_io_uring) { + int ring_rc = fuse_uring_start(se); + + if (ring_rc != 0) { + fuse_log(FUSE_LOG_INFO, + "fuse: failed to start io-uring: %s\n", + strerror(ring_rc)); + outargflags &= ~FUSE_OVER_IO_URING; + enable_io_uring = false; + } + } + + if (inargflags & FUSE_INIT_EXT) { + outargflags |= FUSE_INIT_EXT; + outarg.flags2 = outargflags >> 32; + } + outarg.flags = outargflags; + + /* + * Has to be set before replying, as new kernel requests might + * immediately arrive and got_init is used for op-code sanity. + * Especially with external handlers, where we have no control + * over the thread scheduling. + */ + se->got_init = 1; send_reply_ok(req, &outarg, outargsize); + if (enable_io_uring) + fuse_uring_wake_ring_threads(se); } -static void do_destroy(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) +static __attribute__((no_sanitize("thread"))) void +do_init(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) +{ + _do_init(req, nodeid, inarg, NULL); +} + +static void _do_destroy(fuse_req_t req, const fuse_ino_t nodeid, + const void *op_in, const void *in_payload) { struct fuse_session *se = req->se; char *mountpoint; (void) nodeid; - (void) inarg; + (void)op_in; + (void)in_payload; mountpoint = atomic_exchange(&se->mountpoint, NULL); free(mountpoint); @@ -2394,6 +3027,11 @@ static void do_destroy(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) send_reply_ok(req, NULL, 0); } +static void do_destroy(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) +{ + _do_destroy(req, nodeid, inarg, NULL); +} + static void list_del_nreq(struct fuse_notify_req *nreq) { struct fuse_notify_req *prev = nreq->prev; @@ -2418,7 +3056,7 @@ static void list_init_nreq(struct fuse_notify_req *nreq) nreq->prev = nreq; } -static void do_notify_reply(fuse_req_t req, fuse_ino_t nodeid, +static void _do_notify_reply(fuse_req_t req, fuse_ino_t nodeid, const void *inarg, const struct fuse_buf *buf) { struct fuse_session *se = req->se; @@ -2439,10 +3077,20 @@ static void do_notify_reply(fuse_req_t req, fuse_ino_t nodeid, nreq->reply(nreq, req, nodeid, inarg, buf); } +static void do_notify_reply(fuse_req_t req, fuse_ino_t nodeid, + const void *inarg, const struct fuse_buf *ibuf) +{ + struct fuse_buf buf = *ibuf; + + buf.size -= sizeof(struct fuse_in_header); + _do_notify_reply(req, nodeid, inarg, &buf); +} + static int send_notify_iov(struct fuse_session *se, int notify_code, struct iovec *iov, int count) { struct fuse_out_header out; + struct fuse_req *req = NULL; if (!se->got_init) return -ENOTCONN; @@ -2452,7 +3100,7 @@ static int send_notify_iov(struct fuse_session *se, int notify_code, iov[0].iov_base = &out; iov[0].iov_len = sizeof(struct fuse_out_header); - return fuse_send_msg(se, NULL, iov, count); + return fuse_send_msg(se, NULL, iov, count, req); } int fuse_lowlevel_notify_poll(struct fuse_pollhandle *ph) @@ -2494,6 +3142,19 @@ int fuse_lowlevel_notify_inval_inode(struct fuse_session *se, fuse_ino_t ino, return send_notify_iov(se, FUSE_NOTIFY_INVAL_INODE, iov, 2); } +int fuse_lowlevel_notify_increment_epoch(struct fuse_session *se) +{ + struct iovec iov[1]; + + if (!se) + return -EINVAL; + + if (se->conn.proto_minor < 44) + return -ENOSYS; + + return send_notify_iov(se, FUSE_NOTIFY_INC_EPOCH, iov, 1); +} + /** * Notify parent attributes and the dentry matching parent/name * @@ -2594,6 +3255,7 @@ int fuse_lowlevel_notify_store(struct fuse_session *se, fuse_ino_t ino, struct iovec iov[3]; size_t size = fuse_buf_size(bufv); int res; + struct fuse_req *req = NULL; if (!se) return -EINVAL; @@ -2614,7 +3276,7 @@ int fuse_lowlevel_notify_store(struct fuse_session *se, fuse_ino_t ino, iov[1].iov_base = &outarg; iov[1].iov_len = sizeof(outarg); - res = fuse_send_data_iov(se, NULL, iov, 2, bufv, flags); + res = fuse_send_data_iov(se, NULL, iov, 2, bufv, flags, req); if (res > 0) res = -res; @@ -2643,8 +3305,7 @@ static void fuse_ll_retrieve_reply(struct fuse_notify_req *nreq, if (!(bufv.buf[0].flags & FUSE_BUF_IS_FD)) bufv.buf[0].mem = PARAM(arg); - bufv.buf[0].size -= sizeof(struct fuse_in_header) + - sizeof(struct fuse_notify_retrieve_in); + bufv.buf[0].size -= sizeof(struct fuse_notify_retrieve_in); if (bufv.buf[0].size < arg->size) { fuse_log(FUSE_LOG_ERR, "fuse: retrieve reply: buffer size too small\n"); @@ -2744,8 +3405,25 @@ int fuse_req_interrupted(fuse_req_t req) return interrupted; } +bool fuse_req_is_uring(fuse_req_t req) +{ + return req->flags.is_uring; +} + +#ifndef HAVE_URING +int fuse_req_get_payload(fuse_req_t req, char **payload, size_t *payload_sz, + void **mr) +{ + (void)req; + (void)payload; + (void)payload_sz; + (void)mr; + return -ENOTSUP; +} +#endif + static struct { - void (*func)(fuse_req_t, fuse_ino_t, const void *); + void (*func)(fuse_req_t req, const fuse_ino_t node, const void *arg); const char *name; } fuse_ll_ops[] = { [FUSE_LOOKUP] = { do_lookup, "LOOKUP" }, @@ -2781,7 +3459,7 @@ static struct { [FUSE_SETLKW] = { do_setlkw, "SETLKW" }, [FUSE_ACCESS] = { do_access, "ACCESS" }, [FUSE_CREATE] = { do_create, "CREATE" }, - [FUSE_TMPFILE] = { do_tmpfile, "TMPFILE" }, + [FUSE_TMPFILE] = { do_tmpfile, "TMPFILE" }, [FUSE_INTERRUPT] = { do_interrupt, "INTERRUPT" }, [FUSE_BMAP] = { do_bmap, "BMAP" }, [FUSE_IOCTL] = { do_ioctl, "IOCTL" }, @@ -2793,10 +3471,68 @@ static struct { [FUSE_READDIRPLUS] = { do_readdirplus, "READDIRPLUS"}, [FUSE_RENAME2] = { do_rename2, "RENAME2" }, [FUSE_COPY_FILE_RANGE] = { do_copy_file_range, "COPY_FILE_RANGE" }, + [FUSE_COPY_FILE_RANGE_64] = { do_copy_file_range_64, "COPY_FILE_RANGE_64" }, [FUSE_LSEEK] = { do_lseek, "LSEEK" }, + [FUSE_STATX] = { do_statx, "STATX" }, [CUSE_INIT] = { cuse_lowlevel_init, "CUSE_INIT" }, }; +static struct { + void (*func)(fuse_req_t req, const fuse_ino_t ino, const void *op_in, + const void *op_payload); + const char *name; +} fuse_ll_ops2[] __attribute__((unused)) = { + [FUSE_LOOKUP] = { _do_lookup, "LOOKUP" }, + [FUSE_FORGET] = { _do_forget, "FORGET" }, + [FUSE_GETATTR] = { _do_getattr, "GETATTR" }, + [FUSE_SETATTR] = { _do_setattr, "SETATTR" }, + [FUSE_READLINK] = { _do_readlink, "READLINK" }, + [FUSE_SYMLINK] = { _do_symlink, "SYMLINK" }, + [FUSE_MKNOD] = { _do_mknod, "MKNOD" }, + [FUSE_MKDIR] = { _do_mkdir, "MKDIR" }, + [FUSE_UNLINK] = { _do_unlink, "UNLINK" }, + [FUSE_RMDIR] = { _do_rmdir, "RMDIR" }, + [FUSE_RENAME] = { _do_rename, "RENAME" }, + [FUSE_LINK] = { _do_link, "LINK" }, + [FUSE_OPEN] = { _do_open, "OPEN" }, + [FUSE_READ] = { _do_read, "READ" }, + [FUSE_WRITE] = { _do_write, "WRITE" }, + [FUSE_STATFS] = { _do_statfs, "STATFS" }, + [FUSE_RELEASE] = { _do_release, "RELEASE" }, + [FUSE_FSYNC] = { _do_fsync, "FSYNC" }, + [FUSE_SETXATTR] = { _do_setxattr, "SETXATTR" }, + [FUSE_GETXATTR] = { _do_getxattr, "GETXATTR" }, + [FUSE_LISTXATTR] = { _do_listxattr, "LISTXATTR" }, + [FUSE_REMOVEXATTR] = { _do_removexattr, "REMOVEXATTR" }, + [FUSE_FLUSH] = { _do_flush, "FLUSH" }, + [FUSE_INIT] = { _do_init, "INIT" }, + [FUSE_OPENDIR] = { _do_opendir, "OPENDIR" }, + [FUSE_READDIR] = { _do_readdir, "READDIR" }, + [FUSE_RELEASEDIR] = { _do_releasedir, "RELEASEDIR" }, + [FUSE_FSYNCDIR] = { _do_fsyncdir, "FSYNCDIR" }, + [FUSE_GETLK] = { _do_getlk, "GETLK" }, + [FUSE_SETLK] = { _do_setlk, "SETLK" }, + [FUSE_SETLKW] = { _do_setlkw, "SETLKW" }, + [FUSE_ACCESS] = { _do_access, "ACCESS" }, + [FUSE_CREATE] = { _do_create, "CREATE" }, + [FUSE_TMPFILE] = { _do_tmpfile, "TMPFILE" }, + [FUSE_INTERRUPT] = { _do_interrupt, "INTERRUPT" }, + [FUSE_BMAP] = { _do_bmap, "BMAP" }, + [FUSE_IOCTL] = { _do_ioctl, "IOCTL" }, + [FUSE_POLL] = { _do_poll, "POLL" }, + [FUSE_FALLOCATE] = { _do_fallocate, "FALLOCATE" }, + [FUSE_DESTROY] = { _do_destroy, "DESTROY" }, + [FUSE_NOTIFY_REPLY] = { (void *)1, "NOTIFY_REPLY" }, + [FUSE_BATCH_FORGET] = { _do_batch_forget, "BATCH_FORGET" }, + [FUSE_READDIRPLUS] = { _do_readdirplus, "READDIRPLUS" }, + [FUSE_RENAME2] = { _do_rename2, "RENAME2" }, + [FUSE_COPY_FILE_RANGE] = { _do_copy_file_range, "COPY_FILE_RANGE" }, + [FUSE_COPY_FILE_RANGE_64] = { _do_copy_file_range_64, "COPY_FILE_RANGE_64" }, + [FUSE_LSEEK] = { _do_lseek, "LSEEK" }, + [FUSE_STATX] = { _do_statx, "STATX" }, + [CUSE_INIT] = { _cuse_lowlevel_init, "CUSE_INIT" }, +}; + /* * For ABI compatibility we cannot allow higher values than CUSE_INIT. * Without ABI compatibility we could use the size of the array. @@ -2804,6 +3540,58 @@ static struct { */ #define FUSE_MAXOP (CUSE_INIT + 1) + +/** + * + * @return 0 if sanity is ok, error otherwise + */ +static inline int +fuse_req_opcode_sanity_ok(struct fuse_session *se, enum fuse_opcode in_op) +{ + int err = EIO; + + if (!se->got_init) { + enum fuse_opcode expected; + + expected = se->cuse_data ? CUSE_INIT : FUSE_INIT; + if (in_op != expected) + return err; + } else if (in_op == FUSE_INIT || in_op == CUSE_INIT) + return err; + + return 0; +} + +static inline void +fuse_session_in2req(struct fuse_req *req, struct fuse_in_header *in) +{ + req->unique = in->unique; + req->ctx.uid = in->uid; + req->ctx.gid = in->gid; + req->ctx.pid = in->pid; +} + +/** + * Implement -o allow_root + */ +static inline int +fuse_req_check_allow_root(struct fuse_session *se, enum fuse_opcode in_op, + uid_t in_uid) +{ + int err = EACCES; + + if (se->deny_others && in_uid != se->owner && in_uid != 0 && + in_op != FUSE_INIT && in_op != FUSE_READ && + in_op != FUSE_WRITE && in_op != FUSE_FSYNC && + in_op != FUSE_RELEASE && in_op != FUSE_READDIR && + in_op != FUSE_FSYNCDIR && in_op != FUSE_RELEASEDIR && + in_op != FUSE_NOTIFY_REPLY && + in_op != FUSE_READDIRPLUS) + return err; + + return 0; +} + static const char *opname(enum fuse_opcode opcode) { if (opcode >= FUSE_MAXOP || !fuse_ll_ops[opcode].name) @@ -2868,9 +3656,11 @@ void fuse_session_process_buf_internal(struct fuse_session *se, in = buf->mem; } + trace_request_process(in->opcode, in->unique); + if (se->debug) { fuse_log(FUSE_LOG_DEBUG, - "unique: %llu, opcode: %s (%i), nodeid: %llu, insize: %zu, pid: %u\n", + "dev unique: %llu, opcode: %s (%i), nodeid: %llu, insize: %zu, pid: %u\n", (unsigned long long) in->unique, opname((enum fuse_opcode) in->opcode), in->opcode, (unsigned long long) in->nodeid, buf->size, in->pid); @@ -2887,35 +3677,19 @@ void fuse_session_process_buf_internal(struct fuse_session *se, .iov_len = sizeof(struct fuse_out_header), }; - fuse_send_msg(se, ch, &iov, 1); + fuse_send_msg(se, ch, &iov, 1, NULL); goto clear_pipe; } - req->unique = in->unique; - req->ctx.uid = in->uid; - req->ctx.gid = in->gid; - req->ctx.pid = in->pid; + fuse_session_in2req(req, in); req->ch = ch ? fuse_chan_get(ch) : NULL; - err = EIO; - if (!se->got_init) { - enum fuse_opcode expected; - - expected = se->cuse_data ? CUSE_INIT : FUSE_INIT; - if (in->opcode != expected) - goto reply_err; - } else if (in->opcode == FUSE_INIT || in->opcode == CUSE_INIT) + err = fuse_req_opcode_sanity_ok(se, in->opcode); + if (err) goto reply_err; - err = EACCES; - /* Implement -o allow_root */ - if (se->deny_others && in->uid != se->owner && in->uid != 0 && - in->opcode != FUSE_INIT && in->opcode != FUSE_READ && - in->opcode != FUSE_WRITE && in->opcode != FUSE_FSYNC && - in->opcode != FUSE_RELEASE && in->opcode != FUSE_READDIR && - in->opcode != FUSE_FSYNCDIR && in->opcode != FUSE_RELEASEDIR && - in->opcode != FUSE_NOTIFY_REPLY && - in->opcode != FUSE_READDIRPLUS) + err = fuse_req_check_allow_root(se, in->opcode, in->uid); + if (err) goto reply_err; err = ENOSYS; @@ -2979,6 +3753,59 @@ void fuse_session_process_buf_internal(struct fuse_session *se, goto out_free; } +void fuse_session_process_uring_cqe(struct fuse_session *se, + struct fuse_req *req, + struct fuse_in_header *in, void *op_in, + void *op_payload, size_t payload_len) +{ + int err; + + fuse_session_in2req(req, in); + + err = fuse_req_opcode_sanity_ok(se, in->opcode); + if (err) + goto reply_err; + + err = fuse_req_check_allow_root(se, in->opcode, in->uid); + if (err) + goto reply_err; + + err = ENOSYS; + if (in->opcode >= FUSE_MAXOP || !fuse_ll_ops2[in->opcode].func) + goto reply_err; + + if (se->debug) { + fuse_log( + FUSE_LOG_DEBUG, + "cqe unique: %llu, opcode: %s (%i), nodeid: %llu, insize: %zu, pid: %u\n", + (unsigned long long)in->unique, + opname((enum fuse_opcode)in->opcode), in->opcode, + (unsigned long long)in->nodeid, payload_len, in->pid); + } + + if (in->opcode == FUSE_WRITE && se->op.write_buf) { + struct fuse_bufvec bufv = { + .buf[0] = { .size = payload_len, + .flags = 0, + .mem = op_payload }, + .count = 1, + }; + _do_write_buf(req, in->nodeid, op_in, &bufv); + } else if (in->opcode == FUSE_NOTIFY_REPLY) { + struct fuse_buf buf = { .size = payload_len, + .mem = op_payload }; + _do_notify_reply(req, in->nodeid, op_payload, &buf); + } else { + fuse_ll_ops2[in->opcode].func(req, in->nodeid, op_in, + op_payload); + } + + return; + +reply_err: + fuse_reply_err(req, err); +} + #define LL_OPTION(n,o,v) \ { n, offsetof(struct fuse_session, o), v } @@ -2987,6 +3814,8 @@ static const struct fuse_opt fuse_ll_opts[] = { LL_OPTION("-d", debug, 1), LL_OPTION("--debug", debug, 1), LL_OPTION("allow_root", deny_others, 1), + LL_OPTION("io_uring", uring.enable, 1), + LL_OPTION("io_uring_q_depth=%u", uring.q_depth, -1), FUSE_OPT_END }; @@ -3004,7 +3833,10 @@ void fuse_lowlevel_help(void) printf( " -o allow_other allow access by all users\n" " -o allow_root allow access by root\n" -" -o auto_unmount auto unmount on process termination\n"); +" -o auto_unmount auto unmount on process termination\n" +" -o io_uring enable io-uring\n" +" -o io_uring_q_depth=<n> io-uring queue depth\n" +); } void fuse_session_destroy(struct fuse_session *se) @@ -3019,6 +3851,8 @@ void fuse_session_destroy(struct fuse_session *se) if (llp != NULL) fuse_ll_pipe_free(llp); pthread_key_delete(se->pipe_key); + sem_destroy(&se->mt_finish); + pthread_mutex_destroy(&se->mt_lock); pthread_mutex_destroy(&se->lock); free(se->cuse_data); if (se->fd != -1) @@ -3026,6 +3860,7 @@ void fuse_session_destroy(struct fuse_session *se) if (se->io != NULL) free(se->io); destroy_mount_opts(se->mo); + free(atomic_exchange(&se->mountpoint, NULL)); free(se); } @@ -3109,12 +3944,15 @@ static int _fuse_session_receive_buf(struct fuse_session *se, res = grow_pipe_to_max(llp->pipe[0]); if (res > 0) llp->size = res; + fuse_ll_clear_pipe(se); goto fallback; } llp->size = res; } - if (llp->size < bufsize) + if (llp->size < bufsize) { + fuse_ll_clear_pipe(se); goto fallback; + } } if (se->io != NULL && se->io->splice_receive != NULL) { @@ -3126,6 +3964,7 @@ static int _fuse_session_receive_buf(struct fuse_session *se, bufsize, 0); } err = errno; + trace_request_receive(err); if (fuse_session_exited(se)) return 0; @@ -3234,6 +4073,7 @@ static int _fuse_session_receive_buf(struct fuse_session *se, res = read(ch ? ch->fd : se->fd, buf->mem, bufsize); } err = errno; + trace_request_receive(err); if (fuse_session_exited(se)) return 0; @@ -3311,12 +4151,23 @@ fuse_session_new_versioned(struct fuse_args *args, struct fuse_session *se; struct mount_opts *mo; + if (op == NULL || op_size == 0) { + fuse_log(FUSE_LOG_ERR, + "fuse: warning: empty op list passed to fuse_session_new()\n"); + return NULL; + } + + if (version == NULL) { + fuse_log(FUSE_LOG_ERR, "fuse: warning: version not passed to fuse_session_new()\n"); + return NULL; + } + if (sizeof(struct fuse_lowlevel_ops) < op_size) { fuse_log(FUSE_LOG_ERR, "fuse: warning: library too old, some operations may not work\n"); op_size = sizeof(struct fuse_lowlevel_ops); } - if (args->argc == 0) { + if (args == NULL || args->argc == 0) { fuse_log(FUSE_LOG_ERR, "fuse: empty argv passed to fuse_session_new().\n"); return NULL; } @@ -3331,6 +4182,17 @@ fuse_session_new_versioned(struct fuse_args *args, se->bufsize = se->conn.max_write + FUSE_BUFFER_HEADER_SIZE; se->conn.max_readahead = UINT_MAX; + /* + * Allow overriding with env, mostly to avoid the need to modify + * all tests. I.e. to test with and without io-uring being enabled. + */ + se->uring.enable = getenv("FUSE_URING_ENABLE") ? + atoi(getenv("FUSE_URING_ENABLE")) : + SESSION_DEF_URING_ENABLE; + se->uring.q_depth = getenv("FUSE_URING_QUEUE_DEPTH") ? + atoi(getenv("FUSE_URING_QUEUE_DEPTH")) : + SESSION_DEF_URING_Q_DEPTH; + /* Parse options */ if(fuse_opt_parse(args, se, fuse_ll_opts, NULL) == -1) goto out2; @@ -3369,6 +4231,8 @@ fuse_session_new_versioned(struct fuse_args *args, list_init_nreq(&se->notify_list); se->notify_ctr = 1; pthread_mutex_init(&se->lock, NULL); + sem_init(&se->mt_finish, 0, 0); + pthread_mutex_init(&se->mt_lock, NULL); err = pthread_key_create(&se->pipe_key, fuse_ll_pipe_destructor); if (err) { @@ -3393,6 +4257,8 @@ fuse_session_new_versioned(struct fuse_args *args, return se; out5: + sem_destroy(&se->mt_finish); + pthread_mutex_destroy(&se->mt_lock); pthread_mutex_destroy(&se->lock); out4: fuse_opt_free_args(args); @@ -3413,9 +4279,22 @@ struct fuse_session *fuse_session_new_30(struct fuse_args *args, size_t op_size, void *userdata) { + struct fuse_lowlevel_ops null_ops = { 0 }; + /* unknown version */ struct libfuse_version version = { 0 }; + /* + * This function is the ABI interface function from fuse_session_new in + * compat.c. External libraries like "fuser" might call fuse_session_new() + * with NULL ops and then pass that session to fuse_session_mount(). + * The actual FUSE operations are handled in their own library. + */ + if (op == NULL) { + op = &null_ops; + op_size = sizeof(null_ops); + } + return fuse_session_new_versioned(args, op, op_size, &version, userdata); } @@ -3424,6 +4303,14 @@ FUSE_SYMVER("fuse_session_custom_io_317", "fuse_session_custom_io@@FUSE_3.17") int fuse_session_custom_io_317(struct fuse_session *se, const struct fuse_custom_io *io, size_t op_size, int fd) { +#ifndef HAVE_CUSTOM_IO + (void)se; + (void)io; + (void)op_size; + (void)fd; + fuse_log(FUSE_LOG_ERR, "fuse: custom io is not enabled in this build\n"); + return -ENOTSUP; +#else if (sizeof(struct fuse_custom_io) < op_size) { fuse_log(FUSE_LOG_ERR, "fuse: warning: library too old, some operations may not work\n"); op_size = sizeof(struct fuse_custom_io); @@ -3458,6 +4345,7 @@ int fuse_session_custom_io_317(struct fuse_session *se, se->fd = fd; memcpy(se->io, io, op_size); return 0; +#endif } int fuse_session_custom_io_30(struct fuse_session *se, @@ -3511,17 +4399,16 @@ int fuse_session_mount(struct fuse_session *se, const char *_mountpoint) fd); goto error_out; } - se->fd = fd; - return 0; + goto out; } /* Open channel */ fd = fuse_kern_mount(mountpoint, se->mo); if (fd == -1) goto error_out; - se->fd = fd; - /* Save mountpoint */ +out: + se->fd = fd; se->mountpoint = mountpoint; return 0; @@ -3623,18 +4510,22 @@ int fuse_req_getgroups(fuse_req_t req, int size, gid_t list[]) __attribute__((no_sanitize_thread)) void fuse_session_exit(struct fuse_session *se) { - se->exited = 1; + atomic_store_explicit(&se->mt_exited, 1, memory_order_relaxed); + sem_post(&se->mt_finish); } __attribute__((no_sanitize_thread)) void fuse_session_reset(struct fuse_session *se) { - se->exited = 0; + se->mt_exited = false; se->error = 0; } __attribute__((no_sanitize_thread)) int fuse_session_exited(struct fuse_session *se) { - return se->exited; + bool exited = + atomic_load_explicit(&se->mt_exited, memory_order_relaxed); + + return exited ? 1 : 0; } diff --git a/lib/fuse_misc.h b/lib/fuse_misc.h index 855edc326..1452593c3 100644 --- a/lib/fuse_misc.h +++ b/lib/fuse_misc.h @@ -3,7 +3,7 @@ Copyright (C) 2001-2007 Miklos Szeredi <miklos@szeredi.hu> This program can be distributed under the terms of the GNU LGPLv2. - See the file COPYING.LIB + See the file LGPL2.txt */ #include <pthread.h> diff --git a/lib/fuse_opt.c b/lib/fuse_opt.c index 1d3b6a1a5..046e57718 100644 --- a/lib/fuse_opt.c +++ b/lib/fuse_opt.c @@ -6,7 +6,7 @@ fuse_args`). This program can be distributed under the terms of the GNU LGPLv2. - See the file COPYING.LIB + See the file LGPL2.txt */ #include "fuse_config.h" diff --git a/lib/fuse_signals.c b/lib/fuse_signals.c index b88278e42..03b0d8f11 100644 --- a/lib/fuse_signals.c +++ b/lib/fuse_signals.c @@ -5,7 +5,7 @@ Utility functions for setting signal handlers. This program can be distributed under the terms of the GNU LGPLv2. - See the file COPYING.LIB + See the file LGPL2.txt */ #include "fuse_config.h" @@ -22,7 +22,12 @@ #include <execinfo.h> #endif +/* + * Must not handle SIGCANCEL, as that is used to wake up threads from + * syscalls reading requests from /dev/fuse + */ static int teardown_sigs[] = { SIGHUP, SIGINT, SIGTERM }; + static int ignore_sigs[] = { SIGPIPE}; static int fail_sigs[] = { SIGILL, SIGTRAP, SIGABRT, SIGBUS, SIGFPE, SIGSEGV }; static struct fuse_session *fuse_instance; @@ -55,8 +60,14 @@ static void dump_stack(void) static void exit_handler(int sig) { - if (fuse_instance == NULL) + if (fuse_instance == NULL) { + fuse_log(FUSE_LOG_ERR, "fuse_instance is NULL\n"); return; + } + + if (fuse_instance->debug) + fuse_log(FUSE_LOG_ERR, "exit_handler called with sig %d\n", + sig); fuse_session_exit(fuse_instance); diff --git a/lib/fuse_uring.c b/lib/fuse_uring.c new file mode 100644 index 000000000..8268fc8bb --- /dev/null +++ b/lib/fuse_uring.c @@ -0,0 +1,959 @@ +/* + * FUSE: Filesystem in Userspace + * Copyright (C) 2025 Bernd Schubert <bschubert@ddn.com> + * + * Implementation of (most of) FUSE-over-io-uring. + * + * This program can be distributed under the terms of the GNU LGPLv2. + * See the file LGPL2.txt + */ + +#define _GNU_SOURCE + +#include "fuse_i.h" +#include "fuse_kernel.h" +#include "fuse_uring_i.h" + +#include <stdlib.h> +#include <liburing.h> +#include <sys/sysinfo.h> +#include <stdint.h> +#include <inttypes.h> +#include <stdbool.h> +#include <string.h> +#include <unistd.h> +#include <numa.h> +#include <pthread.h> +#include <stdio.h> +#include <linux/sched.h> +#include <poll.h> +#include <sys/eventfd.h> + +/* Size of command data area in SQE when IORING_SETUP_SQE128 is used */ +#define FUSE_URING_MAX_SQE128_CMD_DATA 80 + +struct fuse_ring_ent { + struct fuse_ring_queue *ring_queue; /* back pointer */ + struct fuse_req req; + + struct fuse_uring_req_header *req_header; + void *op_payload; + size_t req_payload_sz; + + /* commit id of a fuse request */ + uint64_t req_commit_id; + + enum fuse_uring_cmd last_cmd; + + /* header and payload */ + struct iovec iov[2]; +}; + +struct fuse_ring_queue { + /* back pointer */ + struct fuse_ring_pool *ring_pool; + int qid; + int numa_node; + pthread_t tid; + int eventfd; + size_t req_header_sz; + struct io_uring ring; + + pthread_mutex_t ring_lock; + bool cqe_processing; + + /* size depends on queue depth */ + struct fuse_ring_ent ent[]; +}; + +/** + * Main fuse_ring structure, holds all fuse-ring data + */ +struct fuse_ring_pool { + struct fuse_session *se; + + /* number of queues */ + size_t nr_queues; + + /* number of per queue entries */ + size_t queue_depth; + + /* max payload size for fuse requests*/ + size_t max_req_payload_sz; + + /* size of a single queue */ + size_t queue_mem_size; + + unsigned int started_threads; + unsigned int failed_threads; + + /* Avoid sending queue entries before FUSE_INIT reply*/ + sem_t init_sem; + + pthread_cond_t thread_start_cond; + pthread_mutex_t thread_start_mutex; + + /* pointer to the first queue */ + struct fuse_ring_queue *queues; +}; + +static size_t +fuse_ring_queue_size(const size_t q_depth) +{ + const size_t req_size = sizeof(struct fuse_ring_ent) * q_depth; + + return sizeof(struct fuse_ring_queue) + req_size; +} + +static struct fuse_ring_queue * +fuse_uring_get_queue(struct fuse_ring_pool *fuse_ring, int qid) +{ + void *ptr = + ((char *)fuse_ring->queues) + (qid * fuse_ring->queue_mem_size); + + return ptr; +} + +/** + * return a pointer to the 80B area + */ +static void *fuse_uring_get_sqe_cmd(struct io_uring_sqe *sqe) +{ + return (void *)&sqe->cmd[0]; +} + +static void fuse_uring_sqe_set_req_data(struct fuse_uring_cmd_req *req, + const unsigned int qid, + const uint64_t commit_id) +{ + req->qid = qid; + req->commit_id = commit_id; + req->flags = 0; +} + +static void +fuse_uring_sqe_prepare(struct io_uring_sqe *sqe, struct fuse_ring_ent *req, + __u32 cmd_op) +{ + /* These fields should be written once, never change */ + sqe->opcode = IORING_OP_URING_CMD; + + /* + * IOSQE_FIXED_FILE: fd is the index to the fd *array* + * given to io_uring_register_files() + */ + sqe->flags = IOSQE_FIXED_FILE; + sqe->fd = 0; + + sqe->rw_flags = 0; + sqe->ioprio = 0; + sqe->off = 0; + + io_uring_sqe_set_data(sqe, req); + + sqe->cmd_op = cmd_op; + sqe->__pad1 = 0; +} + +static int fuse_uring_commit_sqe(struct fuse_ring_pool *ring_pool, + struct fuse_ring_queue *queue, + struct fuse_ring_ent *ring_ent) +{ + bool locked = false; + struct fuse_session *se = ring_pool->se; + struct fuse_uring_req_header *rrh = ring_ent->req_header; + struct fuse_out_header *out = (struct fuse_out_header *)&rrh->in_out; + struct fuse_uring_ent_in_out *ent_in_out = + (struct fuse_uring_ent_in_out *)&rrh->ring_ent_in_out; + struct io_uring_sqe *sqe; + + if (pthread_self() != queue->tid) { + pthread_mutex_lock(&queue->ring_lock); + locked = true; + } + + sqe = io_uring_get_sqe(&queue->ring); + + if (sqe == NULL) { + /* This is an impossible condition, unless there is a bug. + * The kernel sent back an SQEs, which is assigned to a request. + * There is no way to get out of SQEs, as the number of + * SQEs matches the number tof requests. + */ + + se->error = -EIO; + fuse_log(FUSE_LOG_ERR, "Failed to get a ring SQEs\n"); + + return -EIO; + } + + ring_ent->last_cmd = FUSE_IO_URING_CMD_COMMIT_AND_FETCH; + fuse_uring_sqe_prepare(sqe, ring_ent, ring_ent->last_cmd); + fuse_uring_sqe_set_req_data(fuse_uring_get_sqe_cmd(sqe), queue->qid, + ring_ent->req_commit_id); + + if (se->debug) { + fuse_log(FUSE_LOG_DEBUG, " unique: %" PRIu64 ", result=%d\n", + out->unique, ent_in_out->payload_sz); + } + + if (!queue->cqe_processing) + io_uring_submit(&queue->ring); + + if (locked) + pthread_mutex_unlock(&queue->ring_lock); + + return 0; +} + +int fuse_req_get_payload(fuse_req_t req, char **payload, size_t *payload_sz, + void **mr) +{ + struct fuse_ring_ent *ring_ent; + + /* Not possible without io-uring interface */ + if (!req->flags.is_uring) + return -EINVAL; + + ring_ent = container_of(req, struct fuse_ring_ent, req); + + *payload = ring_ent->op_payload; + *payload_sz = ring_ent->req_payload_sz; + + /* + * For now unused, but will be used later when the application can + * allocate the buffers itself and register them for rdma. + */ + if (mr) + *mr = NULL; + + return 0; +} + +int send_reply_uring(fuse_req_t req, int error, const void *arg, size_t argsize) +{ + int res; + struct fuse_ring_ent *ring_ent = + container_of(req, struct fuse_ring_ent, req); + struct fuse_uring_req_header *rrh = ring_ent->req_header; + struct fuse_out_header *out = (struct fuse_out_header *)&rrh->in_out; + struct fuse_uring_ent_in_out *ent_in_out = + (struct fuse_uring_ent_in_out *)&rrh->ring_ent_in_out; + + struct fuse_ring_queue *queue = ring_ent->ring_queue; + struct fuse_ring_pool *ring_pool = queue->ring_pool; + size_t max_payload_sz = ring_pool->max_req_payload_sz; + + if (argsize > max_payload_sz) { + fuse_log(FUSE_LOG_ERR, "argsize %zu exceeds buffer size %zu", + argsize, max_payload_sz); + error = -EINVAL; + } else if (argsize) { + if (arg != ring_ent->op_payload) + memcpy(ring_ent->op_payload, arg, argsize); + } + ent_in_out->payload_sz = argsize; + + out->error = error; + out->unique = req->unique; + + res = fuse_uring_commit_sqe(ring_pool, queue, ring_ent); + + fuse_free_req(req); + + return res; +} + +int fuse_reply_data_uring(fuse_req_t req, struct fuse_bufvec *bufv, + enum fuse_buf_copy_flags flags) +{ + struct fuse_ring_ent *ring_ent = + container_of(req, struct fuse_ring_ent, req); + + struct fuse_ring_queue *queue = ring_ent->ring_queue; + struct fuse_ring_pool *ring_pool = queue->ring_pool; + struct fuse_uring_req_header *rrh = ring_ent->req_header; + struct fuse_out_header *out = (struct fuse_out_header *)&rrh->in_out; + struct fuse_uring_ent_in_out *ent_in_out = + (struct fuse_uring_ent_in_out *)&rrh->ring_ent_in_out; + size_t max_payload_sz = ring_ent->req_payload_sz; + struct fuse_bufvec dest_vec = FUSE_BUFVEC_INIT(max_payload_sz); + int res; + + dest_vec.buf[0].mem = ring_ent->op_payload; + dest_vec.buf[0].size = max_payload_sz; + + res = fuse_buf_copy(&dest_vec, bufv, flags); + + out->error = res < 0 ? res : 0; + out->unique = req->unique; + + ent_in_out->payload_sz = res > 0 ? res : 0; + + res = fuse_uring_commit_sqe(ring_pool, queue, ring_ent); + + fuse_free_req(req); + + return res; +} + +/** + * Copy the iov into the ring buffer and submit and commit/fetch sqe + */ +int fuse_send_msg_uring(fuse_req_t req, struct iovec *iov, int count) +{ + struct fuse_ring_ent *ring_ent = + container_of(req, struct fuse_ring_ent, req); + + struct fuse_ring_queue *queue = ring_ent->ring_queue; + struct fuse_ring_pool *ring_pool = queue->ring_pool; + struct fuse_uring_req_header *rrh = ring_ent->req_header; + struct fuse_out_header *out = (struct fuse_out_header *)&rrh->in_out; + struct fuse_uring_ent_in_out *ent_in_out = + (struct fuse_uring_ent_in_out *)&rrh->ring_ent_in_out; + size_t max_buf = ring_pool->max_req_payload_sz; + size_t len = 0; + int res = 0; + + /* copy iov into the payload, idx=0 is the header section */ + for (int idx = 1; idx < count; idx++) { + struct iovec *cur = &iov[idx]; + + if (len + cur->iov_len > max_buf) { + fuse_log(FUSE_LOG_ERR, + "iov[%d] exceeds buffer size %zu", + idx, max_buf); + res = -EINVAL; /* Gracefully handle this? */ + break; + } + + memcpy(ring_ent->op_payload + len, cur->iov_base, cur->iov_len); + len += cur->iov_len; + } + + ent_in_out->payload_sz = len; + + out->error = res; + out->unique = req->unique; + out->len = len; + + return fuse_uring_commit_sqe(ring_pool, queue, ring_ent); +} + +static int fuse_queue_setup_io_uring(struct io_uring *ring, size_t qid, + size_t depth, int fd, int evfd) +{ + int rc; + struct io_uring_params params = {0}; + int files[2] = { fd, evfd }; + + depth += 1; /* for the eventfd poll SQE */ + + params.flags = IORING_SETUP_SQE128; + + /* Replies are batched and flushed in one io_uring_enter; don't let a + * single failing commit SQE stall submission of the rest of the batch. + */ + params.flags |= IORING_SETUP_SUBMIT_ALL; + + /* Avoid cq overflow */ + params.flags |= IORING_SETUP_CQSIZE; + params.cq_entries = depth * 2; + + /* These flags should help to increase performance, but actually + * make it a bit slower - reason should get investigated. + */ + if (0) { + /* Has the main slow down effect */ + params.flags |= IORING_SETUP_SINGLE_ISSUER; + + // params.flags |= IORING_SETUP_DEFER_TASKRUN; + params.flags |= IORING_SETUP_TASKRUN_FLAG; + + /* Second main effect to make it slower */ + params.flags |= IORING_SETUP_COOP_TASKRUN; + } + + rc = io_uring_queue_init_params(depth, ring, ¶ms); + if (rc != 0) { + fuse_log(FUSE_LOG_ERR, "Failed to setup qid %zu: %d (%s)\n", + qid, rc, strerror(-rc)); + return rc; + } + + rc = io_uring_register_files(ring, files, 1); + if (rc != 0) { + rc = -errno; + fuse_log(FUSE_LOG_ERR, + "Failed to register files for ring idx %zu: %s", + qid, strerror(errno)); + return rc; + } + + return 0; +} + +static void fuse_session_destruct_uring(struct fuse_ring_pool *fuse_ring) +{ + for (size_t qid = 0; qid < fuse_ring->nr_queues; qid++) { + struct fuse_ring_queue *queue = + fuse_uring_get_queue(fuse_ring, qid); + + if (queue->tid != 0) { + uint64_t value = 1ULL; + int rc; + + rc = write(queue->eventfd, &value, sizeof(value)); + if (rc != sizeof(value)) + fprintf(stderr, + "Wrote to eventfd=%d err=%s: rc=%d\n", + queue->eventfd, strerror(errno), rc); + pthread_cancel(queue->tid); + pthread_join(queue->tid, NULL); + queue->tid = 0; + } + + if (queue->eventfd >= 0) { + close(queue->eventfd); + queue->eventfd = -1; + } + + if (queue->ring.ring_fd != -1) + io_uring_queue_exit(&queue->ring); + + for (size_t idx = 0; idx < fuse_ring->queue_depth; idx++) { + struct fuse_ring_ent *ent = &queue->ent[idx]; + + numa_free(ent->op_payload, ent->req_payload_sz); + numa_free(ent->req_header, queue->req_header_sz); + } + + pthread_mutex_destroy(&queue->ring_lock); + } + + free(fuse_ring->queues); + pthread_cond_destroy(&fuse_ring->thread_start_cond); + pthread_mutex_destroy(&fuse_ring->thread_start_mutex); + free(fuse_ring); +} + +static int fuse_uring_register_ent(struct fuse_ring_queue *queue, + struct fuse_ring_ent *ent) +{ + struct io_uring_sqe *sqe; + + sqe = io_uring_get_sqe(&queue->ring); + if (sqe == NULL) { + /* + * All SQEs are idle here - no good reason this + * could fail + */ + fuse_log(FUSE_LOG_ERR, "Failed to get all ring SQEs"); + return -EIO; + } + + ent->last_cmd = FUSE_IO_URING_CMD_REGISTER; + fuse_uring_sqe_prepare(sqe, ent, ent->last_cmd); + + /* only needed for fetch */ + ent->iov[0].iov_base = ent->req_header; + ent->iov[0].iov_len = queue->req_header_sz; + + ent->iov[1].iov_base = ent->op_payload; + ent->iov[1].iov_len = ent->req_payload_sz; + + sqe->addr = (uint64_t)(ent->iov); + sqe->len = 2; + + /* this is a fetch, kernel does not read commit id */ + fuse_uring_sqe_set_req_data(fuse_uring_get_sqe_cmd(sqe), queue->qid, 0); + + return 0; + +} + +static int fuse_uring_register_queue(struct fuse_ring_queue *queue) +{ + struct fuse_ring_pool *ring_pool = queue->ring_pool; + unsigned int sq_ready; + struct io_uring_sqe *sqe; + int res; + + for (size_t idx = 0; idx < ring_pool->queue_depth; idx++) { + struct fuse_ring_ent *ent = &queue->ent[idx]; + + res = fuse_uring_register_ent(queue, ent); + if (res != 0) + return res; + } + + sq_ready = io_uring_sq_ready(&queue->ring); + if (sq_ready != ring_pool->queue_depth) { + fuse_log(FUSE_LOG_ERR, + "SQE ready mismatch, expected %zu got %u\n", + ring_pool->queue_depth, sq_ready); + return -EINVAL; + } + + /* Poll SQE for the eventfd to wake up on teardown */ + sqe = io_uring_get_sqe(&queue->ring); + if (sqe == NULL) { + fuse_log(FUSE_LOG_ERR, "Failed to get eventfd SQE"); + return -EIO; + } + + io_uring_prep_poll_add(sqe, queue->eventfd, POLLIN); + io_uring_sqe_set_data(sqe, (void *)(uintptr_t)queue->eventfd); + + /* Only preparation until here, no submission yet */ + + return 0; +} + +static struct fuse_ring_pool *fuse_create_ring(struct fuse_session *se) +{ + struct fuse_ring_pool *fuse_ring = NULL; + const size_t nr_queues = get_nprocs_conf(); + size_t payload_sz = se->bufsize - FUSE_BUFFER_HEADER_SIZE; + size_t queue_sz; + + if (se->debug) + fuse_log(FUSE_LOG_DEBUG, "starting io-uring q-depth=%d\n", + se->uring.q_depth); + + fuse_ring = calloc(1, sizeof(*fuse_ring)); + if (fuse_ring == NULL) { + fuse_log(FUSE_LOG_ERR, "Allocating the ring failed\n"); + goto err; + } + + queue_sz = fuse_ring_queue_size(se->uring.q_depth); + fuse_ring->queues = calloc(1, queue_sz * nr_queues); + if (fuse_ring->queues == NULL) { + fuse_log(FUSE_LOG_ERR, "Allocating the queues failed\n"); + goto err; + } + + fuse_ring->se = se; + fuse_ring->nr_queues = nr_queues; + fuse_ring->queue_depth = se->uring.q_depth; + fuse_ring->max_req_payload_sz = payload_sz; + fuse_ring->queue_mem_size = queue_sz; + + /* + * very basic queue initialization, that cannot fail and will + * allow easy cleanup if something (like mmap) fails in the middle + * below + */ + for (size_t qid = 0; qid < nr_queues; qid++) { + struct fuse_ring_queue *queue = + fuse_uring_get_queue(fuse_ring, qid); + + queue->ring.ring_fd = -1; + queue->numa_node = numa_node_of_cpu(qid); + queue->qid = qid; + queue->ring_pool = fuse_ring; + queue->eventfd = -1; + pthread_mutex_init(&queue->ring_lock, NULL); + } + + pthread_cond_init(&fuse_ring->thread_start_cond, NULL); + pthread_mutex_init(&fuse_ring->thread_start_mutex, NULL); + sem_init(&fuse_ring->init_sem, 0, 0); + + return fuse_ring; + +err: + if (fuse_ring) + fuse_session_destruct_uring(fuse_ring); + + return NULL; +} + +static void fuse_uring_resubmit(struct fuse_ring_queue *queue, + struct fuse_ring_ent *ent) +{ + struct io_uring_sqe *sqe; + + sqe = io_uring_get_sqe(&queue->ring); + if (sqe == NULL) { + /* This is an impossible condition, unless there is a bug. + * The kernel sent back an SQEs, which is assigned to a request. + * There is no way to get out of SQEs, as the number of + * SQEs matches the number tof requests. + */ + + queue->ring_pool->se->error = -EIO; + fuse_log(FUSE_LOG_ERR, "Failed to get a ring SQEs\n"); + + return; + } + + fuse_uring_sqe_prepare(sqe, ent, ent->last_cmd); + + switch (ent->last_cmd) { + case FUSE_IO_URING_CMD_REGISTER: + sqe->addr = (uint64_t)(ent->iov); + sqe->len = 2; + fuse_uring_sqe_set_req_data(fuse_uring_get_sqe_cmd(sqe), + queue->qid, 0); + break; + case FUSE_IO_URING_CMD_COMMIT_AND_FETCH: + fuse_uring_sqe_set_req_data(fuse_uring_get_sqe_cmd(sqe), + queue->qid, ent->req_commit_id); + break; + default: + fuse_log(FUSE_LOG_ERR, "Unknown command type: %d\n", + ent->last_cmd); + queue->ring_pool->se->error = -EINVAL; + break; + } + + /* caller submits */ +} + +static void fuse_uring_handle_cqe(struct fuse_ring_queue *queue, + struct io_uring_cqe *cqe) +{ + struct fuse_ring_ent *ent = io_uring_cqe_get_data(cqe); + + if (!ent) { + fuse_log(FUSE_LOG_ERR, + "cqe=%p io_uring_cqe_get_data returned NULL\n", cqe); + return; + } + + struct fuse_req *req = &ent->req; + struct fuse_ring_pool *fuse_ring = queue->ring_pool; + struct fuse_uring_req_header *rrh = ent->req_header; + + struct fuse_in_header *in = (struct fuse_in_header *)&rrh->in_out; + struct fuse_uring_ent_in_out *ent_in_out = &rrh->ring_ent_in_out; + + ent->req_commit_id = ent_in_out->commit_id; + if (unlikely(ent->req_commit_id == 0)) { + /* + * If this happens kernel will not find the response - it will + * be stuck forever - better to abort immediately. + */ + fuse_log(FUSE_LOG_ERR, "Received invalid commit_id=0\n"); + abort(); + } + + memset(&req->flags, 0, sizeof(req->flags)); + memset(&req->u, 0, sizeof(req->u)); + req->flags.is_uring = 1; + req->ref_cnt++; + req->ch = NULL; /* not needed for uring */ + req->interrupted = 0; + list_init_req(req); + + fuse_session_process_uring_cqe(fuse_ring->se, req, in, &rrh->op_in, + ent->op_payload, ent_in_out->payload_sz); +} + +static int fuse_uring_queue_handle_cqes(struct fuse_ring_queue *queue) +{ + struct fuse_ring_pool *ring_pool = queue->ring_pool; + struct fuse_session *se = ring_pool->se; + size_t num_completed = 0; + struct io_uring_cqe *cqe; + unsigned int head; + struct fuse_ring_ent *ent; + int ret = 0; + + io_uring_for_each_cqe(&queue->ring, head, cqe) { + int err = 0; + + num_completed++; + + err = cqe->res; + if (unlikely(err != 0)) { + if (err > 0 && ((uintptr_t)io_uring_cqe_get_data(cqe) == + (unsigned int)queue->eventfd)) { + /* teardown from eventfd */ + return -ENOTCONN; + } + + + switch (err) { + case -EAGAIN: + fallthrough; + case -EINTR: + ent = io_uring_cqe_get_data(cqe); + fuse_uring_resubmit(queue, ent); + continue; + default: + break; + } + + /* -ENOTCONN is ok on umount */ + if (err != -ENOTCONN) { + se->error = cqe->res; + + /* return first error */ + if (ret == 0) + ret = err; + } + + } else { + fuse_uring_handle_cqe(queue, cqe); + } + } + + if (num_completed) + io_uring_cq_advance(&queue->ring, num_completed); + + return ret == 0 ? 0 : num_completed; +} + +/** + * In per-core-queue configuration we have thread per core - the thread + * to that core + */ +static void fuse_uring_set_thread_core(int qid) +{ + cpu_set_t mask; + int rc; + + CPU_ZERO(&mask); + CPU_SET(qid, &mask); + rc = sched_setaffinity(0, sizeof(cpu_set_t), &mask); + if (rc != 0) + fuse_log(FUSE_LOG_ERR, "Failed to bind qid=%d to its core: %s\n", + qid, strerror(errno)); + + if (0) { + const int policy = SCHED_IDLE; + const struct sched_param param = { + .sched_priority = sched_get_priority_min(policy), + }; + + /* Set the lowest possible priority, so that the application + * submitting requests is not moved away from the current core. + */ + rc = sched_setscheduler(0, policy, ¶m); + if (rc != 0) + fuse_log(FUSE_LOG_ERR, "Failed to set scheduler: %s\n", + strerror(errno)); + } +} + +/* + * @return negative error code or io-uring file descriptor + */ +static int fuse_uring_init_queue(struct fuse_ring_queue *queue) +{ + struct fuse_ring_pool *ring = queue->ring_pool; + struct fuse_session *se = ring->se; + int res; + size_t page_sz = sysconf(_SC_PAGESIZE); + + queue->eventfd = eventfd(0, EFD_CLOEXEC); + if (queue->eventfd < 0) { + res = -errno; + fuse_log(FUSE_LOG_ERR, + "Failed to create eventfd for qid %d: %s\n", + queue->qid, strerror(errno)); + return res; + } + + res = fuse_queue_setup_io_uring(&queue->ring, queue->qid, + ring->queue_depth, se->fd, + queue->eventfd); + if (res != 0) { + fuse_log(FUSE_LOG_ERR, "qid=%d io_uring init failed\n", + queue->qid); + return res; + } + + queue->req_header_sz = ROUND_UP(sizeof(struct fuse_uring_req_header), + page_sz); + + for (size_t idx = 0; idx < ring->queue_depth; idx++) { + struct fuse_ring_ent *ring_ent = &queue->ent[idx]; + struct fuse_req *req = &ring_ent->req; + + ring_ent->ring_queue = queue; + + /* + * Also allocate the header to have it page aligned, which + * is a requirement for page pinning + */ + ring_ent->req_header = + numa_alloc_local(queue->req_header_sz); + if (!ring_ent->req_header) + return -ENOMEM; + ring_ent->req_payload_sz = ring->max_req_payload_sz; + + ring_ent->op_payload = + numa_alloc_local(ring_ent->req_payload_sz); + if (!ring_ent->op_payload) + return -ENOMEM; + + req->se = se; + pthread_mutex_init(&req->lock, NULL); + req->flags.is_uring = 1; + req->ref_cnt = 1; /* extra ref to avoid destruction */ + list_init_req(req); + } + + res = fuse_uring_register_queue(queue); + if (res != 0) { + fuse_log( + FUSE_LOG_ERR, + "Grave fuse-uring error on preparing SQEs, aborting\n"); + se->error = -EIO; + fuse_session_exit(se); + return res; + } + + return queue->ring.ring_fd; +} + +static void *fuse_uring_thread(void *arg) +{ + struct fuse_ring_queue *queue = arg; + struct fuse_ring_pool *ring_pool = queue->ring_pool; + struct fuse_session *se = ring_pool->se; + int err; + char thread_name[16] = { 0 }; + + snprintf(thread_name, 16, "fuse-ring-%d", queue->qid); + thread_name[15] = '\0'; + fuse_set_thread_name(thread_name); + + fuse_uring_set_thread_core(queue->qid); + + err = fuse_uring_init_queue(queue); + pthread_mutex_lock(&ring_pool->thread_start_mutex); + if (err < 0) + ring_pool->failed_threads++; + ring_pool->started_threads++; + pthread_cond_broadcast(&ring_pool->thread_start_cond); + pthread_mutex_unlock(&ring_pool->thread_start_mutex); + + if (err < 0) { + fuse_log(FUSE_LOG_ERR, "qid=%d queue setup failed\n", + queue->qid); + goto err_non_fatal; + } + + sem_wait(&ring_pool->init_sem); + + /* Not using fuse_session_exited(se), as that cannot be inlined */ + while (!atomic_load_explicit(&se->mt_exited, memory_order_relaxed)) { + io_uring_submit_and_wait(&queue->ring, 1); + + pthread_mutex_lock(&queue->ring_lock); + queue->cqe_processing = true; + err = fuse_uring_queue_handle_cqes(queue); + queue->cqe_processing = false; + pthread_mutex_unlock(&queue->ring_lock); + if (err < 0) + goto err; + } + + return NULL; + +err: + fuse_session_exit(se); +err_non_fatal: + return NULL; +} + +static int fuse_uring_start_ring_threads(struct fuse_ring_pool *ring) +{ + int rc = 0; + + for (size_t qid = 0; qid < ring->nr_queues; qid++) { + struct fuse_ring_queue *queue = fuse_uring_get_queue(ring, qid); + + rc = pthread_create(&queue->tid, NULL, fuse_uring_thread, queue); + if (rc != 0) + break; + } + + return rc; +} + +static int fuse_uring_sanity_check(struct fuse_session *se) +{ + if (se->uring.q_depth == 0) { + fuse_log(FUSE_LOG_ERR, "io-uring queue depth must be > 0\n"); + return -EINVAL; + } + + _Static_assert(sizeof(struct fuse_uring_cmd_req) <= + FUSE_URING_MAX_SQE128_CMD_DATA, + "SQE128_CMD_DATA has 80B cmd data"); + + return 0; +} + +int fuse_uring_start(struct fuse_session *se) +{ + int err = 0; + struct fuse_ring_pool *fuse_ring; + + fuse_uring_sanity_check(se); + + fuse_ring = fuse_create_ring(se); + if (fuse_ring == NULL) { + err = -EADDRNOTAVAIL; + goto err; + } + + se->uring.pool = fuse_ring; + + /* Hold off threads from send fuse ring entries (SQEs) */ + sem_init(&fuse_ring->init_sem, 0, 0); + pthread_cond_init(&fuse_ring->thread_start_cond, NULL); + pthread_mutex_init(&fuse_ring->thread_start_mutex, NULL); + + err = fuse_uring_start_ring_threads(fuse_ring); + if (err) + goto err; + + /* + * Wait for all threads to start or to fail + */ + pthread_mutex_lock(&fuse_ring->thread_start_mutex); + while (fuse_ring->started_threads < fuse_ring->nr_queues) + pthread_cond_wait(&fuse_ring->thread_start_cond, + &fuse_ring->thread_start_mutex); + + if (fuse_ring->failed_threads != 0) + err = -EADDRNOTAVAIL; + pthread_mutex_unlock(&fuse_ring->thread_start_mutex); + +err: + if (err) { + /* Note all threads need to have been started */ + if (fuse_ring) + fuse_session_destruct_uring(fuse_ring); + se->uring.pool = NULL; + } + return err; +} + +int fuse_uring_stop(struct fuse_session *se) +{ + struct fuse_ring_pool *ring = se->uring.pool; + + if (ring == NULL) + return 0; + + fuse_session_destruct_uring(ring); + + return 0; +} + +void fuse_uring_wake_ring_threads(struct fuse_session *se) +{ + struct fuse_ring_pool *ring = se->uring.pool; + + /* Wake up the threads to let them send SQEs */ + for (size_t qid = 0; qid < ring->nr_queues; qid++) + sem_post(&ring->init_sem); +} diff --git a/lib/fuse_uring_i.h b/lib/fuse_uring_i.h new file mode 100644 index 000000000..c1da73ad7 --- /dev/null +++ b/lib/fuse_uring_i.h @@ -0,0 +1,86 @@ +/* + * FUSE: Filesystem in Userspace + * Copyright (C) 2025 Bernd Schubert <bschubert@ddn.com> + * This program can be distributed under the terms of the GNU LGPLv2. + * See the file LGPL2.txt + */ + +#ifndef FUSE_URING_I_H_ +#define FUSE_URING_I_H_ + +#include "fuse_config.h" +#include "fuse_lowlevel.h" +#include "fuse_kernel.h" + +#ifndef HAVE_URING +#include "util.h" +#endif + +#include <errno.h> // IWYU pragma: keep + +/* io-uring defaults */ +#define SESSION_DEF_URING_ENABLE (0) +#define SESSION_DEF_URING_Q_DEPTH (8) + +void fuse_session_process_uring_cqe(struct fuse_session *se, + struct fuse_req *req, + struct fuse_in_header *in, void *in_header, + void *in_payload, size_t payload_len); + +#ifdef HAVE_URING + +struct fuse_in_header; + +int fuse_uring_start(struct fuse_session *se); +void fuse_uring_wake_ring_threads(struct fuse_session *se); +int fuse_uring_stop(struct fuse_session *se); +int send_reply_uring(fuse_req_t req, int error, const void *arg, + size_t argsize); + +int fuse_reply_data_uring(fuse_req_t req, struct fuse_bufvec *bufv, + enum fuse_buf_copy_flags flags); +int fuse_send_msg_uring(fuse_req_t req, struct iovec *iov, int count); + +#else // HAVE_URING + +static inline int fuse_uring_start(struct fuse_session *se FUSE_VAR_UNUSED) +{ + return -ENOTSUP; +} + +static inline void +fuse_uring_wake_ring_threads(struct fuse_session *se FUSE_VAR_UNUSED) +{ +} + +static inline int fuse_uring_stop(struct fuse_session *se FUSE_VAR_UNUSED) +{ + return -ENOTSUP; +} + +static inline int send_reply_uring(fuse_req_t req FUSE_VAR_UNUSED, + int error FUSE_VAR_UNUSED, + const void *arg FUSE_VAR_UNUSED, + size_t argsize FUSE_VAR_UNUSED) +{ + return -ENOTSUP; +} + +static inline int +fuse_reply_data_uring(fuse_req_t req FUSE_VAR_UNUSED, + struct fuse_bufvec *bufv FUSE_VAR_UNUSED, + enum fuse_buf_copy_flags flags FUSE_VAR_UNUSED) +{ + return -ENOTSUP; +} + +static inline int fuse_send_msg_uring(fuse_req_t req FUSE_VAR_UNUSED, + struct iovec *iov FUSE_VAR_UNUSED, + int count FUSE_VAR_UNUSED) +{ + return -ENOTSUP; +} + +#endif // HAVE_URING + +#endif // FUSE_URING_I_H_ diff --git a/lib/fuse_versionscript b/lib/fuse_versionscript index a2653fcdd..b58360feb 100644 --- a/lib/fuse_versionscript +++ b/lib/fuse_versionscript @@ -208,10 +208,20 @@ FUSE_3.17.3 { fuse_unset_feature_flag; fuse_get_feature_flag; - # Not part of public API, for internal test use only + # Not part of public API, for internal testing only fuse_convert_to_conn_want_ext; } FUSE_3.17; +FUSE_3.18 { + global: + fuse_req_is_uring; + fuse_req_get_payload; + fuse_lowlevel_notify_increment_epoch; + + fuse_reply_statx; + fuse_fs_statx; +} FUSE_3.17; + # Local Variables: # indent-tabs-mode: t # End: diff --git a/lib/helper.c b/lib/helper.c index aceff9fd5..357a0b6b6 100644 --- a/lib/helper.c +++ b/lib/helper.c @@ -7,7 +7,7 @@ file system by implementing nothing but the request handlers. This program can be distributed under the terms of the GNU LGPLv2. - See the file COPYING.LIB. + See the file LGPL2.txt. */ #include "fuse_config.h" @@ -269,16 +269,25 @@ int fuse_daemonize(int foreground) switch(fork()) { case -1: perror("fuse_daemonize: fork"); + close(waiter[0]); + close(waiter[1]); return -1; case 0: + /* child */ + close(waiter[0]); + waiter[0] = -1; break; default: + /* parent */ (void) read(waiter[0], &completed, sizeof(completed)); + close(waiter[0]); + close(waiter[1]); _exit(0); } if (setsid() == -1) { perror("fuse_daemonize: setsid"); + close(waiter[1]); return -1; } @@ -296,11 +305,12 @@ int fuse_daemonize(int foreground) /* Propagate completion of daemon initialization */ completed = 1; (void) write(waiter[1], &completed, sizeof(completed)); - close(waiter[0]); close(waiter[1]); + waiter[1] = -1; } else { - (void) chdir("/"); + (void)chdir("/"); } + return 0; } diff --git a/lib/meson.build b/lib/meson.build index df5f8afbe..91f51392e 100644 --- a/lib/meson.build +++ b/lib/meson.build @@ -19,6 +19,14 @@ if private_cfg.get('HAVE_ICONV') endif endif +if private_cfg.get('HAVE_URING', false) + libfuse_sources += [ 'fuse_uring.c' ] + deps += [ dependency('liburing') ] + deps += [ dependency('numa') ] +endif + + + libdl = cc.find_library('dl', required: false) if libdl.found() deps += [ libdl ] @@ -35,10 +43,11 @@ endif fusermount_path = join_paths(get_option('prefix'), get_option('bindir')) libfuse = library('fused', libfuse_sources, - version: meson.project_version(), + version: base_version, soversion: '1', include_directories: include_dirs, - dependencies: deps, install: true, + dependencies: deps, + install: true, link_depends: 'fuse_versionscript', c_args: [ '-DFUSE_USE_VERSION=317', '-DFUSERMOUNT_DIR="@0@"'.format(fusermount_path) ], diff --git a/lib/modules/iconv.c b/lib/modules/iconv.c index a0bf72be5..5c055a7ec 100644 --- a/lib/modules/iconv.c +++ b/lib/modules/iconv.c @@ -3,7 +3,7 @@ Copyright (C) 2007 Miklos Szeredi <miklos@szeredi.hu> This program can be distributed under the terms of the GNU LGPLv2. - See the file COPYING.LIB + See the file LGPL2.txt */ #include <fuse_config.h> @@ -568,6 +568,22 @@ static off_t iconv_lseek(const char *path, off_t off, int whence, return res; } +#ifdef HAVE_STATX +static int iconv_statx(const char *path, int flags, int mask, struct statx *stxbuf, + struct fuse_file_info *fi) +{ + struct iconv *ic = iconv_get(); + char *newpath; + int res = iconv_convpath(ic, path, &newpath, 0); + + if (!res) { + res = fuse_fs_statx(ic->next, newpath, flags, mask, stxbuf, fi); + free(newpath); + } + return res; +} +#endif + static void *iconv_init(struct fuse_conn_info *conn, struct fuse_config *cfg) { @@ -627,6 +643,9 @@ static const struct fuse_operations iconv_oper = { .flock = iconv_flock, .bmap = iconv_bmap, .lseek = iconv_lseek, +#ifdef HAVE_STATX + .statx = iconv_statx, +#endif }; static const struct fuse_opt iconv_opts[] = { @@ -703,7 +722,7 @@ static struct fuse_fs *iconv_new(struct fuse_args *args, goto out_free; } ic->fromfs = iconv_open(to, from); - if (ic->tofs == (iconv_t) -1) { + if (ic->fromfs == (iconv_t) -1) { fuse_log(FUSE_LOG_ERR, "fuse-iconv: cannot convert from %s to %s\n", from, to); goto out_iconv_close_to; diff --git a/lib/modules/subdir.c b/lib/modules/subdir.c index e92eb6284..67c469799 100644 --- a/lib/modules/subdir.c +++ b/lib/modules/subdir.c @@ -3,7 +3,7 @@ Copyright (C) 2007 Miklos Szeredi <miklos@szeredi.hu> This program can be distributed under the terms of the GNU LGPLv2. - See the file COPYING.LIB + See the file LGPL2.txt */ #include <fuse_config.h> @@ -553,6 +553,22 @@ static off_t subdir_lseek(const char *path, off_t off, int whence, return res; } +#ifdef HAVE_STATX +static int subdir_statx(const char *path, int flags, int mask, struct statx *stxbuf, + struct fuse_file_info *fi) +{ + struct subdir *ic = subdir_get(); + char *newpath; + int res = subdir_addpath(ic, path, &newpath); + + if (!res) { + res = fuse_fs_statx(ic->next, newpath, flags, mask, stxbuf, fi); + free(newpath); + } + return res; +} +#endif + static void *subdir_init(struct fuse_conn_info *conn, struct fuse_config *cfg) { @@ -608,6 +624,9 @@ static const struct fuse_operations subdir_oper = { .flock = subdir_flock, .bmap = subdir_bmap, .lseek = subdir_lseek, +#ifdef HAVE_STATX + .statx = subdir_statx, +#endif }; static const struct fuse_opt subdir_opts[] = { diff --git a/lib/mount.c b/lib/mount.c index be7600636..f521090da 100644 --- a/lib/mount.c +++ b/lib/mount.c @@ -5,9 +5,11 @@ Architecture specific file system mounting (Linux). This program can be distributed under the terms of the GNU LGPLv2. - See the file COPYING.LIB. + See the file LGPL2.txt. */ +#define _GNU_SOURCE + #include "fuse_config.h" #include "fuse_i.h" #include "fuse_misc.h" @@ -45,10 +47,7 @@ #define FUSERMOUNT_PROG "fusermount3" #define FUSE_COMMFD_ENV "_FUSE_COMMFD" #define FUSE_COMMFD2_ENV "_FUSE_COMMFD2" - -#ifndef HAVE_FORK -#define fork() vfork() -#endif +#define FUSE_KERN_DEVICE_ENV "FUSE_KERN_DEVICE" #ifndef MS_DIRSYNC #define MS_DIRSYNC 128 @@ -121,21 +120,61 @@ static const struct fuse_opt fuse_mount_opts[] = { FUSE_OPT_END }; -static void exec_fusermount(const char *argv[]) +/* + * Running fusermount by calling fork + exec. DAOS: posix_spawn() is not used + * because its CLONE_VM|CLONE_VFORK child cannot run under valgrind. + * + * @param quiet redirect the child's stdout/stderr to /dev/null + * @param close_fd fd to close in the child, or -1 + * @param out_pid might be NULL + */ +static int fusermount_fork_exec(char const * const argv[], int quiet, + int close_fd, pid_t *out_pid) { - execv(FUSERMOUNT_DIR "/" FUSERMOUNT_PROG, (char **) argv); - execvp(FUSERMOUNT_PROG, (char **) argv); + pid_t pid = fork(); + + if (pid == -1) { + int err = errno; + + fuse_log(FUSE_LOG_ERR, "fork() for '%s' failed: %s\n", + FUSERMOUNT_PROG, strerror(err)); + return -err; + } + + if (pid == 0) { + if (quiet) { + int fd = open("/dev/null", O_WRONLY); + + if (fd != -1) { + dup2(fd, STDOUT_FILENO); + dup2(fd, STDERR_FILENO); + } + } + if (close_fd != -1) + close(close_fd); + + /* first try the install path, then a system install */ + execv(FUSERMOUNT_DIR "/" FUSERMOUNT_PROG, (char * const *) argv); + execvp(FUSERMOUNT_PROG, (char * const *) argv); + _exit(1); + } + + if (out_pid) + *out_pid = pid; + else + waitpid(pid, NULL, 0); /* FIXME: check exit code and return error if any */ + + return 0; } void fuse_mount_version(void) { - int pid = fork(); - if (!pid) { - const char *argv[] = { FUSERMOUNT_PROG, "--version", NULL }; - exec_fusermount(argv); - _exit(1); - } else if (pid != -1) - waitpid(pid, NULL, 0); + char const *const argv[] = {FUSERMOUNT_PROG, "--version", NULL}; + int status = fusermount_fork_exec(argv, 0, -1, NULL); + + if(status != 0) + fuse_log(FUSE_LOG_ERR, "Running '%s --version' failed", + FUSERMOUNT_PROG); } struct mount_flags { @@ -255,7 +294,7 @@ static int receive_fd(int fd) while(((rv = recvmsg(fd, &msg, 0)) == -1) && errno == EINTR); if (rv == -1) { - perror("recvmsg"); + fuse_log(FUSE_LOG_ERR, "recvmsg failed: %s", strerror(errno)); return -1; } if(!rv) { @@ -264,6 +303,10 @@ static int receive_fd(int fd) } cmsg = CMSG_FIRSTHDR(&msg); + if (!cmsg) { + fuse_log(FUSE_LOG_ERR, "error retrieving control message header\n"); + return -1; + } if (cmsg->cmsg_type != SCM_RIGHTS) { fuse_log(FUSE_LOG_ERR, "got control message of unknown type %d\n", cmsg->cmsg_type); @@ -275,7 +318,6 @@ static int receive_fd(int fd) void fuse_kern_unmount(const char *mountpoint, int fd) { int res; - int pid; if (fd != -1) { struct pollfd pfd; @@ -303,27 +345,25 @@ void fuse_kern_unmount(const char *mountpoint, int fd) return; } - res = umount2(mountpoint, 2); + res = umount2(mountpoint, MNT_DETACH | UMOUNT_NOFOLLOW); if (res == 0) return; - pid = fork(); - if(pid == -1) + char const * const argv[] = + { FUSERMOUNT_PROG, "--unmount", "--quiet", "--lazy", + "--", mountpoint, NULL }; + int status = fusermount_fork_exec(argv, 0, -1, NULL); + if(status != 0) { + fuse_log(FUSE_LOG_ERR, "Spawning %s to unmount failed: %s", + FUSERMOUNT_PROG, strerror(-status)); return; - - if(pid == 0) { - const char *argv[] = { FUSERMOUNT_PROG, "-u", "-q", "-z", - "--", mountpoint, NULL }; - - exec_fusermount(argv); - _exit(1); } - waitpid(pid, NULL, 0); } static int setup_auto_unmount(const char *mountpoint, int quiet) { - int fds[2], pid; + int fds[2]; + pid_t pid; int res; if (!mountpoint) { @@ -333,59 +373,59 @@ static int setup_auto_unmount(const char *mountpoint, int quiet) res = socketpair(PF_UNIX, SOCK_STREAM, 0, fds); if(res == -1) { - perror("fuse: socketpair() failed"); + fuse_log(FUSE_LOG_ERR, "Setting up auto-unmount socketpair() failed: %s\n", + strerror(errno)); return -1; } - pid = fork(); - if(pid == -1) { - perror("fuse: fork() failed"); + char arg_fd_entry[30]; + snprintf(arg_fd_entry, sizeof(arg_fd_entry), "%i", fds[0]); + setenv(FUSE_COMMFD_ENV, arg_fd_entry, 1); + /* + * This helps to identify the FD hold by parent process. + * In auto-unmount case, parent process can close this FD explicitly to do unmount. + * The FD[1] can be got via getenv(FUSE_COMMFD2_ENV). + * One potential use case is to satisfy FD-Leak checks. + */ + snprintf(arg_fd_entry, sizeof(arg_fd_entry), "%i", fds[1]); + setenv(FUSE_COMMFD2_ENV, arg_fd_entry, 1); + + char const *const argv[] = { + FUSERMOUNT_PROG, + "--auto-unmount", + "--", + mountpoint, + NULL, + }; + + /* + * auto-umount runs in the background - it is not waiting for the + * process + */ + int status = fusermount_fork_exec(argv, quiet, fds[1], &pid); + + if(status != 0) { close(fds[0]); close(fds[1]); + fuse_log(FUSE_LOG_ERR, "fuse: Setting up auto-unmount failed (fork): %s", + strerror(-status)); return -1; } - - if(pid == 0) { - char env[10]; - const char *argv[32]; - int a = 0; - - if (quiet) { - int fd = open("/dev/null", O_RDONLY); - if (fd != -1) { - dup2(fd, 1); - dup2(fd, 2); - } - } - - argv[a++] = FUSERMOUNT_PROG; - argv[a++] = "--auto-unmount"; - argv[a++] = "--"; - argv[a++] = mountpoint; - argv[a++] = NULL; - - close(fds[1]); - fcntl(fds[0], F_SETFD, 0); - snprintf(env, sizeof(env), "%i", fds[0]); - setenv(FUSE_COMMFD_ENV, env, 1); - exec_fusermount(argv); - perror("fuse: failed to exec fusermount3"); - _exit(1); - } - + // passed to child now, so can close here. close(fds[0]); // Now fusermount3 will only exit when fds[1] closes automatically when our // process exits. return 0; + // Note: fds[1] is leakend and doesn't get FD_CLOEXEC } static int fuse_mount_fusermount(const char *mountpoint, struct mount_opts *mo, const char *opts, int quiet) { - int fds[2], pid; + int fds[2]; + pid_t pid; int res; - int rv; if (!mountpoint) { fuse_log(FUSE_LOG_ERR, "fuse: missing mountpoint parameter\n"); @@ -394,51 +434,46 @@ static int fuse_mount_fusermount(const char *mountpoint, struct mount_opts *mo, res = socketpair(PF_UNIX, SOCK_STREAM, 0, fds); if(res == -1) { - perror("fuse: socketpair() failed"); + fuse_log(FUSE_LOG_ERR, "Running %s: socketpair() failed: %s\n", + FUSERMOUNT_PROG, strerror(errno)); return -1; } - pid = fork(); - if(pid == -1) { - perror("fuse: fork() failed"); + char arg_fd_entry[30]; + snprintf(arg_fd_entry, sizeof(arg_fd_entry), "%i", fds[0]); + setenv(FUSE_COMMFD_ENV, arg_fd_entry, 1); + /* + * This helps to identify the FD hold by parent process. + * In auto-unmount case, parent process can close this FD explicitly to do unmount. + * The FD[1] can be got via getenv(FUSE_COMMFD2_ENV). + * One potential use case is to satisfy FD-Leak checks. + */ + snprintf(arg_fd_entry, sizeof(arg_fd_entry), "%i", fds[1]); + setenv(FUSE_COMMFD2_ENV, arg_fd_entry, 1); + + char const *const argv[] = { + FUSERMOUNT_PROG, + "-o", opts ? opts : "", + "--", + mountpoint, + NULL, + }; + + + int status = fusermount_fork_exec(argv, quiet, fds[1], &pid); + + if(status != 0) { close(fds[0]); close(fds[1]); + fuse_log(FUSE_LOG_ERR, "fork() for %s failed: %s", + FUSERMOUNT_PROG, strerror(-status)); return -1; } - if(pid == 0) { - char env[10]; - const char *argv[32]; - int a = 0; - - if (quiet) { - int fd = open("/dev/null", O_RDONLY); - if (fd != -1) { - dup2(fd, 1); - dup2(fd, 2); - } - } - - argv[a++] = FUSERMOUNT_PROG; - if (opts) { - argv[a++] = "-o"; - argv[a++] = opts; - } - argv[a++] = "--"; - argv[a++] = mountpoint; - argv[a++] = NULL; - - close(fds[1]); - fcntl(fds[0], F_SETFD, 0); - snprintf(env, sizeof(env), "%i", fds[0]); - setenv(FUSE_COMMFD_ENV, env, 1); - exec_fusermount(argv); - perror("fuse: failed to exec fusermount3"); - _exit(1); - } - + // passed to child now, so can close here. close(fds[0]); - rv = receive_fd(fds[1]); + + int fd = receive_fd(fds[1]); if (!mo->auto_unmount) { /* with auto_unmount option fusermount3 will not exit until @@ -447,10 +482,10 @@ static int fuse_mount_fusermount(const char *mountpoint, struct mount_opts *mo, waitpid(pid, NULL, 0); /* bury zombie */ } - if (rv >= 0) - fcntl(rv, F_SETFD, FD_CLOEXEC); + if (fd >= 0) + fcntl(fd, F_SETFD, FD_CLOEXEC); - return rv; + return fd; } #ifndef O_CLOEXEC @@ -461,7 +496,7 @@ static int fuse_mount_sys(const char *mnt, struct mount_opts *mo, const char *mnt_opts) { char tmp[128]; - const char *devname = "/dev/fuse"; + const char *devname = getenv(FUSE_KERN_DEVICE_ENV) ?: "/dev/fuse"; char *source = NULL; char *type = NULL; struct stat stbuf; @@ -483,7 +518,9 @@ static int fuse_mount_sys(const char *mnt, struct mount_opts *mo, fd = open(devname, O_RDWR | O_CLOEXEC); if (fd == -1) { if (errno == ENODEV || errno == ENOENT) - fuse_log(FUSE_LOG_ERR, "fuse: device not found, try 'modprobe fuse' first\n"); + fuse_log(FUSE_LOG_ERR, + "fuse: device %s not found. Kernel module not loaded?\n", + devname); else fuse_log(FUSE_LOG_ERR, "fuse: failed to open %s: %s\n", devname, strerror(errno)); @@ -571,7 +608,7 @@ static int fuse_mount_sys(const char *mnt, struct mount_opts *mo, return fd; out_umount: - umount2(mnt, 2); /* lazy umount */ + umount2(mnt, MNT_DETACH | UMOUNT_NOFOLLOW); /* lazy umount */ out_close: free(type); free(source); @@ -645,7 +682,8 @@ int fuse_kern_mount(const char *mountpoint, struct mount_opts *mo) if (res >= 0 && mo->auto_unmount) { if(0 > setup_auto_unmount(mountpoint, 0)) { // Something went wrong, let's umount like in fuse_mount_sys. - umount2(mountpoint, MNT_DETACH); /* lazy umount */ + umount2(mountpoint, + MNT_DETACH | UMOUNT_NOFOLLOW); /* lazy umount */ res = -1; } } else if (res == -2) { diff --git a/lib/mount_bsd.c b/lib/mount_bsd.c index bd95a76d3..c12ab322e 100644 --- a/lib/mount_bsd.c +++ b/lib/mount_bsd.c @@ -5,7 +5,7 @@ Architecture specific file system mounting (FreeBSD). This program can be distributed under the terms of the GNU LGPLv2. - See the file COPYING.LIB. + See the file LGPL2.txt. */ #include "fuse_config.h" @@ -187,7 +187,7 @@ static int fuse_mount_core(const char *mountpoint, const char *opts) if (pid == 0) { const char *argv[32]; int a = 0; - int ret = -1; + int ret = -1; if (! fdnam) { @@ -214,7 +214,10 @@ static int fuse_mount_core(const char *mountpoint, const char *opts) _exit(EXIT_FAILURE); } - _exit(EXIT_SUCCESS); + waitpid(pid, &status, 0); + if (!WIFEXITED(status)) + _exit(EXIT_FAILURE); + _exit(WEXITSTATUS(status)); } if (waitpid(cpid, &status, 0) == -1 || WEXITSTATUS(status) != 0) { diff --git a/lib/mount_util.c b/lib/mount_util.c index f19dfb4c8..9a02745dd 100644 --- a/lib/mount_util.c +++ b/lib/mount_util.c @@ -5,11 +5,12 @@ Architecture-independent mounting code. This program can be distributed under the terms of the GNU LGPLv2. - See the file COPYING.LIB. + See the file LGPL2.txt. */ #include "fuse_config.h" #include "mount_util.h" +#include "fuse_log.h" #include <stdio.h> #include <unistd.h> @@ -85,6 +86,19 @@ static int mtab_needs_update(const char *mnt) } #endif /* IGNORE_MTAB */ +/* + * These values become command line arguments of /bin/mount and /bin/umount. + * BusyBox mount(8) parses an argument starting with '-' as an option even + * behind an end-of-options marker ("--"); NULL would end the argument list + * early. + * + * @return 1 if @value is unsafe as a /bin/mount or /bin/umount argument. + */ +static int unsafe_operand(const char *value) +{ + return value == NULL || value[0] == '-'; +} + static int add_mount(const char *progname, const char *fsname, const char *mnt, const char *type, const char *opts) { @@ -93,6 +107,14 @@ static int add_mount(const char *progname, const char *fsname, sigset_t blockmask; sigset_t oldmask; + if (unsafe_operand(fsname) || unsafe_operand(mnt) || + unsafe_operand(type) || unsafe_operand(opts)) { + fuse_log(FUSE_LOG_DEBUG, + "%s: option-like mount argument, skipping mtab update\n", + progname); + return 0; + } + sigemptyset(&blockmask); sigaddset(&blockmask, SIGCHLD); res = sigprocmask(SIG_BLOCK, &blockmask, &oldmask); @@ -117,8 +139,15 @@ static int add_mount(const char *progname, const char *fsname, goto out_restore; } + /* + * fsname comes from -ofsname=, so it can start with '-'. The + * setuid() above raises the real uid to 0, so mount(8) is not + * in restricted mode either. Terminate the options with "--" + * to keep it from parsing the operands as further options. + */ execle("/bin/mount", "/bin/mount", "--no-canonicalize", "-i", - "-f", "-t", type, "-o", opts, fsname, mnt, NULL, &env); + "-f", "-t", type, "-o", opts, "--", fsname, mnt, NULL, + &env); fprintf(stderr, "%s: failed to execute /bin/mount: %s\n", progname, strerror(errno)); exit(1); @@ -177,11 +206,11 @@ static int exec_umount(const char *progname, const char *rel_mnt, int lazy) } if (lazy) { - execle("/bin/umount", "/bin/umount", "-i", rel_mnt, - "-l", NULL, &env); + execle("/bin/umount", "/bin/umount", "-i", "-l", + "--", rel_mnt, NULL, &env); } else { - execle("/bin/umount", "/bin/umount", "-i", rel_mnt, - NULL, &env); + execle("/bin/umount", "/bin/umount", "-i", + "--", rel_mnt, NULL, &env); } fprintf(stderr, "%s: failed to execute /bin/umount: %s\n", progname, strerror(errno)); @@ -206,7 +235,11 @@ int fuse_mnt_umount(const char *progname, const char *abs_mnt, { int res; - if (!mtab_needs_update(abs_mnt)) { + /* + * umount(8) may read an option-like rel_mnt as an option: unmount here + * instead and leave the mtab/utab record behind + */ + if (!mtab_needs_update(abs_mnt) || unsafe_operand(rel_mnt)) { res = umount2(rel_mnt, lazy ? 2 : 0); if (res == -1) fprintf(stderr, "%s: failed to unmount %s: %s\n", @@ -224,6 +257,9 @@ static int remove_mount(const char *progname, const char *mnt) sigset_t blockmask; sigset_t oldmask; + if (unsafe_operand(mnt)) + return 0; + sigemptyset(&blockmask); sigaddset(&blockmask, SIGCHLD); res = sigprocmask(SIG_BLOCK, &blockmask, &oldmask); @@ -249,7 +285,7 @@ static int remove_mount(const char *progname, const char *mnt) } execle("/bin/umount", "/bin/umount", "--no-canonicalize", "-i", - "--fake", mnt, NULL, &env); + "--fake", "--", mnt, NULL, &env); fprintf(stderr, "%s: failed to execute /bin/umount: %s\n", progname, strerror(errno)); exit(1); diff --git a/lib/mount_util.h b/lib/mount_util.h index 0ef0fbe81..9cb9077dd 100644 --- a/lib/mount_util.h +++ b/lib/mount_util.h @@ -3,7 +3,7 @@ Copyright (C) 2001-2007 Miklos Szeredi <miklos@szeredi.hu> This program can be distributed under the terms of the GNU LGPLv2. - See the file COPYING.LIB. + See the file LGPL2.txt. */ #include <sys/types.h> diff --git a/lib/usdt.h b/lib/usdt.h new file mode 100644 index 000000000..6a0bc4258 --- /dev/null +++ b/lib/usdt.h @@ -0,0 +1,540 @@ +/* + * Copied from https://github.com/libbpf/usdt/ + */ + +// SPDX-License-Identifier: BSD-2-Clause +/* + * This single-header library defines a collection of variadic macros for + * defining and triggering USDTs (User Statically-Defined Tracepoints): + * + * - For USDTs without associated semaphore: + * USDT(group, name, args...) + * + * - For USDTs with implicit (transparent to the user) semaphore: + * USDT_WITH_SEMA(group, name, args...) + * USDT_IS_ACTIVE(group, name) + * + * - For USDTs with explicit (user-defined and provided) semaphore: + * USDT_WITH_EXPLICIT_SEMA(sema, group, name, args...) + * USDT_SEMA_IS_ACTIVE(sema) + * + * all of which emit a NOP instruction into the instruction stream, and so + * have *zero* overhead for the surrounding code. USDTs are identified by + * a combination of `group` and `name` identifiers, which is used by external + * tracing tooling (tracers) for identifying exact USDTs of interest. + * + * USDTs can have an associated (2-byte) activity counter (USDT semaphore), + * automatically maintained by Linux kernel whenever any correctly written + * BPF-based tracer is attached to the USDT. This USDT semaphore can be used + * to check whether there is a need to do any extra data collection and + * processing for a given USDT (if necessary), and otherwise avoid extra work + * for a common case of USDT not being traced ("active"). + * + * See documentation for USDT_WITH_SEMA()/USDT_IS_ACTIVE() or + * USDT_WITH_EXPLICIT_SEMA()/USDT_SEMA_IS_ACTIVE() APIs below for details on + * working with USDTs with implicitly or explicitly associated + * USDT semaphores, respectively. + * + * There is also some additional data recorded into an auxiliary note + * section. The data in the note section describes the operands, in terms of + * size and location, used by tracing tooling to know where to find USDT + * arguments. Each location is encoded as an assembler operand string. + * Tracing tools (bpftrace and BPF-based tracers, systemtap, etc) insert + * breakpoints on top of the nop, and decode the location operand-strings, + * like an assembler, to find the values being passed. + * + * The operand strings are selected by the compiler for each operand. + * They are constrained by inline-assembler codes.The default is: + * + * #define USDT_ARG_CONSTRAINT nor + * + * This is a good default if the operands tend to be integral and + * moderate in number (smaller than number of registers). In other + * cases, the compiler may report "'asm' requires impossible reload" or + * similar. In this case, consider simplifying the macro call (fewer + * and simpler operands), reduce optimization, or override the default + * constraints string via: + * + * #define USDT_ARG_CONSTRAINT g + * #include <usdt.h> + * + * For some historical description of USDT v3 format (the one used by this + * library and generally recognized and assumed by BPF-based tracing tools) + * see [0]. The more formal specification can be found at [1]. Additional + * argument constraints information can be found at [2]. + * + * Original SystemTap's sys/sdt.h implementation ([3]) was used as a base for + * this USDT library implementation. Current implementation differs *a lot* in + * terms of exposed user API and general usability, which was the main goal + * and focus of the reimplementation work. Nevertheless, underlying recorded + * USDT definitions are fully binary compatible and any USDT-based tooling + * should work equally well with USDTs defined by either SystemTap's or this + * library's USDT implementation. + * + * [0] https://ecos.sourceware.org/ml/systemtap/2010-q3/msg00145.html + * [1] https://sourceware.org/systemtap/wiki/UserSpaceProbeImplementation + * [2] https://gcc.gnu.org/onlinedocs/gcc/Constraints.html + * [3] https://sourceware.org/git/?p=systemtap.git;a=blob;f=includes/sys/sdt.h + */ +#ifndef __USDT_H +#define __USDT_H + +/* + * Changelog: + * + * 0.1.0 + * ----- + * - Initial release + */ +#define USDT_MAJOR_VERSION 0 +#define USDT_MINOR_VERSION 1 +#define USDT_PATCH_VERSION 0 + +/* C++20 and C23 added __VA_OPT__ as a standard replacement for non-standard `##__VA_ARGS__` extension */ +#if (defined(__STDC_VERSION__) && __STDC_VERSION__ > 201710L) || (defined(__cplusplus) && __cplusplus > 201703L) +#define __usdt_va_opt 1 +#define __usdt_va_args(...) __VA_OPT__(,) __VA_ARGS__ +#else +#define __usdt_va_args(...) , ##__VA_ARGS__ +#endif + +/* + * Trigger USDT with `group`:`name` identifier and pass through `args` as its + * arguments. Zero arguments are acceptable as well. No USDT semaphore is + * associated with this USDT. + * + * Such "semaphoreless" USDTs are commonly used when there is no extra data + * collection or processing needed to collect and prepare USDT arguments and + * they are just available in the surrounding code. USDT() macro will just + * record their locations in CPU registers or in memory for tracing tooling to + * be able to access them, if necessary. + */ +#ifdef __usdt_va_opt +#define USDT(group, name, ...) \ + __usdt_probe(group, name, __usdt_sema_none, 0 __VA_OPT__(,) __VA_ARGS__) +#else +#define USDT(group, name, ...) \ + __usdt_probe(group, name, __usdt_sema_none, 0, ##__VA_ARGS__) +#endif + +/* + * Trigger USDT with `group`:`name` identifier and pass through `args` as its + * arguments. Zero arguments are acceptable as well. USDT also get an + * implicitly-defined associated USDT semaphore, which will be "activated" by + * tracing tooling and can be used to check whether USDT is being actively + * observed. + * + * USDTs with semaphore are commonly used when there is a need to perform + * additional data collection and processing to prepare USDT arguments, which + * otherwise might not be necessary for the rest of application logic. In such + * case, USDT semaphore can be used to avoid unnecessary extra work. If USDT + * is not traced (which is presumed to be a common situation), the associated + * USDT semaphore is "inactive", and so there is no need to waste resources to + * prepare USDT arguments. Use USDT_IS_ACTIVE(group, name) to check whether + * USDT is "active". + * + * N.B. There is an inherent (albeit short) gap between checking whether USDT + * is active and triggering corresponding USDT, in which external tracer can + * be attached to an USDT and activate USDT semaphore after the activity check. + * If such a race occurs, tracers might miss one USDT execution. Tracers are + * expected to accommodate such possibility and this is expected to not be + * a problem for applications and tracers. + * + * N.B. Implicit USDT semaphore defined by USDT_WITH_SEMA() is contained + * within a single executable or shared library and is not shared outside + * them. I.e., if you use USDT_WITH_SEMA() with the same USDT group and name + * identifier across executable and shared library, it will work and won't + * conflict, per se, but will define independent USDT semaphores, one for each + * shared library/executable in which USDT_WITH_SEMA(group, name) is used. + * That is, if you attach to this USDT in one shared library (or executable), + * then only USDT semaphore within that shared library (or executable) will be + * updated by the kernel, while other libraries (or executable) will not see + * activated USDT semaphore. In short, it's best to use unique USDT group:name + * identifiers across different shared libraries (and, equivalently, between + * executable and shared library). This is advanced consideration and is + * rarely (if ever) seen in practice, but just to avoid surprises this is + * called out here. (Static libraries become a part of final executable, once + * linked by linker, so the above considerations don't apply to them.) + */ +#ifdef __usdt_va_opt +#define USDT_WITH_SEMA(group, name, ...) \ + __usdt_probe(group, name, \ + __usdt_sema_implicit, __usdt_sema_name(group, name) \ + __VA_OPT__(,) __VA_ARGS__) +#else +#define USDT_WITH_SEMA(group, name, ...) \ + __usdt_probe(group, name, \ + __usdt_sema_implicit, __usdt_sema_name(group, name), \ + ##__VA_ARGS__) +#endif + +struct usdt_sema { volatile unsigned short active; }; + +/* + * Check if USDT with `group`:`name` identifier is "active" (i.e., whether it + * is attached to by external tracing tooling and is actively observed). + * + * This macro can be used to decide whether any additional and potentially + * expensive data collection or processing should be done to pass extra + * information into the given USDT. It is assumed that USDT is triggered with + * USDT_WITH_SEMA() macro which will implicitly define associated USDT + * semaphore. (If one needs more control over USDT semaphore, see + * USDT_DEFINE_SEMA() and USDT_WITH_EXPLICIT_SEMA() macros below.) + * + * N.B. Such checks are necessarily racy and speculative. Between checking + * whether USDT is active and triggering the USDT itself, tracer can be + * detached with no notification. This race should be extremely rare and worst + * case should result in one-time wasted extra data collection and processing. + */ +#define USDT_IS_ACTIVE(group, name) ({ \ + extern struct usdt_sema __usdt_sema_name(group, name) \ + __usdt_asm_name(__usdt_sema_name(group, name)); \ + __usdt_sema_implicit(__usdt_sema_name(group, name)); \ + __usdt_sema_name(group, name).active > 0; \ +}) + +/* + * APIs for working with user-defined explicit USDT semaphores. + * + * This is a less commonly used advanced API for use cases in which user needs + * an explicit control over (potentially shared across multiple USDTs) USDT + * semaphore instance. This can be used when there is a group of logically + * related USDTs that all need extra data collection and processing whenever + * any of a family of related USDTs are "activated" (i.e., traced). In such + * a case, all such related USDTs will be associated with the same shared USDT + * semaphore defined with USDT_DEFINE_SEMA() and the USDTs themselves will be + * triggered with USDT_WITH_EXPLICIT_SEMA() macros, taking an explicit extra + * USDT semaphore identifier as an extra parameter. + */ + +/** + * Underlying C global variable name for user-defined USDT semaphore with + * `sema` identifier. Could be useful for debugging, but normally shouldn't be + * used explicitly. + */ +#define USDT_SEMA(sema) __usdt_sema_##sema + +/* + * Define storage for user-defined USDT semaphore `sema`. + * + * Should be used only once in non-header source file to let compiler allocate + * space for the semaphore variable. Just like with any other global variable. + * + * This macro can be used anywhere where global variable declaration is + * allowed. Just like with global variable definitions, there should be only + * one definition of user-defined USDT semaphore with given `sema` identifier, + * otherwise compiler or linker will complain about duplicate variable + * definition. + * + * For C++, it is allowed to use USDT_DEFINE_SEMA() both in global namespace + * and inside namespaces (including nested namespaces). Just make sure that + * USDT_DECLARE_SEMA() is placed within the namespace where this semaphore is + * referenced, or any of its parent namespaces, so the C++ language-level + * identifier is visible to the code that needs to reference the semaphore. + * At the lowest layer, USDT semaphores have global naming and visibility + * (they have a corresponding `__usdt_sema_<name>` symbol, which can be linked + * against from C or C++ code, if necessary). To keep it simple, putting + * USDT_DECLARE_SEMA() declarations into global namespaces is the simplest + * no-brainer solution. All these aspects are irrelevant for plain C, because + * C doesn't have namespaces and everything is always in the global namespace. + * + * N.B. Due to USDT metadata being recorded in non-allocatable ELF note + * section, it has limitations when it comes to relocations, which, in + * practice, means that it's not possible to correctly share USDT semaphores + * between main executable and shared libraries, or even between multiple + * shared libraries. USDT semaphore has to be contained to individual shared + * library or executable to avoid unpleasant surprises with half-working USDT + * semaphores. We enforce this by marking semaphore ELF symbols as having + * a hidden visibility. This is quite an advanced use case and consideration + * and for most users this should have no consequences whatsoever. + */ +#define USDT_DEFINE_SEMA(sema) \ + struct usdt_sema __usdt_sema_sec USDT_SEMA(sema) \ + __usdt_asm_name(USDT_SEMA(sema)) \ + __attribute__((visibility("hidden"))) = { 0 } + +/* + * Declare extern reference to user-defined USDT semaphore `sema`. + * + * Refers to a variable defined in another compilation unit by + * USDT_DEFINE_SEMA() and allows to use the same USDT semaphore across + * multiple compilation units (i.e., .c and .cpp files). + * + * See USDT_DEFINE_SEMA() notes above for C++ language usage peculiarities. + */ +#define USDT_DECLARE_SEMA(sema) \ + extern struct usdt_sema USDT_SEMA(sema) __usdt_asm_name(USDT_SEMA(sema)) + +/* + * Check if user-defined USDT semaphore `sema` is "active" (i.e., whether it + * is attached to by external tracing tooling and is actively observed). + * + * This macro can be used to decide whether any additional and potentially + * expensive data collection or processing should be done to pass extra + * information into USDT(s) associated with USDT semaphore `sema`. + * + * N.B. Such checks are necessarily racy. Between checking the state of USDT + * semaphore and triggering associated USDT(s), the active tracer might attach + * or detach. This race should be extremely rare and worst case should result + * in one-time missed USDT event or wasted extra data collection and + * processing. USDT-using tracers should be written with this in mind and is + * not a concern of the application defining USDTs with associated semaphore. + */ +#define USDT_SEMA_IS_ACTIVE(sema) (USDT_SEMA(sema).active > 0) + +/* + * Invoke USDT specified by `group` and `name` identifiers and associate + * explicitly user-defined semaphore `sema` with it. Pass through `args` as + * USDT arguments. `args` are optional and zero arguments are acceptable. + * + * Semaphore is defined with the help of USDT_DEFINE_SEMA() macro and can be + * checked whether active with USDT_SEMA_IS_ACTIVE(). + */ +#ifdef __usdt_va_opt +#define USDT_WITH_EXPLICIT_SEMA(sema, group, name, ...) \ + __usdt_probe(group, name, __usdt_sema_explicit, USDT_SEMA(sema), ##__VA_ARGS__) +#else +#define USDT_WITH_EXPLICIT_SEMA(sema, group, name, ...) \ + __usdt_probe(group, name, __usdt_sema_explicit, USDT_SEMA(sema) __VA_OPT__(,) __VA_ARGS__) +#endif + +/* + * Adjustable implementation aspects + */ +#ifndef USDT_ARG_CONSTRAINT +#if defined __powerpc__ +#define USDT_ARG_CONSTRAINT nZr +#elif defined __arm__ +#define USDT_ARG_CONSTRAINT g +#elif defined __loongarch__ +#define USDT_ARG_CONSTRAINT nmr +#else +#define USDT_ARG_CONSTRAINT nor +#endif +#endif /* USDT_ARG_CONSTRAINT */ + +#ifndef USDT_NOP +#if defined(__ia64__) || defined(__s390__) || defined(__s390x__) +#define USDT_NOP nop 0 +#else +#define USDT_NOP nop +#endif +#endif /* USDT_NOP */ + +/* + * Implementation details + */ +/* USDT name for implicitly-defined USDT semaphore, derived from group:name */ +#define __usdt_sema_name(group, name) __usdt_sema_##group##__##name +/* ELF section into which USDT semaphores are put */ +#define __usdt_sema_sec __attribute__((section(".probes"))) + +#define __usdt_concat(a, b) a ## b +#define __usdt_apply(fn, n) __usdt_concat(fn, n) + +#ifndef __usdt_nth +#define __usdt_nth(_, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, N, ...) N +#endif + +#ifndef __usdt_narg +#ifdef __usdt_va_opt +#define __usdt_narg(...) __usdt_nth(_ __VA_OPT__(,) __VA_ARGS__, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0) +#else +#define __usdt_narg(...) __usdt_nth(_, ##__VA_ARGS__, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0) +#endif +#endif /* __usdt_narg */ + +#define __usdt_hash # +#define __usdt_str_(x) #x +#define __usdt_str(x) __usdt_str_(x) + +#ifndef __usdt_asm_name +#define __usdt_asm_name(name) __asm__(__usdt_str(name)) +#endif + +#define __usdt_asm1(a) __usdt_str(a) "\n" +#define __usdt_asm2(a,b) __usdt_str(a) "," __usdt_str(b) "\n" +#define __usdt_asm3(a,b,c) __usdt_str(a) "," __usdt_str(b) "," __usdt_str(c) "\n" +#define __usdt_asm5(a,b,c,d,e) __usdt_str(a) "," __usdt_str(b) "," __usdt_str(c) "," \ + __usdt_str(d) "," __usdt_str(e) "\n" + +#ifdef __LP64__ +#define __usdt_asm_addr .8byte +#else +#define __usdt_asm_addr .4byte +#endif + +#define __usdt_asm_strz_(x) __usdt_asm1(.asciz #x) +#define __usdt_asm_strz(x) __usdt_asm_strz_(x) +#define __usdt_asm_str_(x) __usdt_asm1(.ascii #x) +#define __usdt_asm_str(x) __usdt_asm_str_(x) + +/* "semaphoreless" USDT case */ +#ifndef __usdt_sema_none +#define __usdt_sema_none(sema) +#endif + +/* implicitly defined __usdt_sema__group__name semaphore (using weak symbols) */ +#ifndef __usdt_sema_implicit +#define __usdt_sema_implicit(sema) \ + __asm__ __volatile__ ( \ + __usdt_asm1(.ifndef sema) \ + __usdt_asm3( .pushsection .probes, "aw", "progbits") \ + __usdt_asm1( .weak sema) \ + __usdt_asm1( .hidden sema) \ + __usdt_asm1( .align 2) \ + __usdt_asm1(sema:) \ + __usdt_asm1( .zero 2) \ + __usdt_asm2( .type sema, @object) \ + __usdt_asm2( .size sema, 2) \ + __usdt_asm1( .popsection) \ + __usdt_asm1(.endif) \ + ); +#endif + +/* externally defined semaphore using USDT_DEFINE_SEMA() and passed explicitly by user */ +#ifndef __usdt_sema_explicit +#define __usdt_sema_explicit(sema) \ + __asm__ __volatile__ ("" :: "m" (sema)); +#endif + +/* main USDT definition (nop and .note.stapsdt metadata) */ +#define __usdt_probe(group, name, sema_def, sema, ...) do { \ + sema_def(sema) \ + __asm__ __volatile__ ( \ + __usdt_asm1(990: USDT_NOP) \ + __usdt_asm3( .pushsection .note.stapsdt, "", "note") \ + __usdt_asm1( .balign 4) \ + __usdt_asm3( .4byte 992f-991f,994f-993f,3) \ + __usdt_asm1(991: .asciz "stapsdt") \ + __usdt_asm1(992: .balign 4) \ + __usdt_asm1(993: __usdt_asm_addr 990b) \ + __usdt_asm1( __usdt_asm_addr _.stapsdt.base) \ + __usdt_asm1( __usdt_asm_addr sema) \ + __usdt_asm_strz(group) \ + __usdt_asm_strz(name) \ + __usdt_asm_args(__VA_ARGS__) \ + __usdt_asm1( .ascii "\0") \ + __usdt_asm1(994: .balign 4) \ + __usdt_asm1( .popsection) \ + __usdt_asm1(.ifndef _.stapsdt.base) \ + __usdt_asm5( .pushsection .stapsdt.base,"aG","progbits",.stapsdt.base,comdat)\ + __usdt_asm1( .weak _.stapsdt.base) \ + __usdt_asm1( .hidden _.stapsdt.base) \ + __usdt_asm1(_.stapsdt.base:) \ + __usdt_asm1( .space 1) \ + __usdt_asm2( .size _.stapsdt.base, 1) \ + __usdt_asm1( .popsection) \ + __usdt_asm1(.endif) \ + :: __usdt_asm_ops(__VA_ARGS__) \ + ); \ +} while (0) + +/* + * NB: gdb PR24541 highlighted an unspecified corner of the sdt.h + * operand note format. + * + * The named register may be a longer or shorter (!) alias for the + * storage where the value in question is found. For example, on + * i386, 64-bit value may be put in register pairs, and a register + * name stored would identify just one of them. Previously, gcc was + * asked to emit the %w[id] (16-bit alias of some registers holding + * operands), even when a wider 32-bit value was used. + * + * Bottom line: the byte-width given before the @ sign governs. If + * there is a mismatch between that width and that of the named + * register, then a sys/sdt.h note consumer may need to employ + * architecture-specific heuristics to figure out where the compiler + * has actually put the complete value. + */ +#if defined(__powerpc__) || defined(__powerpc64__) +#define __usdt_argref(id) %I[id]%[id] +#elif defined(__i386__) +#define __usdt_argref(id) %k[id] /* gcc.gnu.org/PR80115 sourceware.org/PR24541 */ +#else +#define __usdt_argref(id) %[id] +#endif + +#define __usdt_asm_arg(n) __usdt_asm_str(%c[__usdt_asz##n]) \ + __usdt_asm1(.ascii "@") \ + __usdt_asm_str(__usdt_argref(__usdt_aval##n)) + +#define __usdt_asm_args0 /* no arguments */ +#define __usdt_asm_args1 __usdt_asm_arg(1) +#define __usdt_asm_args2 __usdt_asm_args1 __usdt_asm1(.ascii " ") __usdt_asm_arg(2) +#define __usdt_asm_args3 __usdt_asm_args2 __usdt_asm1(.ascii " ") __usdt_asm_arg(3) +#define __usdt_asm_args4 __usdt_asm_args3 __usdt_asm1(.ascii " ") __usdt_asm_arg(4) +#define __usdt_asm_args5 __usdt_asm_args4 __usdt_asm1(.ascii " ") __usdt_asm_arg(5) +#define __usdt_asm_args6 __usdt_asm_args5 __usdt_asm1(.ascii " ") __usdt_asm_arg(6) +#define __usdt_asm_args7 __usdt_asm_args6 __usdt_asm1(.ascii " ") __usdt_asm_arg(7) +#define __usdt_asm_args8 __usdt_asm_args7 __usdt_asm1(.ascii " ") __usdt_asm_arg(8) +#define __usdt_asm_args9 __usdt_asm_args8 __usdt_asm1(.ascii " ") __usdt_asm_arg(9) +#define __usdt_asm_args10 __usdt_asm_args9 __usdt_asm1(.ascii " ") __usdt_asm_arg(10) +#define __usdt_asm_args11 __usdt_asm_args10 __usdt_asm1(.ascii " ") __usdt_asm_arg(11) +#define __usdt_asm_args12 __usdt_asm_args11 __usdt_asm1(.ascii " ") __usdt_asm_arg(12) +#define __usdt_asm_args(...) __usdt_apply(__usdt_asm_args, __usdt_narg(__VA_ARGS__)) + +#define __usdt_is_arr(x) (__builtin_classify_type(x) == 14 || __builtin_classify_type(x) == 5) +#define __usdt_arg_size(x) (__usdt_is_arr(x) ? sizeof(void *) : sizeof(x)) + +/* + * We can't use __builtin_choose_expr() in C++, so fall back to table-based + * signedness determination for known types, utilizing templates magic. + */ +#ifdef __cplusplus + +#define __usdt_is_signed(x) (!__usdt_is_arr(x) && __usdt_t<__typeof(x)>::is_signed) + +#include <cstddef> + +template<typename T> struct __usdt_t { static const bool is_signed = false; }; +template<typename A> struct __usdt_t<A[]> : public __usdt_t<A *> {}; +template<typename A, size_t N> struct __usdt_t<A[N]> : public __usdt_t<A *> {}; + +#define __usdt_def_signed(T) \ +template<> struct __usdt_t<T> { static const bool is_signed = true; }; \ +template<> struct __usdt_t<const T> { static const bool is_signed = true; }; \ +template<> struct __usdt_t<volatile T> { static const bool is_signed = true; }; \ +template<> struct __usdt_t<const volatile T> { static const bool is_signed = true; } +#define __usdt_maybe_signed(T) \ +template<> struct __usdt_t<T> { static const bool is_signed = (T)-1 < (T)1; }; \ +template<> struct __usdt_t<const T> { static const bool is_signed = (T)-1 < (T)1; }; \ +template<> struct __usdt_t<volatile T> { static const bool is_signed = (T)-1 < (T)1; }; \ +template<> struct __usdt_t<const volatile T> { static const bool is_signed = (T)-1 < (T)1; } + +__usdt_def_signed(signed char); +__usdt_def_signed(short); +__usdt_def_signed(int); +__usdt_def_signed(long); +__usdt_def_signed(long long); +__usdt_maybe_signed(char); +__usdt_maybe_signed(wchar_t); + +#else /* !__cplusplus */ + +#define __usdt_is_inttype(x) (__builtin_classify_type(x) >= 1 && __builtin_classify_type(x) <= 4) +#define __usdt_inttype(x) __typeof(__builtin_choose_expr(__usdt_is_inttype(x), (x), 0U)) +#define __usdt_is_signed(x) ((__usdt_inttype(x))-1 < (__usdt_inttype(x))1) + +#endif /* __cplusplus */ + +#define __usdt_asm_op(n, x) \ + [__usdt_asz##n] "n" ((__usdt_is_signed(x) ? (int)-1 : 1) * (int)__usdt_arg_size(x)), \ + [__usdt_aval##n] __usdt_str(USDT_ARG_CONSTRAINT)(x) + +#define __usdt_asm_ops0() [__usdt_dummy] "g" (0) +#define __usdt_asm_ops1(x) __usdt_asm_op(1, x) +#define __usdt_asm_ops2(a,x) __usdt_asm_ops1(a), __usdt_asm_op(2, x) +#define __usdt_asm_ops3(a,b,x) __usdt_asm_ops2(a,b), __usdt_asm_op(3, x) +#define __usdt_asm_ops4(a,b,c,x) __usdt_asm_ops3(a,b,c), __usdt_asm_op(4, x) +#define __usdt_asm_ops5(a,b,c,d,x) __usdt_asm_ops4(a,b,c,d), __usdt_asm_op(5, x) +#define __usdt_asm_ops6(a,b,c,d,e,x) __usdt_asm_ops5(a,b,c,d,e), __usdt_asm_op(6, x) +#define __usdt_asm_ops7(a,b,c,d,e,f,x) __usdt_asm_ops6(a,b,c,d,e,f), __usdt_asm_op(7, x) +#define __usdt_asm_ops8(a,b,c,d,e,f,g,x) __usdt_asm_ops7(a,b,c,d,e,f,g), __usdt_asm_op(8, x) +#define __usdt_asm_ops9(a,b,c,d,e,f,g,h,x) __usdt_asm_ops8(a,b,c,d,e,f,g,h), __usdt_asm_op(9, x) +#define __usdt_asm_ops10(a,b,c,d,e,f,g,h,i,x) __usdt_asm_ops9(a,b,c,d,e,f,g,h,i), __usdt_asm_op(10, x) +#define __usdt_asm_ops11(a,b,c,d,e,f,g,h,i,j,x) __usdt_asm_ops10(a,b,c,d,e,f,g,h,i,j), __usdt_asm_op(11, x) +#define __usdt_asm_ops12(a,b,c,d,e,f,g,h,i,j,k,x) __usdt_asm_ops11(a,b,c,d,e,f,g,h,i,j,k), __usdt_asm_op(12, x) +#define __usdt_asm_ops(...) __usdt_apply(__usdt_asm_ops, __usdt_narg(__VA_ARGS__))(__VA_ARGS__) + +#endif /* __USDT_H */ diff --git a/lib/util.c b/lib/util.c index 956c3d2e9..2914f1cfc 100644 --- a/lib/util.c +++ b/lib/util.c @@ -1,3 +1,12 @@ + +#include "fuse_config.h" + +#ifdef HAVE_PTHREAD_SETNAME_NP +#define _GNU_SOURCE +#include <pthread.h> +#endif + +#include <errno.h> #include <stdlib.h> #include <errno.h> @@ -33,3 +42,12 @@ int libfuse_strtol(const char *str, long *res) return 0; } +void fuse_set_thread_name(const char *name) +{ +#ifdef HAVE_PTHREAD_SETNAME_NP + pthread_setname_np(pthread_self(), name); +#else + (void)name; +#endif +} + diff --git a/lib/util.h b/lib/util.h index f24401a29..107a2bfdd 100644 --- a/lib/util.h +++ b/lib/util.h @@ -12,6 +12,7 @@ struct fuse_conn_info; int libfuse_strtol(const char *str, long *res); +void fuse_set_thread_name(const char *name); /** * Return the low bits of a number @@ -30,7 +31,7 @@ static inline uint64_t fuse_higher_32_bits(uint64_t nr) } #ifndef FUSE_VAR_UNUSED -#define FUSE_VAR_UNUSED(var) (__attribute__((unused)) var) +#define FUSE_VAR_UNUSED __attribute__((__unused__)) #endif #define container_of(ptr, type, member) \ @@ -39,4 +40,10 @@ static inline uint64_t fuse_higher_32_bits(uint64_t nr) ((type *)(__mptr - offsetof(type, member))); \ }) +#if __has_attribute(__fallthrough__) +#define fallthrough __attribute__((__fallthrough__)) +#else +#define fallthrough do {} while (0) #endif + +#endif /* FUSE_UTIL_H_ */ diff --git a/make_release_tarball.sh b/make_release_tarball.sh index ce2fe16c5..bd97125f5 100755 --- a/make_release_tarball.sh +++ b/make_release_tarball.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/sh -x # # Create tarball from Git tag, removing and adding # some files. @@ -30,7 +30,7 @@ cp -a doc/html "${TAG}/doc/" tar -czf "${TAG}.tar.gz" "${TAG}/" signify-openbsd -S -s signify/$MAJOR_REV.sec -m $TAG.tar.gz -signify-openbsd -V -m ${TAG}.tar.gz -p signify/.$MAJOR_REV.pub +signify-openbsd -V -m ${TAG}.tar.gz -p signify/$MAJOR_REV.pub echo "Contributors from ${PREV_TAG} to ${TAG}:" diff --git a/meson.build b/meson.build index 32795fc8a..01cd1a372 100644 --- a/meson.build +++ b/meson.build @@ -1,11 +1,10 @@ -project('libfused', - ['c'], - version: '1.1.0', # synced with fuse-3.17.4 +project('libfused', ['c'], + version: '1.2.0', # synced with fuse-3.18.3 meson_version: '>= 0.60.0', default_options: [ 'buildtype=debugoptimized', 'c_std=gnu11', - 'cpp_std=c++17', + 'cpp_std=c++20', 'warning_level=2', ]) @@ -74,7 +73,7 @@ private_cfg.set_quoted('PACKAGE_VERSION', meson.project_version()) # Test for presence of some functions test_funcs = [ 'fork', 'fstatat', 'openat', 'readlinkat', 'pipe2', 'splice', 'vmsplice', 'posix_fallocate', 'fdatasync', - 'utimensat', 'copy_file_range', 'fallocate' ] + 'utimensat', 'copy_file_range', 'fallocate', 'fspacectl' ] foreach func : test_funcs private_cfg.set('HAVE_' + func.to_upper(), cc.has_function(func, prefix: include_default, args: args_default)) @@ -93,11 +92,32 @@ special_funcs = { 'close_range': ''' #include <unistd.h> #include <fcntl.h> + #ifdef linux #include <linux/close_range.h> + #endif int main(void) { unsigned int flags = CLOSE_RANGE_UNSHARE; return close_range(3, ~0U, flags); } + ''', + 'listmount': ''' + #include <syscall.h> + #include <linux/mount.h> + #include <unistd.h> + #include <stdint.h> + + int main(int argc, char *argv[]) { + struct mnt_id_req req = { + .size = sizeof(struct mnt_id_req), + .mnt_id = LSMT_ROOT, + }; + uint64_t mnt_ids[1]; + + int n = syscall(SYS_listmount, &req, &mnt_ids, 1, 0); + if (n == -1) { + return -1; + } + } ''' } @@ -107,6 +127,13 @@ foreach name, code : special_funcs name: name + ' check')) endforeach +# Headers checks +test_headers = [ 'sys/xattr.h', 'linux/limits.h' ] +foreach header : test_headers + private_cfg.set('HAVE_' + header.underscorify().to_upper(), + cc.has_header(header)) +endforeach + # Regular function checks private_cfg.set('HAVE_SETXATTR', cc.has_function('setxattr', prefix: '#include <sys/xattr.h>')) @@ -114,6 +141,8 @@ private_cfg.set('HAVE_ICONV', cc.has_function('iconv', prefix: '#include <iconv.h>')) private_cfg.set('HAVE_BACKTRACE', cc.has_function('backtrace', prefix: '#include <execinfo.h>')) +private_cfg.set('HAVE_STATX', + cc.has_function('statx', prefix : '#define _GNU_SOURCE\n#include <sys/stat.h>')) # Struct member checks private_cfg.set('HAVE_STRUCT_STAT_ST_ATIM', @@ -125,6 +154,42 @@ private_cfg.set('HAVE_STRUCT_STAT_ST_ATIMESPEC', prefix: include_default + '#include <sys/stat.h>', args: args_default)) +private_cfg.set('USDT_ENABLED', get_option('enable-usdt')) + +# Check for liburing with SQE128 support +code = ''' +#include <liburing.h> +#include <stdio.h> +int main(void) { + struct io_uring ring; + int ret = io_uring_queue_init(1, &ring, 0); +#ifndef IORING_SETUP_SQE128 +#error "No SQE128 support" +#endif + return ret; +}''' + +liburing = dependency('liburing', required: false) +libnuma = dependency('numa', required: false) + +if get_option('enable-io-uring') and liburing.found() and libnuma.found() + if cc.links(code, + name: 'liburing linking and SQE128 support', + dependencies: [liburing]) + private_cfg.set('HAVE_URING', true) + endif +endif + +# A custom io peer might not be a kernel and can forge requests libfuse parses +# without bounds checks. Off unless a build asks for it. +private_cfg.set('HAVE_CUSTOM_IO', get_option('enable-custom-io')) +if get_option('enable-custom-io') + warning('enable-custom-io lets a peer that might not be a kernel send ' + + 'requests that libfuse parses without bounds checks, so a ' + + 'forged request can crash or corrupt the filesystem process - ' + + 'see doc/README.custom-io.') +endif + # # Compiler configuration # diff --git a/meson_options.txt b/meson_options.txt index fa4749c72..969686eac 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -22,3 +22,11 @@ option('tests', type : 'boolean', value : true, option('disable-libc-symbol-version', type : 'boolean', value : false, description: 'Disable versioned symbols through libc') +option('enable-usdt', type : 'boolean', value : false, + description: 'Enable user statically defined tracepoints for extra observability') + +option('enable-io-uring', type: 'boolean', value: true, + description: 'Enable fuse-over-io-uring support') + +option('enable-custom-io', type: 'boolean', value: false, + description: 'Enable fuse_session_custom_io(), which serves the protocol over a caller-provided fd instead of /dev/fuse. The peer might not be a kernel and can forge requests that libfuse parses without bounds checks, crashing or corrupting the filesystem process - see doc/README.custom-io') diff --git a/requirements.txt b/requirements.txt index 2c6a4a0c6..1471ce585 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,5 +3,4 @@ # Build packages: meson ninja -# Test packages: -pytest +# Test packages: the suite itself is standard library only. diff --git a/signify/fuse-3.19.pub b/signify/fuse-3.19.pub new file mode 100644 index 000000000..317f8f78f --- /dev/null +++ b/signify/fuse-3.19.pub @@ -0,0 +1,2 @@ +untrusted comment: signify public key +RWR4cEcMGJhD3Dnd3NOeJck3WiuVt9A7mrkq+nQYwrwwmMdDDAan/YiU diff --git a/test/cases/README.md b/test/cases/README.md new file mode 100644 index 000000000..3c0d0206b --- /dev/null +++ b/test/cases/README.md @@ -0,0 +1,421 @@ +# Writing a libfuse test + +## The runner + +`test/run-tests.py` is the suite. A plain Python program with no test framework +under it — run it directly; `meson test` only wraps it. Per run it + +- discovers every executable `cases/**/*.sh`, then narrows by `-t`, `-g`, `-X` + and `test/exclude`; +- runs `printcap` once, so every script sees the same `$FUSE_CAPS`; +- gives each test a private work directory and its own cgroup; +- launches the script as `bash -e`, cwd its own log directory, and enforces + `# TIMEOUT:` as a wall-clock bound; +- kills that cgroup afterwards, pass or fail, so a leaked daemon cannot wedge + the next test; +- turns the exit status plus what it collected — cores, suspicious log lines — + into PASS/SKIP/FAIL, prints the counts and the slowest five, and exits + non-zero if anything failed. + +`-j` tests run at once, so nothing may depend on a global name. + +One run, end to end — `->` calls or execs, `=>` spawns and waits, `..>` writes a +file, `[ ]` a process of its own: + +```text +meson test -C build (two entries: fuse-tests, fuse-tests-io-uring) + -> test/run-tests.py --build-dir <build> + -> discover() cases/**/*.sh + cases/unit/<binary> markers + -> TestRunner.run_all() + -> _run_batch(serial group, jobs=1) + -> _run_batch(rest, jobs=N) + -> run_one(spec) + -> prepare() mnt/ src/ tmp/ logs/ + env contract + -> write_repro() ..> logs/repro.sh + => [ /bin/sh -c LAUNCHER ] + ..> <leaf>/cgroup.procs (self-move) + -> ulimit -S -c "$(ulimit -H -c)" + -> exec bash -e cases/<name>.sh + -> . cases/lib/common.sh + -> fuse_mount_at + => [ fs daemon ] ..> logs/fs-N-*.out + -> fuse_wait_mount + -> _fuse_assert_uring + -> _check <op> -> python3 cases/lib/checks.py + -> fuse_umount -> fusermount3 -u + -> _stream() ..> logs/script.out, logs/timestamps.out + -- timeout? -> dump_stacks() ..> ps.txt kstack.N.txt gdbstack.N.txt + -> _kill() ..> <leaf>/cgroup.kill + -> collect_cores() -> gdb ..> backtrace.<exe>.<pid>.txt + -> scan_logs() suspicious words, minus logs/.allow + -> classify() PASS | FAIL | SKIP(77) + -> report() / discard_workdir() + -> summarise() +``` + +## What a test is + +An executable `*.sh` anywhere under `test/cases/`. Its **name** is the path +below `cases/` without the extension, so `cases/examples/hello-direct.sh` is +`examples/hello-direct`. + +| exit code | meaning | +|---------------|------------------------------------| +| 0 | PASS | +| 77 | SKIP — the script called `_notrun` | +| anything else | FAIL | + +Nothing registers a test anywhere. Dropping the file in and making it +executable is the whole step; `test/run-tests.py` discovers it. + +`test/cases/unit/` is the exception to all of the above. A C test that needs +neither a mount nor a gate has no script there — only a file named after the +binary, which the runner execs from `$FUSE_TEST_BIN_DIR` with no shell in +between. It carries no exec bit, and its content is a header block or nothing +at all. + +## The skeleton + +```sh +#!/usr/bin/env bash +# GROUP: examples quick + +. "$TEST_LIB/common.sh" + +_require_cap FUSE_CAP_WRITEBACK_CACHE # skip (77) if unsupported + +fuse_mount passthrough_ll -o writeback,source="$TEST_SRC" +_check fuse_test_open_write "$TEST_SRC" "$TEST_MNT" +fuse_umount +``` + +`#!/usr/bin/env bash`, not `#!/bin/bash`: FreeBSD has no bash in base, and +`common.sh` needs bash for the `FUSE_FS_*` arrays and `$SECONDS`. + +A case that sources a body from `lib/` gets `common.sh` with it and does not +name it itself. Source it directly only when there is no body, or when the case +gates before one — a gate has to run before the body, which is the whole test. + +## The environment you get + +The runner exports all of this into every script: + +```sh +TEST_NAME=examples/hello-ll-direct # discovery name +TEST_SCRIPT=/…/test/cases/examples/hello-ll-direct.sh +TEST_DIR=/…/test # source tree, never the build dir +TEST_LIB=/…/test/cases/lib # what tests source from +TEST_WORKDIR=/…/run/examples/hello-ll-direct +TEST_MNT=$TEST_WORKDIR/mnt +TEST_SRC=$TEST_WORKDIR/src +TEST_TMP=$TEST_WORKDIR/tmp +TEST_LOGDIR=$TEST_WORKDIR/logs # also the cwd of your script +BUILD_DIR=/…/build # meson build root +FUSE_EXAMPLE_DIR=$BUILD_DIR/example +FUSE_TEST_BIN_DIR=$BUILD_DIR/test +FUSE_UTIL_DIR=$BUILD_DIR/util +FUSE_CAPS="FUSE_CAP_ASYNC_READ FUSE_CAP_POSIX_LOCKS …" # printcap, run once +FUSE_UID=1000 # effective uid +FUSE_OS=Linux # platform.system() +PATH=$FUSE_UTIL_DIR:$FUSE_EXAMPLE_DIR:$PATH +``` + +`$TEST_MNT`, `$TEST_SRC` and `$TEST_TMP` already exist and are yours alone, so +a test never invents a temporary directory or worries about colliding with a +concurrent test. `$FUSE_OS` is exported so no script shells out to `uname -s`. + +## Header directives + +Only two exist. Anything else in the leading comment block is an ordinary +comment. + +```sh +# TIMEOUT: 180 # seconds; the default is 60 +# GROUP: examples serial +``` + +`# GROUP:` names are used by `run-tests.py -g`. + +## Errexit is on + +The runner runs your script as `bash -e`, so any command that fails and is not +checked ends the test there. Nothing to add to the script. What it means when +writing one is that a command you *expect* to fail must say so — `cmd || true`, +or an `if`, or `_assert_errno`. + +The same applies to a test you write as a bare condition: `[ -n "$x" ] && cmd` +returns non-zero when `$x` is empty and ends the test. Write +`[ -z "$x" ] || cmd` instead. + +Run a test by hand the same way (`bash -e cases/…`, or the `repro.sh` the +runner writes) or it will behave differently from CI. + +## Skipping + +> Gate on a feature, never on a version. `_require_cap FUSE_CAP_X` asks the +> running kernel what it supports; `_require_binary`, `_require_root` and +> `_require_prog` ask about the environment. There is deliberately no +> `_require_kernel_version`: a version comparison is wrong the moment the fix +> is backported. If a feature has no capability flag, grep the filesystem's own +> log for whatever it prints when the kernel refuses +> (`_require_fs_marker_absent`), or let the test fail on old kernels and have +> those targets exclude it by name via `test/exclude` or `-X`. +> +> A *platform* is the one exception, because it is not a version: +> `/proc/self/mountinfo` will not be backported to FreeBSD. `_require_linux +> <what>` states which Linux-only facility the test needs. Put it in the shared +> body when a whole family is affected, not in each caller. + +```sh +_fail <msg...> # exit 1 with a message +_notrun <reason...> # exit 77; the runner records the reason +_require_root # / _require_nonroot +_is_linux # true/false, for a conditional section +_require_linux <what> # skip off Linux, naming the facility +_require_cap FUSE_CAP_NAME # skip unless printcap reported it +_require_binary example/null # skip unless $BUILD_DIR/<path> is executable +_require_prog losetup # skip unless it is on $PATH +_require_fuse_device # the kernel module, without the setuid check +_require_not_32bit_on_64 <path> +_require_reachable_without_caps <fs-name> +_require_fs_marker_absent <regex> [fs-index] +``` + +Sourcing `common.sh` also gates the case on FUSE being usable at all — the +module loaded, and a setuid `fusermount3` when you are not root — because +every case that mounts needs both. A case that never mounts says so *before* +sourcing, and runs anyway: + +```sh +_fuse_no_mount_needed=1 +. "$TEST_LIB/common.sh" +``` + +## Mounting + +```sh +fuse_mount <fs-name> [args...] # $TEST_MNT, foreground (-f) +fuse_mount_at <mnt> <fs-name> [args...] # explicit mountpoint, no -f +fuse_mount_helper <fs-name> [args...] # via mount.fuse3 +fuse_umount [idx] # and require the daemon to exit 0 +fuse_umount_lazy [idx] # the give-up path; the trap uses it +fuse_wait_mount <path> [predicate] +fuse_fs_pid [idx] / fuse_fs_log [idx] +``` + +A filesystem name is resolved under `$FUSE_EXAMPLE_DIR`, or under +`$FUSE_TEST_BIN_DIR` when it is prefixed `test/` (so `test/hello` is the +`hello` built in `test/`). Each mount gets an index, and every one of them is +unmounted and killed by an EXIT trap, so an aborting test never leaks a mount. +Register your own cleanup with `_at_exit <command>` rather than `trap`, or you +will replace that trap. + +### Daemons run in the foreground + +`fuse_mount` passes `-f`, and a case that launches a filesystem itself is +expected to do the same. The runner owns the process: the daemon has to stay +in the test's cgroup for the timeout to reach it, and its stderr has to stay +attached to `logs/fs-N-*.out`. + +That leaves the backgrounded path — `fuse_daemonize_early_start()`, the parent +waiting for the mount, `stdout`/`stderr` closed once FUSE_INIT is answered — +covered only by the `mount.fuse3` cases, which is where synchronous FUSE_INIT +is reached as well. Covering it properly needs foreground and background +variants of the existing cases; that has not been done. + +## Assertions + +> Shell owns mounting, sequencing and skipping. It does not own checks. +> `_assert_eq`, `_assert_ne`, `_assert_file_eq` and `_wait_for` exist because +> shell expresses them exactly. Everything else is a `checks.py` subcommand — +> and if what you need is not there, **add a subcommand rather than +> approximating it in shell**. An `errno` compared against `strerror` text, a +> timestamp rounded by `touch -d`, an option set split by `awk`: each is a +> weaker test than the Python one line of `checks.py` would have been. + +A check is a subcommand of `test/cases/lib/checks.py`, not a command and not a +shell function. `_check` runs it: + +```sh +_check fuse_test_open_write "$TEST_SRC" "$TEST_MNT" +# runs: python3 test/cases/lib/checks.py fuse_test_open_write <src> <mnt> +``` + +Every name is prefixed `fuse_test_` so a reader never has to wonder whether +`unlink` in a test script means coreutils or a check. The list below is all +of them; if what you need is missing, add it to `checks.py`. + +```sh +# file and directory operations +fuse_test_unlink <mnt> [--src <dir>] # create, unlink, require ENOENT +fuse_test_mkdir <mnt> # directory create +fuse_test_rmdir <mnt> [--src <dir>] # directory remove +fuse_test_symlink <mnt> # symlink and read it back +fuse_test_create <mnt> # O_CREAT, then mode/nlink/size +fuse_test_link <mnt> # hard link, nlink, unlink +fuse_test_chown <mnt> # chown uid then gid separately +fuse_test_utimens <mnt> [--ns-tol N] # nanosecond timestamps +fuse_test_xattr <path> # set/get/remove +fuse_test_statvfs <mnt> # statvfs succeeds + +# reading and writing +fuse_test_open_read <src> <mnt> # write via src, read through the mount +fuse_test_open_write <src> <mnt> # create via src, write through the mount +fuse_test_append <src> <mnt> # O_APPEND writes +fuse_test_seek <src> <mnt> # lseek writes +fuse_test_open_unlink <mnt> # write to an unlinked fd +fuse_test_truncate_path <mnt> # grow and shrink by path +fuse_test_truncate_fd <mnt> # grow and shrink by fd + +# listing and comparison +fuse_test_listdir <dir> # print the entries +fuse_test_listdir_first <dir> # print the first entry +fuse_test_assert_listdir <dir> <name>... # exactly these entries +fuse_test_readdir <src> <mnt> [--inode-check M] # listing and inodes +fuse_test_readdir_big <src> <mnt> [--inode-check M] # 500 entries +fuse_test_passthrough <src> <mnt> [--inode-check M] # both sides agree + +# predicates and errno +fuse_test_size <path> # print st_size +fuse_test_ismount <path> # is it a mountpoint +fuse_test_isbigger <path> <size> # is it bigger than that +fuse_test_expect_errno <ERRNO> <op> [args...] # require exactly that errno + +# mount state +fuse_test_mountinfo <mnt> <field> # one /proc/self/mountinfo field +fuse_test_assert_mount_opt <mnt> <opt>... # present in mount_options +fuse_test_refute_mount_opt <mnt> <opt>... # absent from mount_options +fuse_test_assert_super_opt <mnt> <opt>... # present in super_options +fuse_test_assert_super_opt_prefix <mnt> <p>... # e.g. user_id= +fuse_test_assert_fstype <mnt> <fstype>... # any one is acceptable +fuse_test_assert_source <mnt> <source>... # any one is acceptable +fuse_test_fusermount_rejects <mnt> <reason> # fusermount3 refuses to mount it + +# the remainder +fuse_test_printcap_caps <src-root> # every FUSE_CAP_* is in the header +fuse_test_reachable_without_caps <path> # mode-bit walk up the tree +fuse_test_elf_class <path> # 32 or 64, from e_ident +fuse_test_null_roundtrip <path> # read zeros, then write +fuse_test_cuse_roundtrip <devpath> <client> # read/write/offset exchange +fuse_test_uds_init <sockpath> # a FUSE INIT handshake +``` + +`--inode-check M` is `exact` (the default) or `nonzero`. `<op>` for +`fuse_test_expect_errno` is one of `fuse_test_open_rw`, `fuse_test_open_ro` +or `fuse_test_stat`. `<reason>` for `fuse_test_fusermount_rejects` is +`not_dir_or_regular`, `no_write_access` or `no_such_file`. + +`common.sh` wraps the common ones in shell functions, which keep the +underscore-prefixed naming shell uses elsewhere: `_assert_errno`, +`_assert_listdir`, `_assert_xattr_roundtrip`, `mountinfo_field`, +`_assert_mount_opt`, `_refute_mount_opt`, `_assert_super_opt`, +`_assert_super_opt_prefix`, `_assert_fstype`, `_assert_source`. + +## Adding a variant of an existing test + +A parametrized case becomes a real file, so it gets its own duration, log +directory and name in the summary. The variant sets a few variables and sources +a shared body from `lib/`: + +```sh +#!/usr/bin/env bash +# cases/examples/hello-ll-mount-fuse-clone_fd.sh +# GROUP: examples quick + +FS_NAME=hello_ll # which example to mount +FS_OPTS=clone_fd # -o option list, empty for none +LAUNCH=mount_fuse # direct | mount_fuse | mount_fuse_dropcaps + +. "$TEST_LIB/hello.sh" +``` + +The shared body carries the gates for the whole family — one +`_require_linux "the clone_fd mount option"` in `lib/hello.sh` covers every +`clone_fd` variant — so a new caller inherits them. + +## Suspicious output fails a test + +Anything a daemon writes matching `error`, `warning`, `fatal`, `crash`, +`abort`, `exception`, `traceback`, `fault` or `uninitialised` fails the test +even when it exited 0. If that is a false positive for your test, exempt the +exact line: + +```sh +fuse_allow_output '^ \d\d \[[^]]+ message: .No error: 0.\]' +``` + +A core dump fails the test the same way. A test that deliberately crashes +something opts out per executable with `fuse_allow_core <executable-name>`. + +## When a test fails + +``` +$TEST_WORKDIR/logs/script.out what the test printed +$TEST_WORKDIR/logs/fs-0-*.out what the filesystem daemon printed +$TEST_WORKDIR/logs/fs-0-*.status its exit status +$TEST_WORKDIR/logs/core.<exe>.<pid> core, one per crashed process +$TEST_WORKDIR/logs/backtrace.<exe>.<pid>.txt its gdb backtrace +$TEST_WORKDIR/logs/repro.sh re-run this one test, same env, by hand +``` + +On a CI failure this whole tree is downloadable from the job's `test-logs` +artifact, minus the raw cores — the backtraces the runner already resolved are +what is worth keeping. + +A test's directory is removed as soon as it passes or skips, and the run +directory itself when nothing failed at all — so what is left afterwards is +exactly what is worth looking at (and uploading). `--keep-logs` keeps +everything anyway. + +## Running them + +`--build-dir` is the only option a run normally needs; it defaults to `.`. + +```sh +./test/run-tests.py --build-dir build # everything +./test/run-tests.py -t 'examples/hello-*' # fnmatch on the name, repeatable +./test/run-tests.py -g quick # only this GROUP, repeatable +./test/run-tests.py -X misc/hello-uds # skip one, repeatable +./test/run-tests.py -l # list the selection, run nothing +./test/run-tests.py -j1 -v # serially, output live +./test/run-tests.py -j8 # default is cpu_count // 2 +./test/run-tests.py --repeat 20 -t misc/x # chase a flake +./test/run-tests.py --timeout 300 # override every # TIMEOUT: +./test/run-tests.py --keep-logs # keep passing tests' output too +``` + +`-t` and `-g` narrow; `-X` and `test/exclude` remove from what is left. + +A `-t` pattern may also be the path to a case, so it can be tab-completed: +`test/cases/examples/cuse.sh` and `examples/cuse` name the same test, from any +directory. A directory selects everything under it, by path or by bare name — +`-t examples` and `-t test/cases/examples` are the same selection. A pattern +that matches no test is an error rather than an empty run. + +Where the output lands: + +```sh +--run-dir DIR # this directory, verbatim; so does $FUSE_TEST_RUN_DIR +--print-base-dir # print where a run would land, and exit +--persist-base-dir DIR # remember DIR in ~/.config/libfuse/tests.conf +``` + +Without `--run-dir`, a run gets a `run-<user>-<timestamp>-<pid>` leaf under the +base, and the leaf is deleted when nothing failed. + +Unprivileged, without a setuid `fusermount3`, everything that mounts skips +itself. `--setuid-helpers` makes the build tree's helpers setuid root (with +`sudo`) before the run; it is a no-op once they are. + +`test/exclude` holds one test name per line for a host that cannot support a +test; `-X <name>` adds to it for one run. + +`TEST_WITH_VALGRIND=1` traces every daemon and scales the timeouts to match. + +## What breaks under -j + +A test that binds a fixed path, names a device node globally, or takes the +first free loop device cannot run concurrently with a copy of itself and must +declare `# GROUP: serial`. The three existing cases are `misc/hello-uds` +(`hello_ll_uds` hard-codes a socket path in /tmp), `examples/cuse` (it names a +device node globally) and `mount/blkdev-fsname-*` (`losetup -f` races). diff --git a/test/cases/ctests/api-30.sh b/test/cases/ctests/api-30.sh new file mode 100755 index 000000000..dae29c7d5 --- /dev/null +++ b/test/cases/ctests/api-30.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# GROUP: ctests +# Serve a filesystem built against the FUSE_USE_VERSION 30 API. + +. "$TEST_LIB/common.sh" + +"$FUSE_TEST_BIN_DIR/test_api_30" "$TEST_MNT" diff --git a/test/cases/ctests/signals.sh b/test/cases/ctests/signals.sh new file mode 100755 index 000000000..d2cd1dd1f --- /dev/null +++ b/test/cases/ctests/signals.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# GROUP: ctests +# Signal handling, issue #1182. + +. "$TEST_LIB/common.sh" + +"$FUSE_TEST_BIN_DIR/test_signals" diff --git a/test/cases/ctests/write-cache-writeback.sh b/test/cases/ctests/write-cache-writeback.sh new file mode 100755 index 000000000..ba3b3b810 --- /dev/null +++ b/test/cases/ctests/write-cache-writeback.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# GROUP: ctests + +. "$TEST_LIB/common.sh" + +_require_cap FUSE_CAP_WRITEBACK_CACHE + +"$FUSE_TEST_BIN_DIR/test_write_cache" "$TEST_MNT" -owriteback_cache diff --git a/test/cases/ctests/write-cache.sh b/test/cases/ctests/write-cache.sh new file mode 100755 index 000000000..20efa8edf --- /dev/null +++ b/test/cases/ctests/write-cache.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# GROUP: ctests + +. "$TEST_LIB/common.sh" + +_require_cap FUSE_CAP_WRITEBACK_CACHE + +# --delay_ms tests that close(rofd) does not block waiting for pending writes, +# which needs a390ccb316be ("fuse: add FOPEN_NOFLUSH"). The suite assumes a +# current kernel; an older target excludes this test by name. +"$FUSE_TEST_BIN_DIR/test_write_cache" "$TEST_MNT" --delay_ms=200 diff --git a/test/cases/examples/cuse.sh b/test/cases/examples/cuse.sh new file mode 100755 index 000000000..0f5e46265 --- /dev/null +++ b/test/cases/examples/cuse.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# GROUP: examples serial + +. "$TEST_LIB/common.sh" + +# example/cuse is built on FreeBSD too, so an unbuilt binary is not a proxy +# for "cannot run": CUSE is a Linux FUSE protocol feature. +_require_linux CUSE +_require_root +_require_binary example/cuse +_require_not_32bit_on_64 "$FUSE_EXAMPLE_DIR/cuse" + +# The device node is named globally, hence GROUP serial. +devname=cuse-test-$$ +devpath=/dev/$devname + +# cuse takes no mountpoint: it names its device with --name, and the test +# ends by terminating it rather than unmounting anything. +log=$TEST_LOGDIR/fs-0-cuse.out +"$FUSE_EXAMPLE_DIR/cuse" -f --name="$devname" >"$log" 2>&1 & +cuse_pid=$! +_at_exit "kill $cuse_pid 2>/dev/null" + +_wait_for 30 "[ -e '$devpath' ]" || { + cat "$log" >&2 + _fail "cuse did not create $devpath" +} + +_check fuse_test_cuse_roundtrip "$devpath" "$FUSE_EXAMPLE_DIR/cuse_client" diff --git a/test/cases/examples/dev-auto-unmount-nonroot.sh b/test/cases/examples/dev-auto-unmount-nonroot.sh new file mode 100755 index 000000000..159376466 --- /dev/null +++ b/test/cases/examples/dev-auto-unmount-nonroot.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# GROUP: examples + +. "$TEST_LIB/common.sh" + +# Root can mount with dev and auto_unmount; a non-root user cannot use the +# device nodes it exposes. Split in two so the summary says which half ran. +_require_nonroot +_require_binary example/passthrough_ll + +fuse_mount passthrough_ll -o source=/dev,dev,auto_unmount >/dev/null + +_assert_errno EACCES fuse_test_open_ro "$TEST_MNT/null" + +fuse_umount diff --git a/test/cases/examples/dev-auto-unmount-root.sh b/test/cases/examples/dev-auto-unmount-root.sh new file mode 100755 index 000000000..fcf29dc99 --- /dev/null +++ b/test/cases/examples/dev-auto-unmount-root.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# GROUP: examples + +. "$TEST_LIB/common.sh" + +# Root can mount with dev and auto_unmount; a non-root user cannot use the +# device nodes it exposes. Split in two so the summary says which half ran. +_require_root +_require_binary example/passthrough_ll + +fuse_mount passthrough_ll -o source=/dev,dev,auto_unmount >/dev/null + +cat "$TEST_MNT/null" >/dev/null + +fuse_umount diff --git a/test/cases/examples/hello-direct-clone_fd.sh b/test/cases/examples/hello-direct-clone_fd.sh new file mode 100755 index 000000000..ae6da37ab --- /dev/null +++ b/test/cases/examples/hello-direct-clone_fd.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# GROUP: examples quick + +FS_NAME=hello +FS_OPTS=clone_fd +LAUNCH=direct + +. "$TEST_LIB/hello.sh" diff --git a/test/cases/examples/hello-direct.sh b/test/cases/examples/hello-direct.sh new file mode 100755 index 000000000..9b36214bd --- /dev/null +++ b/test/cases/examples/hello-direct.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# GROUP: examples quick + +FS_NAME=hello +FS_OPTS= +LAUNCH=direct + +. "$TEST_LIB/hello.sh" diff --git a/test/cases/examples/hello-dropcaps-clone_fd.sh b/test/cases/examples/hello-dropcaps-clone_fd.sh new file mode 100755 index 000000000..f7919628b --- /dev/null +++ b/test/cases/examples/hello-dropcaps-clone_fd.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# GROUP: examples quick + +FS_NAME=hello +FS_OPTS=clone_fd +LAUNCH=mount_fuse_dropcaps + +. "$TEST_LIB/hello.sh" diff --git a/test/cases/examples/hello-dropcaps.sh b/test/cases/examples/hello-dropcaps.sh new file mode 100755 index 000000000..72dec471b --- /dev/null +++ b/test/cases/examples/hello-dropcaps.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# GROUP: examples quick + +FS_NAME=hello +FS_OPTS= +LAUNCH=mount_fuse_dropcaps + +. "$TEST_LIB/hello.sh" diff --git a/test/cases/examples/hello-ll-direct-clone_fd.sh b/test/cases/examples/hello-ll-direct-clone_fd.sh new file mode 100755 index 000000000..dadf5f64a --- /dev/null +++ b/test/cases/examples/hello-ll-direct-clone_fd.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# GROUP: examples quick + +FS_NAME=hello_ll +FS_OPTS=clone_fd +LAUNCH=direct + +. "$TEST_LIB/hello.sh" diff --git a/test/cases/examples/hello-ll-direct.sh b/test/cases/examples/hello-ll-direct.sh new file mode 100755 index 000000000..9e1b7f7f5 --- /dev/null +++ b/test/cases/examples/hello-ll-direct.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# GROUP: examples quick + +FS_NAME=hello_ll +FS_OPTS= +LAUNCH=direct + +. "$TEST_LIB/hello.sh" diff --git a/test/cases/examples/hello-ll-dropcaps-clone_fd.sh b/test/cases/examples/hello-ll-dropcaps-clone_fd.sh new file mode 100755 index 000000000..37f93e18d --- /dev/null +++ b/test/cases/examples/hello-ll-dropcaps-clone_fd.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# GROUP: examples quick + +FS_NAME=hello_ll +FS_OPTS=clone_fd +LAUNCH=mount_fuse_dropcaps + +. "$TEST_LIB/hello.sh" diff --git a/test/cases/examples/hello-ll-dropcaps.sh b/test/cases/examples/hello-ll-dropcaps.sh new file mode 100755 index 000000000..c03529fc2 --- /dev/null +++ b/test/cases/examples/hello-ll-dropcaps.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# GROUP: examples quick + +FS_NAME=hello_ll +FS_OPTS= +LAUNCH=mount_fuse_dropcaps + +. "$TEST_LIB/hello.sh" diff --git a/test/cases/examples/hello-ll-mount-fuse-clone_fd.sh b/test/cases/examples/hello-ll-mount-fuse-clone_fd.sh new file mode 100755 index 000000000..381ea2491 --- /dev/null +++ b/test/cases/examples/hello-ll-mount-fuse-clone_fd.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# GROUP: examples quick + +FS_NAME=hello_ll +FS_OPTS=clone_fd +LAUNCH=mount_fuse + +. "$TEST_LIB/hello.sh" diff --git a/test/cases/examples/hello-ll-mount-fuse.sh b/test/cases/examples/hello-ll-mount-fuse.sh new file mode 100755 index 000000000..97feb3829 --- /dev/null +++ b/test/cases/examples/hello-ll-mount-fuse.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# GROUP: examples quick + +FS_NAME=hello_ll +FS_OPTS= +LAUNCH=mount_fuse + +. "$TEST_LIB/hello.sh" diff --git a/test/cases/examples/hello-mount-fuse-clone_fd.sh b/test/cases/examples/hello-mount-fuse-clone_fd.sh new file mode 100755 index 000000000..4e41e57d1 --- /dev/null +++ b/test/cases/examples/hello-mount-fuse-clone_fd.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# GROUP: examples quick + +FS_NAME=hello +FS_OPTS=clone_fd +LAUNCH=mount_fuse + +. "$TEST_LIB/hello.sh" diff --git a/test/cases/examples/hello-mount-fuse.sh b/test/cases/examples/hello-mount-fuse.sh new file mode 100755 index 000000000..b71468d5e --- /dev/null +++ b/test/cases/examples/hello-mount-fuse.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# GROUP: examples quick + +FS_NAME=hello +FS_OPTS= +LAUNCH=mount_fuse + +. "$TEST_LIB/hello.sh" diff --git a/test/cases/examples/ioctl-ll.sh b/test/cases/examples/ioctl-ll.sh new file mode 100755 index 000000000..b2761edfe --- /dev/null +++ b/test/cases/examples/ioctl-ll.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# GROUP: examples + +. "$TEST_LIB/common.sh" + +_require_binary example/ioctl_ll +_require_not_32bit_on_64 "$FUSE_EXAMPLE_DIR/ioctl_ll" + +fuse_mount ioctl_ll >/dev/null + +testfile=$TEST_MNT/fioc +client=$FUSE_EXAMPLE_DIR/ioctl_ll_client + +# Restricted ioctls only. The unrestricted FIOC_READ/FIOC_WRITE work with +# CUSE rather than a regular FUSE mount, and are covered by examples/cuse. +_assert_eq "$("$client" get_size "$testfile")" 0 "initial size" +printf 'foobar' >"$testfile" +_assert_eq "$("$client" get_size "$testfile")" 6 "size after write" + +"$client" set_size "$testfile" 3 +printf 'foo' >"$TEST_TMP/expected" +_assert_file_eq "$testfile" "$TEST_TMP/expected" + +fuse_umount diff --git a/test/cases/examples/ioctl.sh b/test/cases/examples/ioctl.sh new file mode 100755 index 000000000..fd7ab9de5 --- /dev/null +++ b/test/cases/examples/ioctl.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# GROUP: examples + +. "$TEST_LIB/common.sh" + +_require_binary example/ioctl +_require_not_32bit_on_64 "$FUSE_EXAMPLE_DIR/ioctl" + +fuse_mount ioctl >/dev/null + +testfile=$TEST_MNT/fioc +client=$FUSE_EXAMPLE_DIR/ioctl_client + +_assert_eq "$("$client" "$testfile")" 0 "initial size" +printf 'foobar' >"$testfile" +_assert_eq "$("$client" "$testfile")" 6 "size after write" + +# With a size argument the client truncates instead of reporting. +"$client" "$testfile" 3 +printf 'foo' >"$TEST_TMP/expected" +_assert_file_eq "$testfile" "$TEST_TMP/expected" + +fuse_umount diff --git a/test/cases/examples/null.sh b/test/cases/examples/null.sh new file mode 100755 index 000000000..69beac02a --- /dev/null +++ b/test/cases/examples/null.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# GROUP: examples + +. "$TEST_LIB/common.sh" + +_require_binary example/null + +# The null filesystem replaces a regular file rather than a directory, so +# there is no mountpoint to poll for; it announces itself by the size it +# reports growing past what the placeholder holds. +mnt_file=$TEST_TMP/file +printf 'dummy' >"$mnt_file" + +null_is_up() +{ + python3 "$TEST_LIB/checks.py" fuse_test_isbigger "$1" 4000 +} + +FUSE_WAIT_PREDICATE=null_is_up +fuse_mount_at "$mnt_file" null -f >/dev/null + +_check fuse_test_null_roundtrip "$mnt_file" + +fuse_umount diff --git a/test/cases/examples/passthrough-base-debug.sh b/test/cases/examples/passthrough-base-debug.sh new file mode 100755 index 000000000..86bfa628f --- /dev/null +++ b/test/cases/examples/passthrough-base-debug.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# GROUP: examples passthrough +# 500-entry readdir plus test_syscalls over FUSE; the default 60s is +# not enough on a loaded runner, and -d makes it slower again. +# TIMEOUT: 300 + +FS_NAME=passthrough +FS_ARGS="-f -o entry_timeout=0,negative_timeout=0,attr_timeout=0,ac_attr_timeout=0 -d" +INODE_CHECK=exact +PT_MIRROR=1 +PT_SRC_VISIBLE=1 + +. "$TEST_LIB/passthrough.sh" diff --git a/test/cases/examples/passthrough-base.sh b/test/cases/examples/passthrough-base.sh new file mode 100755 index 000000000..38b2d683d --- /dev/null +++ b/test/cases/examples/passthrough-base.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# GROUP: examples passthrough +# 500-entry readdir plus test_syscalls over FUSE; the default 60s is +# not enough on a loaded runner, and -d makes it slower again. +# TIMEOUT: 300 + +FS_NAME=passthrough +FS_ARGS="-f -o entry_timeout=0,negative_timeout=0,attr_timeout=0,ac_attr_timeout=0" +INODE_CHECK=exact +PT_MIRROR=1 +PT_SRC_VISIBLE=1 + +. "$TEST_LIB/passthrough.sh" diff --git a/test/cases/examples/passthrough-fh-debug.sh b/test/cases/examples/passthrough-fh-debug.sh new file mode 100755 index 000000000..4ba7145af --- /dev/null +++ b/test/cases/examples/passthrough-fh-debug.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# GROUP: examples passthrough +# 500-entry readdir plus test_syscalls over FUSE; the default 60s is +# not enough on a loaded runner, and -d makes it slower again. +# TIMEOUT: 300 + +FS_NAME=passthrough_fh +FS_ARGS="-f -o entry_timeout=0,negative_timeout=0,attr_timeout=0,ac_attr_timeout=0 -d" +INODE_CHECK=exact +PT_MIRROR=1 +PT_SRC_VISIBLE=1 + +. "$TEST_LIB/passthrough.sh" diff --git a/test/cases/examples/passthrough-fh.sh b/test/cases/examples/passthrough-fh.sh new file mode 100755 index 000000000..ea664338a --- /dev/null +++ b/test/cases/examples/passthrough-fh.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# GROUP: examples passthrough +# 500-entry readdir plus test_syscalls over FUSE; the default 60s is +# not enough on a loaded runner, and -d makes it slower again. +# TIMEOUT: 300 + +FS_NAME=passthrough_fh +FS_ARGS="-f -o entry_timeout=0,negative_timeout=0,attr_timeout=0,ac_attr_timeout=0" +INODE_CHECK=exact +PT_MIRROR=1 +PT_SRC_VISIBLE=1 + +. "$TEST_LIB/passthrough.sh" diff --git a/test/cases/examples/passthrough-hp-nocache.sh b/test/cases/examples/passthrough-hp-nocache.sh new file mode 100755 index 000000000..48a7dee03 --- /dev/null +++ b/test/cases/examples/passthrough-hp-nocache.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# GROUP: examples passthrough +# 500-entry readdir plus test_syscalls over FUSE; the default 60s is +# not enough on a loaded runner, and -d makes it slower again. +# TIMEOUT: 300 + +FS_NAME=passthrough_hp +FS_ARGS="--foreground --nocache" +PT_POSITIONAL="$TEST_SRC" +INODE_CHECK=exact +PT_MIRROR=0 +# With the cache on the mount is allowed a stale view of the +# source, so nothing may be compared against it. +PT_SRC_VISIBLE=1 +# The unlinked-testfiles check needs "fuse: fix illegal access to +# inode with reused nodeid"; the suite assumes a current kernel. +SYSCALL_ARGS=-u + +. "$TEST_LIB/passthrough.sh" diff --git a/test/cases/examples/passthrough-hp.sh b/test/cases/examples/passthrough-hp.sh new file mode 100755 index 000000000..3375e890d --- /dev/null +++ b/test/cases/examples/passthrough-hp.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# GROUP: examples passthrough +# 500-entry readdir plus test_syscalls over FUSE; the default 60s is +# not enough on a loaded runner, and -d makes it slower again. +# TIMEOUT: 300 + +FS_NAME=passthrough_hp +FS_ARGS="--foreground" +PT_POSITIONAL="$TEST_SRC" +INODE_CHECK=exact +PT_MIRROR=0 +# With the cache on the mount is allowed a stale view of the +# source, so nothing may be compared against it. +PT_SRC_VISIBLE=0 + +. "$TEST_LIB/passthrough.sh" diff --git a/test/cases/examples/passthrough-ll-debug.sh b/test/cases/examples/passthrough-ll-debug.sh new file mode 100755 index 000000000..42259e202 --- /dev/null +++ b/test/cases/examples/passthrough-ll-debug.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# GROUP: examples passthrough +# 500-entry readdir plus test_syscalls over FUSE; the default 60s is +# not enough on a loaded runner, and -d makes it slower again. +# TIMEOUT: 300 + +FS_NAME=passthrough_ll +FS_ARGS="-f -o timeout=0 -d" +INODE_CHECK=exact +PT_MIRROR=1 +PT_SRC_VISIBLE=1 + +. "$TEST_LIB/passthrough.sh" diff --git a/test/cases/examples/passthrough-ll-writeback-debug.sh b/test/cases/examples/passthrough-ll-writeback-debug.sh new file mode 100755 index 000000000..bf1490af1 --- /dev/null +++ b/test/cases/examples/passthrough-ll-writeback-debug.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# GROUP: examples passthrough +# 500-entry readdir plus test_syscalls over FUSE; the default 60s is +# not enough on a loaded runner, and -d makes it slower again. +# TIMEOUT: 300 + +FS_NAME=passthrough_ll +FS_ARGS="-f -o timeout=0 -o writeback -d" +INODE_CHECK=exact +PT_MIRROR=1 +PT_SRC_VISIBLE=1 +# With writeback caching the kernel opens files for reading even +# when userspace asked for O_WRONLY, which fails unless the +# filesystem process has special permission. +SYSCALL_ARGS=-53 + +. "$TEST_LIB/passthrough.sh" diff --git a/test/cases/examples/passthrough-ll-writeback.sh b/test/cases/examples/passthrough-ll-writeback.sh new file mode 100755 index 000000000..d57fecb60 --- /dev/null +++ b/test/cases/examples/passthrough-ll-writeback.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# GROUP: examples passthrough +# 500-entry readdir plus test_syscalls over FUSE; the default 60s is +# not enough on a loaded runner, and -d makes it slower again. +# TIMEOUT: 300 + +FS_NAME=passthrough_ll +FS_ARGS="-f -o timeout=0 -o writeback" +INODE_CHECK=exact +PT_MIRROR=1 +PT_SRC_VISIBLE=1 +# With writeback caching the kernel opens files for reading even +# when userspace asked for O_WRONLY, which fails unless the +# filesystem process has special permission. +SYSCALL_ARGS=-53 + +. "$TEST_LIB/passthrough.sh" diff --git a/test/cases/examples/passthrough-ll.sh b/test/cases/examples/passthrough-ll.sh new file mode 100755 index 000000000..994175ab8 --- /dev/null +++ b/test/cases/examples/passthrough-ll.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# GROUP: examples passthrough +# 500-entry readdir plus test_syscalls over FUSE; the default 60s is +# not enough on a loaded runner, and -d makes it slower again. +# TIMEOUT: 300 + +FS_NAME=passthrough_ll +FS_ARGS="-f -o timeout=0" +INODE_CHECK=exact +PT_MIRROR=1 +PT_SRC_VISIBLE=1 + +. "$TEST_LIB/passthrough.sh" diff --git a/test/cases/examples/passthrough-plus-debug.sh b/test/cases/examples/passthrough-plus-debug.sh new file mode 100755 index 000000000..0e0fe6a6b --- /dev/null +++ b/test/cases/examples/passthrough-plus-debug.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# GROUP: examples passthrough +# 500-entry readdir plus test_syscalls over FUSE; the default 60s is +# not enough on a loaded runner, and -d makes it slower again. +# TIMEOUT: 300 + +FS_NAME=passthrough +FS_ARGS="-f --plus -o entry_timeout=0,negative_timeout=0,attr_timeout=0,ac_attr_timeout=0 -d" +INODE_CHECK=exact +PT_MIRROR=1 +PT_SRC_VISIBLE=1 + +. "$TEST_LIB/passthrough.sh" diff --git a/test/cases/examples/passthrough-plus.sh b/test/cases/examples/passthrough-plus.sh new file mode 100755 index 000000000..3ab2aad3c --- /dev/null +++ b/test/cases/examples/passthrough-plus.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# GROUP: examples passthrough +# 500-entry readdir plus test_syscalls over FUSE; the default 60s is +# not enough on a loaded runner, and -d makes it slower again. +# TIMEOUT: 300 + +FS_NAME=passthrough +FS_ARGS="-f --plus -o entry_timeout=0,negative_timeout=0,attr_timeout=0,ac_attr_timeout=0" +INODE_CHECK=exact +PT_MIRROR=1 +PT_SRC_VISIBLE=1 + +. "$TEST_LIB/passthrough.sh" diff --git a/test/cases/examples/passthrough-zero-ino-debug.sh b/test/cases/examples/passthrough-zero-ino-debug.sh new file mode 100755 index 000000000..7cd628729 --- /dev/null +++ b/test/cases/examples/passthrough-zero-ino-debug.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# GROUP: examples passthrough +# 500-entry readdir plus test_syscalls over FUSE; the default 60s is +# not enough on a loaded runner, and -d makes it slower again. +# TIMEOUT: 300 + +FS_NAME=passthrough +FS_ARGS="-f --plus --readdir-zero-inodes -o entry_timeout=0,negative_timeout=0," +FS_ARGS+="attr_timeout=0,ac_attr_timeout=0 -d" +INODE_CHECK=nonzero +PT_MIRROR=1 +PT_SRC_VISIBLE=1 + +. "$TEST_LIB/passthrough.sh" diff --git a/test/cases/examples/passthrough-zero-ino.sh b/test/cases/examples/passthrough-zero-ino.sh new file mode 100755 index 000000000..f8358e49a --- /dev/null +++ b/test/cases/examples/passthrough-zero-ino.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# GROUP: examples passthrough +# 500-entry readdir plus test_syscalls over FUSE; the default 60s is +# not enough on a loaded runner, and -d makes it slower again. +# TIMEOUT: 300 + +FS_NAME=passthrough +FS_ARGS="-f --plus --readdir-zero-inodes -o entry_timeout=0,negative_timeout=0," +FS_ARGS+="attr_timeout=0,ac_attr_timeout=0" +INODE_CHECK=nonzero +PT_MIRROR=1 +PT_SRC_VISIBLE=1 + +. "$TEST_LIB/passthrough.sh" diff --git a/test/cases/examples/poll.sh b/test/cases/examples/poll.sh new file mode 100755 index 000000000..de517e7c7 --- /dev/null +++ b/test/cases/examples/poll.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# GROUP: examples + +. "$TEST_LIB/common.sh" + +fuse_mount poll >/dev/null + +# poll_client looks at files in its own working directory. +(cd "$TEST_MNT" && "$FUSE_EXAMPLE_DIR/poll_client") + +fuse_umount diff --git a/test/cases/examples/release-unlink-race.sh b/test/cases/examples/release-unlink-race.sh new file mode 100755 index 000000000..bfdfc12ce --- /dev/null +++ b/test/cases/examples/release-unlink-race.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# GROUP: examples +# Issue #746: RELEASE and UNLINK sent back to back must not race, which is +# only triggered when nullpath_ok is set. + +. "$TEST_LIB/common.sh" + +fuse_mount test/release_unlink_race >/dev/null + +# The filesystem mirrors the real tree, so the mount's view of a directory +# is that directory prefixed with the mountpoint. +race_dir=$TEST_TMP/race +mkdir "$race_dir" + +racefile=$TEST_MNT$race_dir/racefile +: >"$racefile" +rm "$racefile" + +# Slow CI pipelines need this for the unlink to finish processing. +sleep 3 + +_assert_listdir "$race_dir" + +fuse_umount diff --git a/test/cases/examples/test-hello-direct-clone_fd.sh b/test/cases/examples/test-hello-direct-clone_fd.sh new file mode 100755 index 000000000..c419195e9 --- /dev/null +++ b/test/cases/examples/test-hello-direct-clone_fd.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# GROUP: examples quick + +FS_NAME=test/hello +FS_OPTS=clone_fd +LAUNCH=direct + +. "$TEST_LIB/hello.sh" diff --git a/test/cases/examples/test-hello-direct.sh b/test/cases/examples/test-hello-direct.sh new file mode 100755 index 000000000..a588e1304 --- /dev/null +++ b/test/cases/examples/test-hello-direct.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# GROUP: examples quick + +FS_NAME=test/hello +FS_OPTS= +LAUNCH=direct + +. "$TEST_LIB/hello.sh" diff --git a/test/cases/examples/test-hello-dropcaps-clone_fd.sh b/test/cases/examples/test-hello-dropcaps-clone_fd.sh new file mode 100755 index 000000000..40647d071 --- /dev/null +++ b/test/cases/examples/test-hello-dropcaps-clone_fd.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# GROUP: examples quick + +FS_NAME=test/hello +FS_OPTS=clone_fd +LAUNCH=mount_fuse_dropcaps + +. "$TEST_LIB/hello.sh" diff --git a/test/cases/examples/test-hello-dropcaps.sh b/test/cases/examples/test-hello-dropcaps.sh new file mode 100755 index 000000000..da9218034 --- /dev/null +++ b/test/cases/examples/test-hello-dropcaps.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# GROUP: examples quick + +FS_NAME=test/hello +FS_OPTS= +LAUNCH=mount_fuse_dropcaps + +. "$TEST_LIB/hello.sh" diff --git a/test/cases/examples/test-hello-mount-fuse-clone_fd.sh b/test/cases/examples/test-hello-mount-fuse-clone_fd.sh new file mode 100755 index 000000000..c8e80eb22 --- /dev/null +++ b/test/cases/examples/test-hello-mount-fuse-clone_fd.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# GROUP: examples quick + +FS_NAME=test/hello +FS_OPTS=clone_fd +LAUNCH=mount_fuse + +. "$TEST_LIB/hello.sh" diff --git a/test/cases/examples/test-hello-mount-fuse.sh b/test/cases/examples/test-hello-mount-fuse.sh new file mode 100755 index 000000000..f02d16627 --- /dev/null +++ b/test/cases/examples/test-hello-mount-fuse.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# GROUP: examples quick + +FS_NAME=test/hello +FS_OPTS= +LAUNCH=mount_fuse + +. "$TEST_LIB/hello.sh" diff --git a/test/cases/lib/checks.py b/test/cases/lib/checks.py new file mode 100755 index 000000000..53c8bc56c --- /dev/null +++ b/test/cases/lib/checks.py @@ -0,0 +1,1016 @@ +#!/usr/bin/env python3 +"""Everything the shell tests check that shell cannot express exactly. + +Shell is the dispatch unit, not the implementation language: a test calls this +module like any other command and it exits 0 on success, non-zero with a +diagnostic on stderr on failure. One subcommand per check, each named +fuse_test_* so a test script cannot mistake it for a system command. + +Re-expressing these in coreutils would silently weaken them -- an errno becomes +a strerror string match, an ftruncate on a held fd has no shell equivalent, a +nanosecond utimensat becomes whatever `touch -d` rounds to, and a +/proc/self/mountinfo parser becomes an awk script that gets the separator field +or the \\040 escapes subtly wrong. +""" + +import argparse +import errno +import filecmp +import os +import re +import shutil +import stat +import struct +import subprocess +import sys +import time +from enum import Enum +from os.path import join as pjoin +from tempfile import NamedTemporaryFile + + +class CheckFailed(Exception): + """An assertion inside a subcommand did not hold; message goes to stderr.""" + + +class InodeCheck(Enum): + EXACT = 1 + NONZERO = 2 + + +TEST_FILE = os.path.abspath(__file__) + +with open(TEST_FILE, 'rb') as _fh: + TEST_DATA = _fh.read() + + +def name_generator(): + """A file name no other invocation in this test will produce. + + Each check is its own process, so the pid goes into the name -- a plain + counter would make the second subcommand collide with the files the first + one left behind. + """ + name_generator.counter += 1 + return 'testfile_%d_%d' % (os.getpid(), name_generator.counter) + + +name_generator.counter = 0 + + +def _require(condition, message): + if not condition: + raise CheckFailed(message) + + +def readdir_inode(path): + """Sorted "<inode> <name>" lines, from the test/readdir_inode helper.""" + binary = pjoin(os.environ['FUSE_TEST_BIN_DIR'], 'readdir_inode') + with subprocess.Popen([binary, path], stdout=subprocess.PIPE, + universal_newlines=True) as proc: + lines = proc.communicate()[0].splitlines() + lines.sort() + return lines + + +def check_inode(inode_check, actual, expected): + if inode_check == InodeCheck.EXACT: + _require(expected == actual, + 'inode mismatch: got %d, want %d' % (actual, expected)) + elif inode_check == InodeCheck.NONZERO: + _require(actual != 0, 'inode is zero') + + +def _inode_check(name): + return InodeCheck.EXACT if name == 'exact' else InodeCheck.NONZERO + + +class _open_fd: + """os.open() as a context manager.""" + + def __init__(self, name, flags): + self.fd = os.open(name, flags) + + def __enter__(self): + return self.fd + + def __exit__(self, *_exc): + os.close(self.fd) + + +def os_create(name): + os.close(os.open(name, os.O_CREAT | os.O_RDWR)) + + +def expect_enoent(path): + """Require that stat(path) fails with ENOENT.""" + try: + os.stat(path) + except OSError as exc: + _require(exc.errno == errno.ENOENT, + '%s: expected ENOENT, got %s' % (path, exc.strerror)) + return + raise CheckFailed('%s: expected ENOENT, but it exists' % path) + + +# ----------------------------------------------------------- POSIX operations + +def cmd_unlink(mnt_dir, src_dir=None): + name = name_generator() + fullname = mnt_dir + "/" + name + srcname = fullname + if src_dir is not None: + srcname = pjoin(src_dir, name) + with open(srcname, 'wb') as fh: + fh.write(b'hello') + _require(name in os.listdir(mnt_dir), '%s did not appear' % name) + os.unlink(fullname) + expect_enoent(fullname) + _require(name not in os.listdir(mnt_dir), '%s still listed' % name) + + +def cmd_mkdir(mnt_dir): + dirname = name_generator() + fullname = mnt_dir + "/" + dirname + os.mkdir(fullname) + fstat = os.stat(fullname) + _require(stat.S_ISDIR(fstat.st_mode), '%s is not a directory' % fullname) + _require(os.listdir(fullname) == [], '%s is not empty' % fullname) + # Some filesystems (e.g. BTRFS) don't track st_nlink for directories + _require(fstat.st_nlink in (1, 2), 'st_nlink is %d' % fstat.st_nlink) + _require(dirname in os.listdir(mnt_dir), '%s did not appear' % dirname) + + +def cmd_rmdir(mnt_dir, src_dir=None): + name = name_generator() + fullname = mnt_dir + "/" + name + srcname = fullname + if src_dir is not None: + srcname = pjoin(src_dir, name) + os.mkdir(srcname) + _require(name in os.listdir(mnt_dir), '%s did not appear' % name) + os.rmdir(fullname) + expect_enoent(fullname) + _require(name not in os.listdir(mnt_dir), '%s still listed' % name) + + +def cmd_symlink(mnt_dir): + linkname = name_generator() + fullname = mnt_dir + "/" + linkname + os.symlink("/imaginary/dest", fullname) + fstat = os.lstat(fullname) + _require(stat.S_ISLNK(fstat.st_mode), '%s is not a symlink' % fullname) + _require(os.readlink(fullname) == "/imaginary/dest", + 'wrong link target: %s' % os.readlink(fullname)) + _require(fstat.st_nlink == 1, 'st_nlink is %d' % fstat.st_nlink) + _require(linkname in os.listdir(mnt_dir), '%s did not appear' % linkname) + + +def cmd_create(mnt_dir): + name = name_generator() + fullname = pjoin(mnt_dir, name) + expect_enoent(fullname) + _require(name not in os.listdir(mnt_dir), '%s already listed' % name) + + os.close(os.open(fullname, os.O_CREAT | os.O_RDWR)) + + _require(name in os.listdir(mnt_dir), '%s did not appear' % name) + fstat = os.lstat(fullname) + _require(stat.S_ISREG(fstat.st_mode), '%s is not a regular file' % fullname) + _require(fstat.st_nlink == 1, 'st_nlink is %d' % fstat.st_nlink) + _require(fstat.st_size == 0, 'st_size is %d' % fstat.st_size) + + +def cmd_chown(mnt_dir): + filename = pjoin(mnt_dir, name_generator()) + os.mkdir(filename) + fstat = os.lstat(filename) + uid = fstat.st_uid + gid = fstat.st_gid + + uid_new = uid + 1 + os.chown(filename, uid_new, -1) + fstat = os.lstat(filename) + _require(fstat.st_uid == uid_new, 'uid is %d' % fstat.st_uid) + _require(fstat.st_gid == gid, 'gid changed to %d' % fstat.st_gid) + + gid_new = gid + 1 + os.chown(filename, -1, gid_new) + fstat = os.lstat(filename) + _require(fstat.st_uid == uid_new, 'uid changed to %d' % fstat.st_uid) + _require(fstat.st_gid == gid_new, 'gid is %d' % fstat.st_gid) + + +def cmd_open_read(src_dir, mnt_dir): + name = name_generator() + with open(pjoin(src_dir, name), 'wb') as fh_out, \ + open(TEST_FILE, 'rb') as fh_in: + shutil.copyfileobj(fh_in, fh_out) + + _require(filecmp.cmp(pjoin(mnt_dir, name), TEST_FILE, False), + '%s differs from %s' % (pjoin(mnt_dir, name), TEST_FILE)) + + +def cmd_open_write(src_dir, mnt_dir): + name = name_generator() + os_create(pjoin(src_dir, name)) + fullname = pjoin(mnt_dir, name) + with open(fullname, 'wb') as fh_out, \ + open(TEST_FILE, 'rb') as fh_in: + shutil.copyfileobj(fh_in, fh_out) + + _require(filecmp.cmp(fullname, TEST_FILE, False), + '%s differs from %s' % (fullname, TEST_FILE)) + + +def cmd_append(src_dir, mnt_dir): + name = name_generator() + os_create(pjoin(src_dir, name)) + fullname = pjoin(mnt_dir, name) + with _open_fd(fullname, os.O_WRONLY) as fd: + os.write(fd, b'foo\n') + with _open_fd(fullname, os.O_WRONLY | os.O_APPEND) as fd: + os.write(fd, b'bar\n') + + with open(fullname, 'rb') as fh: + got = fh.read() + _require(got == b'foo\nbar\n', 'appended file holds %r' % got) + + +def cmd_seek(src_dir, mnt_dir): + name = name_generator() + os_create(pjoin(src_dir, name)) + fullname = pjoin(mnt_dir, name) + with _open_fd(fullname, os.O_WRONLY) as fd: + os.lseek(fd, 1, os.SEEK_SET) + os.write(fd, b'foobar\n') + with _open_fd(fullname, os.O_WRONLY) as fd: + os.lseek(fd, 4, os.SEEK_SET) + os.write(fd, b'com') + + with open(fullname, 'rb') as fh: + got = fh.read() + _require(got == b'\0foocom\n', 'file holds %r' % got) + + +def cmd_open_unlink(mnt_dir): + name = name_generator() + data1 = b'foo' + data2 = b'bar' + fullname = pjoin(mnt_dir, name) + with open(fullname, 'wb+', buffering=0) as fh: + fh.write(data1) + os.unlink(fullname) + expect_enoent(fullname) + _require(name not in os.listdir(mnt_dir), '%s still listed' % name) + fh.write(data2) + fh.seek(0) + got = fh.read() + _require(got == data1 + data2, 'unlinked file holds %r' % got) + + +def cmd_statvfs(mnt_dir): + os.statvfs(mnt_dir) + + +def cmd_link(mnt_dir): + name1 = pjoin(mnt_dir, name_generator()) + name2 = pjoin(mnt_dir, name_generator()) + shutil.copyfile(TEST_FILE, name1) + _require(filecmp.cmp(name1, TEST_FILE, False), + '%s differs from %s' % (name1, TEST_FILE)) + + fstat1 = os.lstat(name1) + _require(fstat1.st_nlink == 1, 'st_nlink is %d' % fstat1.st_nlink) + + os.link(name1, name2) + + fstat1 = os.lstat(name1) + fstat2 = os.lstat(name2) + _require(fstat1 == fstat2, 'stat of the two links differs') + _require(fstat1.st_nlink == 2, 'st_nlink is %d' % fstat1.st_nlink) + _require(os.path.basename(name2) in os.listdir(mnt_dir), + '%s did not appear' % name2) + _require(filecmp.cmp(name1, name2, False), '%s and %s differ' + % (name1, name2)) + + # Since RELEASE requests are asynchronous, it is possible that libfuse + # still considers the file to be open at this point and (since + # -o hard_remove is not used) renames it instead of deleting it. + os.unlink(name2) + + _require(os.path.basename(name2) not in os.listdir(mnt_dir), + '%s still listed' % name2) + expect_enoent(name2) + + # See above, we may have to wait until RELEASE has been received before + # the st_nlink value is correct. + maxwait = time.time() + 2 + fstat1 = os.lstat(name1) + while fstat1.st_nlink == 2 and time.time() < maxwait: + fstat1 = os.lstat(name1) + time.sleep(0.1) + _require(fstat1.st_nlink == 1, 'st_nlink is %d' % fstat1.st_nlink) + + os.unlink(name1) + + +def cmd_readdir(src_dir, mnt_dir, inode_check='exact'): + inode_check = _inode_check(inode_check) + newdir = name_generator() + + src_newdir = pjoin(src_dir, newdir) + mnt_newdir = pjoin(mnt_dir, newdir) + file_ = src_newdir + "/" + name_generator() + subdir = src_newdir + "/" + name_generator() + subfile = subdir + "/" + name_generator() + + os.mkdir(src_newdir) + shutil.copyfile(TEST_FILE, file_) + os.mkdir(subdir) + shutil.copyfile(TEST_FILE, subfile) + + listdir_is = sorted(os.listdir(mnt_newdir)) + listdir_should = sorted([os.path.basename(file_), + os.path.basename(subdir)]) + _require(listdir_is == listdir_should, + 'readdir returned %r, want %r' % (listdir_is, listdir_should)) + + inodes_is = readdir_inode(mnt_newdir) + if inode_check == InodeCheck.EXACT: + inodes_should = readdir_inode(src_newdir) + _require(inodes_is == inodes_should, + 'inodes %r, want %r' % (inodes_is, inodes_should)) + else: + _require(all(int(line.split()[0]) != 0 for line in inodes_is), + 'a readdir inode is zero: %r' % inodes_is) + + os.unlink(file_) + os.unlink(subfile) + os.rmdir(subdir) + os.rmdir(src_newdir) + + +def cmd_readdir_big(src_dir, mnt_dir, inode_check='exact'): + inode_check = _inode_check(inode_check) + # Add enough entries so that readdir needs to be called multiple times. + fnames = [] + for i in range(500): + fname = ('A rather long filename to make sure that we ' + 'fill up the buffer - ' * 3) + str(i) + with open(pjoin(src_dir, fname), 'w') as fh: + fh.write('File %d' % i) + fnames.append(fname) + + listdir_is = sorted(os.listdir(mnt_dir)) + listdir_should = sorted(os.listdir(src_dir)) + _require(listdir_is == listdir_should, + 'mount lists %d entries, source lists %d' + % (len(listdir_is), len(listdir_should))) + + inodes_is = readdir_inode(mnt_dir) + if inode_check == InodeCheck.EXACT: + inodes_should = readdir_inode(src_dir) + _require(inodes_is == inodes_should, 'readdir inodes differ') + else: + _require(all(int(line.split()[0]) != 0 for line in inodes_is), + 'a readdir inode is zero') + + for fname in fnames: + stat_src = os.stat(pjoin(src_dir, fname)) + stat_mnt = os.stat(pjoin(mnt_dir, fname)) + check_inode(inode_check, stat_mnt.st_ino, stat_src.st_ino) + _require(stat_src.st_mtime == stat_mnt.st_mtime, 'st_mtime differs') + _require(stat_src.st_ctime == stat_mnt.st_ctime, 'st_ctime differs') + _require(stat_src.st_size == stat_mnt.st_size, 'st_size differs') + os.unlink(pjoin(src_dir, fname)) + + +def cmd_truncate_path(mnt_dir): + _require(len(TEST_DATA) > 1024, 'test data is too short') + + filename = pjoin(mnt_dir, name_generator()) + with open(filename, 'wb') as fh: + fh.write(TEST_DATA) + + size = os.stat(filename).st_size + _require(size == len(TEST_DATA), 'st_size is %d' % size) + + # Add zeros at the end + os.truncate(filename, size + 1024) + _require(os.stat(filename).st_size == size + 1024, 'grow failed') + with open(filename, 'rb') as fh: + _require(fh.read(size) == TEST_DATA, 'data changed on grow') + _require(fh.read(1025) == b'\0' * 1024, 'grow did not zero-fill') + + # Truncate data + os.truncate(filename, size - 1024) + _require(os.stat(filename).st_size == size - 1024, 'shrink failed') + with open(filename, 'rb') as fh: + _require(fh.read(size) == TEST_DATA[:size - 1024], + 'data changed on shrink') + + os.unlink(filename) + + +def cmd_truncate_fd(mnt_dir): + _require(len(TEST_DATA) > 1024, 'test data is too short') + with NamedTemporaryFile('w+b', 0, dir=mnt_dir) as fh: + fd = fh.fileno() + fh.write(TEST_DATA) + size = os.fstat(fd).st_size + _require(size == len(TEST_DATA), 'st_size is %d' % size) + + # Add zeros at the end + os.ftruncate(fd, size + 1024) + _require(os.fstat(fd).st_size == size + 1024, 'grow failed') + fh.seek(0) + _require(fh.read(size) == TEST_DATA, 'data changed on grow') + _require(fh.read(1025) == b'\0' * 1024, 'grow did not zero-fill') + + # Truncate data + os.ftruncate(fd, size - 1024) + _require(os.fstat(fd).st_size == size - 1024, 'shrink failed') + fh.seek(0) + _require(fh.read(size) == TEST_DATA[:size - 1024], + 'data changed on shrink') + + +def cmd_utimens(mnt_dir, ns_tol=0): + ns_tol = int(ns_tol) + filename = pjoin(mnt_dir, name_generator()) + os.mkdir(filename) + fstat = os.lstat(filename) + + atime = fstat.st_atime + 42.28 + mtime = fstat.st_mtime - 42.23 + atime_ns = fstat.st_atime_ns + int(42.28 * 1e9) + mtime_ns = fstat.st_mtime_ns - int(42.23 * 1e9) + os.utime(filename, None, ns=(atime_ns, mtime_ns)) + + fstat = os.lstat(filename) + + _require(abs(fstat.st_atime - atime) < 1, 'st_atime off by too much') + _require(abs(fstat.st_mtime - mtime) < 1, 'st_mtime off by too much') + _require(abs(fstat.st_atime_ns - atime_ns) <= ns_tol, + 'st_atime_ns off by %d' % abs(fstat.st_atime_ns - atime_ns)) + _require(abs(fstat.st_mtime_ns - mtime_ns) <= ns_tol, + 'st_mtime_ns off by %d' % abs(fstat.st_mtime_ns - mtime_ns)) + + +def cmd_passthrough(src_dir, mnt_dir, inode_check='exact'): + inode_check = _inode_check(inode_check) + name = name_generator() + src_name = pjoin(src_dir, name) + mnt_name = pjoin(mnt_dir, name) + + # First: write to the source directory + _require(name not in os.listdir(src_dir), '%s already in source' % name) + _require(name not in os.listdir(mnt_dir), '%s already in mount' % name) + with open(src_name, 'w') as fh: + fh.write('Hello, world') + + start_time = time.time() + while time.time() - start_time < 10: + if name in os.listdir(mnt_dir): + break + time.sleep(0.1) + else: + raise CheckFailed('%s did not appear in the mount within 10s' % name) + + _require(name in os.listdir(src_dir), '%s vanished from source' % name) + + src_stat = os.stat(src_name) + mnt_stat = os.stat(mnt_name) + _require(src_stat.st_mode == mnt_stat.st_mode, 'st_mode differs') + check_inode(inode_check, mnt_stat.st_ino, src_stat.st_ino) + _require(src_stat.st_size == mnt_stat.st_size, 'st_size differs') + _require(src_stat.st_mtime == mnt_stat.st_mtime, 'st_mtime differs') + + # Second: write to the mount directory + name = name_generator() + src_name = pjoin(src_dir, name) + mnt_name = pjoin(mnt_dir, name) + _require(name not in os.listdir(src_dir), '%s already in source' % name) + _require(name not in os.listdir(mnt_dir), '%s already in mount' % name) + with open(mnt_name, 'w') as fh: + fh.write('Hello, world') + _require(name in os.listdir(src_dir), '%s did not reach the source' % name) + _require(name in os.listdir(mnt_dir), '%s not in the mount' % name) + + src_stat = os.stat(src_name) + mnt_stat = os.stat(mnt_name) + _require(src_stat.st_mode == mnt_stat.st_mode, 'st_mode differs') + check_inode(inode_check, mnt_stat.st_ino, src_stat.st_ino) + _require(src_stat.st_size == mnt_stat.st_size, 'st_size differs') + _require(abs(src_stat.st_mtime - mnt_stat.st_mtime) < 0.01, + 'st_mtime differs') + + +def cmd_xattr(path): + os.setxattr(path, b'hello_ll_setxattr_name', b'hello_ll_setxattr_value') + got = os.getxattr(path, b'hello_ll_getxattr_name') + _require(got == b'hello_ll_getxattr_value', 'getxattr returned %r' % got) + os.removexattr(path, b'hello_ll_removexattr_name') + + +# ------------------------------------------------------- generic assertions + +# Operations expect-errno can run. One line each; add one rather than +# approximating the check in shell. +OPS = { + 'fuse_test_open_rw': lambda path: os.close(os.open(path, os.O_RDWR)), + 'fuse_test_open_ro': lambda path: os.close(os.open(path, os.O_RDONLY)), + 'fuse_test_stat': os.stat, +} + + +def cmd_expect_errno(want, op, args): + """Run one filesystem operation and require it to fail with *want*. + + Comparing OSError.errno keeps the check exact -- matching strerror text + would be locale-dependent and would conflate distinct errnos. + """ + code = getattr(errno, want, None) + if code is None: + raise CheckFailed('unknown errno name %s' % want) + handler = OPS.get(op) + if handler is None: + raise CheckFailed('unknown operation %s (have: %s)' + % (op, ' '.join(sorted(OPS)))) + try: + handler(*args) + except OSError as exc: + if exc.errno != code: + raise CheckFailed('%s %s: expected %s, got %s' + % (op, ' '.join(args), want, + errno.errorcode.get(exc.errno, exc.errno))) + return + raise CheckFailed('%s %s: expected %s, but it succeeded' + % (op, ' '.join(args), want)) + + +def cmd_assert_listdir(directory, names): + got = sorted(os.listdir(directory)) + want = sorted(names) + _require(got == want, '%s lists %r, want %r' % (directory, got, want)) + + +def cmd_listdir(directory): + for name in sorted(os.listdir(directory)): + print(name) + + +def cmd_listdir_first(directory): + """Print the first entry of *directory*. + + A subcommand rather than `listdir | head -1` because pipefail is off, so a + failing listdir in a pipeline would hand the caller an empty name instead + of an error. + """ + names = sorted(os.listdir(directory)) + _require(names, '%s is empty' % directory) + print(names[0]) + + +def cmd_size(path): + print(os.path.getsize(path)) + + +def cmd_ismount(path): + """Exit 0 when *path* is a mountpoint. + + os.path.ismount() is portable; mountpoint(1) is util-linux only. + """ + _require(os.path.ismount(path), '%s is not a mountpoint' % path) + + +def cmd_isbigger(path, size): + """Exit 0 when *path* is larger than *size* bytes. + + The mount predicate for a filesystem with no mountpoint of its own: the + null filesystem is mounted over a regular file. + """ + _require(os.stat(path).st_size > int(size), + '%s is not larger than %s bytes' % (path, size)) + + +# --------------------------------------------------------------- mount state + +def parse_mountinfo(mnt_dir): + """Return the /proc/self/mountinfo entry for *mnt_dir*, or None. + + Parses the line for the mountpoint and returns a dict with keys: + 'mountpoint' - the mount point path (str) + 'fstype' - filesystem type as the kernel sees it, + e.g. 'fuse' or 'fuse.<subtype>' (str) + 'source' - mount source field, e.g. 'hello' or + '<subtype>#<fsname>' fallback form (str) + 'mount_options' - per-mount options/attrs (set of str) + 'super_options' - superblock options from the filesystem (set of str) + + These fields are exactly what /proc/self/mountinfo exposes; they capture + the post-mount state that differs between the legacy mount(2) path and the + new fsopen/fsconfig/fsmount path. + """ + target = os.path.realpath(mnt_dir) + with open('/proc/self/mountinfo') as fh: + for line in fh: + parts = line.rstrip('\n').split(' ') + try: + sep = parts.index('-') + except ValueError: + continue + if len(parts) < sep + 4 or sep < 6: + continue + mountpoint = parts[4].replace('\\040', ' ') + if mountpoint != target: + continue + return { + 'mountpoint': mountpoint, + 'fstype': parts[sep + 1], + 'source': parts[sep + 2].replace('\\040', ' '), + 'mount_options': set(parts[5].split(',')), + 'super_options': set(parts[sep + 3].split(',')), + } + return None + + +def _mountinfo(mnt_dir): + info = parse_mountinfo(mnt_dir) + if info is None: + raise CheckFailed('%s not found in /proc/self/mountinfo' % mnt_dir) + return info + + +def cmd_mountinfo(mnt_dir, field): + """Echo one field of the mountinfo line. Empty output means not mounted.""" + info = parse_mountinfo(mnt_dir) + if info is None: + return + _require(field in info, + 'no such mountinfo field: %s (have %s)' + % (field, ' '.join(sorted(info)))) + value = info[field] + print(','.join(sorted(value)) if isinstance(value, set) else value) + + +def cmd_assert_mount_opt(mnt_dir, opts): + info = _mountinfo(mnt_dir) + for opt in opts: + _require(opt in info['mount_options'], + '%r missing from mount_options=%r' + % (opt, sorted(info['mount_options']))) + + +def cmd_refute_mount_opt(mnt_dir, opts): + info = _mountinfo(mnt_dir) + for opt in opts: + _require(opt not in info['mount_options'], + 'unexpected %r in mount_options=%r' + % (opt, sorted(info['mount_options']))) + + +def cmd_assert_super_opt(mnt_dir, opts): + info = _mountinfo(mnt_dir) + for opt in opts: + _require(opt in info['super_options'], + '%r missing from super_options=%r' + % (opt, sorted(info['super_options']))) + + +def cmd_assert_super_opt_prefix(mnt_dir, prefixes): + info = _mountinfo(mnt_dir) + for prefix in prefixes: + _require(any(o.startswith(prefix) for o in info['super_options']), + 'no super option starts with %r: %r' + % (prefix, sorted(info['super_options']))) + + +def cmd_assert_fstype(mnt_dir, expected): + """The fstype must be one of *expected*; several are legitimate.""" + info = _mountinfo(mnt_dir) + _require(info['fstype'] in expected, + 'unexpected fstype %r (expected one of %r)' + % (info['fstype'], expected)) + + +def cmd_assert_source(mnt_dir, expected): + """The source must be one of *expected*. + + Several are accepted because the ENODEV fallback legitimately produces + either 'myfsname' or 'mysub#myfsname'. + """ + info = _mountinfo(mnt_dir) + _require(info['source'] in expected, + 'unexpected source %r (expected one of %r)' + % (info['source'], expected)) + + +# ------------------------------------------------------------- odds and ends + +def cmd_printcap_caps(src_root): + """Verify that the FUSE_CAP_* name table printcap uses includes every + flag from fuse_common.h.""" + header_path = pjoin(src_root, 'include', 'fuse_common.h') + printcap_path = pjoin(src_root, 'lib', 'fuse_cap_names_i.h') + if not os.path.exists(header_path): + raise CheckFailed('%s not found' % header_path) + + header_caps = set() + with open(header_path) as fh: + for line in fh: + match = re.match(r'#define\s+(FUSE_CAP_\w+)\s+', line) + if match: + header_caps.add(match.group(1)) + + printcap_caps = set() + with open(printcap_path) as fh: + for line in fh: + if re.match(r'^\s*//', line): # commented out + continue + match = re.search(r'\{\s*(FUSE_CAP_\w+),\s*"', line) + if match: + printcap_caps.add(match.group(1)) + + missing = header_caps - printcap_caps + extra = printcap_caps - header_caps + if missing or extra: + msg = ['%d caps in %s, %d in %s' + % (len(header_caps), header_path, + len(printcap_caps), printcap_path)] + if missing: + msg.append('missing in %s: %s' % (printcap_path, sorted(missing))) + if extra: + msg.append('extra in %s: %s' % (printcap_path, sorted(extra))) + raise CheckFailed('\n'.join(msg)) + + +def cmd_reachable_without_caps(path): + """Exit 0 when *path* is reachable by a process without CAP_DAC_OVERRIDE. + + mount.fuse3 execs the fs binary only after dropping all capabilities; the + process keeps uid/gid 0 but loses CAP_DAC_OVERRIDE, so the binary and every + directory on the way to it must be executable for it by plain permission + bits (a user-owned mode-0700 home directory is not). + """ + def executable_without_caps(st): + if st.st_uid == 0: + return st.st_mode & stat.S_IXUSR + if st.st_gid == 0: + return st.st_mode & stat.S_IXGRP + return st.st_mode & stat.S_IXOTH + + path = os.path.realpath(path) + while True: + if not executable_without_caps(os.stat(path)): + raise CheckFailed('%s is not reachable by the capability-less ' + 'file system process' % path) + if path == '/': + return + path = os.path.dirname(path) + + +def cmd_elf_class(path): + """Print the ELF class of *path*, 32 or 64. + + Reads e_ident[EI_CLASS] directly rather than grepping file(1) output for + the string 'ELF 32-bit'. + """ + with open(path, 'rb') as fh: + ident = fh.read(5) + if ident[:4] != b'\x7fELF': + raise CheckFailed('%s is not an ELF binary' % path) + print({1: 32, 2: 64}.get(ident[4], 0)) + + +def cmd_null_roundtrip(path): + """Read zeros from the null filesystem, then write to it.""" + with open(path, 'rb') as fh: + got = fh.read(382) + _require(got == b'\0' * 382, 'read %r rather than zeros' % got[:32]) + with open(path, 'wb') as fh: + fh.write(b'whatever') + + +def cmd_cuse_roundtrip(devpath, client): + """The binary read/write/offset exchange test_cuse performed.""" + output = subprocess.check_output([client, devpath, 's']) + _require(output == b'0\n', 'initial size is %r' % output) + + data = b'some test data' + off = 5 + proc = subprocess.Popen([client, devpath, 'w', str(len(data)), str(off)], + stdin=subprocess.PIPE) + proc.stdin.write(data) + proc.stdin.close() + ret = proc.wait(timeout=10) + _require(ret == 0, 'write client exited with %d' % ret) + + want = str(off + len(data)).encode() + b'\n' + output = subprocess.check_output([client, devpath, 's']) + _require(output == want, 'size is %r, want %r' % (output, want)) + + out = subprocess.check_output( + [client, devpath, 'r', str(off + len(data) + 2), '0']) + _require(out == (b'\0' * off) + data, 'read back %r' % out) + + +# ------------------------------------------------------ FUSE INIT over AF_UNIX + +FUSE_OP_INIT = 26 +FUSE_MAJOR_VERSION = 7 +FUSE_MINOR_VERSION = 38 + +_fuse_in_header_fmt = '<IIQQIIII' +_fuse_out_header_fmt = '<IiQ' +_fuse_init_in_fmt = '<IIIII44x' +_fuse_init_out_fmt = '<IIIIHHIIHHI28x' + + +def cmd_uds_init(sockpath): + """Speak a FUSE INIT handshake over an AF_UNIX socket. + + The request is hand-built rather than relayed from the kernel, with + flags = 0, so nothing is negotiated. + """ + import socket + + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.settimeout(10) + sock.connect(sockpath) + try: + unique_req = 10 + header = struct.pack( + _fuse_in_header_fmt, + struct.calcsize(_fuse_in_header_fmt) + + struct.calcsize(_fuse_init_in_fmt), + FUSE_OP_INIT, unique_req, 0, 0, 0, 0, 0) + payload = struct.pack(_fuse_init_in_fmt, FUSE_MAJOR_VERSION, + FUSE_MINOR_VERSION, 0, 0, 0) + sock.sendall(header + payload) + + response_header = _recvall(sock, struct.calcsize(_fuse_out_header_fmt)) + packet_len, _, unique_res = struct.unpack(_fuse_out_header_fmt, + response_header) + _require(unique_res == unique_req, + 'reply carries unique %d, want %d' % (unique_res, unique_req)) + + response = _recvall(sock, packet_len - len(response_header)) + response = struct.unpack(_fuse_init_out_fmt, response) + _require(response[0] == FUSE_MAJOR_VERSION, + 'reply advertises major %d' % response[0]) + finally: + sock.close() + + +def _recvall(sock, bufsize): + buf = bytes() + while len(buf) < bufsize: + chunk = sock.recv(bufsize - len(buf)) + if not chunk: + raise CheckFailed('peer closed the socket after %d bytes' + % len(buf)) + buf += chunk + return buf + + +_FUSERMOUNT_REJECTIONS = { + 'not_dir_or_regular': 'is not a directory or a regular file', + 'no_write_access': 'user has no write access to mountpoint', + 'no_such_file': 'failed to access mountpoint', +} + + +def cmd_fusermount_rejects(mountpoint, reason): + """Require fusermount3 to refuse <mountpoint> for the named reason. + + fusermount3 reaches its mountpoint checks only once _FUSE_COMMFD names a + socket it could hand the /dev/fuse fd back over, so a real socketpair goes + in even though the mount is never meant to get that far. + """ + import socket + + wanted = _FUSERMOUNT_REJECTIONS.get(reason) + if wanted is None: + raise CheckFailed('unknown rejection reason %r' % reason) + + binary = pjoin(os.environ['FUSE_UTIL_DIR'], 'fusermount3') + env = dict(os.environ) + parent, child = socket.socketpair() + try: + env['_FUSE_COMMFD'] = str(child.fileno()) + proc = subprocess.run([binary, '-o', 'rw', mountpoint], env=env, + pass_fds=(child.fileno(),), + stderr=subprocess.PIPE, + universal_newlines=True) + finally: + parent.close() + child.close() + + _require(proc.returncode != 0, + 'fusermount3 accepted mountpoint %s' % mountpoint) + _require(wanted in proc.stderr, + 'mountpoint %s: expected %r on stderr, got: %s' + % (mountpoint, wanted, proc.stderr.strip())) + _require(not os.path.ismount(mountpoint), + '%s is a mountpoint after a refused mount' % mountpoint) + + +# ------------------------------------------------------------------ dispatch + +_INODE_CHECK = {'default': 'exact', 'choices': ('exact', 'nonzero')} + + +def build_parser(): + """One subparser per check. Each records how to call its function, so + adding a check is one line here and one function above.""" + parser = argparse.ArgumentParser(description=__doc__) + sub = parser.add_subparsers(dest='cmd', required=True) + + def add(name, func, *positional, **options): + cmd = sub.add_parser(name) + for arg in positional: + if isinstance(arg, tuple): + cmd.add_argument(arg[0], **arg[1]) + else: + cmd.add_argument(arg) + for flag, kwargs in options.items(): + cmd.add_argument('--' + flag.replace('_', '-'), dest=flag, + **kwargs) + names = [a[0] if isinstance(a, tuple) else a for a in positional] + names += list(options) + cmd.set_defaults(call=lambda args: func(*[getattr(args, n) + for n in names])) + return cmd + + add('fuse_test_unlink', cmd_unlink, 'mnt', src={'default': None}) + add('fuse_test_mkdir', cmd_mkdir, 'mnt') + add('fuse_test_rmdir', cmd_rmdir, 'mnt', src={'default': None}) + add('fuse_test_symlink', cmd_symlink, 'mnt') + add('fuse_test_create', cmd_create, 'mnt') + add('fuse_test_chown', cmd_chown, 'mnt') + add('fuse_test_open_read', cmd_open_read, 'src', 'mnt') + add('fuse_test_open_write', cmd_open_write, 'src', 'mnt') + add('fuse_test_append', cmd_append, 'src', 'mnt') + add('fuse_test_seek', cmd_seek, 'src', 'mnt') + add('fuse_test_open_unlink', cmd_open_unlink, 'mnt') + add('fuse_test_statvfs', cmd_statvfs, 'mnt') + add('fuse_test_link', cmd_link, 'mnt') + add('fuse_test_readdir', cmd_readdir, 'src', 'mnt', + inode_check=_INODE_CHECK) + add('fuse_test_readdir_big', cmd_readdir_big, 'src', 'mnt', + inode_check=_INODE_CHECK) + add('fuse_test_truncate_path', cmd_truncate_path, 'mnt') + add('fuse_test_truncate_fd', cmd_truncate_fd, 'mnt') + add('fuse_test_utimens', cmd_utimens, 'mnt', ns_tol={'default': 0}) + add('fuse_test_passthrough', cmd_passthrough, 'src', 'mnt', + inode_check=_INODE_CHECK) + add('fuse_test_xattr', cmd_xattr, 'path') + + add('fuse_test_expect_errno', cmd_expect_errno, 'want', 'op', + ('args', {'nargs': '*'})) + add('fuse_test_assert_listdir', cmd_assert_listdir, 'dir', + ('names', {'nargs': '*'})) + add('fuse_test_listdir', cmd_listdir, 'dir') + add('fuse_test_listdir_first', cmd_listdir_first, 'dir') + add('fuse_test_size', cmd_size, 'path') + add('fuse_test_ismount', cmd_ismount, 'path') + add('fuse_test_isbigger', cmd_isbigger, 'path', 'size') + + add('fuse_test_mountinfo', cmd_mountinfo, 'mnt', 'field') + for name, func in (('fuse_test_assert_mount_opt', cmd_assert_mount_opt), + ('fuse_test_refute_mount_opt', cmd_refute_mount_opt), + ('fuse_test_assert_super_opt', cmd_assert_super_opt), + ('fuse_test_assert_super_opt_prefix', + cmd_assert_super_opt_prefix), + ('fuse_test_assert_fstype', cmd_assert_fstype), + ('fuse_test_assert_source', cmd_assert_source)): + add(name, func, 'mnt', ('values', {'nargs': '+'})) + add('fuse_test_fusermount_rejects', cmd_fusermount_rejects, + 'mountpoint', 'reason') + + add('fuse_test_printcap_caps', cmd_printcap_caps, 'src_root') + add('fuse_test_reachable_without_caps', cmd_reachable_without_caps, 'path') + add('fuse_test_elf_class', cmd_elf_class, 'path') + add('fuse_test_null_roundtrip', cmd_null_roundtrip, 'path') + add('fuse_test_cuse_roundtrip', cmd_cuse_roundtrip, 'devpath', 'client') + add('fuse_test_uds_init', cmd_uds_init, 'sockpath') + return parser + + +def main(argv): + args = build_parser().parse_args(argv) + try: + args.call(args) + # struct.error is what a daemon replying with a short or overlong packet + # raises, which is a check failing rather than this script breaking. + except (CheckFailed, OSError, subprocess.SubprocessError, + struct.error) as exc: + print('checks.py %s: %s' % (args.cmd, exc), file=sys.stderr) + return 1 + return 0 + + +if __name__ == '__main__': + sys.exit(main(sys.argv[1:])) diff --git a/test/cases/lib/common.sh b/test/cases/lib/common.sh new file mode 100644 index 000000000..4516620c2 --- /dev/null +++ b/test/cases/lib/common.sh @@ -0,0 +1,561 @@ +# common.sh - helper library for the libfuse shell tests. +# +# Sourced by each lib/ body, and by a case that has none or that gates before +# sourcing one. Provides the FUSE verbs (mount, umount, assertions, skip +# protocol); it knows nothing about scheduling, which belongs to run-tests.py. +# +# Scripts run under errexit, imposed once by the runner (`bash -e`), so an +# unchecked command that fails ends the test instead of letting the rest of it +# run against a broken state. Two consequences show up below: a status that is +# examined must be captured in the same command, and the cleanup trap has to +# swallow each of its own failures. +# +# Nothing here is sourced, probed or defaulted: the runner puts the whole +# contract in the environment before exec'ing the script, and the variables +# below are read as given. A case is therefore not runnable by hand; replay +# logs/repro.sh, which restores the environment first. +# +# TEST_NAME the name the runner reports this test under +# TEST_LIB this directory, where checks.py is found +# TEST_MNT empty mount point, one per test +# TEST_LOGDIR where daemon logs and status files are written +# BUILD_DIR build tree root; _require_binary resolves against it +# FUSE_EXAMPLE_DIR $BUILD_DIR/example +# FUSE_TEST_BIN_DIR $BUILD_DIR/test +# FUSE_UTIL_DIR $BUILD_DIR/util, holding fusermount3 and mount.fuse3 +# FUSE_CAPS capability names printcap reported, space separated +# FUSE_UID effective uid +# FUSE_OS uname -s +# FUSE_URING_ENABLE 1 when the run expects the daemons to use io_uring +# FUSE_VALGRIND command prefix, empty unless the run is under valgrind +# +# The runner exports more for the cases themselves (TEST_SRC, TEST_TMP, +# TEST_WORKDIR); those are not read here. A case may set FUSE_WAIT_PREDICATE +# before mounting, to replace the readiness check fuse_mount waits on, and +# _fuse_no_mount_needed before sourcing this file, when it never mounts. + +# A case that gates before sourcing a lib/ body reaches this file twice, and +# the second pass must not reset the mount bookkeeping the first one is +# already tracking a mount in. +[ -z "${_FUSE_COMMON_SH:-}" ] || return 0 +_FUSE_COMMON_SH=1 + +# --------------------------------------------------------------- result protocol + +# _fail <msg...> +# Print msg to stderr with the test name, then exit 1 (FAIL). +# +# A test whose subject only reports an unsupported kernel once it has tried to +# use it cannot gate on that up front. $FAIL_CALLBACK is its last look, called +# with the message; _notrun from there turns the failure into a skip. +_fail() +{ + if [ -n "${FAIL_CALLBACK:-}" ]; then + local callback=$FAIL_CALLBACK + FAIL_CALLBACK= # a failing callback must not recurse + "$callback" "$@" + fi + echo "FAIL: ${TEST_NAME}: $*" >&2 + exit 1 +} + +# _notrun <reason...> +# Print reason to stderr, then exit 77 (SKIP). The runner records the reason. +# stderr, because fuse_mount echoes the mount index on stdout and its callers +# redirect that away; a skip raised inside it would go with it. +_notrun() +{ + echo "SKIP: $*" >&2 + exit 77 +} + +# _require_root +# _notrun unless the effective uid is 0. +_require_root() +{ + [ "$FUSE_UID" = 0 ] || _notrun "needs root" +} + +# _require_nonroot +# _notrun unless the effective uid is not 0. +_require_nonroot() +{ + [ "$FUSE_UID" != 0 ] || _notrun "needs to run as non-root" +} + +# _is_linux +# True on Linux. For conditional sections inside a shared body; a whole script +# that cannot run elsewhere calls _require_linux instead. +_is_linux() +{ + [ "$FUSE_OS" = Linux ] +} + +# _require_linux <what> +# _notrun unless $FUSE_OS is Linux. <what> names the Linux-only facility, so +# the summary says which one rather than just "not linux". +_require_linux() +{ + _is_linux || _notrun "$1 is Linux-only" +} + +# _require_cap <FUSE_CAP_NAME> +# _notrun unless the name appears in $FUSE_CAPS. +_require_cap() +{ + local cap=$1 + + case " $FUSE_CAPS " in + *" $cap "*) return 0 ;; + esac + _notrun "kernel does not offer $cap" +} + +# _require_fuse_device +# _notrun unless the kernel side is there. A case that reaches fusermount3 but +# never completes a mount needs this much and no more. +_require_fuse_device() +{ + [ -e /dev/fuse ] || + _notrun "the FUSE kernel module does not seem to be loaded" +} + +# _require_fuse +# _notrun unless this user can mount a FUSE filesystem at all: the kernel side +# has to be there, and an unprivileged user needs a setuid fusermount3. It runs +# when a case sources this file, so a build tree whose fusermount3 was never +# chmod 4755'd reports skips rather than a wall of mount failures. +_require_fuse() +{ + local fusermount + + # BSD has no fusermount3 at all; vfs.usermount decides there. + _is_linux || return 0 + _require_fuse_device + [ "$FUSE_UID" != 0 ] || return 0 + fusermount=$(command -v fusermount3) || + _notrun "no fusermount3 on \$PATH" + [ -u "$fusermount" ] || + _notrun "$fusermount is not setuid, and we are not root" +} + +# _require_binary <path-relative-to-BUILD_DIR> +# _notrun unless the file exists and is executable. +_require_binary() +{ + [ -x "$BUILD_DIR/$1" ] || _notrun "$1 not built" +} + +# _require_prog <name> +# _notrun unless `command -v <name>` succeeds. +_require_prog() +{ + command -v "$1" >/dev/null 2>&1 || _notrun "$1 not installed" +} + +# _require_not_32bit_on_64 <path> +# _notrun for a 32-bit ELF binary on an x86_64 host. +_require_not_32bit_on_64() +{ + local class + + class=$(_check fuse_test_elf_class "$1") + if [ "$class" = 32 ] && [ "$(uname -m)" = x86_64 ]; then + _notrun "$1 is a 32-bit binary on a 64-bit host" + fi +} + +# _require_reachable_without_caps <fs-name> +# _notrun unless the fs binary and every directory above it are executable by +# plain permission bits. mount.fuse3 -o drop_privileges execs the fs after +# dropping CAP_DAC_OVERRIDE, so a mode-0700 home makes it unreachable. +_require_reachable_without_caps() +{ + python3 "$TEST_LIB/checks.py" fuse_test_reachable_without_caps \ + "$(_fuse_fs_binary "$1")" || + _notrun "$1 is not reachable without CAP_DAC_OVERRIDE" +} + +# _require_fs_marker_absent <regex> [fs-index] +# _notrun if the mounted daemon's log matched <regex>. Used for runtime +# "not supported by kernel" markers. +# +# The daemon may not have written the marker yet when the test looks; the +# examples emit it during the first update interval, so poll briefly. +_require_fs_marker_absent() +{ + local regex=$1 idx=${2:-0} deadline=$((SECONDS + 3)) + + while [ $SECONDS -lt $deadline ]; do + _skip_if_fs_marker "$regex" "$idx" + sleep 0.2 + done +} + +# _skip_if_fs_marker <regex> [fs-index] +# _notrun if the daemon's log already matches <regex>, without waiting. For a +# marker whose arrival the caller has already waited for, polling again would +# only add delay. +_skip_if_fs_marker() +{ + local regex=$1 idx=${2:-0} + + if grep -qE -- "$regex" "$(fuse_fs_log "$idx")"; then + _notrun "filesystem reported: $regex" + fi +} + +# _wait_fs_marker <regex> [fs-index] +# _fail unless the daemon's log comes to match <regex>. For progress a daemon +# reports itself: waiting for the line rather than for a duration lets a slow +# machine take longer instead of failing, and lets a fast one stop early. +_wait_fs_marker() +{ + local regex=$1 idx=${2:-0} deadline=$((SECONDS + 10)) + + while [ $SECONDS -lt $deadline ]; do + if grep -qE -- "$regex" "$(fuse_fs_log "$idx")"; then + return 0 + fi + sleep 0.1 + done + _fail "filesystem did not report: $regex" +} + +# ------------------------------------------------------------------- assertions + +# _assert_eq <actual> <expected> [msg] +_assert_eq() +{ + [ "$1" = "$2" ] || _fail "${3:-assertion failed}: got '$1', want '$2'" +} + +# _assert_ne <actual> <unexpected> [msg] +_assert_ne() +{ + [ "$1" != "$2" ] || _fail "${3:-assertion failed}: both are '$1'" +} + +# _assert_file_eq <path-a> <path-b> +_assert_file_eq() +{ + cmp -s "$1" "$2" || _fail "$1 and $2 differ" +} + +# _wait_for <secs> <shell-cmd> +# Poll until cmd succeeds. Returns non-zero when the deadline passes, so the +# caller decides whether that is a failure or a skip. +_wait_for() +{ + # Separate statements: one `local` expands all its words before it + # assigns any of them, so a shared one computes the deadline from an + # unset secs and the loop ends before it has tried once. + local secs=$1 cmd=$2 + local deadline=$((SECONDS + secs)) + + while [ $SECONDS -lt $deadline ]; do + if eval "$cmd"; then + return 0 + fi + sleep 0.1 + done + return 1 +} + +# Everything shell cannot express exactly goes to checks.py: an exact errno, an +# fd held across an operation, nanosecond timestamps, directory listings, +# /proc/self/mountinfo fields. Add a subcommand there rather than approximating +# a check here. + +# _check <subcommand> [args...] +# Run the Python check helper. Its stdout/stderr already carry the diagnostic, +# so just propagate the failure. +_check() +{ + # Silent on success otherwise, so a step that hangs is invisible. + # stderr: callers capture stdout for the check's value. + echo "check: $*" >&2 + python3 "$TEST_LIB/checks.py" "$@" || _fail "checks.py $1 failed" +} + +# _assert_errno <ERRNO-NAME> <op> [args...] +# op is a checks.py operation name (fuse_test_open_rw, fuse_test_open_ro, +# fuse_test_stat). The errno is compared against OSError.errno, never against +# locale-dependent strerror text. +_assert_errno() +{ + _check fuse_test_expect_errno "$@" +} + +_assert_listdir() { _check fuse_test_assert_listdir "$@"; } +_assert_xattr_roundtrip() { _check fuse_test_xattr "$@"; } # set/get/remove + +# mountinfo_field <mnt> <fstype|source|mount_options|super_options> +# Echo one field of the /proc/self/mountinfo line for <mnt>. Empty output means +# the mountpoint is not mounted. +mountinfo_field() +{ + _check fuse_test_mountinfo "$1" "$2" +} + +_assert_mount_opt() { _check fuse_test_assert_mount_opt "$@"; } +_refute_mount_opt() { _check fuse_test_refute_mount_opt "$@"; } +_assert_super_opt() { _check fuse_test_assert_super_opt "$@"; } +_assert_super_opt_prefix() { _check fuse_test_assert_super_opt_prefix "$@"; } +_assert_fstype() { _check fuse_test_assert_fstype "$@"; } +_assert_source() { _check fuse_test_assert_source "$@"; } + +# ---------------------------------------------------------------- cleanup hooks + +_FUSE_ATEXIT=() + +# _at_exit <command...> +# Register a command to run when the script exits, newest first. Registering is +# what installs the EXIT trap, so a test never has to write its own and cannot +# clobber the mount unwinding. +_at_exit() +{ + _FUSE_ATEXIT+=("$*") + trap _fuse_cleanup_all EXIT +} + +# _fuse_cleanup_all +# Unwind every mount still registered, newest first, then run the _at_exit +# commands. Every step swallows its own failure: under errexit one stuck umount +# would otherwise end the trap and leak the remaining mounts, which is exactly +# what the trap exists to prevent. +_fuse_cleanup_all() +{ + local idx + + for ((idx = _fuse_fs_count - 1; idx >= 0; idx--)); do + fuse_umount_lazy "$idx" || true + done + for ((idx = ${#_FUSE_ATEXIT[@]} - 1; idx >= 0; idx--)); do + eval "${_FUSE_ATEXIT[$idx]}" || true + done +} + +# ------------------------------------------------------------------ mount verbs + +_fuse_fs_count=0 +FUSE_FS_PID=() +FUSE_FS_LOG=() +FUSE_FS_MNT=() +FUSE_FS_NAME=() + +# _fuse_fs_binary <fs-name> +# Echo the build-tree path of <fs-name>: $FUSE_TEST_BIN_DIR when the name is +# prefixed "test/", $FUSE_EXAMPLE_DIR otherwise. +_fuse_fs_binary() +{ + case $1 in + test/*) echo "$FUSE_TEST_BIN_DIR/${1#test/}" ;; + *) echo "$FUSE_EXAMPLE_DIR/$1" ;; + esac +} + +# fuse_mount_at <mnt> <fs-name> [args...] +# Launch the filesystem in the foreground on <mnt> with its stdout+stderr +# redirected to logs/fs-<idx>-<fs-name>.out. Waits for the mountpoint to +# appear. Registers an EXIT trap that lazily unmounts and kills the daemon, so +# an aborting test never leaks a mount. +# Sets FUSE_FS_PID[idx] and FUSE_FS_LOG[idx]; echoes idx. +fuse_mount_at() +{ + local mnt=$1 fs=$2; shift 2 + local idx=$_fuse_fs_count + local bin log + + bin=$(_fuse_fs_binary "$fs") + [ -x "$bin" ] || _notrun "$fs not built" + + # Each daemon gets its own log so a multi-mount test stays readable. + log=$TEST_LOGDIR/fs-$idx-$(basename "$fs").out + "$bin" "$@" "$mnt" >"$log" 2>&1 & + FUSE_FS_PID[$idx]=$! + FUSE_FS_LOG[$idx]=$log + FUSE_FS_MNT[$idx]=$mnt + FUSE_FS_NAME[$idx]=$fs + _fuse_fs_count=$((idx + 1)) + + trap _fuse_cleanup_all EXIT + fuse_wait_mount "$mnt" "${FUSE_WAIT_PREDICATE:-}" || { + _fuse_dump_fs_log $idx + _fail "$fs did not mount on $mnt" + } + echo $idx +} + +# fuse_mount <fs-name> [args...] +# fuse_mount_at on $TEST_MNT, with -f so the daemon stays in the foreground. +fuse_mount() +{ + local fs=$1; shift + + fuse_mount_at "$TEST_MNT" "$fs" -f "$@" +} + +# fuse_mount_helper <fs-name> [args...] +# Mount via $FUSE_UTIL_DIR/mount.fuse3 instead of exec'ing the fs directly. +# +# mount.fuse3 execs the filesystem type through /bin/sh, so an example resolves +# via $PATH but a binary in test/ has to be named by its absolute path. +fuse_mount_helper() +{ + local fs=$1; shift + local idx=$_fuse_fs_count + local spec=$fs log + + case $fs in + test/*) spec=$(_fuse_fs_binary "$fs") ;; + esac + + log=$TEST_LOGDIR/fs-$idx-$(basename "$fs").out + "$FUSE_UTIL_DIR/mount.fuse3" "$spec" "$TEST_MNT" "$@" >"$log" 2>&1 & + FUSE_FS_PID[$idx]=$! + FUSE_FS_LOG[$idx]=$log + FUSE_FS_MNT[$idx]=$TEST_MNT + FUSE_FS_NAME[$idx]=$fs + _fuse_fs_count=$((idx + 1)) + + trap _fuse_cleanup_all EXIT + fuse_wait_mount "$TEST_MNT" || { + _fuse_dump_fs_log $idx + _fail "mount.fuse3 $fs did not mount on $TEST_MNT" + } + echo $idx +} + +# fuse_wait_mount <path> [predicate] +# Poll until `checks.py fuse_test_ismount <path>` succeeds, or until the +# caller-supplied predicate (a shell command taking <path>) succeeds. Fails if +# the daemon exits first. +fuse_wait_mount() +{ + local path=$1 predicate=${2:-} + local idx=$((_fuse_fs_count - 1)) + local deadline=$((SECONDS + 30)) + local test_cmd + + if [ -n "$predicate" ]; then + test_cmd="$predicate '$path'" + else + test_cmd="python3 '$TEST_LIB/checks.py' fuse_test_ismount '$path'" + fi + + while [ $SECONDS -lt $deadline ]; do + if eval "$test_cmd" >/dev/null 2>&1; then + return 0 + fi + if ! kill -0 "${FUSE_FS_PID[$idx]}" 2>/dev/null; then + # The daemon may have exited *after* completing the + # mount (mount.fuse3 does), so look once more. + eval "$test_cmd" >/dev/null 2>&1 && return 0 + return 1 + fi + sleep 0.1 + done + return 1 +} + +# fuse_umount [idx] +# fusermount3 -u on Linux, umount(8) on BSD (no util/ is built there), then +# wait for the daemon; _fail unless it exits 0. Writes +# logs/fs-<idx>-<name>.status. +fuse_umount() +{ + local idx=${1:-0} + local mnt=${FUSE_FS_MNT[$idx]} pid=${FUSE_FS_PID[$idx]} + local rc + + if _is_linux; then + "$FUSE_UTIL_DIR/fusermount3" -u "$mnt" || + _fail "fusermount3 -u $mnt failed" + else + umount "$mnt" || _fail "umount $mnt failed" + fi + # The daemon exits on its own once the kernel drops the connection; a + # daemon that does not is a bug the test must report, not paper over. + _wait_for 30 "! kill -0 $pid 2>/dev/null" || + _fail "${FUSE_FS_NAME[$idx]} still running 30s after umount" + # One command, not `wait; rc=$?`: under errexit a non-zero wait would + # abort the script before its status could be reported. + rc=0 + wait "$pid" || rc=$? + # Reaped: the number is free for any process to be assigned next, so + # nothing may signal it again. + FUSE_FS_PID[$idx]= + echo "$rc" >"$TEST_LOGDIR/fs-$idx-$(basename "${FUSE_FS_NAME[$idx]}").status" + [ $rc -eq 0 ] || { + _fuse_dump_fs_log $idx + _fail "${FUSE_FS_NAME[$idx]} exited with $rc" + } +} + +# fuse_umount_lazy [idx] +# fusermount3 -z -u (Linux) or umount -f (BSD), + SIGTERM + SIGKILL, ignoring +# errors. The trap path. +fuse_umount_lazy() +{ + local idx=${1:-0} + local mnt=${FUSE_FS_MNT[$idx]} pid=${FUSE_FS_PID[$idx]} + + # The trap runs over every index, fuse_umount'ed ones included. + [ -n "$pid" ] || return 0 + if _is_linux; then + "$FUSE_UTIL_DIR/fusermount3" -z -u "$mnt" >/dev/null 2>&1 || true + else + umount -f "$mnt" >/dev/null 2>&1 || true + fi + kill "$pid" 2>/dev/null || true + _wait_for 1 "! kill -0 $pid 2>/dev/null" || kill -9 "$pid" 2>/dev/null || true + wait "$pid" 2>/dev/null || true + FUSE_FS_PID[$idx]= +} + +fuse_fs_pid() +{ + echo "${FUSE_FS_PID[${1:-0}]}" +} + +fuse_fs_log() +{ + echo "${FUSE_FS_LOG[${1:-0}]}" +} + +# _fuse_dump_fs_log <idx> +# Copy that daemon's log to stderr before failing. The log file survives in +# logs/, but a failure line with the daemon's own last words beside it is what +# makes a CI log readable without downloading anything. +_fuse_dump_fs_log() +{ + local idx=${1:-0} + + echo "--- ${FUSE_FS_NAME[$idx]} log (${FUSE_FS_LOG[$idx]}) ---" >&2 + cat "${FUSE_FS_LOG[$idx]}" >&2 || true + echo "--- end log ---" >&2 +} + +# --------------------------------------------------------------- output scanning + +# fuse_allow_output <extended-regex> +# Append a false-positive pattern to logs/.allow, one per line. The runner +# strips every match before scanning for suspicious words. +fuse_allow_output() +{ + printf '%s\n' "$1" >>"$TEST_LOGDIR/.allow" +} + +# fuse_allow_core <executable-name> +# Record that a core from <executable-name> is expected in this test. +fuse_allow_core() +{ + printf '%s\n' "$1" >>"$TEST_LOGDIR/.allow-core" +} + +# ----------------------------------------------------------------- the fuse gate +# Nearly every case reaches /dev/fuse, through the mount verbs or through a +# binary that mounts itself, so this is applied on source rather than left to +# each case to remember. The few that never mount say so instead. +[ -n "${_fuse_no_mount_needed:-}" ] || _require_fuse diff --git a/test/cases/lib/hello.sh b/test/cases/lib/hello.sh new file mode 100644 index 000000000..64362ea15 --- /dev/null +++ b/test/cases/lib/hello.sh @@ -0,0 +1,63 @@ +# lib/hello.sh - body for the hello / hello_ll / test/hello cases. +# +# Caller sets FS_NAME, FS_OPTS and LAUNCH before sourcing: +# FS_NAME hello | hello_ll | test/hello +# FS_OPTS -o option list, empty for none +# LAUNCH direct | mount_fuse | mount_fuse_dropcaps +# +# The platform gates live here rather than in the 18 callers: three of them +# apply to whole columns of the matrix, and a caller that forgot one would +# fail on BSD instead of skipping. + +. "$TEST_LIB/common.sh" + +# clone_fd needs the /dev/fuse clone device, which is Linux-only. +case $FS_OPTS in +*clone_fd*) _require_linux "the clone_fd mount option" ;; +esac + +# hello_ll supports single-threading only. +FS_EXTRA= +[ "$FS_NAME" != hello_ll ] || FS_EXTRA=-s + +case $LAUNCH in +direct) + fuse_mount "$FS_NAME" $FS_EXTRA ${FS_OPTS:+-o "$FS_OPTS"} >/dev/null + ;; +mount_fuse) + # util/ is not built on BSD, so there is no mount.fuse3 to invoke. + _require_linux "mount.fuse3" + fuse_mount_helper "$FS_NAME" ${FS_OPTS:+-o "$FS_OPTS"} >/dev/null + ;; +mount_fuse_dropcaps) + _require_linux "mount.fuse3" + _require_root + # mount.fuse3 execs the fs only after dropping all capabilities, so + # every directory on the way to the binary must be traversable by + # plain permission bits - a mode-0700 home is not. + _require_reachable_without_caps "$FS_NAME" + fuse_mount_helper "$FS_NAME" \ + -o "${FS_OPTS:+$FS_OPTS,}drop_privileges" >/dev/null + ;; +*) + _fail "unknown LAUNCH '$LAUNCH'" + ;; +esac + +_assert_listdir "$TEST_MNT" hello +# Compared against a file rather than against "$(cat ...)": command +# substitution eats the trailing newline, so the shell form would not notice +# if the example stopped writing one. +printf 'Hello World!\n' >"$TEST_TMP/expected" +_assert_file_eq "$TEST_MNT/hello" "$TEST_TMP/expected" +_assert_errno EACCES fuse_test_open_rw "$TEST_MNT/hello" +_assert_errno ENOENT fuse_test_open_rw "$TEST_MNT/hello-does-not-exist" + +# os.setxattr/getxattr/removexattr, which tst_xattr() used, exist only on Linux +# in CPython; FreeBSD's equivalent is extattr(2), a different API. +if [ "$FS_NAME" = hello_ll ] && _is_linux; then + _assert_xattr_roundtrip "$TEST_MNT" + _assert_xattr_roundtrip "$TEST_MNT/hello" +fi + +fuse_umount diff --git a/test/cases/lib/mountinfo.sh b/test/cases/lib/mountinfo.sh new file mode 100644 index 000000000..1e8f86a84 --- /dev/null +++ b/test/cases/lib/mountinfo.sh @@ -0,0 +1,31 @@ +# lib/mountinfo.sh - body for the /proc/self/mountinfo cases. +# +# Caller sets FS_NAME (hello or hello_ll), optionally MOUNT_OPTS, and defines +# mountinfo_assert(). It may also define mountinfo_setup(), which runs before +# the mount and may extend MOUNT_OPTS. +# +# These tests assert what the kernel recorded for a mount, which is what +# differs between the legacy mount(2) path and the fsopen/fsconfig/fsmount +# path: an option dropped on one of them shows up here and nowhere else. + +. "$TEST_LIB/common.sh" + +# Every assertion below is about what /proc/self/mountinfo reports, and +# getmntinfo(3) exposes neither the subtype nor the mount/super option split +# these tests exist to check. +_require_linux "/proc/self/mountinfo" + +# hello_ll supports single-threading only. +FS_EXTRA= +[ "$FS_NAME" != hello_ll ] || FS_EXTRA=-s + +if declare -F mountinfo_setup >/dev/null; then + mountinfo_setup +fi + +fuse_mount "$FS_NAME" $FS_EXTRA ${MOUNT_OPTS:+-o "$MOUNT_OPTS"} >/dev/null + +# While it is still mounted: the mountinfo line disappears with the mount. +mountinfo_assert + +fuse_umount diff --git a/test/cases/lib/notify.sh b/test/cases/lib/notify.sh new file mode 100644 index 000000000..30fc3d011 --- /dev/null +++ b/test/cases/lib/notify.sh @@ -0,0 +1,112 @@ +# lib/notify.sh - body for the notification / invalidation cases. +# +# Caller sets before sourcing: +# FS_NAME example to mount +# NOTIFY 1 to let the filesystem notify the kernel, 0 for --no-notify +# NOTIFY_MODE content | size | entry | prune +# FS_ARGS extra arguments, optional +# MARKER_ABSENT regex the daemon prints when the kernel refuses, optional +# +# Every mode has the same shape: mount, look at the filesystem, wait for the +# filesystem to have moved on, look again. With notification on the kernel must +# have seen the change; with it off the kernel's cache must still be serving +# what it saw first. + +. "$TEST_LIB/common.sh" + +notify_args="--update-interval=1" +[ "$NOTIFY" = 1 ] || notify_args="$notify_args --no-notify" + +# What notify_prune prints once it has issued a prune the kernel accepted, and +# once one has taken effect and the contents are new. The prune mode waits for +# these rather than for a duration. +prune_sent="prune sent" +prune_done="prune complete" + +# $FAIL_CALLBACK, for the marker the daemon may still be about to write. +_notify_recheck_marker() +{ + _require_fs_marker_absent "$MARKER_ABSENT" +} + +fuse_mount "$FS_NAME" $notify_args ${FS_ARGS:-} >/dev/null + +if [ -n "${MARKER_ABSENT:-}" ]; then + # Not gated here: nothing has been looked up yet, so no example can + # have tried to notify, and none of them reports on kernel support + # before it has. The prune mode gates on the report itself below; for + # the rest $FAIL_CALLBACK is what catches it. + FAIL_CALLBACK=_notify_recheck_marker +fi + +case $NOTIFY_MODE in +content) + # The file holds a timestamp the filesystem rewrites; notification is + # what makes the kernel re-read it. + read1=$(cat "$TEST_MNT/current_time") + sleep 2 + read2=$(cat "$TEST_MNT/current_time") + if [ "$NOTIFY" = 1 ]; then + _assert_ne "$read1" "$read2" "content did not change" + else + _assert_eq "$read1" "$read2" "cached content changed" + fi + ;; +size) + size=$(_check fuse_test_size "$TEST_MNT/growing") + sleep 2 + new_size=$(_check fuse_test_size "$TEST_MNT/growing") + if [ "$NOTIFY" = 1 ]; then + [ "$new_size" -gt "$size" ] || + _fail "growing file stayed at $size bytes" + else + _assert_eq "$new_size" "$size" "cached size changed" + fi + ;; +entry) + fname=$(_check fuse_test_listdir_first "$TEST_MNT") + if [ ! -e "$TEST_MNT/$fname" ]; then + # We hit a race and issued readdir just before the name changed. + fname=$(_check fuse_test_listdir_first "$TEST_MNT") + [ -e "$TEST_MNT/$fname" ] || + _fail "$fname vanished twice in a row" + fi + + sleep 2 + if [ "$NOTIFY" != 1 ]; then + # Without notification the entry stays visible until the + # kernel's own --timeout expires. + [ -e "$TEST_MNT/$fname" ] || + _fail "$fname expired before the entry timeout" + sleep 5 + fi + _assert_errno ENOENT fuse_test_stat "$TEST_MNT/$fname" + ;; +prune) + fname=$(_check fuse_test_listdir_first "$TEST_MNT") + content=$(cat "$TEST_MNT/$fname") + if [ "$NOTIFY" = 1 ]; then + # The read above is this file's first lookup, which is what lets + # the daemon prune at all. From here it reports exactly one of + # the two, so the kernel's answer is decided rather than guessed. + _wait_fs_marker "$prune_sent|$MARKER_ABSENT" + _skip_if_fs_marker "$MARKER_ABSENT" + # A prune did go out, so nothing below can be blamed on the + # kernel not having the notification at all. + _wait_fs_marker "$prune_done" + new_content=$(cat "$TEST_MNT/$fname") + _assert_ne "$content" "$new_content" "content did not change" + else + # Nothing is pruned, so there is no progress to wait for; the + # point is that the cache keeps serving the first read. + sleep 2 + new_content=$(cat "$TEST_MNT/$fname") + _assert_eq "$content" "$new_content" "cached content changed" + fi + ;; +*) + _fail "unknown NOTIFY_MODE '$NOTIFY_MODE'" + ;; +esac + +fuse_umount diff --git a/test/cases/lib/passthrough.sh b/test/cases/lib/passthrough.sh new file mode 100644 index 000000000..6455b4410 --- /dev/null +++ b/test/cases/lib/passthrough.sh @@ -0,0 +1,82 @@ +# lib/passthrough.sh - body for the passthrough* cases. +# +# Caller sets before sourcing: +# FS_NAME example to mount +# FS_ARGS its arguments, without the mountpoint +# INODE_CHECK exact | nonzero +# PT_MIRROR 1 when the example mirrors the whole tree, so the work +# directory is $TEST_MNT$TEST_SRC rather than $TEST_MNT +# PT_SRC_VISIBLE 1 when a change made directly in $TEST_SRC is expected to +# show up under the mount straight away +# PT_POSITIONAL extra trailing arguments before the mountpoint, optional +# SYSCALL_ARGS extra test_syscalls arguments, optional +# +# PT_SRC_VISIBLE is one flag covering three decisions the Python made +# separately, because they are the same question: with attribute caching on, +# passthrough_hp's mount must not be compared against a source directory it is +# allowed to have a stale view of. + +. "$TEST_LIB/common.sh" + +fuse_mount_at "$TEST_MNT" "$FS_NAME" $FS_ARGS ${PT_POSITIONAL:-} >/dev/null + +if [ "$PT_MIRROR" = 1 ]; then + work_dir=$TEST_MNT$TEST_SRC +else + work_dir=$TEST_MNT +fi + +# test_syscalls prints "No error" under FreeBSD. +fuse_allow_output "^ [0-9][0-9] \[[^]]+ message: 'No error: 0'\]" + +_check fuse_test_statvfs "$work_dir" +_check fuse_test_readdir "$TEST_SRC" "$work_dir" --inode-check "$INODE_CHECK" +_check fuse_test_readdir_big "$TEST_SRC" "$work_dir" --inode-check "$INODE_CHECK" +_check fuse_test_open_read "$TEST_SRC" "$work_dir" +_check fuse_test_open_write "$TEST_SRC" "$work_dir" +_check fuse_test_create "$work_dir" +[ "$PT_SRC_VISIBLE" != 1 ] || + _check fuse_test_passthrough "$TEST_SRC" "$work_dir" --inode-check "$INODE_CHECK" +_check fuse_test_append "$TEST_SRC" "$work_dir" +_check fuse_test_seek "$TEST_SRC" "$work_dir" +_check fuse_test_mkdir "$work_dir" +if [ "$PT_SRC_VISIBLE" = 1 ]; then + _check fuse_test_rmdir "$work_dir" --src "$TEST_SRC" + _check fuse_test_unlink "$work_dir" --src "$TEST_SRC" +else + # With the cache on, no operation may go through $TEST_SRC: the + # cache would become stale. + _check fuse_test_rmdir "$work_dir" + _check fuse_test_unlink "$work_dir" +fi +_check fuse_test_symlink "$work_dir" +[ "$FUSE_UID" -ne 0 ] || _check fuse_test_chown "$work_dir" +# The underlying fs may not have full nanosecond resolution. +_check fuse_test_utimens "$work_dir" --ns-tol 1000 +[ "$INODE_CHECK" != exact ] || _check fuse_test_link "$work_dir" +_check fuse_test_truncate_path "$work_dir" +_check fuse_test_truncate_fd "$work_dir" +_check fuse_test_open_unlink "$work_dir" + +# test_syscalls assumes a change in the source directory is reflected in the +# mountpoint immediately, so it runs only where that holds. It is an extra +# layer on top of the checks above, not a substitute for them. +if [ "$PT_SRC_VISIBLE" = 1 ]; then + # Its socket test binds under the work directory, and sun_path holds + # 108 bytes -- $TEST_MNT$TEST_SRC alone is longer than that. Hand it a + # short symlink instead; a symlinked prefix resolves the same for every + # syscall it makes. /tmp literally, since shortness is the point. + # + # A bare link rather than a directory to put one in: _at_exit does not + # run when the runner's timeout kills the test, and a dangling link is + # the smallest thing to leak outside the work directory. ln without -f, + # so a name mktemp -u handed out twice fails here instead of silently + # stealing another run's link. + short_link=$(mktemp -u /tmp/fuse-pt.XXXXXX) + _at_exit "rm -f '$short_link'" + ln -s "$work_dir" "$short_link" + "$FUSE_TEST_BIN_DIR/test_syscalls" "$short_link" ":$TEST_SRC" \ + ${SYSCALL_ARGS:-} || _fail "test_syscalls failed" +fi + +fuse_umount diff --git a/test/cases/misc/hello-uds.sh b/test/cases/misc/hello-uds.sh new file mode 100755 index 000000000..95f3715df --- /dev/null +++ b/test/cases/misc/hello-uds.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# GROUP: misc + +# The protocol goes over a socket here, not /dev/fuse. +_fuse_no_mount_needed=1 +. "$TEST_LIB/common.sh" + +_require_binary example/hello_ll_uds + +# Relative, hence short: sun_path holds 108 bytes, which the run directory +# alone can exhaust. The daemon inherits this script's cwd ($TEST_LOGDIR) and +# never chdirs, and checks.py runs in it too, so a bare name is the same +# socket from both ends. A name of its own per test also keeps a second run, +# or the root pass beside the non-root one, out of the way. +sockpath=hello_ll.sock + +# It never calls fuse_mount, so none of the mount verbs apply: it serves the +# FUSE protocol over an AF_UNIX socket through fuse_session_custom_io(). +"$FUSE_EXAMPLE_DIR/hello_ll_uds" --socket="$sockpath" \ + >"$TEST_LOGDIR/fs-0-hello_ll_uds.out" 2>&1 & +uds_pid=$! +_at_exit "kill $uds_pid 2>/dev/null" + +_wait_for 10 "[ -S '$sockpath' ]" || _fail "$sockpath never appeared" + +_check fuse_test_uds_init "$sockpath" diff --git a/test/cases/misc/printcap.sh b/test/cases/misc/printcap.sh new file mode 100755 index 000000000..6bec9c693 --- /dev/null +++ b/test/cases/misc/printcap.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# GROUP: misc quick + +. "$TEST_LIB/common.sh" + +_require_binary example/printcap + +# The runner already ran printcap once to build $FUSE_CAPS; this is the case +# that fails rather than silently reporting no capabilities when it breaks. +"$FUSE_EXAMPLE_DIR/printcap" +[ -n "$FUSE_CAPS" ] || _fail "printcap reported no capabilities" diff --git a/test/cases/mount/attrs-default-hello-ll.sh b/test/cases/mount/attrs-default-hello-ll.sh new file mode 100755 index 000000000..fd7596595 --- /dev/null +++ b/test/cases/mount/attrs-default-hello-ll.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# GROUP: mount + +FS_NAME=hello_ll + +mountinfo_assert() +{ + _assert_mount_opt "$TEST_MNT" rw nosuid nodev + _refute_mount_opt "$TEST_MNT" ro +} + +. "$TEST_LIB/mountinfo.sh" diff --git a/test/cases/mount/attrs-default-hello.sh b/test/cases/mount/attrs-default-hello.sh new file mode 100755 index 000000000..72c0b4f57 --- /dev/null +++ b/test/cases/mount/attrs-default-hello.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# GROUP: mount + +FS_NAME=hello + +mountinfo_assert() +{ + _assert_mount_opt "$TEST_MNT" rw nosuid nodev + _refute_mount_opt "$TEST_MNT" ro +} + +. "$TEST_LIB/mountinfo.sh" diff --git a/test/cases/mount/attrs-nodev-hello-ll.sh b/test/cases/mount/attrs-nodev-hello-ll.sh new file mode 100755 index 000000000..73d5a0a00 --- /dev/null +++ b/test/cases/mount/attrs-nodev-hello-ll.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# GROUP: mount + +FS_NAME=hello_ll +MOUNT_OPTS=nodev + +mountinfo_assert() +{ + _assert_mount_opt "$TEST_MNT" nodev +} + +. "$TEST_LIB/mountinfo.sh" diff --git a/test/cases/mount/attrs-nodev-hello.sh b/test/cases/mount/attrs-nodev-hello.sh new file mode 100755 index 000000000..1b13cbd4b --- /dev/null +++ b/test/cases/mount/attrs-nodev-hello.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# GROUP: mount + +FS_NAME=hello +MOUNT_OPTS=nodev + +mountinfo_assert() +{ + _assert_mount_opt "$TEST_MNT" nodev +} + +. "$TEST_LIB/mountinfo.sh" diff --git a/test/cases/mount/attrs-nosuid-hello-ll.sh b/test/cases/mount/attrs-nosuid-hello-ll.sh new file mode 100755 index 000000000..294a220b0 --- /dev/null +++ b/test/cases/mount/attrs-nosuid-hello-ll.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# GROUP: mount + +FS_NAME=hello_ll +MOUNT_OPTS=nosuid + +mountinfo_assert() +{ + _assert_mount_opt "$TEST_MNT" nosuid +} + +. "$TEST_LIB/mountinfo.sh" diff --git a/test/cases/mount/attrs-nosuid-hello.sh b/test/cases/mount/attrs-nosuid-hello.sh new file mode 100755 index 000000000..61cabf8c5 --- /dev/null +++ b/test/cases/mount/attrs-nosuid-hello.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# GROUP: mount + +FS_NAME=hello +MOUNT_OPTS=nosuid + +mountinfo_assert() +{ + _assert_mount_opt "$TEST_MNT" nosuid +} + +. "$TEST_LIB/mountinfo.sh" diff --git a/test/cases/mount/attrs-ro-hello-ll.sh b/test/cases/mount/attrs-ro-hello-ll.sh new file mode 100755 index 000000000..1b59097f3 --- /dev/null +++ b/test/cases/mount/attrs-ro-hello-ll.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# GROUP: mount + +FS_NAME=hello_ll +MOUNT_OPTS=ro + +mountinfo_assert() +{ + _assert_mount_opt "$TEST_MNT" ro + _refute_mount_opt "$TEST_MNT" rw + _assert_super_opt "$TEST_MNT" ro +} + +. "$TEST_LIB/mountinfo.sh" diff --git a/test/cases/mount/attrs-ro-hello.sh b/test/cases/mount/attrs-ro-hello.sh new file mode 100755 index 000000000..4990e6051 --- /dev/null +++ b/test/cases/mount/attrs-ro-hello.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# GROUP: mount + +FS_NAME=hello +MOUNT_OPTS=ro + +mountinfo_assert() +{ + _assert_mount_opt "$TEST_MNT" ro + _refute_mount_opt "$TEST_MNT" rw + _assert_super_opt "$TEST_MNT" ro +} + +. "$TEST_LIB/mountinfo.sh" diff --git a/test/cases/mount/attrs-root-dev-hello-ll.sh b/test/cases/mount/attrs-root-dev-hello-ll.sh new file mode 100755 index 000000000..48afdb6db --- /dev/null +++ b/test/cases/mount/attrs-root-dev-hello-ll.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# GROUP: mount + +. "$TEST_LIB/common.sh" + +_require_root + +FS_NAME=hello_ll +MOUNT_OPTS=dev + +mountinfo_assert() +{ + _refute_mount_opt "$TEST_MNT" nodev +} + +. "$TEST_LIB/mountinfo.sh" diff --git a/test/cases/mount/attrs-root-dev-hello.sh b/test/cases/mount/attrs-root-dev-hello.sh new file mode 100755 index 000000000..e61deaebe --- /dev/null +++ b/test/cases/mount/attrs-root-dev-hello.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# GROUP: mount + +. "$TEST_LIB/common.sh" + +_require_root + +FS_NAME=hello +MOUNT_OPTS=dev + +mountinfo_assert() +{ + _refute_mount_opt "$TEST_MNT" nodev +} + +. "$TEST_LIB/mountinfo.sh" diff --git a/test/cases/mount/attrs-root-suid-hello-ll.sh b/test/cases/mount/attrs-root-suid-hello-ll.sh new file mode 100755 index 000000000..cc6282855 --- /dev/null +++ b/test/cases/mount/attrs-root-suid-hello-ll.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# GROUP: mount + +. "$TEST_LIB/common.sh" + +_require_root + +FS_NAME=hello_ll +MOUNT_OPTS=suid + +mountinfo_assert() +{ + _refute_mount_opt "$TEST_MNT" nosuid +} + +. "$TEST_LIB/mountinfo.sh" diff --git a/test/cases/mount/attrs-root-suid-hello.sh b/test/cases/mount/attrs-root-suid-hello.sh new file mode 100755 index 000000000..3cec3d7b5 --- /dev/null +++ b/test/cases/mount/attrs-root-suid-hello.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# GROUP: mount + +. "$TEST_LIB/common.sh" + +_require_root + +FS_NAME=hello +MOUNT_OPTS=suid + +mountinfo_assert() +{ + _refute_mount_opt "$TEST_MNT" nosuid +} + +. "$TEST_LIB/mountinfo.sh" diff --git a/test/cases/mount/blkdev-fsname-hello-ll.sh b/test/cases/mount/blkdev-fsname-hello-ll.sh new file mode 100755 index 000000000..d21b94320 --- /dev/null +++ b/test/cases/mount/blkdev-fsname-hello-ll.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# GROUP: mount serial + +. "$TEST_LIB/common.sh" + +_require_root +_require_prog losetup + +FS_NAME=hello_ll + +mountinfo_setup() +{ + loop_file=$TEST_TMP/loop.img + dd if=/dev/zero of="$loop_file" bs=1M count=1 status=none + loop_dev=$(losetup -f --show "$loop_file") + _at_exit "losetup -d '$loop_dev'" + MOUNT_OPTS="blkdev,fsname=$loop_dev,subtype=mysub" +} + +mountinfo_assert() +{ + # fstype carries the subtype only where the kernel took it. + _assert_fstype "$TEST_MNT" fuseblk.mysub fuseblk + # The source must be the device name, not 'fuseblk'. + _assert_source "$TEST_MNT" "$loop_dev" +} + +. "$TEST_LIB/mountinfo.sh" diff --git a/test/cases/mount/blkdev-fsname-hello.sh b/test/cases/mount/blkdev-fsname-hello.sh new file mode 100755 index 000000000..d4fc047f7 --- /dev/null +++ b/test/cases/mount/blkdev-fsname-hello.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# GROUP: mount serial + +. "$TEST_LIB/common.sh" + +_require_root +_require_prog losetup + +FS_NAME=hello + +mountinfo_setup() +{ + loop_file=$TEST_TMP/loop.img + dd if=/dev/zero of="$loop_file" bs=1M count=1 status=none + loop_dev=$(losetup -f --show "$loop_file") + _at_exit "losetup -d '$loop_dev'" + MOUNT_OPTS="blkdev,fsname=$loop_dev,subtype=mysub" +} + +mountinfo_assert() +{ + # fstype carries the subtype only where the kernel took it. + _assert_fstype "$TEST_MNT" fuseblk.mysub fuseblk + # The source must be the device name, not 'fuseblk'. + _assert_source "$TEST_MNT" "$loop_dev" +} + +. "$TEST_LIB/mountinfo.sh" diff --git a/test/cases/mount/mountinfo-baseline-hello-ll.sh b/test/cases/mount/mountinfo-baseline-hello-ll.sh new file mode 100755 index 000000000..79660e29e --- /dev/null +++ b/test/cases/mount/mountinfo-baseline-hello-ll.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# GROUP: mount + +FS_NAME=hello_ll + +mountinfo_assert() +{ + _assert_fstype "$TEST_MNT" fuse.hello_ll fuse + _assert_super_opt_prefix "$TEST_MNT" user_id= group_id= +} + +. "$TEST_LIB/mountinfo.sh" diff --git a/test/cases/mount/mountinfo-baseline-hello.sh b/test/cases/mount/mountinfo-baseline-hello.sh new file mode 100755 index 000000000..9419f7bcc --- /dev/null +++ b/test/cases/mount/mountinfo-baseline-hello.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# GROUP: mount + +FS_NAME=hello + +mountinfo_assert() +{ + _assert_fstype "$TEST_MNT" fuse.hello fuse + _assert_super_opt_prefix "$TEST_MNT" user_id= group_id= +} + +. "$TEST_LIB/mountinfo.sh" diff --git a/test/cases/mount/mountinfo-fsname-hello-ll.sh b/test/cases/mount/mountinfo-fsname-hello-ll.sh new file mode 100755 index 000000000..270146282 --- /dev/null +++ b/test/cases/mount/mountinfo-fsname-hello-ll.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# GROUP: mount + +FS_NAME=hello_ll +MOUNT_OPTS=fsname=myfsname + +mountinfo_assert() +{ + _assert_source "$TEST_MNT" myfsname +} + +. "$TEST_LIB/mountinfo.sh" diff --git a/test/cases/mount/mountinfo-fsname-hello.sh b/test/cases/mount/mountinfo-fsname-hello.sh new file mode 100755 index 000000000..342bf49c3 --- /dev/null +++ b/test/cases/mount/mountinfo-fsname-hello.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# GROUP: mount + +FS_NAME=hello +MOUNT_OPTS=fsname=myfsname + +mountinfo_assert() +{ + _assert_source "$TEST_MNT" myfsname +} + +. "$TEST_LIB/mountinfo.sh" diff --git a/test/cases/mount/mountinfo-subtype-fsname-hello-ll.sh b/test/cases/mount/mountinfo-subtype-fsname-hello-ll.sh new file mode 100755 index 000000000..359adb6da --- /dev/null +++ b/test/cases/mount/mountinfo-subtype-fsname-hello-ll.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# GROUP: mount + +FS_NAME=hello_ll +MOUNT_OPTS=subtype=mysub,fsname=myfsname + +mountinfo_assert() +{ + _assert_fstype "$TEST_MNT" fuse.mysub + _assert_source "$TEST_MNT" myfsname mysub#myfsname +} + +. "$TEST_LIB/mountinfo.sh" diff --git a/test/cases/mount/mountinfo-subtype-fsname-hello.sh b/test/cases/mount/mountinfo-subtype-fsname-hello.sh new file mode 100755 index 000000000..2e22b905c --- /dev/null +++ b/test/cases/mount/mountinfo-subtype-fsname-hello.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# GROUP: mount + +FS_NAME=hello +MOUNT_OPTS=subtype=mysub,fsname=myfsname + +mountinfo_assert() +{ + _assert_fstype "$TEST_MNT" fuse.mysub + _assert_source "$TEST_MNT" myfsname mysub#myfsname +} + +. "$TEST_LIB/mountinfo.sh" diff --git a/test/cases/mount/mountinfo-subtype-hello-ll.sh b/test/cases/mount/mountinfo-subtype-hello-ll.sh new file mode 100755 index 000000000..b7d8a2f9f --- /dev/null +++ b/test/cases/mount/mountinfo-subtype-hello-ll.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# GROUP: mount + +FS_NAME=hello_ll +MOUNT_OPTS=subtype=mysub + +mountinfo_assert() +{ + _assert_fstype "$TEST_MNT" fuse.mysub +} + +. "$TEST_LIB/mountinfo.sh" diff --git a/test/cases/mount/mountinfo-subtype-hello.sh b/test/cases/mount/mountinfo-subtype-hello.sh new file mode 100755 index 000000000..5d9e7fd71 --- /dev/null +++ b/test/cases/mount/mountinfo-subtype-hello.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# GROUP: mount + +FS_NAME=hello +MOUNT_OPTS=subtype=mysub + +mountinfo_assert() +{ + _assert_fstype "$TEST_MNT" fuse.mysub +} + +. "$TEST_LIB/mountinfo.sh" diff --git a/test/cases/mount/mountpoint-symlink-parent.sh b/test/cases/mount/mountpoint-symlink-parent.sh new file mode 100755 index 000000000..11479f4e3 --- /dev/null +++ b/test/cases/mount/mountpoint-symlink-parent.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# GROUP: mount +# +# Only the last component of a mountpoint is refused as a symlink, so a +# mountpoint reached through a symlinked parent still mounts. Guards against a +# resolution rule that rejects a symlink anywhere in the path and so breaks +# every user whose mountpoint sits below a symlinked home. + +. "$TEST_LIB/common.sh" + +# Root mounts with mount(2) and reaches fusermount3 only on EPERM, so the +# resolution rule under test is not on the path a root run takes. +_require_nonroot + +mkdir -p "$TEST_TMP/real/mnt" +ln -s "$TEST_TMP/real" "$TEST_TMP/link" + +fuse_mount_at "$TEST_TMP/link/mnt" hello -f >/dev/null + +# Named through the real path, not through the symlink: what is being checked +# is that the mount landed on the inode the symlink resolved to. +_check fuse_test_ismount "$TEST_TMP/real/mnt" +_assert_listdir "$TEST_TMP/real/mnt" hello + +fuse_umount diff --git a/test/cases/mount/mountpoint-validation.sh b/test/cases/mount/mountpoint-validation.sh new file mode 100755 index 000000000..7111cba4a --- /dev/null +++ b/test/cases/mount/mountpoint-validation.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# GROUP: mount quick +# +# fusermount3 refuses a mountpoint it cannot validate. Every case here is +# rejected before mount(2) is reached, so this needs the kernel module but +# not a setuid fusermount3. + +_fuse_no_mount_needed=1 +. "$TEST_LIB/common.sh" + +_require_linux "fusermount3" +# Root skips the mountpoint checks entirely and follows a symlinked mountpoint. +_require_nonroot +_require_fuse_device +_require_binary util/fusermount3 + +: >"$TEST_TMP/plain" +ln -s "$TEST_SRC" "$TEST_TMP/link-dir" +ln -s "$TEST_TMP/plain" "$TEST_TMP/link-reg" +ln -s "$TEST_TMP/gone" "$TEST_TMP/link-dangling" +mkfifo "$TEST_TMP/fifo" +mkdir "$TEST_TMP/nowrite" +chmod 500 "$TEST_TMP/nowrite" + +# O_NOFOLLOW makes fstat() see the final symlink itself, so the target type +# cannot make a symlink acceptable as a mountpoint. +_check fuse_test_fusermount_rejects "$TEST_TMP/link-dir" not_dir_or_regular +_check fuse_test_fusermount_rejects "$TEST_TMP/link-reg" not_dir_or_regular +_check fuse_test_fusermount_rejects "$TEST_TMP/link-dangling" not_dir_or_regular + +_check fuse_test_fusermount_rejects "$TEST_TMP/fifo" not_dir_or_regular +_check fuse_test_fusermount_rejects "$TEST_TMP/nowrite" no_write_access +_check fuse_test_fusermount_rejects "$TEST_TMP/missing" no_such_file diff --git a/test/cases/notify/file-size-no-notify.sh b/test/cases/notify/file-size-no-notify.sh new file mode 100755 index 000000000..103b8d246 --- /dev/null +++ b/test/cases/notify/file-size-no-notify.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# GROUP: notify + +FS_NAME=invalidate_path +NOTIFY=0 +NOTIFY_MODE=size + +. "$TEST_LIB/notify.sh" diff --git a/test/cases/notify/file-size.sh b/test/cases/notify/file-size.sh new file mode 100755 index 000000000..86d1c7589 --- /dev/null +++ b/test/cases/notify/file-size.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# GROUP: notify + +FS_NAME=invalidate_path +NOTIFY=1 +NOTIFY_MODE=size + +. "$TEST_LIB/notify.sh" diff --git a/test/cases/notify/inval-entry-expire-no-notify.sh b/test/cases/notify/inval-entry-expire-no-notify.sh new file mode 100755 index 000000000..b13c353af --- /dev/null +++ b/test/cases/notify/inval-entry-expire-no-notify.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# GROUP: notify + +. "$TEST_LIB/common.sh" + +# The kernel has to support expiring an entry rather than +# invalidating it; printcap reports whether it does. +_require_cap FUSE_CAP_EXPIRE_ONLY + +FS_NAME=notify_inval_entry +NOTIFY=0 +NOTIFY_MODE=entry +FS_ARGS="--timeout=5 --only-expire" + +. "$TEST_LIB/notify.sh" diff --git a/test/cases/notify/inval-entry-expire.sh b/test/cases/notify/inval-entry-expire.sh new file mode 100755 index 000000000..91b63d6ea --- /dev/null +++ b/test/cases/notify/inval-entry-expire.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# GROUP: notify + +. "$TEST_LIB/common.sh" + +# The kernel has to support expiring an entry rather than +# invalidating it; printcap reports whether it does. +_require_cap FUSE_CAP_EXPIRE_ONLY + +FS_NAME=notify_inval_entry +NOTIFY=1 +NOTIFY_MODE=entry +FS_ARGS="--timeout=5 --only-expire" + +. "$TEST_LIB/notify.sh" diff --git a/test/cases/notify/inval-entry-inc-epoch-no-notify.sh b/test/cases/notify/inval-entry-inc-epoch-no-notify.sh new file mode 100755 index 000000000..9f7852afc --- /dev/null +++ b/test/cases/notify/inval-entry-inc-epoch-no-notify.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# GROUP: notify + +FS_NAME=notify_inval_entry +NOTIFY=0 +NOTIFY_MODE=entry +FS_ARGS="--timeout=5 --inc-epoch" +# There is no capability flag for the epoch counter, so the gate +# is what notify_inval_entry itself prints when the kernel +# refuses the request. +MARKER_ABSENT="not supported by kernel" + +. "$TEST_LIB/notify.sh" diff --git a/test/cases/notify/inval-entry-inc-epoch.sh b/test/cases/notify/inval-entry-inc-epoch.sh new file mode 100755 index 000000000..5fcfb06e1 --- /dev/null +++ b/test/cases/notify/inval-entry-inc-epoch.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# GROUP: notify + +FS_NAME=notify_inval_entry +NOTIFY=1 +NOTIFY_MODE=entry +FS_ARGS="--timeout=5 --inc-epoch" +# There is no capability flag for the epoch counter, so the gate +# is what notify_inval_entry itself prints when the kernel +# refuses the request. +MARKER_ABSENT="not supported by kernel" + +. "$TEST_LIB/notify.sh" diff --git a/test/cases/notify/inval-entry-invalidate-no-notify.sh b/test/cases/notify/inval-entry-invalidate-no-notify.sh new file mode 100755 index 000000000..702950a18 --- /dev/null +++ b/test/cases/notify/inval-entry-invalidate-no-notify.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# GROUP: notify + +FS_NAME=notify_inval_entry +NOTIFY=0 +NOTIFY_MODE=entry +FS_ARGS="--timeout=5" + +. "$TEST_LIB/notify.sh" diff --git a/test/cases/notify/inval-entry-invalidate.sh b/test/cases/notify/inval-entry-invalidate.sh new file mode 100755 index 000000000..d81f75a9c --- /dev/null +++ b/test/cases/notify/inval-entry-invalidate.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# GROUP: notify + +FS_NAME=notify_inval_entry +NOTIFY=1 +NOTIFY_MODE=entry +FS_ARGS="--timeout=5" + +. "$TEST_LIB/notify.sh" diff --git a/test/cases/notify/inval-inode-no-notify.sh b/test/cases/notify/inval-inode-no-notify.sh new file mode 100755 index 000000000..ae7aac1c9 --- /dev/null +++ b/test/cases/notify/inval-inode-no-notify.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# GROUP: notify + +FS_NAME=notify_inval_inode +NOTIFY=0 +NOTIFY_MODE=content + +. "$TEST_LIB/notify.sh" diff --git a/test/cases/notify/inval-inode.sh b/test/cases/notify/inval-inode.sh new file mode 100755 index 000000000..f52b43f3a --- /dev/null +++ b/test/cases/notify/inval-inode.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# GROUP: notify + +FS_NAME=notify_inval_inode +NOTIFY=1 +NOTIFY_MODE=content + +. "$TEST_LIB/notify.sh" diff --git a/test/cases/notify/invalidate-path-no-notify.sh b/test/cases/notify/invalidate-path-no-notify.sh new file mode 100755 index 000000000..fb66a8519 --- /dev/null +++ b/test/cases/notify/invalidate-path-no-notify.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# GROUP: notify + +FS_NAME=invalidate_path +NOTIFY=0 +NOTIFY_MODE=content + +. "$TEST_LIB/notify.sh" diff --git a/test/cases/notify/invalidate-path.sh b/test/cases/notify/invalidate-path.sh new file mode 100755 index 000000000..5f2c7c1e8 --- /dev/null +++ b/test/cases/notify/invalidate-path.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# GROUP: notify + +FS_NAME=invalidate_path +NOTIFY=1 +NOTIFY_MODE=content + +. "$TEST_LIB/notify.sh" diff --git a/test/cases/notify/prune-no-notify.sh b/test/cases/notify/prune-no-notify.sh new file mode 100755 index 000000000..84b74349c --- /dev/null +++ b/test/cases/notify/prune-no-notify.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# GROUP: notify + +FS_NAME=notify_prune +NOTIFY=0 +NOTIFY_MODE=prune +# There is no capability flag for FUSE_NOTIFY_PRUNE, so the gate +# is what notify_prune itself prints when the kernel refuses the +# request. +MARKER_ABSENT="not supported by kernel" + +. "$TEST_LIB/notify.sh" diff --git a/test/cases/notify/prune.sh b/test/cases/notify/prune.sh new file mode 100755 index 000000000..4e260906d --- /dev/null +++ b/test/cases/notify/prune.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# GROUP: notify + +FS_NAME=notify_prune +NOTIFY=1 +NOTIFY_MODE=prune +# There is no capability flag for FUSE_NOTIFY_PRUNE, so the gate +# is what notify_prune itself prints when the kernel refuses the +# request. +MARKER_ABSENT="not supported by kernel" + +. "$TEST_LIB/notify.sh" diff --git a/test/cases/notify/store-retrieve-no-notify.sh b/test/cases/notify/store-retrieve-no-notify.sh new file mode 100755 index 000000000..8fbf28895 --- /dev/null +++ b/test/cases/notify/store-retrieve-no-notify.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# GROUP: notify + +FS_NAME=notify_store_retrieve +NOTIFY=0 +NOTIFY_MODE=content + +. "$TEST_LIB/notify.sh" diff --git a/test/cases/notify/store-retrieve.sh b/test/cases/notify/store-retrieve.sh new file mode 100755 index 000000000..328402eac --- /dev/null +++ b/test/cases/notify/store-retrieve.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# GROUP: notify + +FS_NAME=notify_store_retrieve +NOTIFY=1 +NOTIFY_MODE=content + +. "$TEST_LIB/notify.sh" diff --git a/test/cases/unit/test_abi b/test/cases/unit/test_abi new file mode 100644 index 000000000..7dafe0e5d --- /dev/null +++ b/test/cases/unit/test_abi @@ -0,0 +1 @@ +# GROUP: unit quick diff --git a/test/ci-build.sh b/test/ci-build.sh index 40bb79e8d..3006795f8 100755 --- a/test/ci-build.sh +++ b/test/ci-build.sh @@ -1,155 +1,214 @@ -#!/bin/bash -x +#!/bin/bash +# +# Build one libfuse configuration and run the test suite against it. +# +# One invocation is one configuration. The workflow's build matrix supplies +# the arguments for each of them, and CI runs them as parallel matrix jobs. set -e -TEST_CMD="pytest -v --maxfail=1 --log-level=INFO --log-cli-level=INFO test/" -SAN="-Db_sanitize=address,undefined" - -# not default -export UBSAN_OPTIONS=halt_on_error=1 +usage() +{ + cat >&2 <<EOF +usage: $0 --name NAME [options] + + --name NAME label for the build, install and log directories + --cc CC C compiler (default: cc) + --cxx CXX C++ compiler; left unset when not given + --sanitize build with the address and undefined-behaviour sanitizers + --valgrind run the filesystem daemons under valgrind + --root run the suite as root instead of an unprivileged user + --io-uring also exercise the fuse-io-uring transport + --meson-opt OPT extra meson option; repeatable + --work-dir DIR where to build and log, verbatim +EOF + exit 1 +} + +need_arg() +{ + [ $# -ge 2 ] || { echo "$0: $1 needs an argument" >&2; exit 1; } +} + +NAME= +CC_BIN=cc +CXX_BIN= +SANITIZE=0 +VALGRIND=0 +ROOT=0 +IO_URING=0 +MESON_OPTS=() +cli_work_dir= + +while [ $# -gt 0 ]; do + case $1 in + --name) need_arg "$@"; NAME=$2; shift 2 ;; + --cc) need_arg "$@"; CC_BIN=$2; shift 2 ;; + --cxx) need_arg "$@"; CXX_BIN=$2; shift 2 ;; + --meson-opt) need_arg "$@"; MESON_OPTS+=("$2"); shift 2 ;; + --work-dir) need_arg "$@"; cli_work_dir=$2; shift 2 ;; + --sanitize) SANITIZE=1; shift ;; + --valgrind) VALGRIND=1; shift ;; + --root) ROOT=1; shift ;; + --io-uring) IO_URING=1; shift ;; + *) usage ;; + esac +done +[ -n "${NAME}" ] || usage # Make sure binaries can be accessed when invoked by root. umask 0022 -# There are tests that run as root but without CAP_DAC_OVERRIDE. To allow these -# to launch built binaries, the directory tree must be accessible to the root -# user. Since the source directory isn't necessarily accessible to root, we -# build and run tests in a temporary directory that we can set up to be world -# readable/executable. -SOURCE_DIR="$(readlink -f .)" -TEST_DIR="$(mktemp -dt libfuse-build-XXXXXX)" - -PREFIX_DIR="$(mktemp -dt libfuse-install-XXXXXXX)" - -chmod 0755 "${TEST_DIR}" -cd "${TEST_DIR}" -echo "Running in ${TEST_DIR}" +SOURCE_DIR="$(readlink -f "$(dirname "$0")/..")" + +# Where this run's builds and logs go. --work-dir is used verbatim so CI can +# predict the path it later uploads; otherwise take the base from run-tests.py +# (persisted config, else /var/tmp/fuse-tests) and append a dedicated leaf. +# Asking the runner keeps the precedence in one place instead of parsing an +# INI in bash. +WORK_DIR="${cli_work_dir:-}" +if [ -z "${WORK_DIR}" ]; then + WORK_DIR="$(python3 "${SOURCE_DIR}/test/run-tests.py" --print-base-dir)" + WORK_DIR="${WORK_DIR}/ci-${USER:-unknown}-$(date +%y%m%d%H%M%S)" +fi +WORK_DIR="$(readlink -f "${WORK_DIR}")" # resolve before the cd below + +# Named after the configuration, so several of them share one work directory +# without colliding and the logs of a whole run stay together. +BUILD_DIR="${WORK_DIR}/build-${NAME}" +PREFIX_DIR="${WORK_DIR}/install-${NAME}" +RUN_DIR="${WORK_DIR}/run" + +# A stale tree from a previous failed run would poison this one. The install +# prefix is owned by root from the last `ninja install`. +rm -rf "${BUILD_DIR}" +sudo rm -rf "${PREFIX_DIR}" +mkdir -p "${BUILD_DIR}" "${PREFIX_DIR}" "${RUN_DIR}" +# There are tests that run as root but without CAP_DAC_OVERRIDE. To allow those +# to launch built binaries, every directory on the way in must be traversable +# by plain permission bits. +chmod 0755 "${WORK_DIR}" "${BUILD_DIR}" "${PREFIX_DIR}" "${RUN_DIR}" +cd "${BUILD_DIR}" +echo "Building ${NAME} in ${BUILD_DIR}" + +echo "=== System ===" +uname -a +lsb_release -a 2>/dev/null || cat /etc/os-release 2>/dev/null || true +lscpu || true +echo "===============" + +export CC="${CC_BIN}" +if [ -n "${CXX_BIN}" ]; then + export CXX="${CXX_BIN}" +else + unset CXX +fi + +# Exported rather than assigned: run-tests.py reads it from the environment of +# the process meson spawns. +if [ "${VALGRIND}" = 1 ]; then + export TEST_WITH_VALGRIND=true +else + export TEST_WITH_VALGRIND=false +fi cp -v "${SOURCE_DIR}/test/lsan_suppress.txt" . -export LSAN_OPTIONS="suppressions=$(pwd)/lsan_suppress.txt" +export LSAN_OPTIONS="suppressions=${BUILD_DIR}/lsan_suppress.txt" export ASAN_OPTIONS="detect_leaks=1" -export CC - -non_sanitized_build() -( - echo "Standard build (without sanitizers)" - for CC in gcc gcc-9 gcc-10 clang; do - echo "=== Building with ${CC} ===" - mkdir build-${CC}; pushd build-${CC} - if [ "${CC}" == "clang" ]; then - export CXX="clang++" - export TEST_WITH_VALGRIND=false - else - unset CXX - export TEST_WITH_VALGRIND=true - fi - if [ ${CC} == 'gcc-7' ]; then - build_opts='-D b_lundef=false' - else - build_opts='' - fi - if [ ${CC} == 'gcc-10' ]; then - build_opts='-Dc_args=-flto=auto' - else - build_opts='' - fi - - meson setup -Dprefix=${PREFIX_DIR} -D werror=true ${build_opts} "${SOURCE_DIR}" || (cat meson-logs/meson-log.txt; false) - ninja - sudo env PATH=$PATH ninja install - - # libfuse will first try the install path and then system defaults - sudo chmod 4755 ${PREFIX_DIR}/bin/fusermount3 - - # also needed for some of the tests - sudo chown root:root util/fusermount3 - sudo chmod 4755 util/fusermount3 - - ${TEST_CMD} - popd - rm -fr build-${CC} - sudo rm -fr ${PREFIX_DIR} - - done -) - -sanitized_build() -( - echo "=== Building with clang and sanitizers" - - mkdir build-san; pushd build-san - - meson setup -Dprefix=${PREFIX_DIR} -D werror=true\ - "${SOURCE_DIR}" \ - || (cat meson-logs/meson-log.txt; false) - meson configure $SAN - - # b_lundef=false is required to work around clang - # bug, cf. https://groups.google.com/forum/#!topic/mesonbuild/tgEdAXIIdC4 - meson configure -D b_lundef=false - - # additional options - if [[ $# -gt 0 ]]; then - meson configure "$@" - fi - - # print all options - meson configure --no-pager - - # reconfigure to ensure it uses all additional options +export UBSAN_OPTIONS=halt_on_error=1 # not the default + +echo "=== Environment ===" +echo "Configuration: ${NAME}" +echo "CC: ${CC}" +echo "CXX: ${CXX-}" +echo "Sanitize: ${SANITIZE}" +echo "LSAN_OPTIONS: ${LSAN_OPTIONS}" +echo "ASAN_OPTIONS: ${ASAN_OPTIONS}" +echo "UBSAN_OPTIONS: ${UBSAN_OPTIONS}" +echo "Valgrind: ${TEST_WITH_VALGRIND}" +echo "Root: ${ROOT}" +echo "IO-uring: ${IO_URING}" +echo "===================" + +meson setup -Dprefix="${PREFIX_DIR}" -Dwerror=true "${MESON_OPTS[@]}" \ + "${SOURCE_DIR}" || { cat meson-logs/meson-log.txt; false; } + +if [ "${SANITIZE}" = 1 ]; then + meson configure -Db_sanitize=address,undefined + # b_lundef=false is required to work around a clang bug, cf. + # https://groups.google.com/forum/#!topic/mesonbuild/tgEdAXIIdC4 + meson configure -Db_lundef=false + # Reconfigure so the build actually uses them. meson setup --reconfigure "${SOURCE_DIR}" - ninja - sudo env PATH=$PATH ninja install - sudo chmod 4755 ${PREFIX_DIR}/bin/fusermount3 - - # also needed for some of the tests - sudo chown root:root util/fusermount3 - sudo chmod 4755 util/fusermount3 - - # Test as root and regular user - sudo env PATH=$PATH ${TEST_CMD} - # Cleanup temporary files (since they are now owned by root) - sudo rm -rf test/.pytest_cache/ test/__pycache__ - - ${TEST_CMD} - - popd - rm -fr build-san - sudo rm -fr ${PREFIX_DIR} -) - -# 32-bit sanitized build -export CC=clang -export CXX=clang++ -export CFLAGS="-m32" -export CXXFLAGS="-m32" -export LDFLAGS="-m32" -export PKG_CONFIG_PATH="/usr/lib/i386-linux-gnu/pkgconfig" -TEST_WITH_VALGRIND=false -sanitized_build -unset CFLAGS -unset CXXFLAGS -unset LDFLAGS -unset PKG_CONFIG_PATH -unset TEST_WITH_VALGRIND -unset CC -unset CXX - -# Sanitized build -export CC=clang -export CXX=clang++ -TEST_WITH_VALGRIND=false -sanitized_build - -# Sanitized build without libc versioned symbols -export CC=clang -export CXX=clang++ -sanitized_build "-Ddisable-libc-symbol-version=true" - -non_sanitized_build - -# Documentation. -(cd "${SOURCE_DIR}"; doxygen doc/Doxyfile) - -# Clean up. -rm -rf "${TEST_DIR}" +fi + +meson configure --no-pager # what was actually configured +ninja +sudo env PATH="$PATH" ninja install + +# libfuse will first try the install path and then system defaults. +sudo chmod 4755 "${PREFIX_DIR}/bin/fusermount3" +if [ -x "${PREFIX_DIR}/sbin/fuservicemount3" ]; then + sudo chmod 4755 "${PREFIX_DIR}/sbin/fuservicemount3" +fi + +restore_io_uring() +{ + echo "${FUSE_URING_WAS}" | sudo tee "$1" >/dev/null + [ -z "${IO_URING_DISABLED_WAS}" ] || + sudo sysctl -q -w "kernel.io_uring_disabled=${IO_URING_DISABLED_WAS}" +} + +if [ "${IO_URING}" = 1 ]; then + # The kernel parameter is global and defaults to off, and this script is + # also run by hand, so put back whatever the machine had. Failing to enable + # it is fatal: run-tests.py skips its io-uring invocation where the kernel + # does not offer the transport, which is right for a developer machine and + # wrong here - a job that silently stops testing io-uring is what this + # exists to prevent. + param=/sys/module/fuse/parameters/enable_uring + sysctl_param=/proc/sys/kernel/io_uring_disabled + FUSE_URING_WAS="$(cat "${param}")" || + { echo "this kernel has no fuse io-uring support"; exit 1; } + # Non-zero denies the daemons io_uring_setup(), which drops them back to + # /dev/fuse. Empty before 6.6, where nothing restricts ring creation. + IO_URING_DISABLED_WAS="$(cat "${sysctl_param}" 2>/dev/null || true)" + # Armed before either write, so a failure between them still restores. + trap "restore_io_uring ${param}" EXIT + echo Y | sudo tee "${param}" >/dev/null + [ -z "${IO_URING_DISABLED_WAS}" ] || + sudo sysctl -q -w kernel.io_uring_disabled=0 +fi + +RUN_TESTS_OPTS=(--build-dir .) +[ "${IO_URING}" = 1 ] && RUN_TESTS_OPTS+=(--io-uring) + +if [ "${ROOT}" = 1 ]; then + SUDO=(sudo) +else + SUDO=() + RUN_TESTS_OPTS+=(--setuid-helpers) +fi + +# sudo resets the environment, so PATH and FUSE_TEST_RUN_DIR are passed +# through env rather than shell-prefix assignments, which sudo would +# otherwise drop; harmless when SUDO is empty. Calling run-tests.py directly, +# never through `meson test`, keeps its per-test results in the job log +# instead of only printing output on failure. +rc=0 +"${SUDO[@]}" env PATH="$PATH" FUSE_TEST_RUN_DIR="${RUN_DIR}/${NAME}" \ + timeout 1800 python3 "${SOURCE_DIR}/test/run-tests.py" \ + "${RUN_TESTS_OPTS[@]}" || rc=$? + +if [ "${ROOT}" = 1 ]; then + # upload-artifact has to read what root wrote -- before the failure is + # propagated, or one unreadable file fails the whole upload of the very + # run whose logs are wanted. + sudo chown -R "$(id -u):$(id -g)" "${RUN_DIR}" +fi +[ "${rc}" = 0 ] || exit "${rc}" + +# Only reached when everything above passed, because of set -e: a failed run +# has to stay inspectable. The logs are kept either way, and are the product. +rm -rf "${BUILD_DIR}" +sudo rm -rf "${PREFIX_DIR}" diff --git a/test/ci/doxygen.sh b/test/ci/doxygen.sh new file mode 100755 index 000000000..3c103cfee --- /dev/null +++ b/test/ci/doxygen.sh @@ -0,0 +1,8 @@ +#!/bin/bash +# The API documentation build. Not a build configuration: it needs no compiler +# of its own and none of them has to have run first. + +set -e + +cd "$(readlink -f "$(dirname "$0")/../..")" +doxygen doc/Doxyfile diff --git a/test/ci/prepare-runner.sh b/test/ci/prepare-runner.sh new file mode 100755 index 000000000..92dda111a --- /dev/null +++ b/test/ci/prepare-runner.sh @@ -0,0 +1,48 @@ +#!/bin/bash +# +# Host tuning a CI runner needs and a developer machine must not get. +# +# Cores must land in the test's own log directory, which run-tests.py makes the +# cwd of every test. ubuntu-latest ships apport, so core_pattern is a pipe and +# every core disappears into a collector the job then throws away. %p keeps two +# crashing processes in one test from overwriting each other, and %e is how the +# runner finds the binary to hand gdb. +# +# Deliberately not part of ci-build.sh: that script is also run by hand, and a +# build script has no business rewriting a global kernel tunable -- or, below, +# the permission bits on directories outside the tree it builds -- on someone's +# workstation. Locally the runner's own warning is enough, and it still fetches +# cores through coredumpctl. +# +# Usage: prepare-runner.sh <work-dir> + +set -e + +# Before the writes below, so a mistyped invocation leaves the host alone. +# Absolute, because the ancestor walk further down ends at / and `dirname` on +# a relative path stops at "." instead -- which is a loop, not a walk. +WORK_DIR="$(readlink -f "${1:?usage: prepare-runner.sh <work-dir>}")" +SOURCE_DIR="$(readlink -f "$(dirname "$0")/../..")" + +sudo sysctl -w kernel.core_pattern=core.%e.%p +sudo sysctl -w kernel.core_uses_pid=0 + +# There are tests that run as root but without CAP_DAC_OVERRIDE. To allow those +# to launch built binaries, every ancestor of the work directory must be +# traversable by plain permission bits; a 0750 home is the usual culprit. This +# script does not create the work directory itself -- ci-build.sh chmods that +# and the directories below it -- only the ancestors above it. +dir="$(dirname "${WORK_DIR}")" +while [ "${dir}" != / ]; do + if [ -z "$(find "${dir}" -maxdepth 0 -perm -001)" ]; then + echo "making ${dir} traversable for the capability-less tests" + chmod o+x "${dir}" 2>/dev/null || sudo chmod o+x "${dir}" + fi + dir="$(dirname "${dir}")" +done +# Assert it rather than trust the loop: a test that skips itself here leaves a +# green job that stopped exercising drop_privileges, which is the one outcome +# CI must not produce. +python3 "${SOURCE_DIR}/test/cases/lib/checks.py" fuse_test_reachable_without_caps \ + "$(dirname "${WORK_DIR}")" || + { echo "use --work-dir to build somewhere reachable"; exit 1; } diff --git a/test/conftest.py b/test/conftest.py deleted file mode 100644 index f52818928..000000000 --- a/test/conftest.py +++ /dev/null @@ -1,97 +0,0 @@ -#!/usr/bin/env python3 - -import sys -import pytest -import time -import re -import os -import threading - - -# If a test fails, wait a moment before retrieving the captured -# stdout/stderr. When using a server process, this makes sure that we capture -# any potential output of the server that comes *after* a test has failed. For -# example, if a request handler raises an exception, the server first signals an -# error to FUSE (causing the test to fail), and then logs the exception. Without -# the extra delay, the exception will go into nowhere. -@pytest.hookimpl(hookwrapper=True) -def pytest_pyfunc_call(pyfuncitem): - outcome = yield - failed = outcome.excinfo is not None - if failed: - time.sleep(1) - - -class OutputChecker: - '''Check output data for suspicious patterns. - - Everything written to check_output.fd will be scanned for suspicious - messages and then written to sys.stdout. - ''' - - def __init__(self): - (fd_r, fd_w) = os.pipe() - self.fd = fd_w - self._false_positives = [] - self._buf = bytearray() - self._thread = threading.Thread(target=self._loop, daemon=True, args=(fd_r,)) - self._thread.start() - - def register_output(self, pattern, count=1, flags=re.MULTILINE): - '''Register *pattern* as false positive for output checking - - This prevents the test from failing because the output otherwise - appears suspicious. - ''' - - self._false_positives.append((pattern, flags, count)) - - def _loop(self, ifd): - BUFSIZE = 128*1024 - ofd = sys.stdout.fileno() - while True: - buf = os.read(ifd, BUFSIZE) - if not buf: - break - os.write(ofd, buf) - self._buf += buf - - def _check(self): - os.close(self.fd) - self._thread.join() - - buf = self._buf.decode('utf8', errors='replace') - - # Strip out false positives - for (pattern, flags, count) in self._false_positives: - cp = re.compile(pattern, flags) - (buf, cnt) = cp.subn('', buf, count=count) - - patterns = [ r'\b{}\b'.format(x) for x in - ('exception', 'error', 'warning', 'fatal', 'traceback', - 'fault', 'crash(?:ed)?', 'abort(?:ed)', - 'uninitiali[zs]ed') ] - patterns += ['^==[0-9]+== '] - - for pattern in patterns: - cp = re.compile(pattern, re.IGNORECASE | re.MULTILINE) - hit = cp.search(buf) - if hit: - raise AssertionError('Suspicious output to stderr (matched "%s")' - % hit.group(0)) - -@pytest.fixture() -def output_checker(request): - checker = OutputChecker() - yield checker - checker._check() - - -# Make test outcome available to fixtures -# (from https://github.com/pytest-dev/pytest/issues/230) -@pytest.hookimpl(hookwrapper=True, tryfirst=True) -def pytest_runtest_makereport(item, call): - outcome = yield - rep = outcome.get_result() - setattr(item, "rep_" + rep.when, rep) - return rep diff --git a/test/hello.c b/test/hello.c index a07df0e48..cf576689b 100644 --- a/test/hello.c +++ b/test/hello.c @@ -3,7 +3,7 @@ * Copyright (C) 2001-2007 Miklos Szeredi <miklos@szeredi.hu> * * This program can be distributed under the terms of the GNU GPLv2. - * See the file COPYING. + * See the file GPL2.txt. */ /** @file diff --git a/test/meson.build b/test/meson.build index a9e339fc1..c34b62027 100644 --- a/test/meson.build +++ b/test/meson.build @@ -1,6 +1,6 @@ # Compile helper programs td = [] -foreach prog: [ 'test_write_cache', 'test_setattr', 'hello' ] +foreach prog: [ 'test_write_cache', 'test_setattr', 'test_api_30', 'hello' ] td += executable(prog, prog + '.c', include_directories: include_dirs, link_with: [ libfuse ], @@ -19,24 +19,43 @@ td += executable('release_unlink_race', 'release_unlink_race.c', td += executable('test_want_conversion', 'test_want_conversion.c', dependencies: [ libfuse_dep ], install: false) +td += executable('test_signals', 'test_signals.c', + dependencies: [ libfuse_dep, thread_dep ], + install: false) td += executable('test_abi', 'test_abi.c', dependencies: [ libfuse_dep ], install: false) -test_scripts = [ 'conftest.py', 'pytest.ini', 'test_examples.py', - 'util.py', 'test_ctests.py', 'test_custom_io.py' ] -td += custom_target('test_scripts', input: test_scripts, - output: test_scripts, build_by_default: true, - command: ['cp', '-fPp', - '@INPUT@', meson.current_build_dir() ]) - -# Provide something helpful when running 'ninja test' - if meson.is_subproject() - test('libfuse is a subproject, skipping tests', executable('wrong_command', - 'wrong_command.c', install: false, - c_args: [ '-DMESON_IS_SUBPROJECT' ])) + # Skipped rather than run: the tests mount filesystems, which is not + # something a parent project's `meson test` should do. Exit 77 is how a + # test tells meson SKIP. python3 is optional here because nothing else + # in this branch needs it, and a parent must still be able to configure. + python3 = find_program('python3', required: false) + if python3.found() + test('libfuse is a subproject, skipping tests', python3, + args: ['-c', + 'import sys; print("run them with test/run-tests.py ' + + '--build-dir <builddir>", file=sys.stderr); sys.exit(77)']) + endif else - test('wrong_command', executable('wrong_command', 'wrong_command.c', - install: false)) + # The same definition works both as a regular user and under sudo: + # invoke 'meson test -C build' for the unprivileged subset, or + # 'sudo -E meson test -C build' to also exercise the tests that require + # root (they pick the uid up at runtime). + # + # Nothing is copied into the build tree: the runner, + # its library and the test scripts are read from the source dir, and + # the only build artefacts a test needs are the binaries, which it + # reaches through --build-dir. That also means no custom_target and no + # rebuild after editing a test. + # + # No --run-dir: it defaults under /var/tmp/fuse-tests, and CI redirects + # it with $FUSE_TEST_RUN_DIR so the logs outlive the build tree. + runner = find_program('run-tests.py') + test('fuse-tests', runner, + args: ['--build-dir', meson.project_build_root()], + depends: td, + timeout: 1800, + is_parallel: false) # the runner does its own -j endif diff --git a/test/pytest.ini b/test/pytest.ini deleted file mode 100644 index bbc8de878..000000000 --- a/test/pytest.ini +++ /dev/null @@ -1,6 +0,0 @@ -[pytest] -addopts = --verbose --assert=rewrite --tb=native -x -r a -markers = - uses_fuse: Indicates that FUSE is supported. -log_cli=true -faulthandler_timeout=60 diff --git a/test/release_unlink_race.c b/test/release_unlink_race.c index 2edb20044..f7b7b81be 100644 --- a/test/release_unlink_race.c +++ b/test/release_unlink_race.c @@ -1,6 +1,6 @@ /* This program can be distributed under the terms of the GNU GPLv2. - See the file COPYING. + See the file GPL2.txt. */ #define FUSE_USE_VERSION 31 diff --git a/test/run-tests.py b/test/run-tests.py new file mode 100755 index 000000000..d536c0c71 --- /dev/null +++ b/test/run-tests.py @@ -0,0 +1,1648 @@ +#!/usr/bin/env python3 +"""Run the libfuse shell test suite. + +Every executable cases/**/*.sh outside cases/lib is a test, as is every entry +in a directory is_shell_exception_dir() names, where the file name is a +build-tree binary to run instead. A test's name is the path below cases/ +without the extension. Exit code 0 is a pass, 77 a self-declared skip and +anything else a failure. + +The runner knows nothing about FUSE: it owns discovery, working directories, +containment, timeouts, parallelism, logs, core dumps and the summary. The FUSE +verbs live in cases/lib/common.sh. +""" + +import argparse +import ctypes +import fnmatch +import os +import platform +import re +import resource +import shutil +import signal +import stat +import subprocess +import sys +import threading +import time +from concurrent.futures import ThreadPoolExecutor, as_completed +from dataclasses import dataclass, field, replace +from pathlib import Path + +# Everything the suite reads lives beside this file in the source tree; the +# build directory supplies binaries and nothing else, so a test edited in the +# source tree runs without a rebuild. +TEST_DIR = Path(__file__).resolve().parent +CASES_DIR = TEST_DIR / 'cases' +# What the cases source, beside them rather than above them; discovery skips it. +LIB_DIR = CASES_DIR / 'lib' +EXCLUDE_FILE = TEST_DIR / 'exclude' + +DEFAULT_TIMEOUT = 60.0 # seconds; per-script "# TIMEOUT:" overrides +SKIP_EXIT_CODE = 77 # automake convention +LAUNCH_FAILED_EXIT_CODE = 125 # launcher could not establish containment +LAUNCH_FAILED_MARKER = 'run-tests: containment failed' + +# util/install_helper.sh installs exactly these setuid; nothing else in util/ +# needs the bit. +SETUID_HELPERS = ('fusermount3', 'fuservicemount3') + +GDB_TIMEOUT = 120.0 # gdb can wedge on a huge core or absent symbols +GDB_TOP_FRAMES = 12 # frames inlined into the summary + +# A task wedged in an uninterruptible FUSE request does not die on SIGKILL, so +# waiting for one without a bound hangs the run itself. +REAP_TIMEOUT = 30.0 # seconds to wait for a killed test to be gone + +# A daemon under valgrind is an order of magnitude slower, so every timeout is +# scaled by one constant rather than each script carrying a second number for a +# mode most runs do not use. +VALGRIND_TIMEOUT_FACTOR = 10 +# --error-exitcode is what makes a finding fail the test: the daemon exits 99 +# and fuse_umount's existing status check catches it. Leak checking stays off; +# turning it on is a separate decision with its own noise budget. +VALGRIND_PREFIX = 'valgrind -q --error-exitcode=99 --' +# ==PID== is valgrind's standard output, --PID-- its warnings; the report is +# not the verdict, the daemon's exit status is. +_VALGRIND_LINE_RE = re.compile(r'^(?:==|--)[0-9]+(?:==|--) .*$', re.MULTILINE) + +# Test output must land somewhere predictable, so this is the default base for +# everything the suite writes. /var/tmp rather than /tmp because it survives a +# reboot and no tmpfiles cleaner walks it mid-run, and a fixed name rather than +# a mktemp directory because a path you cannot predict is a path you cannot +# look in after CI hands you a red build. +DEFAULT_BASE_DIR = Path('/var/tmp/fuse-tests') + +# Tests declaring this group cannot run concurrently with a copy of themselves: +# they bind a fixed path, name a device node globally, or take the first free +# loop device. They run first, with one job, before the rest fan out. +SERIAL_GROUP = 'serial' + +_CONFIG_SECTION = 'tests' +_CONFIG_BASE_DIR_KEY = 'base_dir' + +IS_LINUX = platform.system() == 'Linux' + +# FreeBSD's default kern.corefile is "%N.core"; CI sets "core.%N.%P" to get the +# shape Linux's "core.%e.%p" produces. Glob those plus the bare "core" and +# "core.%p" a stock Linux box writes, so an unconfigured developer machine +# still yields a core. +CORE_GLOBS = ('core', 'core.*', '*.core') + +# A daemon writing any of these to its stderr fails the test even when it +# exited 0. +SUSPICIOUS_WORDS = ('exception', 'error', 'warning', 'fatal', 'traceback', + 'fault', 'crash(?:ed)?', 'abort(?:ed)?', + 'uninitiali[zs]ed') + +# libfuse logs this at FUSE_LOG_INFO and then carries on over /dev/fuse, so an +# io-uring run that nobody checks passes green while testing the transport it +# was meant to replace. +IO_URING_FALLBACK = 'failed to start io-uring' +IO_URING_CAP = 'FUSE_CAP_OVER_IO_URING' +# Every session states its transport on its own stderr, which is a log the +# suite keeps either way. off:custom_io is the only refusal a daemon cannot +# help: no /dev/fuse fd of its own, so no ring to issue against. +IO_URING_STATE_KEY = 'FUSE_INIT: io_uring=' +IO_URING_STATES_OK = ('FUSE_INIT: io_uring=on', + 'FUSE_INIT: io_uring=off:custom_io') +# What -Dsync-init=always/never leave in fuse_config.h; auto writes neither. +SYNC_INIT_ENABLED = '#define FUSE_SYNC_INIT_DEFAULT FUSE_SYNC_INIT_ENABLED' +SYNC_INIT_DISABLED = '#define FUSE_SYNC_INIT_DEFAULT FUSE_SYNC_INIT_DISABLED' +FUSE_URING_PARAM = Path('/sys/module/fuse/parameters/enable_uring') +# glibc has no io_uring_setup() wrapper, so this goes through syscall(2), the +# way liburing does it. 425 on every architecture but alpha. +IO_URING_SETUP = 425 +IO_URING_PARAMS_SIZE = 120 # sizeof(struct io_uring_params) + +# FUSE debug messages "unique: X, error: -Y (...), outsize: Z" contain the word +# "error" but only report a request's return code. +_FUSE_DEBUG_RE = re.compile( + r'^.*unique: \d+, error: -\d+ \(.*\), outsize: \d+.*$', re.MULTILINE) + +_HEADER_RE = re.compile(r'^#\s*(TIMEOUT|GROUP):\s*(.+?)\s*$') + +# Two things must happen in the child before the test runs: it must enter its +# cgroup leaf and raise its soft core limit. Neither can be done with a +# preexec_fn -- run_all() is threaded, and Python between fork() and exec() can +# deadlock inside Popen(), where no timeout is watching yet. Only builtins run +# here, so nothing forks before the leaf is joined and a daemon the script +# spawns cannot escape it. +# +# `ulimit -H -c` rather than a limit computed in Python: ulimit counts 512-byte +# blocks in dash and 1024-byte blocks in bash, and RLIM_INFINITY is -1 in +# CPython, which every shell parses as an option. Only the soft limit is raised; +# raising a finite hard limit needs privilege. +# +# The command to exec is passed in rather than built here: the runner decides +# whether a test is a script or a binary, and both the spawn and repro.sh must +# name the same one. +LAUNCHER = f''' +[ -z "$1" ] || printf '%s\\n' "$$" >"$1" || {{ + echo "{LAUNCH_FAILED_MARKER}: cgroup" >&2; exit {LAUNCH_FAILED_EXIT_CODE} +}} +ulimit -S -c "$(ulimit -H -c)" || {{ + echo "{LAUNCH_FAILED_MARKER}: core limit" >&2 + exit {LAUNCH_FAILED_EXIT_CODE} +}} +shift +exec "$@" +''' + + +@dataclass(frozen=True) +class TestSpec: + """One discovered test and the two facts parsed from its header.""" + + name: str # "examples/hello-ll-direct" -- path under cases/, no .sh + script: Path # absolute path to the script, or to the binary marker + timeout: float # seconds; DEFAULT_TIMEOUT unless "# TIMEOUT:" overrides + groups: frozenset # from "# GROUP:"; used by -g/--group selection + binary: str = '' # marker: the build-tree binary to run; "" for a script + + +@dataclass +class CoreDump: + """One core file found in a test's log directory.""" + + path: Path + executable: str # %e from the file name, e.g. "passthrough_ll" + pid: int # %p from the file name + binary: Path | None = None # resolved build-tree binary + # logs/backtrace.<exe>.<pid>.txt -- deliberately not sharing the "core." + # prefix, so CI can upload backtraces while excluding the huge cores with a + # single "!**/core.*" pattern. + backtrace: Path | None = None # None if gdb is absent + top_frames: str = '' # first GDB_TOP_FRAMES frames + + +@dataclass +class TestResult: + """Outcome of one test script run.""" + + name: str + status: str # "PASS" | "FAIL" | "SKIP" + duration: float # wall clock seconds, monotonic + exit_code: int # script exit status; negative == killed by signal + reason: str # skip reason, kill reason, or "" on a clean pass + workdir: Path + cores: list = field(default_factory=list) # list[CoreDump], backtraced + suspicious: str = '' # first offending log line found by scan_logs() + + +def parse_spec(script: Path, cases_dir: Path) -> TestSpec: + """Read the leading comment block of *script* for TIMEOUT and GROUP.""" + timeout, groups = DEFAULT_TIMEOUT, set() + with script.open() as fh: + for line in fh: + if not line.startswith('#'): + if line.strip(): # first non-comment line ends the header + break + continue + hit = _HEADER_RE.match(line) + if not hit: + continue + if hit.group(1) == 'TIMEOUT': + timeout = float(hit.group(2)) + else: + groups.update(hit.group(2).split()) + return TestSpec(name=str(script.relative_to(cases_dir).with_suffix('')), + script=script, timeout=timeout, groups=frozenset(groups), + # discover() collects nothing else without a suffix + binary='' if script.suffix else script.name) + + +def read_core_pattern() -> str: + """The kernel's core-file pattern, Linux and FreeBSD spelling alike.""" + if IS_LINUX: + try: + return Path('/proc/sys/kernel/core_pattern').read_text().strip() + except OSError: + return '' + proc = subprocess.run(['sysctl', '-n', 'kern.corefile'], + capture_output=True, text=True, check=False) + return proc.stdout.strip() + + +def io_uring_setup_error() -> str: + """The errno text when io_uring_setup() is refused, else "". + + Asked as the user the daemons will run as, so it answers for them. + """ + libc = ctypes.CDLL(None, use_errno=True) + libc.syscall.restype = ctypes.c_long + ring = libc.syscall(IO_URING_SETUP, 1, + ctypes.create_string_buffer(IO_URING_PARAMS_SIZE)) + if ring < 0: + return os.strerror(ctypes.get_errno()) + os.close(ring) + return '' + + +def raise_nofile_limit() -> None: + """Raise RLIMIT_NOFILE to the hard limit; -j multiplies fd pressure.""" + soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE) + if soft >= hard: + return + try: + resource.setrlimit(resource.RLIMIT_NOFILE, (hard, hard)) + except (ValueError, OSError): + return + if hard != resource.RLIM_INFINITY and hard < 1024: + print(f'note: RLIMIT_NOFILE hard limit is only {hard}; ' + 'reduce -j if tests fail with EMFILE') + + +def setuid_helpers(build_dir: Path) -> None: + """chown root:root + chmod 4755 the build tree's mount helpers. + + Opt-in because it needs sudo and mutates the build tree; without it an + unprivileged run skips every test that mounts. + """ + sudo = [] if os.geteuid() == 0 else ['sudo'] + for name in SETUID_HELPERS: + helper = build_dir / 'util' / name + if not helper.exists(): # fuservicemount3 is not always built + continue + info = helper.stat() + if info.st_uid == 0 and info.st_mode & stat.S_ISUID: + continue + for cmd in (['chown', 'root:root', str(helper)], + ['chmod', '4755', str(helper)]): + if subprocess.run(sudo + cmd, check=False).returncode: + sys.exit(f'{" ".join(sudo + cmd)} failed') + + +class CgroupManager: + """A cgroup-v2 tree for the run, one leaf per test. + + The launcher puts itself into its leaf before it execs the test, so each + descendant the script spawns -- the fs daemon, fusermount3, anything a + helper backgrounds -- inherits it. Writing "1" to the leaf's cgroup.kill + reaps the whole subtree atomically, which is the only reliable way to stop + a wedged FUSE daemon that has already been reparented to init. + + cgroup-v2 is Linux-only, and FreeBSD does not mount /proc at all by + default, so every method here is disabled off Linux and the runner falls + back to killpg(). + """ + + def __init__(self, root: Path | None): + self.root = root + self._counter = 0 + self._lock = threading.Lock() + + @classmethod + def create(cls, tag: str) -> "CgroupManager": + """Create the run root under the delegated base; disabled on failure.""" + base = cls.delegated_base() + if base is None: + if IS_LINUX: + print('note: no writable delegated cgroup; containing tests ' + 'with killpg() instead of cgroup.kill') + else: + print(f'note: cgroups are Linux-only; containing tests with ' + f'killpg() on {platform.system()}') + return cls(None) + root = base / f'fuse-tests-{tag}' + try: + root.mkdir(exist_ok=True) + except OSError as exc: + print(f'note: cannot create {root} ({exc}); containing tests with ' + 'killpg() instead of cgroup.kill') + return cls(None) + return cls(root) + + @staticmethod + def delegated_base() -> Path | None: + """Parent of this process's own cgroup, if it is writable.""" + if not IS_LINUX: + return None + try: + for line in Path('/proc/self/cgroup').read_text().splitlines(): + if not line.startswith('0::'): + continue + own = Path('/sys/fs/cgroup') / line[3:].lstrip('/') + base = own.parent + if os.access(base, os.W_OK): + return base + except OSError: + pass + return None + + def new_leaf(self) -> Path | None: + """mkdir one leaf; None when disabled.""" + if self.root is None: + return None + with self._lock: + self._counter += 1 + leaf = self.root / f'test-{self._counter}' + try: + leaf.mkdir(exist_ok=True) + except OSError: + return None + return leaf + + def procs_path(self, leaf: Path | None) -> str: + """leaf/cgroup.procs for the launcher to write itself into, or "". + + The runner never writes it: a process moving *itself* needs write + access to the destination cgroup.procs alone, while moving another + process also needs it on the common ancestor -- so the launcher works + under strictly more delegation setups than the runner would. + """ + return '' if leaf is None else str(leaf / 'cgroup.procs') + + def kill_leaf(self, leaf: Path | None) -> None: + """echo 1 > leaf/cgroup.kill, then rmdir. No-op when disabled.""" + if leaf is None: + return + try: + (leaf / 'cgroup.kill').write_text('1\n') + except OSError: + pass + # The kill is asynchronous; a leaf with a process still in it cannot be + # removed, so give it a moment rather than leaking the directory. + for _ in range(50): + try: + leaf.rmdir() + return + except OSError: + time.sleep(0.1) + + def cleanup(self) -> None: + """Kill and remove every remaining leaf, then the root.""" + if self.root is None: + return + for leaf in sorted(self.root.glob('test-*')): + self.kill_leaf(leaf) + try: + self.root.rmdir() + except OSError: + pass + + +REEXEC_SENTINEL = 'FUSE_TESTS_UNDER_SCOPE' # set on the re-exec'd child + + +def reexec_under_user_scope_if_needed() -> None: + """Re-exec under a systemd user scope so cgroup leaves have a writable + parent. Returns unchanged when already delegated or when systemd-run is + unavailable; replaces this process otherwise.""" + if not IS_LINUX or os.environ.get(REEXEC_SENTINEL): + return + if CgroupManager.delegated_base() is not None: + return + systemd_run = shutil.which('systemd-run') + if systemd_run is None: + return + os.environ[REEXEC_SENTINEL] = '1' + argv = [systemd_run, '--user', '--scope', '--quiet', + sys.executable, os.path.abspath(__file__)] + sys.argv[1:] + try: + os.execv(systemd_run, argv) + except OSError: + return + + +def config_path() -> Path: + """Path to the persisted runner config.""" + return Path.home() / '.config' / 'libfuse' / 'tests.conf' + + +def read_configured_base_dir() -> str | None: + """Return the persisted base_dir, or None if unset or unreadable.""" + import configparser + + parser = configparser.ConfigParser() + try: + parser.read(config_path()) + except (OSError, configparser.Error): + return None + return parser.get(_CONFIG_SECTION, _CONFIG_BASE_DIR_KEY, fallback=None) + + +def write_configured_base_dir(base_dir: str) -> None: + """Persist base_dir, preserving any other keys already in the file.""" + import configparser + + parser = configparser.ConfigParser() + path = config_path() + try: + parser.read(path) + except (OSError, configparser.Error): + pass + if not parser.has_section(_CONFIG_SECTION): + parser.add_section(_CONFIG_SECTION) + parser.set(_CONFIG_SECTION, _CONFIG_BASE_DIR_KEY, base_dir) + path.parent.mkdir(parents=True, exist_ok=True) + with path.open('w') as fh: + parser.write(fh) + + +def resolve_base_dir() -> Path: + """The base a per-run leaf is appended to.""" + return Path(read_configured_base_dir() or str(DEFAULT_BASE_DIR)) + + +def resolve_run_dir(args: argparse.Namespace) -> Path: + """Pick the run directory: --run-dir, $FUSE_TEST_RUN_DIR, config, default. + + --run-dir and $FUSE_TEST_RUN_DIR are used verbatim, because a caller that + names a directory means that directory: CI has to hand the path to a later + upload step, and `meson test` cannot pass arguments to a registered test, + so the env var is the only channel it has. + + The persisted config value and DEFAULT_BASE_DIR are *bases*: a + run-<user>-<timestamp>-<pid> leaf is appended so two runs on one machine + -- or two users sharing /var/tmp -- cannot land in the same directory. + """ + if args.run_dir: + return Path(args.run_dir) + env_run_dir = os.environ.get('FUSE_TEST_RUN_DIR') + if env_run_dir: + return Path(env_run_dir) + return resolve_base_dir() / run_leaf_name(args) + + +def make_run_dir(run_dir: Path) -> bool: + """mkdir the run directory, and say whether this call created it. + + Which matters at the end: a directory the runner made is its own to + delete, one it merely found is not. + """ + try: + run_dir.mkdir(parents=True) + except FileExistsError: + return False + return True + + +def resolve_valgrind() -> str: + """The command prefix every daemon launch gets, or "" when off. + + Resolved once here and exported, so the policy lives in one place and no + test re-implements the no/false/0 spellings. + """ + value = os.environ.get('TEST_WITH_VALGRIND', 'no').lower().strip() + return '' if value in ('no', 'false', '0') else VALGRIND_PREFIX + + +def run_leaf_name(args: argparse.Namespace) -> str: + """The per-run leaf appended to a base directory. + + The timestamp is second-resolution, so the pid is what keeps two runs + started in the same second apart: a shared leaf is a shared work + directory, log and pid file for every test, and the first run to finish + clean deletes them all. + + The io-uring invocation gets its own suffix, so which transport a kept + directory holds is visible without opening it. A verbatim --run-dir stays + verbatim; the caller chose that path. + """ + user = os.environ.get('USER') or os.environ.get('LOGNAME') or 'unknown' + mode = '-iouring' if getattr(args, 'io_uring', False) else '' + return f'run-{user}-{time.strftime("%y%m%d%H%M%S")}-{os.getpid()}{mode}' + + +def as_test_name(pattern: str, cases_dir: Path) -> str: + """A -t pattern as a discovery name, so shell completion can supply it: + "test/cases/examples/cuse.sh" and "examples/cuse" name the same test. + + Resolved as a path rather than matched as text, against the cwd first so a + completed path works from wherever it was completed, then against cases/ so + a bare "examples" means the same thing from every directory. A directory + selects what is under it. Anything that is not a path into cases/ is + returned as given and stays an fnmatch pattern. + """ + for base in (Path.cwd(), cases_dir): + resolved = (base / pattern).resolve() + if not resolved.is_relative_to(cases_dir): + continue + rel = resolved.relative_to(cases_dir) + if resolved.is_dir(): + return str(rel / '*') + return str(rel.with_suffix('')) if rel.suffix == '.sh' else str(rel) + return pattern + + +def is_shell_exception_dir(directory: Path) -> bool: + """True when the cases/**/*.sh rule does not apply inside *directory*. + + The runner decides what a file is by where it sits, so the exceptions are + listed once here. Today every one of them holds binary markers. + """ + return directory.name in ('unit',) + + +def shell_scripts(cases_dir: Path) -> list: + """Executable cases/**/*.sh, outside cases/lib and the shell-exception + directories.""" + return [path for path in cases_dir.rglob('*.sh') + if LIB_DIR not in path.parents + and not is_shell_exception_dir(path.parent) + and os.access(path, os.X_OK)] + + +def binary_markers(cases_dir: Path) -> list: + """<dir>/<binary-name> -- one marker per C test that needs no script. + + A binary name has no suffix, so a suffixed entry is malformed and is said + so rather than skipped. Only .md is allowed, for documentation. + """ + markers = [] + for directory in sorted(cases_dir.iterdir()): + if not directory.is_dir() or not is_shell_exception_dir(directory): + continue + for path in sorted(directory.iterdir()): + if path.name.startswith('.') or path.suffix == '.md': + continue + if not path.is_file() or path.suffix: + sys.exit(f'{path}: not a binary name; ' + f'{directory} holds markers only') + markers.append(path) + return markers + + +def discover(cases_dir: Path, patterns: list, groups: set, + exclude: set) -> list: + """Executable cases/**/*.sh plus the binary markers, filtered by fnmatch + patterns, GROUP membership, and the exclude set (CLI -X plus + test/exclude).""" + patterns = [as_test_name(p, cases_dir) for p in patterns] + names, specs = [], [] + for script in sorted([*shell_scripts(cases_dir), + *binary_markers(cases_dir)]): + spec = parse_spec(script, cases_dir) + names.append(spec.name) + if spec.name in exclude: + continue + if patterns and not any(fnmatch.fnmatch(spec.name, p) + for p in patterns): + continue + if groups and not (groups & spec.groups): + continue + specs.append(spec) + # Checked against every name rather than against the selection, so that -g + # or -X narrowing the result to nothing is not reported as a bad pattern. + for pattern in patterns: + if not any(fnmatch.fnmatch(name, pattern) for name in names): + sys.exit(f'{pattern}: matches no test') + return specs + + +class TestRunner: + """Runs TestSpecs, one worker thread each, and reports.""" + + def __init__(self, build_dir: Path, run_dir: Path, run_dir_created: bool, + jobs: int, keep_logs: bool, verbose: bool, + io_uring: bool = False, io_uring_depth: int | None = None): + self.build_dir = build_dir + self.run_dir = run_dir + self.run_dir_created = run_dir_created + self.jobs = jobs + self.keep_logs = keep_logs + self.verbose = verbose + self.io_uring = io_uring + self.io_uring_depth = io_uring_depth + self.valgrind = resolve_valgrind() + self.core_pattern = read_core_pattern() + self.fuse_caps = self.read_fuse_caps() + self.sync_init = self.read_sync_init() + self._cgroup = CgroupManager.create(str(os.getpid())) + self._stop = threading.Event() + self._print_lock = threading.Lock() + self._gdb = shutil.which('gdb') + # Overwritten per run_all() call; set here so run_one() always has a + # reference even if it is ever driven without run_all(). + self._run_started = time.monotonic() + self.report_environment() + + # ---------------------------------------------------------------- startup + + def build_path(self, base: str | None = None) -> str: + """$PATH with the build tree's util/ and example/ ahead of *base*. + + util/ first for anything that spawns the mount helper by name: an + installed one older than the build tree makes printcap emit + "unrecognized option '--sync-init'", and a tree whose helper is not + installed system-wide at all would find no helper whatsoever. + """ + if base is None: + base = os.environ.get('PATH', '') + return os.pathsep.join([str(self.build_dir / 'util'), + str(self.build_dir / 'example'), base]) + + def read_fuse_caps(self) -> frozenset: + """The FUSE_CAP_* names printcap reports, read once per run. + + printcap lives in example/, not util/, so this works the same way on + every platform. It mounts to negotiate, so it needs the same $PATH a + test gets rather than the ambient one -- otherwise it reaches for a + mount helper that this build may not have installed anywhere, and + every _require_cap test silently skips. + """ + printcap = self.build_dir / 'example' / 'printcap' + if not os.access(printcap, os.X_OK): + print(f'note: {printcap} not built; no capability is available ' + 'and every _require_cap test will skip') + return frozenset() + proc = subprocess.run([str(printcap)], capture_output=True, text=True, + timeout=30, check=False, + env=dict(os.environ, PATH=self.build_path())) + if proc.returncode != 0: + print(f'note: printcap failed ({proc.returncode}); ' + 'every _require_cap test will skip') + return frozenset() + return frozenset(line.strip() for line in proc.stdout.splitlines() + if line.startswith('\t')) + + def read_sync_init(self) -> str: + """Which -Dsync-init the library was built with: auto, always, never. + + A property of the build, not of the run, so it is read where the + io-uring preflight reads HAVE_URING rather than selected on the + command line. + """ + try: + config = (self.build_dir / 'fuse_config.h').read_text() + except OSError: + return 'unknown' + for define, mode in ((SYNC_INIT_ENABLED, 'always'), + (SYNC_INIT_DISABLED, 'never')): + if define in config: + return mode + return 'auto' + + def preflight_io_uring(self) -> str: + """Return "" when the io-uring transport can actually be exercised, + else the reason the whole invocation skips with exit 77. + + Checked in this order, so the message names the first thing that is + actually wrong. + """ + if not IS_LINUX: + return f'fuse-io-uring is Linux-only, this is {platform.system()}' + try: + config = (self.build_dir / 'fuse_config.h').read_text() + except OSError: + return f'{self.build_dir}/fuse_config.h is unreadable' + if 'HAVE_URING' not in config: + return 'the library was built with -Denable-io-uring=false' + if IO_URING_CAP in self.fuse_caps: + return '' + # printcap reports capable_ext, so the capability's absence *is* the + # kernel's answer. The module parameter only tells the two reasons + # apart. + try: + enabled = FUSE_URING_PARAM.read_text().strip() + except OSError: + return 'this kernel has no fuse io-uring support' + if enabled in ('N', '0'): + return ('io-uring is disabled in the fuse module\n' + f' echo Y | sudo tee {FUSE_URING_PARAM}') + return f'the kernel did not offer {IO_URING_CAP}' + + def report_environment(self) -> None: + """Say what will silently degrade before any test runs.""" + if self.core_pattern.startswith('|'): + print('note: core_pattern is piped to systemd-coredump; cores ' + 'will be fetched via coredumpctl. For in-workdir cores run:') + print(' sudo sysctl -w kernel.core_pattern=core.%e.%p') + if self._gdb is None: + print('note: gdb not found; cores will be kept but not backtraced') + + # ------------------------------------------------------------ preparation + + def prepare(self, spec: TestSpec): + """Create the workdir tree, build the env dict, allocate the cgroup + leaf. Returns (workdir, env, leaf).""" + workdir = self.run_dir / spec.name + logs = workdir / 'logs' + for sub in ('mnt', 'src', 'tmp', 'logs'): + (workdir / sub).mkdir(parents=True, exist_ok=True) + + util_dir = self.build_dir / 'util' + example_dir = self.build_dir / 'example' + env = dict(os.environ) + env.update({ + 'TEST_NAME': spec.name, + 'TEST_SCRIPT': str(spec.script), + 'TEST_DIR': str(TEST_DIR), + 'TEST_LIB': str(LIB_DIR), + 'TEST_WORKDIR': str(workdir), + 'TEST_MNT': str(workdir / 'mnt'), + 'TEST_SRC': str(workdir / 'src'), + 'TEST_TMP': str(workdir / 'tmp'), + 'TEST_LOGDIR': str(logs), + 'BUILD_DIR': str(self.build_dir), + 'FUSE_EXAMPLE_DIR': str(example_dir), + 'FUSE_TEST_BIN_DIR': str(self.build_dir / 'test'), + 'FUSE_UTIL_DIR': str(util_dir), + 'FUSE_CAPS': ' '.join(sorted(self.fuse_caps)), + 'FUSE_UID': str(os.geteuid()), + 'FUSE_OS': platform.system(), + # Exported as 0 rather than left unset: fuse_session_new() reads it + # from the environment, so a value in the developer's shell would + # otherwise change what the default run tests without saying so. + 'FUSE_URING_ENABLE': '1' if self.io_uring else '0', + # Asked of every session, not only the ones a mount verb started, + # so the transport a test never launched through the shell -- a + # self-mounting C test, a daemon the script backgrounded itself -- + # is on the record too. It lands in that daemon's log, because + # none of them redirects fuse_log() anywhere else. + 'FUSE_INIT_STATUS': '1', + 'FUSE_VALGRIND': self.valgrind, + }) + if self.io_uring_depth is not None: + env['FUSE_URING_QUEUE_DEPTH'] = str(self.io_uring_depth) + env['PATH'] = self.build_path(env.get('PATH', '')) + return workdir, env, self._cgroup.new_leaf() + + def child_argv(self, spec: TestSpec) -> list: + """The command the launcher execs for *spec*. + + bash -e for a script: errexit is the runner's rule, not something 88 + files have to remember. A marker names a binary, which the launcher + execs itself; there is no shell in between to impose errexit on, and + none is wanted -- the binary's exit status is already the verdict. + """ + if spec.binary: + return [str(self.build_dir / 'test' / spec.binary)] + return ['bash', '-e', str(spec.script)] + + def write_repro(self, spec: TestSpec, workdir: Path, env: dict) -> None: + """Write logs/repro.sh: the env deltas, the cd, and the command. + + It execs what the launcher execs, so the reproducer cannot pass where + the run failed. + """ + import shlex + + lines = ['#!/bin/sh', + '# Re-run this one test by hand, with the environment the', + '# runner built for it.', + ''] + for key, value in sorted(env.items()): + if os.environ.get(key) == value and key != 'PATH': + continue + lines.append(f'export {key}={shlex.quote(value)}') + lines += ['', + f'cd {shlex.quote(str(workdir / "logs"))}', + 'exec ' + shlex.join(self.child_argv(spec)), + ''] + repro = workdir / 'logs' / 'repro.sh' + repro.write_text('\n'.join(lines)) + repro.chmod(0o755) + + # -------------------------------------------------------------- execution + + def _stream(self, proc: subprocess.Popen, logs: Path, name: str, + started: float) -> None: + """Copy the script's merged output to logs/script.out until EOF, and + a copy prefixed with elapsed time to logs/timestamps.out. + + A timed-out test's script.out has no way to tell "stalled right + after the last line" from "was still grinding right up to the + kill" -- the CI step that prints it afterwards timestamps when it + printed, not when the test produced it. timestamps.out is named to + pick up the same "*.out" glob so CI shows it for free. + """ + with (logs / 'script.out').open('wb') as out, \ + (logs / 'timestamps.out').open('wb') as timestamps: + for line in proc.stdout: + out.write(line) + out.flush() + elapsed = time.monotonic() - started + timestamps.write(f'+{elapsed:8.3f}s '.encode() + line) + timestamps.flush() + if self.verbose: + text = line.decode('utf8', errors='replace').rstrip('\n') + with self._print_lock: + print(f'[{name}] {text}') + proc.stdout.close() + + def _kill(self, proc: subprocess.Popen, leaf: Path | None) -> None: + """Stop a timed-out or interrupted test: cgroup.kill on the leaf when + there is one, else killpg() on the session the script leads.""" + if leaf is not None: + try: + (leaf / 'cgroup.kill').write_text('1\n') + return + except OSError: + pass + for sig in (signal.SIGTERM, signal.SIGKILL): + try: + os.killpg(proc.pid, sig) + except OSError: + return + try: + proc.wait(timeout=5.0) + return + except subprocess.TimeoutExpired: + continue + + def dump_stacks(self, proc: subprocess.Popen, leaf: Path | None, + logs: Path) -> None: + """On a timeout, before _kill() tears the containment down, write a + snapshot of the whole process table, plus every thread's kernel stack + and (when gdb is available) user backtrace for every process still in + the containment. + + A thread blocked in an uninterruptible syscall never answers + PTRACE_ATTACH, so the kernel stack is taken first -- it needs no + attach and is often the only trace such a thread ever yields. The + process table is taken before either, while nothing has stopped a + thread yet. + """ + self._dump_process_table(logs) + for pid in self._containment_pids(proc, leaf): + self._dump_kernel_stacks(pid, logs) + if self._gdb is not None: + self._dump_gdb_backtrace(pid, logs) + + @staticmethod + def _dump_process_table(logs: Path) -> None: + """ps auxwww, so a hang that involves a process outside the test's own + containment -- a daemon an earlier test leaked, say -- is still + visible in the report. + """ + result = subprocess.run(['ps', 'auxwww'], capture_output=True, + text=True, check=False) + (logs / 'ps.txt').write_text(result.stdout + result.stderr) + + @staticmethod + def _proc_identity(pid: int) -> str: + """A "<pid> (<comm>): <cmdline>" header line for a dump. + + Which pid was the daemon and which the client blocked on it is the + first question asked of a stack dump, and a bare pid answers it for + nobody reading the report minutes or days later. + """ + try: + comm = Path(f'/proc/{pid}/comm').read_text().strip() + except OSError: + comm = '?' + try: + raw = Path(f'/proc/{pid}/cmdline').read_bytes() + cmdline = raw.decode('utf8', errors='replace').replace('\0', ' ') + except OSError: + cmdline = '' + # An argument may hold a newline; a header that is not one line + # breaks every reader that greps the dump for its pid. + return f'=== pid {pid} ({comm}): {" ".join(cmdline.split())} ===' + + @staticmethod + def _as_text(data) -> str: + """TimeoutExpired carries its partial output undecoded even when the + run was in text mode.""" + if data is None: + return '' + return data if isinstance(data, str) \ + else data.decode('utf8', errors='replace') + + @staticmethod + def _uninterruptible_tids(pid: int) -> list: + """Threads of *pid* in uninterruptible sleep, D in /proc. + + Such a thread never answers PTRACE_ATTACH, so gdb waits on it until + GDB_TIMEOUT kills it. That is per pid, and a wedged filesystem leaves + several -- the client blocked on it and the daemon's own threads -- so + the dump can outlast the timeout that asked for it by minutes. + """ + wedged = [] + try: + tids = sorted(int(tid) for tid in os.listdir(f'/proc/{pid}/task')) + except OSError: + return wedged + for tid in tids: + try: + status = Path(f'/proc/{pid}/task/{tid}/status').read_text() + except OSError: + continue # exited between listdir() and here + for line in status.splitlines(): + if line.startswith('State:'): + if line.split()[1] == 'D': + wedged.append(tid) + break + return wedged + + @staticmethod + def _containment_pids(proc: subprocess.Popen, leaf: Path | None) -> list: + """Every pid sharing the test's cgroup leaf, or just proc.pid when + cgroups are disabled.""" + if leaf is not None: + try: + pids = [int(line) for line in + (leaf / 'cgroup.procs').read_text().split()] + if pids: + return pids + except OSError: + pass + return [proc.pid] + + @staticmethod + def _dump_kernel_stacks(pid: int, logs: Path) -> None: + """sudo cat /proc/<pid>/task/*/stack for every thread of *pid*. + + /proc/<pid>/task/<tid>/stack is gated on CAP_SYS_ADMIN unconditionally + -- unlike ptrace, ownership and Yama's exceptions are not enough -- + to keep it from leaking kernel addresses. sudo is what gets it on a + non-root run; -n so a runner without passwordless sudo fails the read + immediately instead of blocking on a password prompt nothing answers. + """ + task_dir = Path(f'/proc/{pid}/task') + try: + tids = sorted(int(tid) for tid in os.listdir(task_dir)) + except OSError: + return + lines = [TestRunner._proc_identity(pid)] + for tid in tids: + stack_path = task_dir / str(tid) / 'stack' + result = subprocess.run(['sudo', '-n', 'cat', str(stack_path)], + capture_output=True, text=True, + check=False) + stack = result.stdout if result.returncode == 0 else \ + f'(unreadable: {result.stderr.strip() or result.returncode})\n' + lines.append(f'=== tid {tid} ===\n{stack}') + (logs / f'kstack.{pid}.txt').write_text('\n'.join(lines)) + + def _dump_gdb_backtrace(self, pid: int, logs: Path) -> None: + """sudo gdb -p <pid> backtraces, written beside the kernel stacks. + + Bare "bt" for every thread first and "bt full" only after it: the + locals are worth having, but they bury the frame list they belong to + under pages of struct dumps, and the frame list is what is read first. + + sudo, not a Yama prctl exception: the wedged pid is as likely to be a + daemon the test forked after launch as the test's own exec-chain, and + an exception only covers the specific task that calls it, not + children it forks afterwards. Root's CAP_SYS_PTRACE reaches either + one uniformly. Best-effort: a thread ptrace cannot stop, or a runner + without passwordless sudo, just leaves gdb's or sudo's own error text + in the file. + + Skipped entirely where a thread is already in uninterruptible sleep: + the attach cannot complete, so the whole GDB_TIMEOUT buys one line + saying gdb was killed, and the kernel stack taken just before is what + such a thread yields instead. + """ + wedged = self._uninterruptible_tids(pid) + if wedged: + tids = ' '.join(str(tid) for tid in wedged) + (logs / f'gdbstack.{pid}.txt').write_text( + f'{self._proc_identity(pid)}\n' + f'(skipped: tid {tids} in uninterruptible sleep, so the ' + f'attach cannot complete -- see kstack.{pid}.txt)\n') + return + argv = ['sudo', '-n', self._gdb, '--batch', '--nx', + '-ex', 'set pagination off', + '-ex', 'thread apply all bt', + '-ex', 'set print pretty on', + '-ex', 'thread apply all bt full', + '-p', str(pid)] + try: + proc = subprocess.run(argv, capture_output=True, text=True, + timeout=GDB_TIMEOUT, check=False) + out = proc.stdout + proc.stderr + except subprocess.TimeoutExpired as expired: + # A pid gdb cannot finish with is the interesting one often + # enough that whatever it did print has to be kept. + out = (self._as_text(expired.stdout) + + self._as_text(expired.stderr) + + f'\n(gdb killed after {GDB_TIMEOUT:.0f}s)\n') + (logs / f'gdbstack.{pid}.txt').write_text( + f'{self._proc_identity(pid)}\n{out}') + + def _log_started(self, name: str) -> None: + """A "test X is now running" line, timestamped against the whole + run's own start rather than the test's -- so under -j the console + log (which CI keeps regardless of pass/fail) shows what was in + flight and since when, not only what has finished. + """ + elapsed = time.monotonic() - self._run_started + with self._print_lock: + print(f'+{elapsed:8.3f}s START {name}') + sys.stdout.flush() + + def run_one(self, spec: TestSpec) -> TestResult: + """Spawn the test, enforce spec.timeout, collect the result.""" + workdir, env, leaf = self.prepare(spec) + self.write_repro(spec, workdir, env) + logs = workdir / 'logs' + timeout = self.effective_timeout(spec) + started = time.monotonic() + self._log_started(spec.name) + + proc = subprocess.Popen( + ['/bin/sh', '-c', LAUNCHER, 'run-tests', + self._cgroup.procs_path(leaf), *self.child_argv(spec)], + env=env, cwd=str(logs), + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + start_new_session=True) + # exec keeps the pid, so this is still the script and still the group + # leader that "kill -- -$(cat pid)" targets. + (workdir / 'pid').write_text(f'{proc.pid}\n') + if leaf is not None: + (workdir / 'cgroup').write_text(f'{leaf}\n') + + # _stream blocks until EOF, so read in a thread and wait with a + # deadline here; that is what makes the timeout a hard wall-clock bound. + reader = threading.Thread(target=self._stream, + args=(proc, logs, spec.name, started), + daemon=True) + reader.start() + reader.join(timeout=timeout) + reason = '' + if reader.is_alive(): + reason = f'timeout after {timeout:.0f}s' + self.dump_stacks(proc, leaf, logs) + self._kill(proc, leaf) + reader.join(timeout=5.0) + try: + proc.wait(timeout=REAP_TIMEOUT) + except subprocess.TimeoutExpired: + # Report it and carry on: the run is worth more than this one + # process, and everything worth reading is already on disk. + unkillable = f'unkillable after {REAP_TIMEOUT:.0f}s' + reason = f'{reason}, {unkillable}' if reason else unkillable + # Always reap the subtree: a passing script may still have leaked a + # daemon, and that would wedge the next test on the same mountpoint. + self._cgroup.kill_leaf(leaf) + + duration = time.monotonic() - started + cores = self.collect_cores(workdir, leaf) + suspicious = self.scan_logs(workdir) if proc.returncode == 0 else '' + result = self.classify(spec, workdir, proc.returncode, duration, + reason, cores, suspicious) + (workdir / 'status').write_text( + f'{result.status} {result.duration:.3f} {result.reason}\n'.rstrip() + + '\n') + return result + + def effective_timeout(self, spec: TestSpec) -> float: + """The wall-clock bound actually enforced for *spec*.""" + if self.valgrind: + return spec.timeout * VALGRIND_TIMEOUT_FACTOR + return spec.timeout + + def classify(self, spec: TestSpec, workdir: Path, code: int, + duration: float, reason: str, cores: list, + suspicious: str) -> TestResult: + """Turn an exit status plus the collected evidence into a verdict.""" + unexpected = [c for c in cores + if not self.core_is_allowed(workdir, c)] + transport = self.wrong_transport(workdir) + if reason: # killed by the runner + status = 'FAIL' + elif code == SKIP_EXIT_CODE: + status, reason = 'SKIP', self.skip_reason(workdir) + elif code == LAUNCH_FAILED_EXIT_CODE and self.launch_failed(workdir): + status, reason = 'FAIL', 'could not establish containment' + elif code != 0: + status = 'FAIL' + reason = (f'killed by signal {-code}' if code < 0 + else f'exit {code}') + elif unexpected: + core = unexpected[0] + status = 'FAIL' + reason = f'core: {core.executable} (pid {core.pid})' + elif suspicious: + status, reason = 'FAIL', f'suspicious output: {suspicious}' + elif transport: + status, reason = 'FAIL', transport + else: + status = 'PASS' + return TestResult(name=spec.name, status=status, duration=duration, + exit_code=code, reason=reason, workdir=workdir, + cores=unexpected, suspicious=suspicious) + + @staticmethod + def skip_reason(workdir: Path) -> str: + """The reason _notrun printed, taken from the script's own output.""" + try: + text = (workdir / 'logs' / 'script.out').read_text(errors='replace') + except OSError: + return '' + for line in reversed(text.splitlines()): + if line.startswith('SKIP: '): + return line[len('SKIP: '):] + return '' + + def wrong_transport(self, workdir: Path) -> str: + """Under --io-uring, the first session that did not get a ring, or "". + + A test that started no session reports nothing and so has nothing to + prove. off:custom_io is the one refusal a daemon cannot avoid: the + ring is issued against the session's own /dev/fuse fd, which a + custom-io session does not have. + + timestamps.out holds the same lines behind an elapsed-time prefix, so + matching on the start of the line reports each session once. + """ + if not self.io_uring: + return '' + for out in sorted((workdir / 'logs').glob('*.out')): + try: + text = out.read_text(errors='replace') + except OSError: + continue + for line in text.splitlines(): + if (line.startswith(IO_URING_STATE_KEY) + and line not in IO_URING_STATES_OK): + return f'{out.name}: {line}' + return '' + + @staticmethod + def launch_failed(workdir: Path) -> bool: + """True when exit 125 came from the launcher, not from the test.""" + try: + text = (workdir / 'logs' / 'script.out').read_text(errors='replace') + except OSError: + return False + return LAUNCH_FAILED_MARKER in text + + # ----------------------------------------------------------- log scanning + + def scan_logs(self, workdir: Path) -> str: + """Strip logs/.allow patterns, then look for suspicious words in every + logs/*.out. Returns the first offending line, or "".""" + logs = workdir / 'logs' + allow = [] + try: + for line in (logs / '.allow').read_text().splitlines(): + if line.strip(): + allow.append(re.compile(line, re.MULTILINE)) + except OSError: + pass + words = [re.compile(r'\b{}\b'.format(w), re.IGNORECASE | re.MULTILINE) + for w in self.suspicious_words()] + for out in sorted(logs.glob('*.out')): + try: + buf = out.read_text(errors='replace') + except OSError: + continue + for pattern in allow: + buf = pattern.sub('', buf) + buf = _FUSE_DEBUG_RE.sub('', buf) + buf = self.strip_extra(buf) + for word in words: + hit = word.search(buf) + if hit: + line_start = buf.rfind('\n', 0, hit.start()) + 1 + line_end = buf.find('\n', hit.end()) + if line_end == -1: + line_end = len(buf) + return f'{out.name}: {buf[line_start:line_end].strip()}' + return '' + + def suspicious_words(self) -> tuple: + """The word list scan_logs() looks for. + + Under --io-uring it gains the message libfuse prints before falling + back to /dev/fuse. The status file keeps only the last session of a + binary that runs several, so the log stays the backstop for the + earlier ones. + """ + if self.io_uring: + return SUSPICIOUS_WORDS + (re.escape(IO_URING_FALLBACK),) + return SUSPICIOUS_WORDS + + def strip_extra(self, buf: str) -> str: + """Drop the noise a mode adds to every daemon's log. + + Valgrind's own lines go before the word scan. Built in rather than a + per-test fuse_allow_output, because it applies to every test whenever + valgrind is on. + """ + if self.valgrind: + return _VALGRIND_LINE_RE.sub('', buf) + return buf + + # ---------------------------------------------------------- core handling + + @staticmethod + def core_is_allowed(workdir: Path, core: CoreDump) -> bool: + """True when the test declared a core from this executable expected.""" + try: + names = (workdir / 'logs' / '.allow-core').read_text().split() + except OSError: + return False + return core.executable in names + + def resolve_binary(self, executable: str) -> Path | None: + """Map a core's %e back to the build-tree binary that produced it. + + %e is a bare basename, so search the three directories the suite runs + binaries from. An unrecognised name (a system tool, or %e truncated at + 16 chars) yields None; gdb still produces a usable backtrace from the + core alone, just without symbol names for the main executable. + """ + for directory in (self.build_dir / 'example', + self.build_dir / 'test', + self.build_dir / 'util'): + candidate = directory / executable + if candidate.is_file(): + return candidate + return None + + def collect_cores(self, workdir: Path, leaf: Path | None) -> list: + """Every core matching CORE_GLOBS in logs/, backtraced. + + When core_pattern is a pipe, nothing lands in logs/ at all. The + fallback then asks the journal for cores recorded against *this test's* + cgroup -- COREDUMP_CGROUP, which is exactly the leaf written to + $TEST_WORKDIR/cgroup. A time-window query is deliberately not used: + under -j the test intervals overlap, so one crash would be handed to + every worker whose window contains it and fail unrelated tests. + + With no leaf to match on -- cgroups disabled, or not Linux -- the + fallback yields nothing and the startup note is the only signal. CI + sets a relative pattern on both platforms, so this costs no CI + coverage. + """ + logs = workdir / 'logs' + found = [] + for glob in CORE_GLOBS: + for path in sorted(logs.glob(glob)): + found.append(self._core_from_name(path)) + if not found and self.core_pattern.startswith('|'): + found = self._cores_from_journal(logs, leaf) + for core in found: + core.binary = self.resolve_binary(core.executable) + self.backtrace(core) + return found + + @staticmethod + def _core_from_name(path: Path) -> CoreDump: + """Parse a core file name into the %e and %p the pattern encoded. + + core.<exe>.<pid> is what CI configures and <exe>.core is FreeBSD's + default, but a developer machine often has a bare "core" or "core.<pid>" + pattern, which names no executable at all. Every shape still yields a + CoreDump -- the executable is only used to find symbols for gdb. + """ + parts = path.name.split('.') + if parts[0] == 'core': + if len(parts) >= 3 and parts[-1].isdigit(): + return CoreDump(path=path, executable='.'.join(parts[1:-1]), + pid=int(parts[-1])) + if len(parts) == 2 and parts[1].isdigit(): + return CoreDump(path=path, executable='unknown', + pid=int(parts[1])) + if len(parts) == 1: + return CoreDump(path=path, executable='unknown', pid=0) + if path.name.endswith('.core'): + return CoreDump(path=path, executable=path.name[:-len('.core')], + pid=0) + return CoreDump(path=path, executable=path.name, pid=0) + + def _cores_from_journal(self, logs: Path, leaf: Path | None) -> list: + """Cores systemd-coredump captured for this test's cgroup.""" + if leaf is None or not IS_LINUX or shutil.which('coredumpctl') is None: + return [] + import json + + proc = subprocess.run( + ['journalctl', '-t', 'systemd-coredump', + f'COREDUMP_CGROUP={self._cgroup_relative(leaf)}', '-o', 'json'], + capture_output=True, text=True, check=False) + cores = [] + for line in proc.stdout.splitlines(): + try: + entry = json.loads(line) + except ValueError: + continue + pid = int(entry.get('COREDUMP_PID', 0)) + exe = os.path.basename(entry.get('COREDUMP_COMM', '') or '') + if not pid: + continue + path = logs / f'core.{exe}.{pid}' + dumped = subprocess.run( + ['coredumpctl', 'dump', str(pid), '--output', str(path)], + capture_output=True, check=False) + if dumped.returncode == 0 and path.exists(): + cores.append(CoreDump(path=path, executable=exe, pid=pid)) + return cores + + @staticmethod + def _cgroup_relative(leaf: Path) -> str: + """The leaf path as the journal records it (without the mount point).""" + return '/' + str(leaf.relative_to('/sys/fs/cgroup')) + + def backtrace(self, core: CoreDump) -> None: + """Run gdb on *core*, write the full backtrace beside it as + backtrace.<exe>.<pid>.txt, and keep the top frames for the summary. + + "thread apply all bt full" rather than a plain bt: a FUSE daemon is + multi-threaded and the crashing thread is rarely the only interesting + one. --nx so a developer's ~/.gdbinit cannot change the output. + """ + if self._gdb is None: + return + argv = [self._gdb, '--batch', '--nx', + '-ex', 'set pagination off', + '-ex', 'set print pretty on', + '-ex', 'thread apply all bt full'] + argv += ['--', str(core.binary)] if core.binary else [] + argv += [str(core.path)] + try: + proc = subprocess.run(argv, capture_output=True, text=True, + timeout=GDB_TIMEOUT, check=False) + except subprocess.TimeoutExpired: + return + out = core.path.parent / f'backtrace.{core.executable}.{core.pid}.txt' + out.write_text(proc.stdout + proc.stderr) + core.backtrace = out + frames = [line for line in proc.stdout.splitlines() + if line.startswith('#')] + core.top_frames = '\n'.join(frames[:GDB_TOP_FRAMES]) + if len(frames) > GDB_TOP_FRAMES: + core.top_frames += (f'\n ... {len(frames) - GDB_TOP_FRAMES} ' + f'more frames, full: {out}') + + # ------------------------------------------------------------- scheduling + + def run_all(self, specs: list) -> int: + """Run the serial group first with one job, then fan the rest out. + Returns 0 when nothing failed.""" + self._run_started = time.monotonic() + serial = [s for s in specs if SERIAL_GROUP in s.groups] + parallel = [s for s in specs if SERIAL_GROUP not in s.groups] + results = self._run_batch(serial, 1) + \ + self._run_batch(parallel, self.jobs) + return self.summarise(results, time.monotonic() - self._run_started) + + def _run_batch(self, specs: list, jobs: int) -> list: + """ThreadPoolExecutor over specs; print a live line per finished test.""" + results = [] + if not specs: + return results + # as_completed() rather than map(): map yields in submission order, so + # a finished test would be reported -- and its workdir freed -- only + # once every test submitted before it has landed. + with ThreadPoolExecutor(max_workers=jobs) as pool: + futures = [pool.submit(self._run_guarded, spec) for spec in specs] + for future in as_completed(futures): + result = future.result() + if result is not None: + results.append(result) + self.report(result) + self.discard_workdir(result) + return results + + def _run_guarded(self, spec: TestSpec): + """run_one(), unless SIGINT already asked the run to stop.""" + if self._stop.is_set(): + return None + return self.run_one(spec) + + def report(self, result: TestResult) -> None: + """One line per finished test, plus where to look when it failed.""" + elapsed = time.monotonic() - self._run_started + with self._print_lock: + line = (f'+{elapsed:8.3f}s {result.status:<5} {result.name:<50} ' + f'{result.duration:6.2f}s') + if result.reason: + line += f' {result.reason}' + print(line) + if result.status == 'FAIL': + print(f' logs: {result.workdir / "logs"}') + sys.stdout.flush() + + def discard_workdir(self, result: TestResult) -> None: + """Drop a finished test's directory unless it failed. + + Only a failure is ever read, and deleting as each test lands keeps the + run's disk use flat instead of holding all of them to the end. + """ + if self.keep_logs or result.status == 'FAIL': + return + shutil.rmtree(result.workdir, ignore_errors=True) + + def discard_run_dir(self, results: list) -> None: + """Drop the run directory after a clean run, but only what this run + made. + + --run-dir is used verbatim, so it can name a directory that was + already there holding anything at all; rmtree on that is data loss, + not cleanup. discard_workdir() has taken each test's own directory + by now, so all that is left of this run are the empty parents the + test names needed. + """ + if self.run_dir_created: + shutil.rmtree(self.run_dir, ignore_errors=True) + return + for result in results: + directory = result.workdir.parent + while directory != self.run_dir: + try: + directory.rmdir() # empty only; never recursive + except OSError: + break + directory = directory.parent + + def mode_note(self) -> str: + """Extra text for the summary header naming a non-default mode. + + The io-uring invocation selects nothing, so its counts must match the + default run's; without the label the two are indistinguishable in a CI + log. A -Dsync-init build is invisible for the same reason. + """ + note = ', io-uring' if self.io_uring else '' + if self.sync_init != 'auto': + note += f', sync-init={self.sync_init}' + return note + + def summarise(self, results: list, elapsed: float) -> int: + """Print the counts, the slowest tests and every failure.""" + passed = [r for r in results if r.status == 'PASS'] + failed = [r for r in results if r.status == 'FAIL'] + skipped = [r for r in results if r.status == 'SKIP'] + print() + print(f'{len(results)} tests: {len(passed)} passed, {len(failed)} ' + f'failed, {len(skipped)} skipped in {elapsed:.1f}s ' + f'(-j{self.jobs}{self.mode_note()})') + + slowest = sorted(results, key=lambda r: r.duration, reverse=True)[:5] + if slowest: + print() + print('slowest:') + for result in slowest: + print(f' {result.duration:5.1f}s {result.name}') + + if failed: + print() + print('failed:') + for result in failed: + print(f' {result.name} {result.reason}') + print(f' logs: {result.workdir / "logs" / "script.out"}') + for core in result.cores: + if core.top_frames: + for frame in core.top_frames.splitlines(): + print(f' {frame}') + elif core.backtrace is None: + print(f' core kept, not backtraced: {core.path}') + print(f' repro: sh {result.workdir / "logs" / "repro.sh"}') + + if not failed and not self.keep_logs: + self.discard_run_dir(results) + else: + print() + print(f'test output kept in {self.run_dir}') + return 1 if failed else 0 + + def stop(self) -> None: + """Ask the workers to stop launching new tests.""" + self._stop.set() + + def stopping(self) -> bool: + """True once stop() has been asked for.""" + return self._stop.is_set() + + def cleanup(self) -> None: + self._cgroup.cleanup() + + +def read_exclude_file() -> set: + """One test name per line; blank lines and #-comments ignored, missing + file is fine.""" + try: + text = EXCLUDE_FILE.read_text() + except OSError: + return set() + names = set() + for line in text.splitlines(): + line = line.split('#', 1)[0].strip() + if line: + names.add(line) + return names + + +def parse_args(argv: list) -> argparse.Namespace: + parser = argparse.ArgumentParser( + description='Run the libfuse shell test suite.') + parser.add_argument('-j', '--jobs', type=int, + default=max(1, (os.cpu_count() or 2) // 2), + help='parallel tests (default: cpu_count // 2)') + parser.add_argument('-t', '--test', action='append', default=[], + metavar='PATTERN', + help='fnmatch against the test name, or the path to ' + 'a case; repeatable') + parser.add_argument('-g', '--group', action='append', default=[], + help='only tests declaring "# GROUP: <group>"') + parser.add_argument('-X', '--exclude', action='append', default=[], + metavar='NAME', + help='skip this test; repeatable, adds to test/exclude') + parser.add_argument('--build-dir', default='.', + help='meson build root (default: .)') + parser.add_argument('--setuid-helpers', action='store_true', + help="chown root:root + chmod 4755 the build tree's " + 'fusermount3 (needs sudo); without it an ' + 'unprivileged run skips every mounting test') + parser.add_argument('--run-dir', default=None, + help='where to write per-test output, verbatim') + parser.add_argument('--persist-base-dir', default=None, metavar='DIR', + help='write DIR to ~/.config/libfuse/tests.conf ' + 'and exit') + parser.add_argument('--print-base-dir', action='store_true', + help='print the resolved base directory and exit') + parser.add_argument('--timeout', type=float, default=None, + metavar='SECS', help="override every script's timeout") + parser.add_argument('--io-uring', action='store_true', + help='run every test over fuse-io-uring; exits 77 ' + 'when the kernel or the build does not offer it') + parser.add_argument('--io-uring-queue-depth', type=int, default=None, + metavar='N', + help="export FUSE_URING_QUEUE_DEPTH; default is " + "libfuse's 8") + parser.add_argument('-l', '--list', action='store_true', + help='print the selected tests and exit') + parser.add_argument('-v', '--verbose', action='store_true', + help="stream each script's output live") + parser.add_argument('--keep-logs', action='store_true', + help="keep every test's output, not just the failures'") + parser.add_argument('--repeat', type=int, default=1, metavar='N', + help='run the selection N times') + return parser.parse_args(argv) + + +def main(argv: list) -> int: + args = parse_args(argv) + + if args.persist_base_dir: + write_configured_base_dir(args.persist_base_dir) + return 0 + if args.print_base_dir: + print(resolve_base_dir()) + return 0 + + build_dir = Path(args.build_dir).resolve() + exclude = read_exclude_file() | set(args.exclude) + + if args.list: + for spec in discover(CASES_DIR, args.test, set(args.group), exclude): + print(spec.name) + return 0 + + if args.setuid_helpers: + setuid_helpers(build_dir) + + reexec_under_user_scope_if_needed() + raise_nofile_limit() + + run_dir = resolve_run_dir(args) + runner = TestRunner(build_dir=build_dir, run_dir=run_dir, + run_dir_created=make_run_dir(run_dir), jobs=args.jobs, + keep_logs=args.keep_logs, verbose=args.verbose, + io_uring=args.io_uring, + io_uring_depth=args.io_uring_queue_depth) + if args.io_uring: + # A missing precondition skips the whole invocation rather than + # failing it, so a plain `meson test` on a machine that never enabled + # the module parameter reports one SKIP instead of 88 failures. CI does + # not rely on that: ci-build.sh enables the parameter itself. + reason = runner.preflight_io_uring() + if reason: + print(f'SKIP: {reason}') + runner.cleanup() + return SKIP_EXIT_CODE + # The capability says the kernel offers the transport, not that this + # user may open a ring. Asked here rather than skipped on, because a + # run that then fails every ring check is the honest report. + refused = io_uring_setup_error() + if refused: + print(f'warning: io_uring_setup() failed with {refused}; the ' + 'daemons will fall back to /dev/fuse') + print(' sudo sysctl -w kernel.io_uring_disabled=0') + specs = discover(CASES_DIR, args.test, set(args.group), exclude) + if args.timeout is not None: + specs = [replace(s, timeout=args.timeout) for s in specs] + + def on_signal(signum, frame): + # The first one lets the tests already running finish, which is + # usually what an interrupt means. Asking twice means the test in + # flight is the problem, so take the containment down with us -- + # otherwise the run sits out its full timeout before it can react. + if runner.stopping(): + print('\ninterrupted again: killing the tests still running') + runner.cleanup() + os._exit(128 + signum) + print('\ninterrupted: waiting for the running tests to finish') + runner.stop() + signal.signal(signal.SIGINT, on_signal) + signal.signal(signal.SIGTERM, on_signal) + + print(f'running {len(specs)} tests from {CASES_DIR} in {run_dir}') + rc = 0 + try: + for _ in range(args.repeat): + rc |= runner.run_all(specs) + finally: + runner.cleanup() + return rc + + +if __name__ == '__main__': + sys.exit(main(sys.argv[1:])) diff --git a/test/test_api_30.c b/test/test_api_30.c new file mode 100644 index 000000000..2b9d232f9 --- /dev/null +++ b/test/test_api_30.c @@ -0,0 +1,256 @@ +/* + * FUSE: Filesystem in Userspace + * + * Minimal low level filesystem built against the FUSE_USE_VERSION 30 API, + * so that the old API keeps being compiled and exercised. + * + * This program can be distributed under the terms of the GNU GPLv2. + * See the file GPL2.txt. + */ + +#define FUSE_USE_VERSION 30 + +/* the high level header has to keep building at this version as well */ +#include <fuse.h> + +#include <fuse_config.h> +#include <fuse_lowlevel.h> +#include <assert.h> +#include <dirent.h> +#include <errno.h> +#include <fcntl.h> +#include <pthread.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#ifndef __linux__ +#include <limits.h> +#else +#include <linux/limits.h> +#endif + +#define FILE_INO 2 + +static const char file_name[] = "hello"; +static const char file_contents[] = "Hello World!\n"; + +static int tfs_stat(fuse_ino_t ino, struct stat *stbuf) +{ + stbuf->st_ino = ino; + switch (ino) { + case FUSE_ROOT_ID: + stbuf->st_mode = S_IFDIR | 0755; + stbuf->st_nlink = 2; + break; + + case FILE_INO: + stbuf->st_mode = S_IFREG | 0444; + stbuf->st_nlink = 1; + stbuf->st_size = strlen(file_contents); + break; + + default: + return -1; + } + return 0; +} + +static void tfs_lookup(fuse_req_t req, fuse_ino_t parent, const char *name) +{ + struct fuse_entry_param e; + + if (parent != FUSE_ROOT_ID || strcmp(name, file_name) != 0) { + fuse_reply_err(req, ENOENT); + return; + } + + memset(&e, 0, sizeof(e)); + e.ino = FILE_INO; + e.attr_timeout = 1.0; + e.entry_timeout = 1.0; + tfs_stat(e.ino, &e.attr); + + fuse_reply_entry(req, &e); +} + +static void tfs_getattr(fuse_req_t req, fuse_ino_t ino, + struct fuse_file_info *fi) +{ + struct stat stbuf; + + (void) fi; + + memset(&stbuf, 0, sizeof(stbuf)); + if (tfs_stat(ino, &stbuf) == -1) + fuse_reply_err(req, ENOENT); + else + fuse_reply_attr(req, &stbuf, 1.0); +} + +struct dirbuf { + char *p; + size_t size; +}; + +static void dirbuf_add(fuse_req_t req, struct dirbuf *b, const char *name, + fuse_ino_t ino) +{ + struct stat stbuf; + size_t oldsize = b->size; + + b->size += fuse_add_direntry(req, NULL, 0, name, NULL, 0); + b->p = realloc(b->p, b->size); + assert(b->p != NULL); + + memset(&stbuf, 0, sizeof(stbuf)); + stbuf.st_ino = ino; + fuse_add_direntry(req, b->p + oldsize, b->size - oldsize, name, &stbuf, + b->size); +} + +static int reply_buf_limited(fuse_req_t req, const char *buf, size_t bufsize, + off_t off, size_t maxsize) +{ + size_t offset = (size_t) off; + + if (off < 0 || offset >= bufsize) + return fuse_reply_buf(req, NULL, 0); + + if (bufsize - offset < maxsize) + maxsize = bufsize - offset; + + return fuse_reply_buf(req, buf + offset, maxsize); +} + +static void tfs_readdir(fuse_req_t req, fuse_ino_t ino, size_t size, + off_t off, struct fuse_file_info *fi) +{ + struct dirbuf b; + + (void) fi; + + if (ino != FUSE_ROOT_ID) { + fuse_reply_err(req, ENOTDIR); + return; + } + + memset(&b, 0, sizeof(b)); + dirbuf_add(req, &b, ".", FUSE_ROOT_ID); + dirbuf_add(req, &b, "..", FUSE_ROOT_ID); + dirbuf_add(req, &b, file_name, FILE_INO); + reply_buf_limited(req, b.p, b.size, off, size); + free(b.p); +} + +static void tfs_open(fuse_req_t req, fuse_ino_t ino, + struct fuse_file_info *fi) +{ + if (ino != FILE_INO) + fuse_reply_err(req, EISDIR); + else if ((fi->flags & O_ACCMODE) != O_RDONLY) + fuse_reply_err(req, EACCES); + else + fuse_reply_open(req, fi); +} + +static void tfs_read(fuse_req_t req, fuse_ino_t ino, size_t size, off_t off, + struct fuse_file_info *fi) +{ + (void) fi; + + assert(ino == FILE_INO); + reply_buf_limited(req, file_contents, strlen(file_contents), off, size); +} + +static const struct fuse_lowlevel_ops tfs_oper = { + .lookup = tfs_lookup, + .getattr = tfs_getattr, + .readdir = tfs_readdir, + .open = tfs_open, + .read = tfs_read, +}; + +static void *run_fs(void *data) +{ + struct fuse_session *se = (struct fuse_session *)data; + + assert(fuse_session_loop(se) == 0); + return NULL; +} + +static void check_readdir(const char *mountpoint) +{ + DIR *dir = opendir(mountpoint); + const struct dirent *ent; + int found = 0; + + assert(dir != NULL); + while ((ent = readdir(dir)) != NULL) { + if (strcmp(ent->d_name, file_name) == 0) + found = 1; + } + assert(closedir(dir) == 0); + assert(found == 1); +} + +static void check_read(const char *mountpoint) +{ + char path[PATH_MAX]; + char buf[sizeof(file_contents)]; + int fd; + ssize_t len; + + assert(snprintf(path, PATH_MAX, "%s/%s", mountpoint, file_name) > 0); + fd = open(path, O_RDONLY); + if (fd == -1) { + perror(path); + assert(0); + } + + len = read(fd, buf, sizeof(buf)); + assert(len == (ssize_t) strlen(file_contents)); + assert(memcmp(buf, file_contents, len) == 0); + assert(close(fd) == 0); +} + +int main(int argc, char *argv[]) +{ + struct fuse_args args = FUSE_ARGS_INIT(argc, argv); + struct fuse_session *se; + struct fuse_cmdline_opts fuse_opts; + pthread_t fs_thread; + + assert(fuse_parse_cmdline(&args, &fuse_opts) == 0); +#ifndef __FreeBSD__ + assert(fuse_opt_add_arg(&args, "-oauto_unmount") == 0); +#endif + se = fuse_session_new(&args, &tfs_oper, sizeof(tfs_oper), NULL); + assert(se != NULL); + assert(fuse_set_signal_handlers(se) == 0); + assert(fuse_session_mount(se, fuse_opts.mountpoint) == 0); + + assert(pthread_create(&fs_thread, NULL, run_fs, (void *)se) == 0); + + check_readdir(fuse_opts.mountpoint); + check_read(fuse_opts.mountpoint); + + /* + * fuse_session_exit() does not interrupt read(), so nudge this thread + * with the ignored signal to let the loop free its receive buffer. + */ + fuse_session_exit(se); + assert(pthread_kill(fs_thread, SIGPIPE) == 0); + assert(pthread_join(fs_thread, NULL) == 0); + + fuse_session_unmount(se); + fuse_remove_signal_handlers(se); + fuse_session_destroy(se); + free(fuse_opts.mountpoint); + fuse_opt_free_args(&args); + + printf("Test completed successfully.\n"); + return 0; +} diff --git a/test/test_ctests.py b/test/test_ctests.py deleted file mode 100644 index 36b5ff347..000000000 --- a/test/test_ctests.py +++ /dev/null @@ -1,150 +0,0 @@ -#!/usr/bin/env python3 - -if __name__ == '__main__': - import pytest - import sys - sys.exit(pytest.main([__file__] + sys.argv[1:])) - -import subprocess -import pytest -import platform -import sys -import os -import logging -from packaging import version -from util import (wait_for_mount, umount, cleanup, base_cmdline, - safe_sleep, basename, fuse_test_marker, fuse_caps, - fuse_proto, create_tmpdir, parse_kernel_version) -from os.path import join as pjoin -import os.path - -pytestmark = fuse_test_marker() - -def test_abi(): - cmdline = [ pjoin(basename, 'test', 'test_abi') ] - subprocess.check_call(cmdline) - -@pytest.mark.skipif('FUSE_CAP_WRITEBACK_CACHE' not in fuse_caps, - reason='not supported by running kernel') -@pytest.mark.parametrize("writeback", (False, True)) -def test_write_cache(tmpdir, writeback, output_checker): - if writeback and parse_kernel_version(platform.release()) < version.parse('3.14'): - pytest.skip('Requires kernel 3.14 or newer') - # This test hangs under Valgrind when running close(fd) - # test_write_cache.c:test_fs(). Most likely this is because of an internal - # deadlock in valgrind, it probably assumes that until close() returns, - # control does not come to the program. - mnt_dir = str(tmpdir) - print("mnt_dir: '" + mnt_dir + "'") - create_tmpdir(mnt_dir) - - cmdline = [ pjoin(basename, 'test', 'test_write_cache'), - mnt_dir ] - if writeback: - cmdline.append('-owriteback_cache') - elif parse_kernel_version(platform.release()) >= version.parse('5.16'): - # Test that close(rofd) does not block waiting for pending writes. - # This test requires kernel commit a390ccb316be ("fuse: add FOPEN_NOFLUSH") - # so opt-in for this test from kernel 5.16. - cmdline.append('--delay_ms=200') - subprocess.check_call(cmdline, stdout=output_checker.fd, stderr=output_checker.fd) - - -names = [ 'notify_inval_inode', 'invalidate_path' ] -if fuse_proto >= (7,15): - names.append('notify_store_retrieve') -@pytest.mark.skipif(fuse_proto < (7,12), - reason='not supported by running kernel') -@pytest.mark.parametrize("name", names) -@pytest.mark.parametrize("notify", (True, False)) -def test_notify1(tmpdir, name, notify, output_checker): - logger = logging.getLogger(__name__) - mnt_dir = str(tmpdir) - logger.debug(f"Mount directory: {mnt_dir}") - create_tmpdir(mnt_dir) - cmdline = base_cmdline + \ - [ pjoin(basename, 'example', name), - '-f', '--update-interval=1', mnt_dir ] - if not notify: - cmdline.append('--no-notify') - logger.debug(f"Command line: {' '.join(cmdline)}") - mount_process = subprocess.Popen(cmdline, stdout=output_checker.fd, - stderr=output_checker.fd) - try: - wait_for_mount(mount_process, mnt_dir) - logger.debug("Mount completed") - filename = pjoin(mnt_dir, 'current_time') - logger.debug(f"Target filename: {filename}") - with open(filename, 'r') as fh: - read1 = fh.read() - logger.debug(f"First read: {read1}") - logger.debug("Sleeping for 2 seconds...") - safe_sleep(2) - logger.debug("Sleep completed") - with open(filename, 'r') as fh: - read2 = fh.read() - logger.debug(f"Second read: {read2}") - if notify: - logger.debug("Expecting reads to be different") - assert read1 != read2 - else: - logger.debug("Expecting reads to be the same") - assert read1 == read2 - logger.debug("Test completed successfully") - except: - logger.error(f"Failure in notify test: '{' '.join(cmdline)}'") - logger.exception("Exception details:") - cleanup(mount_process, mnt_dir) - raise - else: - logger.debug("Unmounting...") - try: - umount(mount_process, mnt_dir) - logger.debug("Umount disabled") - except: - logger.error(f"Failure in unmount: '{' '.join(cmdline)}'") - cleanup(mount_process, mnt_dir) - logger.debug("Unmount completed") - -@pytest.mark.skipif(fuse_proto < (7,12), - reason='not supported by running kernel') -@pytest.mark.parametrize("notify", (True, False)) -def test_notify_file_size(tmpdir, notify, output_checker): - logger = logging.getLogger(__name__) - mnt_dir = str(tmpdir) - logger.debug(f"Mount directory: {mnt_dir}") - create_tmpdir(mnt_dir) - cmdline = base_cmdline + \ - [ pjoin(basename, 'example', 'invalidate_path'), - '-f', '--update-interval=1', mnt_dir ] - if not notify: - cmdline.append('--no-notify') - logger.debug(f"Command line: {' '.join(cmdline)}") - mount_process = subprocess.Popen(cmdline, stdout=output_checker.fd, - stderr=output_checker.fd) - logger.debug(f"Mount process PID: {mount_process.pid}") - try: - wait_for_mount(mount_process, mnt_dir) - filename = pjoin(mnt_dir, 'growing') - size = os.path.getsize(filename) - logger.debug(f"Initial file size: {size}") - logger.debug("Sleeping for 2 seconds...") - safe_sleep(2) - logger.debug("Sleep completed") - new_size = os.path.getsize(filename) - logger.debug(f"New file size: {new_size}") - if notify: - assert new_size > size - else: - assert new_size == size - logger.debug("Test completed successfully") - except: - cleanup(mount_process, mnt_dir) - raise - else: - try: - umount(mount_process, mnt_dir) - except: - logger.error(f"Failure in unmount: '{' '.join(cmdline)}'") - cleanup(mount_process, mnt_dir) - logger.debug("Unmount completed") diff --git a/test/test_custom_io.py b/test/test_custom_io.py deleted file mode 100644 index 737b939c8..000000000 --- a/test/test_custom_io.py +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env python3 - -if __name__ == '__main__': - import sys - - import pytest - sys.exit(pytest.main([__file__] + sys.argv[1:])) - -import os -import socket -import struct -import subprocess -import sys -import time -from os.path import join as pjoin - -import pytest - -from util import base_cmdline, basename - -FUSE_OP_INIT = 26 - -FUSE_MAJOR_VERSION = 7 -FUSE_MINOR_VERSION = 38 - -fuse_in_header_fmt = '<IIQQIIII' -fuse_out_header_fmt = '<IiQ' - -fuse_init_in_fmt = '<IIIII44x' -fuse_init_out_fmt = '<IIIIHHIIHHI28x' - - -def sock_recvall(sock: socket.socket, bufsize: int) -> bytes: - buf = bytes() - while len(buf) < bufsize: - buf += sock.recv(bufsize - len(buf)) - return buf - - -def tst_init(sock: socket.socket): - unique_req = 10 - dummy_init_req_header = struct.pack( - fuse_in_header_fmt, struct.calcsize(fuse_in_header_fmt) + - struct.calcsize(fuse_init_in_fmt), FUSE_OP_INIT, unique_req, 0, 0, 0, - 0, 0) - dummy_init_req_payload = struct.pack( - fuse_init_in_fmt, FUSE_MAJOR_VERSION, FUSE_MINOR_VERSION, 0, 0, 0) - dummy_init_req = dummy_init_req_header + dummy_init_req_payload - - sock.sendall(dummy_init_req) - - response_header = sock_recvall(sock, struct.calcsize(fuse_out_header_fmt)) - packet_len, _, unique_res = struct.unpack( - fuse_out_header_fmt, response_header) - assert unique_res == unique_req - - response_payload = sock_recvall(sock, packet_len - len(response_header)) - response_payload = struct.unpack(fuse_init_out_fmt, response_payload) - assert response_payload[0] == FUSE_MAJOR_VERSION - - -def test_hello_uds(output_checker): - cmdline = base_cmdline + [pjoin(basename, 'example', 'hello_ll_uds')] - print(cmdline) - uds_process = subprocess.Popen(cmdline, stdout=output_checker.fd, - stderr=output_checker.fd) - time.sleep(1) - - sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - sock.settimeout(1) - sock.connect("/tmp/libfuse-hello-ll.sock") - - tst_init(sock) - - sock.close() - uds_process.terminate() - try: - uds_process.wait(1) - except subprocess.TimeoutExpired: - uds_process.kill() - os.remove("/tmp/libfuse-hello-ll.sock") diff --git a/test/test_examples.py b/test/test_examples.py deleted file mode 100755 index 9c8b77eec..000000000 --- a/test/test_examples.py +++ /dev/null @@ -1,902 +0,0 @@ -#!/usr/bin/env python3 - -if __name__ == '__main__': - import pytest - import sys - sys.exit(pytest.main([__file__] + sys.argv[1:])) - -import subprocess -import os -import sys -import py -import pytest -import stat -import shutil -import filecmp -import tempfile -import time -import errno -import sys -import platform -import re -from packaging import version -from tempfile import NamedTemporaryFile -from contextlib import contextmanager -from util import (wait_for_mount, umount, cleanup, base_cmdline, - safe_sleep, basename, fuse_test_marker, test_printcap, - fuse_proto, fuse_caps, powerset, parse_kernel_version) -from os.path import join as pjoin -import logging - -pytestmark = fuse_test_marker() - -TEST_FILE = __file__ - -with open(TEST_FILE, 'rb') as fh: - TEST_DATA = fh.read() - -def name_generator(__ctr=[0]): - __ctr[0] += 1 - return 'testfile_%d' % __ctr[0] - -options = [] -if sys.platform == 'linux': - options.append('clone_fd') - -def invoke_directly(mnt_dir, name, options): - # Handle test/hello specially since it's not in example/ - if name.startswith('test/'): - path = pjoin(basename, name) - else: - path = pjoin(basename, 'example', name) - - cmdline = base_cmdline + [ path, '-f', mnt_dir, '-o', ','.join(options) ] - if name == 'hello_ll': - # supports single-threading only - cmdline.append('-s') - - return cmdline - -def invoke_mount_fuse(mnt_dir, name, options): - return base_cmdline + [ pjoin(basename, 'util', 'mount.fuse3'), - name, mnt_dir, '-o', ','.join(options) ] - -def invoke_mount_fuse_drop_privileges(mnt_dir, name, options): - if os.getuid() != 0: - pytest.skip('drop_privileges requires root, skipping.') - - return invoke_mount_fuse(mnt_dir, name, options + ('drop_privileges',)) - -class raii_tmpdir: - def __init__(self): - self.d = tempfile.mkdtemp() - - def __str__(self): - return str(self.d) - - def mkdir(self, path): - return py.path.local(str(self.d)).mkdir(path) - -@pytest.fixture -def short_tmpdir(): - return raii_tmpdir() - -def readdir_inode(dir): - cmd = base_cmdline + [ pjoin(basename, 'test', 'readdir_inode'), dir ] - with subprocess.Popen(cmd, stdout=subprocess.PIPE, - universal_newlines=True) as proc: - lines = proc.communicate()[0].splitlines() - lines.sort() - return lines - - -@pytest.mark.parametrize("cmdline_builder", (invoke_directly, invoke_mount_fuse, - invoke_mount_fuse_drop_privileges)) -@pytest.mark.parametrize("options", powerset(options)) -@pytest.mark.parametrize("name", ('hello', 'hello_ll', 'test/hello')) -def test_hello(tmpdir, name, options, cmdline_builder, output_checker): - logger = logging.getLogger(__name__) - mnt_dir = str(tmpdir) - logger.debug(f"Mount directory: {mnt_dir}") - cmdline = cmdline_builder(mnt_dir, name, options) - logger.debug(f"Command line: {' '.join(cmdline)}") - mount_process = subprocess.Popen( - cmdline, - stdout=output_checker.fd, stderr=output_checker.fd) - logger.debug(f"Mount process PID: {mount_process.pid}") - try: - logger.debug("Waiting for mount...") - wait_for_mount(mount_process, mnt_dir) - logger.debug("Mount completed") - assert os.listdir(mnt_dir) == [ 'hello' ] - logger.debug("Verified 'hello' file exists in mount directory") - filename = pjoin(mnt_dir, 'hello') - with open(filename, 'r') as fh: - assert fh.read() == 'Hello World!\n' - logger.debug("Verified contents of 'hello' file") - with pytest.raises(IOError) as exc_info: - open(filename, 'r+') - assert exc_info.value.errno == errno.EACCES - logger.debug("Verified EACCES error when trying to open file for writing") - with pytest.raises(IOError) as exc_info: - open(filename + 'does-not-exist', 'r+') - assert exc_info.value.errno == errno.ENOENT - logger.debug("Verified ENOENT error for non-existent file") - if name == 'hello_ll': - logger.debug("Testing xattr for hello_ll") - tst_xattr(mnt_dir) - path = os.path.join(mnt_dir, 'hello') - tst_xattr(path) - except: - logger.error("Exception occurred during test", exc_info=True) - cleanup(mount_process, mnt_dir) - raise - else: - logger.debug("Unmounting...") - umount(mount_process, mnt_dir) - logger.debug("Test completed successfully") - -@pytest.mark.parametrize("writeback", (False, True)) -@pytest.mark.parametrize("name", ('passthrough', 'passthrough_plus', - 'passthrough_fh', 'passthrough_ll')) -@pytest.mark.parametrize("debug", (False, True)) -def test_passthrough(short_tmpdir, name, debug, output_checker, writeback): - # Avoid false positives from libfuse debug messages - if debug: - output_checker.register_output(r'^ unique: [0-9]+, error: -[0-9]+ .+$', - count=0) - - # test_syscalls prints "No error" under FreeBSD - output_checker.register_output(r"^ \d\d \[[^\]]+ message: 'No error: 0'\]", - count=0) - - mnt_dir = str(short_tmpdir.mkdir('mnt')) - src_dir = str(short_tmpdir.mkdir('src')) - - if name == 'passthrough_plus': - cmdline = base_cmdline + \ - [ pjoin(basename, 'example', 'passthrough'), - '--plus', '-f', mnt_dir ] - else: - cmdline = base_cmdline + \ - [ pjoin(basename, 'example', name), - '-f', mnt_dir ] - if debug: - cmdline.append('-d') - - if writeback: - if name != 'passthrough_ll': - pytest.skip('example does not support writeback caching') - cmdline.append('-o') - cmdline.append('writeback') - - mount_process = subprocess.Popen(cmdline, stdout=output_checker.fd, - stderr=output_checker.fd) - try: - wait_for_mount(mount_process, mnt_dir) - work_dir = mnt_dir + src_dir - - tst_statvfs(work_dir) - tst_readdir(src_dir, work_dir) - tst_readdir_big(src_dir, work_dir) - tst_open_read(src_dir, work_dir) - tst_open_write(src_dir, work_dir) - tst_create(work_dir) - tst_passthrough(src_dir, work_dir) - tst_append(src_dir, work_dir) - tst_seek(src_dir, work_dir) - tst_mkdir(work_dir) - tst_rmdir(work_dir, src_dir) - tst_unlink(work_dir, src_dir) - tst_symlink(work_dir) - if os.getuid() == 0: - tst_chown(work_dir) - - # Underlying fs may not have full nanosecond resolution - tst_utimens(work_dir, ns_tol=1000) - - tst_link(work_dir) - tst_truncate_path(work_dir) - tst_truncate_fd(work_dir) - tst_open_unlink(work_dir) - - syscall_test_cmd = [ os.path.join(basename, 'test', 'test_syscalls'), - work_dir, ':' + src_dir ] - if writeback: - # When writeback caching is enabled, kernel has to open files for - # reading even when userspace opens with O_WDONLY. This fails if the - # filesystem process doesn't have special permission. - syscall_test_cmd.append('-53') - subprocess.check_call(syscall_test_cmd) - except: - cleanup(mount_process, mnt_dir) - raise - else: - umount(mount_process, mnt_dir) - -@pytest.mark.parametrize("cache", (False, True)) -def test_passthrough_hp(short_tmpdir, cache, output_checker): - mnt_dir = str(short_tmpdir.mkdir('mnt')) - src_dir = str(short_tmpdir.mkdir('src')) - - cmdline = base_cmdline + \ - [ pjoin(basename, 'example', 'passthrough_hp'), - src_dir, mnt_dir ] - - cmdline.append('--foreground') - - if not cache: - cmdline.append('--nocache') - - mount_process = subprocess.Popen(cmdline, stdout=output_checker.fd, - stderr=output_checker.fd) - try: - wait_for_mount(mount_process, mnt_dir) - - tst_statvfs(mnt_dir) - tst_readdir(src_dir, mnt_dir) - tst_readdir_big(src_dir, mnt_dir) - tst_open_read(src_dir, mnt_dir) - tst_open_write(src_dir, mnt_dir) - tst_create(mnt_dir) - if not cache: - tst_passthrough(src_dir, mnt_dir) - tst_append(src_dir, mnt_dir) - tst_seek(src_dir, mnt_dir) - tst_mkdir(mnt_dir) - if cache: - # if cache is enabled, no operations should go through - # src_dir as the cache will become stale. - tst_rmdir(mnt_dir) - tst_unlink(mnt_dir) - else: - tst_rmdir(mnt_dir, src_dir) - tst_unlink(mnt_dir, src_dir) - tst_symlink(mnt_dir) - if os.getuid() == 0: - tst_chown(mnt_dir) - - # Underlying fs may not have full nanosecond resolution - tst_utimens(mnt_dir, ns_tol=1000) - - tst_link(mnt_dir) - tst_truncate_path(mnt_dir) - tst_truncate_fd(mnt_dir) - tst_open_unlink(mnt_dir) - - # test_syscalls assumes that changes in source directory - # will be reflected immediately in mountpoint, so we - # can't use it. - if not cache: - syscall_test_cmd = [ os.path.join(basename, 'test', 'test_syscalls'), - mnt_dir, ':' + src_dir ] - # unlinked testfiles check fails without kernel fix - # "fuse: fix illegal access to inode with reused nodeid" - # so opt-in for this test from kernel 5.14 - if parse_kernel_version(platform.release()) >= version.parse('5.14'): - syscall_test_cmd.append('-u') - subprocess.check_call(syscall_test_cmd) - except: - cleanup(mount_process, mnt_dir) - raise - else: - umount(mount_process, mnt_dir) - - -@pytest.mark.skipif(fuse_proto < (7,11), - reason='not supported by running kernel') -def test_ioctl(tmpdir, output_checker): - progname = pjoin(basename, 'example', 'ioctl') - if not os.path.exists(progname): - pytest.skip('%s not built' % os.path.basename(progname)) - - # Check if binary is 32-bit - file_output = subprocess.check_output(['file', progname]).decode() - if 'ELF 32-bit' in file_output and platform.machine() == 'x86_64': - pytest.skip('ioctl test not supported for 32-bit binary on 64-bit system') - - mnt_dir = str(tmpdir) - testfile = pjoin(mnt_dir, 'fioc') - cmdline = base_cmdline + [progname, '-f', mnt_dir ] - mount_process = subprocess.Popen(cmdline, stdout=output_checker.fd, - stderr=output_checker.fd) - try: - wait_for_mount(mount_process, mnt_dir) - - cmdline = base_cmdline + \ - [ pjoin(basename, 'example', 'ioctl_client'), - testfile ] - assert subprocess.check_output(cmdline) == b'0\n' - with open(testfile, 'wb') as fh: - fh.write(b'foobar') - assert subprocess.check_output(cmdline) == b'6\n' - subprocess.check_call(cmdline + [ '3' ]) - with open(testfile, 'rb') as fh: - assert fh.read()== b'foo' - except: - cleanup(mount_process, mnt_dir) - raise - else: - umount(mount_process, mnt_dir) - -def test_poll(tmpdir, output_checker): - mnt_dir = str(tmpdir) - cmdline = base_cmdline + [pjoin(basename, 'example', 'poll'), - '-f', mnt_dir ] - mount_process = subprocess.Popen(cmdline, stdout=output_checker.fd, - stderr=output_checker.fd) - try: - wait_for_mount(mount_process, mnt_dir) - cmdline = base_cmdline + \ - [ pjoin(basename, 'example', 'poll_client') ] - subprocess.check_call(cmdline, cwd=mnt_dir) - except: - cleanup(mount_process, mnt_dir) - raise - else: - umount(mount_process, mnt_dir) - -def test_null(tmpdir, output_checker): - progname = pjoin(basename, 'example', 'null') - if not os.path.exists(progname): - pytest.skip('%s not built' % os.path.basename(progname)) - - mnt_file = str(tmpdir) + '/file' - with open(mnt_file, 'w') as fh: - fh.write('dummy') - cmdline = base_cmdline + [ progname, '-f', mnt_file ] - mount_process = subprocess.Popen(cmdline, stdout=output_checker.fd, - stderr=output_checker.fd) - def test_fn(name): - return os.stat(name).st_size > 4000 - try: - wait_for_mount(mount_process, mnt_file, test_fn) - with open(mnt_file, 'rb') as fh: - assert fh.read(382) == b'\0' * 382 - with open(mnt_file, 'wb') as fh: - fh.write(b'whatever') - except: - cleanup(mount_process, mnt_file) - raise - else: - umount(mount_process, mnt_file) - - -@pytest.mark.skipif(fuse_proto < (7,12), - reason='not supported by running kernel') -@pytest.mark.parametrize("only_expire", ("invalidate_entries", "expire_entries")) -@pytest.mark.parametrize("notify", (True, False)) -def test_notify_inval_entry(tmpdir, only_expire, notify, output_checker): - mnt_dir = str(tmpdir) - cmdline = base_cmdline + \ - [ pjoin(basename, 'example', 'notify_inval_entry'), - '-f', '--update-interval=1', - '--timeout=5', mnt_dir ] - if not notify: - cmdline.append('--no-notify') - if only_expire == "expire_entries": - cmdline.append('--only-expire') - if "FUSE_CAP_EXPIRE_ONLY" not in fuse_caps: - pytest.skip('only-expire not supported by running kernel') - mount_process = subprocess.Popen(cmdline, stdout=output_checker.fd, - stderr=output_checker.fd) - try: - wait_for_mount(mount_process, mnt_dir) - fname = pjoin(mnt_dir, os.listdir(mnt_dir)[0]) - try: - os.stat(fname) - except FileNotFoundError: - # We may have hit a race condition and issued - # readdir just before the name changed - fname = pjoin(mnt_dir, os.listdir(mnt_dir)[0]) - os.stat(fname) - - safe_sleep(2) - if not notify: - os.stat(fname) - safe_sleep(5) - with pytest.raises(FileNotFoundError): - os.stat(fname) - except: - cleanup(mount_process, mnt_dir) - raise - else: - umount(mount_process, mnt_dir) - -@pytest.mark.parametrize("intended_user", ('root', 'non_root')) -def test_dev_auto_unmount(short_tmpdir, output_checker, intended_user): - """Check that root can mount with dev and auto_unmount - (but non-root cannot). - Split into root vs non-root, so that the output of pytest - makes clear what functionality is being tested.""" - if os.getuid() == 0 and intended_user == 'non_root': - pytest.skip('needs to run as non-root') - if os.getuid() != 0 and intended_user == 'root': - pytest.skip('needs to run as root') - mnt_dir = str(short_tmpdir.mkdir('mnt')) - src_dir = str('/dev') - cmdline = base_cmdline + \ - [ pjoin(basename, 'example', 'passthrough_ll'), - '-o', f'source={src_dir},dev,auto_unmount', - '-f', mnt_dir ] - mount_process = subprocess.Popen(cmdline, stdout=output_checker.fd, - stderr=output_checker.fd) - try: - wait_for_mount(mount_process, mnt_dir) - if os.getuid() == 0: - open(pjoin(mnt_dir, 'null')).close() - else: - with pytest.raises(PermissionError): - open(pjoin(mnt_dir, 'null')).close() - except: - cleanup(mount_process, mnt_dir) - raise - else: - umount(mount_process, mnt_dir) - -@pytest.mark.skipif(os.getuid() != 0, - reason='needs to run as root') -def test_cuse(output_checker): - progname = pjoin(basename, 'example', 'cuse') - if not os.path.exists(progname): - pytest.skip('%s not built' % os.path.basename(progname)) - - # Check if binary is 32-bit - file_output = subprocess.check_output(['file', progname]).decode() - if 'ELF 32-bit' in file_output and platform.machine() == 'x86_64': - pytest.skip('cuse test not supported for 32-bit binary on 64-bit system') - - # Valgrind warns about unknown ioctls, that's ok - output_checker.register_output(r'^==([0-9]+).+unhandled ioctl.+\n' - r'==\1== \s{3}.+\n' - r'==\1== \s{3}.+$', count=0) - - devname = 'cuse-test-%d' % os.getpid() - devpath = '/dev/%s' % devname - cmdline = base_cmdline + \ - [ pjoin(basename, 'example', 'cuse'), - '-f', '--name=%s' % devname ] - mount_process = subprocess.Popen(cmdline, stdout=output_checker.fd, - stderr=output_checker.fd) - - cmdline = base_cmdline + \ - [ pjoin(basename, 'example', 'cuse_client'), - devpath ] - try: - wait_for_mount(mount_process, devpath, - test_fn=os.path.exists) - assert subprocess.check_output(cmdline + ['s']) == b'0\n' - data = b'some test data' - off = 5 - proc = subprocess.Popen(cmdline + [ 'w', str(len(data)), str(off) ], - stdin=subprocess.PIPE) - proc.stdin.write(data) - proc.stdin.close() - assert proc.wait(timeout=10) == 0 - size = str(off + len(data)).encode() + b'\n' - assert subprocess.check_output(cmdline + ['s']) == size - out = subprocess.check_output( - cmdline + [ 'r', str(off + len(data) + 2), '0' ]) - assert out == (b'\0' * off) + data - finally: - mount_process.terminate() - -def test_release_unlink_race(tmpdir, output_checker): - """test case for Issue #746 - - If RELEASE and UNLINK opcodes are sent back to back, and fuse_fs_release() - and fuse_fs_rename() are slow to execute, UNLINK will run while RELEASE is - still executing. UNLINK will try to rename the file and, while the rename - is happening, the RELEASE will finish executing. As a result, RELEASE will - not detect in time that UNLINK has happened, and UNLINK will not detect in - time that RELEASE has happened. - - - NOTE: This is triggered only when nullpath_ok is set. - - If it is NOT SET then get_path_nullok() called by fuse_lib_release() will - call get_path_common() and lock the path, and then the fuse_lib_unlink() - will wait for the path to be unlocked before executing and thus synchronise - with fuse_lib_release(). - - If it is SET then get_path_nullok() will just set the path to null and - return without locking anything and thus allowing fuse_lib_unlink() to - eventually execute unimpeded while fuse_lib_release() is still running. - """ - - fuse_mountpoint = str(tmpdir) - - fuse_binary_command = base_cmdline + \ - [ pjoin(basename, 'test', 'release_unlink_race'), - "-f", fuse_mountpoint] - - fuse_process = subprocess.Popen(fuse_binary_command, - stdout=output_checker.fd, - stderr=output_checker.fd) - - try: - wait_for_mount(fuse_process, fuse_mountpoint) - - temp_dir = tempfile.TemporaryDirectory(dir="/tmp/") - temp_dir_path = temp_dir.name - - fuse_temp_file, fuse_temp_file_path = tempfile.mkstemp(dir=(fuse_mountpoint + temp_dir_path)) - - os.close(fuse_temp_file) - os.unlink(fuse_temp_file_path) - - # needed for slow CI/CD pipelines for unlink OP to complete processing - safe_sleep(3) - - assert os.listdir(temp_dir_path) == [] - - except: - temp_dir.cleanup() - cleanup(fuse_process, fuse_mountpoint) - raise - - else: - temp_dir.cleanup() - umount(fuse_process, fuse_mountpoint) - - -@contextmanager -def os_open(name, flags): - fd = os.open(name, flags) - try: - yield fd - finally: - os.close(fd) - -def os_create(name): - os.close(os.open(name, os.O_CREAT | os.O_RDWR)) - -def tst_unlink(mnt_dir, src_dir=None): - name = name_generator() - fullname = mnt_dir + "/" + name - srcname = fullname - if src_dir is not None: - srcname = pjoin(src_dir, name) - with open(srcname, 'wb') as fh: - fh.write(b'hello') - assert name in os.listdir(mnt_dir) - os.unlink(fullname) - with pytest.raises(OSError) as exc_info: - os.stat(fullname) - assert exc_info.value.errno == errno.ENOENT - assert name not in os.listdir(mnt_dir) - -def tst_mkdir(mnt_dir): - dirname = name_generator() - fullname = mnt_dir + "/" + dirname - os.mkdir(fullname) - fstat = os.stat(fullname) - assert stat.S_ISDIR(fstat.st_mode) - assert os.listdir(fullname) == [] - # Some filesystem (e.g. BTRFS) don't track st_nlink for directories - assert fstat.st_nlink in (1,2) - assert dirname in os.listdir(mnt_dir) - -def tst_rmdir(mnt_dir, src_dir=None): - name = name_generator() - fullname = mnt_dir + "/" + name - srcname = fullname - if src_dir is not None: - srcname = pjoin(src_dir, name) - os.mkdir(srcname) - assert name in os.listdir(mnt_dir) - os.rmdir(fullname) - with pytest.raises(OSError) as exc_info: - os.stat(fullname) - assert exc_info.value.errno == errno.ENOENT - assert name not in os.listdir(mnt_dir) - -def tst_symlink(mnt_dir): - linkname = name_generator() - fullname = mnt_dir + "/" + linkname - os.symlink("/imaginary/dest", fullname) - fstat = os.lstat(fullname) - assert stat.S_ISLNK(fstat.st_mode) - assert os.readlink(fullname) == "/imaginary/dest" - assert fstat.st_nlink == 1 - assert linkname in os.listdir(mnt_dir) - -def tst_create(mnt_dir): - name = name_generator() - fullname = pjoin(mnt_dir, name) - with pytest.raises(OSError) as exc_info: - os.stat(fullname) - assert exc_info.value.errno == errno.ENOENT - assert name not in os.listdir(mnt_dir) - - fd = os.open(fullname, os.O_CREAT | os.O_RDWR) - os.close(fd) - - assert name in os.listdir(mnt_dir) - fstat = os.lstat(fullname) - assert stat.S_ISREG(fstat.st_mode) - assert fstat.st_nlink == 1 - assert fstat.st_size == 0 - -def tst_chown(mnt_dir): - filename = pjoin(mnt_dir, name_generator()) - os.mkdir(filename) - fstat = os.lstat(filename) - uid = fstat.st_uid - gid = fstat.st_gid - - uid_new = uid + 1 - os.chown(filename, uid_new, -1) - fstat = os.lstat(filename) - assert fstat.st_uid == uid_new - assert fstat.st_gid == gid - - gid_new = gid + 1 - os.chown(filename, -1, gid_new) - fstat = os.lstat(filename) - assert fstat.st_uid == uid_new - assert fstat.st_gid == gid_new - -def tst_open_read(src_dir, mnt_dir): - name = name_generator() - with open(pjoin(src_dir, name), 'wb') as fh_out, \ - open(TEST_FILE, 'rb') as fh_in: - shutil.copyfileobj(fh_in, fh_out) - - assert filecmp.cmp(pjoin(mnt_dir, name), TEST_FILE, False) - -def tst_open_write(src_dir, mnt_dir): - name = name_generator() - os_create(pjoin(src_dir, name)) - fullname = pjoin(mnt_dir, name) - with open(fullname, 'wb') as fh_out, \ - open(TEST_FILE, 'rb') as fh_in: - shutil.copyfileobj(fh_in, fh_out) - - assert filecmp.cmp(fullname, TEST_FILE, False) - -def tst_append(src_dir, mnt_dir): - name = name_generator() - os_create(pjoin(src_dir, name)) - fullname = pjoin(mnt_dir, name) - with os_open(fullname, os.O_WRONLY) as fd: - os.write(fd, b'foo\n') - with os_open(fullname, os.O_WRONLY|os.O_APPEND) as fd: - os.write(fd, b'bar\n') - - with open(fullname, 'rb') as fh: - assert fh.read() == b'foo\nbar\n' - -def tst_seek(src_dir, mnt_dir): - name = name_generator() - os_create(pjoin(src_dir, name)) - fullname = pjoin(mnt_dir, name) - with os_open(fullname, os.O_WRONLY) as fd: - os.lseek(fd, 1, os.SEEK_SET) - os.write(fd, b'foobar\n') - with os_open(fullname, os.O_WRONLY) as fd: - os.lseek(fd, 4, os.SEEK_SET) - os.write(fd, b'com') - - with open(fullname, 'rb') as fh: - assert fh.read() == b'\0foocom\n' - -def tst_open_unlink(mnt_dir): - name = pjoin(mnt_dir, name_generator()) - data1 = b'foo' - data2 = b'bar' - fullname = pjoin(mnt_dir, name) - with open(fullname, 'wb+', buffering=0) as fh: - fh.write(data1) - os.unlink(fullname) - with pytest.raises(OSError) as exc_info: - os.stat(fullname) - assert exc_info.value.errno == errno.ENOENT - assert name not in os.listdir(mnt_dir) - fh.write(data2) - fh.seek(0) - assert fh.read() == data1+data2 - -def tst_statvfs(mnt_dir): - os.statvfs(mnt_dir) - -def tst_link(mnt_dir): - name1 = pjoin(mnt_dir, name_generator()) - name2 = pjoin(mnt_dir, name_generator()) - shutil.copyfile(TEST_FILE, name1) - assert filecmp.cmp(name1, TEST_FILE, False) - - fstat1 = os.lstat(name1) - assert fstat1.st_nlink == 1 - - os.link(name1, name2) - - fstat1 = os.lstat(name1) - fstat2 = os.lstat(name2) - assert fstat1 == fstat2 - assert fstat1.st_nlink == 2 - assert os.path.basename(name2) in os.listdir(mnt_dir) - assert filecmp.cmp(name1, name2, False) - - # Since RELEASE requests are asynchronous, it is possible that - # libfuse still considers the file to be open at this point - # and (since -o hard_remove is not used) renames it instead of - # deleting it. In that case, the following lstat() call will - # still report an st_nlink value of 2 (cf. issue #157). - os.unlink(name2) - - assert os.path.basename(name2) not in os.listdir(mnt_dir) - with pytest.raises(FileNotFoundError): - os.lstat(name2) - - # See above, we may have to wait until RELEASE has been - # received before the st_nlink value is correct. - maxwait = time.time() + 2 - fstat1 = os.lstat(name1) - while fstat1.st_nlink == 2 and time.time() < maxwait: - fstat1 = os.lstat(name1) - time.sleep(0.1) - assert fstat1.st_nlink == 1 - - os.unlink(name1) - -def tst_readdir(src_dir, mnt_dir): - newdir = name_generator() - - src_newdir = pjoin(src_dir, newdir) - mnt_newdir = pjoin(mnt_dir, newdir) - file_ = src_newdir + "/" + name_generator() - subdir = src_newdir + "/" + name_generator() - subfile = subdir + "/" + name_generator() - - os.mkdir(src_newdir) - shutil.copyfile(TEST_FILE, file_) - os.mkdir(subdir) - shutil.copyfile(TEST_FILE, subfile) - - listdir_is = os.listdir(mnt_newdir) - listdir_is.sort() - listdir_should = [ os.path.basename(file_), os.path.basename(subdir) ] - listdir_should.sort() - assert listdir_is == listdir_should - - inodes_is = readdir_inode(mnt_newdir) - inodes_should = readdir_inode(src_newdir) - assert inodes_is == inodes_should - - os.unlink(file_) - os.unlink(subfile) - os.rmdir(subdir) - os.rmdir(src_newdir) - -def tst_readdir_big(src_dir, mnt_dir): - - # Add enough entries so that readdir needs to be called - # multiple times. - fnames = [] - for i in range(500): - fname = ('A rather long filename to make sure that we ' - 'fill up the buffer - ' * 3) + str(i) - with open(pjoin(src_dir, fname), 'w') as fh: - fh.write('File %d' % i) - fnames.append(fname) - - listdir_is = sorted(os.listdir(mnt_dir)) - listdir_should = sorted(os.listdir(src_dir)) - assert listdir_is == listdir_should - - inodes_is = readdir_inode(mnt_dir) - inodes_should = readdir_inode(src_dir) - assert inodes_is == inodes_should - - for fname in fnames: - stat_src = os.stat(pjoin(src_dir, fname)) - stat_mnt = os.stat(pjoin(mnt_dir, fname)) - assert stat_src.st_ino == stat_mnt.st_ino - assert stat_src.st_mtime == stat_mnt.st_mtime - assert stat_src.st_ctime == stat_mnt.st_ctime - assert stat_src.st_size == stat_mnt.st_size - os.unlink(pjoin(src_dir, fname)) - -def tst_truncate_path(mnt_dir): - assert len(TEST_DATA) > 1024 - - filename = pjoin(mnt_dir, name_generator()) - with open(filename, 'wb') as fh: - fh.write(TEST_DATA) - - fstat = os.stat(filename) - size = fstat.st_size - assert size == len(TEST_DATA) - - # Add zeros at the end - os.truncate(filename, size + 1024) - assert os.stat(filename).st_size == size + 1024 - with open(filename, 'rb') as fh: - assert fh.read(size) == TEST_DATA - assert fh.read(1025) == b'\0' * 1024 - - # Truncate data - os.truncate(filename, size - 1024) - assert os.stat(filename).st_size == size - 1024 - with open(filename, 'rb') as fh: - assert fh.read(size) == TEST_DATA[:size-1024] - - os.unlink(filename) - -def tst_truncate_fd(mnt_dir): - assert len(TEST_DATA) > 1024 - with NamedTemporaryFile('w+b', 0, dir=mnt_dir) as fh: - fd = fh.fileno() - fh.write(TEST_DATA) - fstat = os.fstat(fd) - size = fstat.st_size - assert size == len(TEST_DATA) - - # Add zeros at the end - os.ftruncate(fd, size + 1024) - assert os.fstat(fd).st_size == size + 1024 - fh.seek(0) - assert fh.read(size) == TEST_DATA - assert fh.read(1025) == b'\0' * 1024 - - # Truncate data - os.ftruncate(fd, size - 1024) - assert os.fstat(fd).st_size == size - 1024 - fh.seek(0) - assert fh.read(size) == TEST_DATA[:size-1024] - -def tst_utimens(mnt_dir, ns_tol=0): - filename = pjoin(mnt_dir, name_generator()) - os.mkdir(filename) - fstat = os.lstat(filename) - - atime = fstat.st_atime + 42.28 - mtime = fstat.st_mtime - 42.23 - if sys.version_info < (3,3): - os.utime(filename, (atime, mtime)) - else: - atime_ns = fstat.st_atime_ns + int(42.28*1e9) - mtime_ns = fstat.st_mtime_ns - int(42.23*1e9) - os.utime(filename, None, ns=(atime_ns, mtime_ns)) - - fstat = os.lstat(filename) - - assert abs(fstat.st_atime - atime) < 1 - assert abs(fstat.st_mtime - mtime) < 1 - if sys.version_info >= (3,3): - assert abs(fstat.st_atime_ns - atime_ns) <= ns_tol - assert abs(fstat.st_mtime_ns - mtime_ns) <= ns_tol - -def tst_passthrough(src_dir, mnt_dir): - name = name_generator() - src_name = pjoin(src_dir, name) - mnt_name = pjoin(src_dir, name) - assert name not in os.listdir(src_dir) - assert name not in os.listdir(mnt_dir) - with open(src_name, 'w') as fh: - fh.write('Hello, world') - assert name in os.listdir(src_dir) - assert name in os.listdir(mnt_dir) - assert os.stat(src_name) == os.stat(mnt_name) - - name = name_generator() - src_name = pjoin(src_dir, name) - mnt_name = pjoin(src_dir, name) - assert name not in os.listdir(src_dir) - assert name not in os.listdir(mnt_dir) - with open(mnt_name, 'w') as fh: - fh.write('Hello, world') - assert name in os.listdir(src_dir) - assert name in os.listdir(mnt_dir) - assert os.stat(src_name) == os.stat(mnt_name) - - -def tst_xattr(path): - os.setxattr(path, b'hello_ll_setxattr_name', b'hello_ll_setxattr_value') - assert os.getxattr(path, b'hello_ll_getxattr_name') == b'hello_ll_getxattr_value' - os.removexattr(path, b'hello_ll_removexattr_name') - - -# avoid warning about unused import -assert test_printcap diff --git a/test/test_setattr.c b/test/test_setattr.c index ac552644c..ed0e93dc5 100644 --- a/test/test_setattr.c +++ b/test/test_setattr.c @@ -3,7 +3,7 @@ Copyright (C) 2016 Nikolaus Rath <Nikolaus@rath.org> This program can be distributed under the terms of the GNU GPLv2. - See the file COPYING. + See the file GPL2.txt. */ diff --git a/test/test_signals.c b/test/test_signals.c new file mode 100644 index 000000000..3ce7fbd32 --- /dev/null +++ b/test/test_signals.c @@ -0,0 +1,202 @@ +/* + * FUSE: Filesystem in Userspace + * Copyright (C) 2025 Bernd Schubert <bernd@bsbernd.com> + * + * Test for signal handling in libfuse. + * + * This program can be distributed under the terms of the GNU LGPLv2. + * See the file GPL2.txt + */ + +#define FUSE_USE_VERSION FUSE_MAKE_VERSION(3, 17) + +#include "fuse_config.h" +#include "fuse_lowlevel.h" +#include "fuse_i.h" + +#include <pthread.h> +#include <stdatomic.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <signal.h> +#include <errno.h> +#include <sys/types.h> +#include <sys/wait.h> + +static void test_ll_lookup(fuse_req_t req, fuse_ino_t parent, const char *name) +{ + (void)parent; + (void)name; + /* Simulate slow lookup to test signal interruption */ + sleep(2); + fuse_reply_err(req, ENOENT); +} + +static void test_ll_getattr(fuse_req_t req, fuse_ino_t ino, + struct fuse_file_info *fi) +{ + (void)ino; + (void)fi; + /* Simulate slow getattr to test signal interruption */ + sleep(2); + fuse_reply_err(req, ENOENT); +} + +static const struct fuse_lowlevel_ops test_ll_ops = { + .lookup = test_ll_lookup, + .getattr = test_ll_getattr, +}; + +static void *signal_sender_thread(void *arg) +{ + (void)arg; + + usleep(2 * 1000 * 1000); + + /* Send SIGTERM to the process */ + kill(getpid(), SIGTERM); + return NULL; +} + +static void fork_child(void) +{ + struct fuse_args args = FUSE_ARGS_INIT(0, NULL); + struct fuse_session *se; + struct fuse_loop_config *loop_config; + pthread_t sig_thread; + char *mountpoint = NULL; + int ret = -1; + + /* Add the program name to arg[0] */ + if (fuse_opt_add_arg(&args, "test_signals")) { + fprintf(stderr, "Failed to add argument\n"); + goto out_free_mountpoint; + } + + /* Add debug flag to see more output */ + fuse_opt_add_arg(&args, "-d"); + + /* Create temporary mount point */ + mountpoint = strdup("/tmp/fuse_test_XXXXXX"); + if (!mountpoint || !mkdtemp(mountpoint)) { + fprintf(stderr, "Failed to create temp dir\n"); + goto out_free_args; + } + + /* Create session */ + se = fuse_session_new(&args, &test_ll_ops, sizeof(test_ll_ops), NULL); + if (!se) { + fprintf(stderr, "Failed to create FUSE session\n"); + goto out_free_mountpoint; + } + + /* Mount filesystem */ + if (fuse_session_mount(se, mountpoint)) { + fprintf(stderr, "Failed to mount FUSE filesystem\n"); + goto out_destroy_session; + } + + /* Create loop config */ + loop_config = fuse_loop_cfg_create(); + if (!loop_config) { + fprintf(stderr, "Failed to create loop config\n"); + goto out_unmount; + } + fuse_loop_cfg_set_clone_fd(loop_config, 0); + fuse_loop_cfg_set_max_threads(loop_config, 2); + + /* Set up signal handlers */ + if (fuse_set_signal_handlers(se)) { + fprintf(stderr, "Failed to set up signal handlers\n"); + goto out_destroy_config; + } + + /* Create thread that will send signals */ + if (pthread_create(&sig_thread, NULL, signal_sender_thread, NULL)) { + fprintf(stderr, "Failed to create signal sender thread\n"); + goto out_remove_handlers; + } + + /* Enter FUSE loop */ + ret = fuse_session_loop_mt_312(se, loop_config); + + printf("Debug: fuse_session_loop_mt_312 returned %d\n", ret); + printf("Debug: session exited state: %d\n", fuse_session_exited(se)); + printf("Debug: session status: %d\n", se->error); + + /* Check exit status before cleanup */ + int clean_exit = (fuse_session_exited(se) && se->error == SIGTERM); + + /* Clean up */ + pthread_join(sig_thread, NULL); + fuse_remove_signal_handlers(se); + fuse_session_unmount(se); + fuse_session_destroy(se); + fuse_loop_cfg_destroy(loop_config); + rmdir(mountpoint); + free(mountpoint); + fuse_opt_free_args(&args); + + /* Use saved exit status */ + if (clean_exit) { + printf("Debug: Clean shutdown via SIGTERM\n"); + exit(0); + } + printf("Debug: Exiting with status %d\n", ret != 0); + exit(ret != 0); + +out_remove_handlers: + fuse_remove_signal_handlers(se); +out_destroy_config: + fuse_loop_cfg_destroy(loop_config); +out_unmount: + fuse_session_unmount(se); +out_destroy_session: + fuse_session_destroy(se); +out_free_mountpoint: + rmdir(mountpoint); + free(mountpoint); +out_free_args: + fuse_opt_free_args(&args); + exit(1); +} + +static void run_test_in_child(void) +{ + pid_t child; + int status; + + child = fork(); + if (child == -1) { + perror("fork"); + exit(1); + } + + if (child == 0) + fork_child(); + + /* In parent process */ + if (waitpid(child, &status, 0) == -1) { + perror("waitpid"); + exit(1); + } + + /* Check if child exited due to SIGTERM - this is expected */ + if (WIFSIGNALED(status) && WTERMSIG(status) == SIGTERM) { + printf("Child process terminated by SIGTERM as expected\n"); + exit(0); + } + + /* For any other type of exit, maintain existing behavior */ + exit(WIFEXITED(status) ? WEXITSTATUS(status) : 1); +} + +int main(void) +{ + printf("Testing SIGTERM handling in libfuse\n"); + run_test_in_child(); + printf("SIGTERM handling test passed\n"); + return 0; +} diff --git a/test/test_syscalls.c b/test/test_syscalls.c index 4bbe97340..115eab8d3 100644 --- a/test/test_syscalls.c +++ b/test/test_syscalls.c @@ -11,6 +11,7 @@ #include <utime.h> #include <errno.h> #include <assert.h> +#include <time.h> #include <sys/socket.h> #include <sys/types.h> #include <sys/stat.h> @@ -84,9 +85,25 @@ static int is_dot_or_dotdot(const char *name) { (name[1] == '\0' || (name[1] == '.' && name[2] == '\0')); } +/* + * Seconds since the first line this process printed. The origin is taken + * lazily so it needs no init call and cannot precede the first test. + */ +static double test_elapsed(void) +{ + static struct timespec start; + struct timespec now; + + clock_gettime(CLOCK_MONOTONIC, &now); + if (start.tv_sec == 0 && start.tv_nsec == 0) + start = now; + return (now.tv_sec - start.tv_sec) + + (now.tv_nsec - start.tv_nsec) / 1e9; +} + static void success(void) { - fprintf(stderr, "%s OK\n", testname); + fprintf(stderr, "+%8.3fs %s OK\n", test_elapsed(), testname); } #define this_test (&tests[testnum-1]) @@ -108,6 +125,11 @@ static void __start_test(const char *fmt, ...) fprintf(stderr, "%s - too many tests\n", testname); exit(1); } + /* A test that never finishes prints nothing otherwise, leaving the log + * ending at the last test that passed. stderr, so a SIGKILL cannot + * swallow it. + */ + fprintf(stderr, "+%8.3fs %s START\n", test_elapsed(), testname); this_test->fd = -1; } @@ -921,6 +943,53 @@ static int test_copy_file_range(void) } #endif +#ifdef HAVE_STATX +static int test_statx(void) +{ + struct statx sb; + char msg[] = "hi"; + size_t msg_size = sizeof(msg); + struct timespec tp; + int res; + + memset(&sb, 0, sizeof(sb)); + unlink(testfile); + + start_test("statx"); + + res = create_testfile(testfile, msg, msg_size); + if (res == -1) + return -1; + + res = statx(-1, testfile, AT_EMPTY_PATH, + STATX_BASIC_STATS | STATX_BTIME, &sb); + if (res == -1) + return -1; + + if (sb.stx_size != msg_size) + return -1; + + clock_gettime(CLOCK_REALTIME, &tp); + + if (sb.stx_btime.tv_sec > tp.tv_sec) + return -1; + + if (sb.stx_btime.tv_sec == tp.tv_sec && + sb.stx_btime.tv_nsec >= tp.tv_nsec) + return -1; + + unlink(testfile); + + success(); + return 0; +} +#else +static int test_statx(void) +{ + return 0; +} +#endif + static int test_utime(void) { struct utimbuf utm; @@ -2059,6 +2128,11 @@ int main(int argc, char *argv[]) int a; int is_root; + /* Piped stdout is fully buffered, so a killed test loses whatever it + * had printed about what it was doing. + */ + setvbuf(stdout, NULL, _IOLBF, 0); + umask(0); if (argc < 2 || argc > 4) { fprintf(stderr, "usage: %s testdir [:realdir] [[-]test#] [-u]\n", argv[0]); @@ -2179,6 +2253,7 @@ int main(int argc, char *argv[]) err += test_create_ro_dir(O_CREAT | O_WRONLY); err += test_create_ro_dir(O_CREAT | O_TRUNC); err += test_copy_file_range(); + err += test_statx(); #ifndef __FreeBSD__ err += test_create_tmpfile(); err += test_create_and_link_tmpfile(); diff --git a/test/test_write_cache.c b/test/test_write_cache.c index 9f21f02fb..00db5a65b 100644 --- a/test/test_write_cache.c +++ b/test/test_write_cache.c @@ -3,10 +3,9 @@ Copyright (C) 2016 Nikolaus Rath <Nikolaus@rath.org> This program can be distributed under the terms of the GNU GPLv2. - See the file COPYING. + See the file GPL2.txt. */ - #define FUSE_USE_VERSION 30 /* Not really needed - just to test build with FUSE_USE_VERSION == 30 */ @@ -37,24 +36,22 @@ /* Command line parsing */ struct options { - int writeback; - int data_size; - int delay_ms; + int writeback; + int data_size; + int delay_ms; } options = { - .writeback = 0, - .data_size = 2048, - .delay_ms = 0, + .writeback = 0, + .data_size = 2048, + .delay_ms = 0, }; #define WRITE_SYSCALLS 64 -#define OPTION(t, p) \ - { t, offsetof(struct options, p), 1 } +#define OPTION(t, p) { t, offsetof(struct options, p), 1 } static const struct fuse_opt option_spec[] = { - OPTION("writeback_cache", writeback), - OPTION("--data-size=%d", data_size), - OPTION("--delay_ms=%d", delay_ms), - FUSE_OPT_END + OPTION("writeback_cache", writeback), + OPTION("--data-size=%d", data_size), OPTION("--delay_ms=%d", delay_ms), + FUSE_OPT_END }; static int got_write; static atomic_int write_cnt; @@ -63,229 +60,241 @@ pthread_cond_t cond = PTHREAD_COND_INITIALIZER; pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; static int write_start, write_done; -static void tfs_init (void *userdata, struct fuse_conn_info *conn) +static void tfs_init(void *userdata, struct fuse_conn_info *conn) { - (void) userdata; + (void)userdata; - if(options.writeback) { - assert(fuse_get_feature_flag(conn, FUSE_CAP_WRITEBACK_CACHE)); - fuse_set_feature_flag(conn, FUSE_CAP_WRITEBACK_CACHE); - } + if (options.writeback) { + assert(fuse_get_feature_flag(conn, FUSE_CAP_WRITEBACK_CACHE)); + fuse_set_feature_flag(conn, FUSE_CAP_WRITEBACK_CACHE); + } } -static int tfs_stat(fuse_ino_t ino, struct stat *stbuf) { - stbuf->st_ino = ino; - if (ino == FUSE_ROOT_ID) { - stbuf->st_mode = S_IFDIR | 0755; - stbuf->st_nlink = 1; - } - - else if (ino == FILE_INO) { - stbuf->st_mode = S_IFREG | 0222; - stbuf->st_nlink = 1; - stbuf->st_size = 0; - } - - else - return -1; - - return 0; +static int tfs_stat(fuse_ino_t ino, struct stat *stbuf) +{ + stbuf->st_ino = ino; + if (ino == FUSE_ROOT_ID) { + stbuf->st_mode = S_IFDIR | 0755; + stbuf->st_nlink = 1; + } + + else if (ino == FILE_INO) { + stbuf->st_mode = S_IFREG | 0222; + stbuf->st_nlink = 1; + stbuf->st_size = 0; + } + + else + return -1; + + return 0; } -static void tfs_lookup(fuse_req_t req, fuse_ino_t parent, - const char *name) { - struct fuse_entry_param e; - memset(&e, 0, sizeof(e)); +static void tfs_lookup(fuse_req_t req, fuse_ino_t parent, const char *name) +{ + struct fuse_entry_param e; + + memset(&e, 0, sizeof(e)); - if (parent != FUSE_ROOT_ID) - goto err_out; - else if (strcmp(name, FILE_NAME) == 0) - e.ino = FILE_INO; - else - goto err_out; + if (parent != FUSE_ROOT_ID) + goto err_out; + else if (strcmp(name, FILE_NAME) == 0) + e.ino = FILE_INO; + else + goto err_out; - if (tfs_stat(e.ino, &e.attr) != 0) - goto err_out; - fuse_reply_entry(req, &e); - return; + if (tfs_stat(e.ino, &e.attr) != 0) + goto err_out; + fuse_reply_entry(req, &e); + return; err_out: - fuse_reply_err(req, ENOENT); + fuse_reply_err(req, ENOENT); } static void tfs_getattr(fuse_req_t req, fuse_ino_t ino, - struct fuse_file_info *fi) { - struct stat stbuf; + struct fuse_file_info *fi) +{ + struct stat stbuf; - (void) fi; + (void)fi; - memset(&stbuf, 0, sizeof(stbuf)); - if (tfs_stat(ino, &stbuf) != 0) - fuse_reply_err(req, ENOENT); - else - fuse_reply_attr(req, &stbuf, 5); + memset(&stbuf, 0, sizeof(stbuf)); + if (tfs_stat(ino, &stbuf) != 0) + fuse_reply_err(req, ENOENT); + else + fuse_reply_attr(req, &stbuf, 5); } -static void tfs_open(fuse_req_t req, fuse_ino_t ino, - struct fuse_file_info *fi) { - if (ino == FUSE_ROOT_ID) - fuse_reply_err(req, EISDIR); - else { - assert(ino == FILE_INO); - /* Test close(rofd) does not block waiting for pending writes */ - fi->noflush = !options.writeback && options.delay_ms && - (fi->flags & O_ACCMODE) == O_RDONLY; - fuse_reply_open(req, fi); - } +static void tfs_open(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info *fi) +{ + if (ino == FUSE_ROOT_ID) + fuse_reply_err(req, EISDIR); + else { + assert(ino == FILE_INO); + /* Test close(rofd) does not block waiting for pending writes */ + fi->noflush = !options.writeback && options.delay_ms && + (fi->flags & O_ACCMODE) == O_RDONLY; + fuse_reply_open(req, fi); + } } static void tfs_write(fuse_req_t req, fuse_ino_t ino, const char *buf, - size_t size, off_t off, struct fuse_file_info *fi) { - (void) fi; (void) buf; (void) off; - size_t expected; - - assert(ino == FILE_INO); - expected = options.data_size; - if(options.writeback) - expected *= 2; - - write_cnt++; - - if(size != expected && !options.writeback) - fprintf(stderr, "ERROR: Expected %zd bytes, got %zd\n!", - expected, size); - else - got_write = 1; - - /* Simulate waiting for pending writes */ - if (options.delay_ms) { - pthread_mutex_lock(&lock); - write_start = 1; - pthread_cond_signal(&cond); - pthread_mutex_unlock(&lock); - - usleep(options.delay_ms * 1000); - - pthread_mutex_lock(&lock); - write_done = 1; - pthread_cond_signal(&cond); - pthread_mutex_unlock(&lock); - } - - fuse_reply_write(req, size); + size_t size, off_t off, struct fuse_file_info *fi) +{ + (void)fi; + (void)buf; + (void)off; + size_t expected; + + assert(ino == FILE_INO); + expected = options.data_size; + if (options.writeback) + expected *= 2; + + write_cnt++; + + if (size != expected && !options.writeback) + fprintf(stderr, "ERROR: Expected %zd bytes, got %zd\n!", + expected, size); + else + got_write = 1; + + /* Simulate waiting for pending writes */ + if (options.delay_ms) { + pthread_mutex_lock(&lock); + write_start = 1; + pthread_cond_signal(&cond); + pthread_mutex_unlock(&lock); + + usleep(options.delay_ms * 1000); + + pthread_mutex_lock(&lock); + write_done = 1; + pthread_cond_signal(&cond); + pthread_mutex_unlock(&lock); + } + + fuse_reply_write(req, size); } static struct fuse_lowlevel_ops tfs_oper = { - .init = tfs_init, - .lookup = tfs_lookup, - .getattr = tfs_getattr, - .open = tfs_open, - .write = tfs_write, + .init = tfs_init, + .lookup = tfs_lookup, + .getattr = tfs_getattr, + .open = tfs_open, + .write = tfs_write, }; -static void* close_rofd(void *data) { - int rofd = (int)(long) data; +static void *close_rofd(void *data) +{ + int rofd = (int)(long)data; - /* Wait for first write to start */ - pthread_mutex_lock(&lock); - while (!write_start && !write_done) - pthread_cond_wait(&cond, &lock); - pthread_mutex_unlock(&lock); + /* Wait for first write to start */ + pthread_mutex_lock(&lock); + while (!write_start && !write_done) + pthread_cond_wait(&cond, &lock); + pthread_mutex_unlock(&lock); - close(rofd); - printf("rofd closed. write_start: %d write_done: %d\n", write_start, write_done); + close(rofd); + printf("rofd closed. write_start: %d write_done: %d\n", write_start, + write_done); - /* First write should not have been completed */ - if (write_done) - fprintf(stderr, "ERROR: close(rofd) blocked on write!\n"); + /* First write should not have been completed */ + if (write_done) + fprintf(stderr, "ERROR: close(rofd) blocked on write!\n"); - return NULL; + return NULL; } -static void* run_fs(void *data) { - struct fuse_session *se = (struct fuse_session*) data; - assert(fuse_session_loop(se) == 0); - return NULL; -} +static void *run_fs(void *data) +{ + struct fuse_session *se = (struct fuse_session *)data; -static void test_fs(char *mountpoint) { - char fname[PATH_MAX]; - char *buf; - const size_t iosize = options.data_size; - const size_t dsize = options.data_size * WRITE_SYSCALLS; - int fd, rofd; - pthread_t rofd_thread; - off_t off = 0; - - buf = malloc(dsize); - assert(buf != NULL); - assert((fd = open("/dev/urandom", O_RDONLY)) != -1); - assert(read(fd, buf, dsize) == dsize); - close(fd); - - assert(snprintf(fname, PATH_MAX, "%s/" FILE_NAME, - mountpoint) > 0); - fd = open(fname, O_WRONLY); - if (fd == -1) { - perror(fname); - assert(0); - } - - if (options.delay_ms) { - /* Verify that close(rofd) does not block waiting for pending writes */ - rofd = open(fname, O_RDONLY); - assert(pthread_create(&rofd_thread, NULL, close_rofd, (void *)(long)rofd) == 0); - /* Give close_rofd time to start */ - usleep(options.delay_ms * 1000); - } - - for (int cnt = 0; cnt < WRITE_SYSCALLS; cnt++) { - assert(pwrite(fd, buf + off, iosize, off) == iosize); - off += iosize; - assert(off <= dsize); - } - free(buf); - close(fd); - - if (options.delay_ms) { - printf("rwfd closed. write_start: %d write_done: %d\n", write_start, write_done); - assert(pthread_join(rofd_thread, NULL) == 0); - } + assert(fuse_session_loop(se) == 0); + return NULL; } -int main(int argc, char *argv[]) { - struct fuse_args args = FUSE_ARGS_INIT(argc, argv); - struct fuse_session *se; - struct fuse_cmdline_opts fuse_opts; - pthread_t fs_thread; +static void test_fs(char *mountpoint) +{ + char fname[PATH_MAX]; + char *buf; + const size_t iosize = options.data_size; + const size_t dsize = options.data_size * WRITE_SYSCALLS; + int fd, rofd; + pthread_t rofd_thread; + off_t off = 0; + + buf = malloc(dsize); + assert(buf != NULL); + assert((fd = open("/dev/urandom", O_RDONLY)) != -1); + assert(read(fd, buf, dsize) == dsize); + close(fd); + + assert(snprintf(fname, PATH_MAX, "%s/" FILE_NAME, mountpoint) > 0); + fd = open(fname, O_WRONLY); + if (fd == -1) { + perror(fname); + assert(0); + } + + if (options.delay_ms) { + /* Verify that close(rofd) does not block waiting for pending writes */ + rofd = open(fname, O_RDONLY); + assert(pthread_create(&rofd_thread, NULL, close_rofd, + (void *)(long)rofd) == 0); + /* Give close_rofd time to start */ + usleep(options.delay_ms * 1000); + } + + for (int cnt = 0; cnt < WRITE_SYSCALLS; cnt++) { + assert(pwrite(fd, buf + off, iosize, off) == iosize); + off += iosize; + assert(off <= dsize); + } + free(buf); + close(fd); + + if (options.delay_ms) { + printf("rwfd closed. write_start: %d write_done: %d\n", + write_start, write_done); + assert(pthread_join(rofd_thread, NULL) == 0); + } +} - assert(fuse_opt_parse(&args, &options, option_spec, NULL) == 0); - assert(fuse_parse_cmdline(&args, &fuse_opts) == 0); -#ifndef __FreeBSD__ - assert(fuse_opt_add_arg(&args, "-oauto_unmount") == 0); +int main(int argc, char *argv[]) +{ + struct fuse_args args = FUSE_ARGS_INIT(argc, argv); + struct fuse_session *se; + struct fuse_cmdline_opts fuse_opts; + pthread_t fs_thread; + + assert(fuse_opt_parse(&args, &options, option_spec, NULL) == 0); + assert(fuse_parse_cmdline(&args, &fuse_opts) == 0); +#ifndef __FreeBSD__ + assert(fuse_opt_add_arg(&args, "-oauto_unmount") == 0); #endif - se = fuse_session_new(&args, &tfs_oper, - sizeof(tfs_oper), NULL); - fuse_opt_free_args(&args); - assert (se != NULL); - assert(fuse_set_signal_handlers(se) == 0); - assert(fuse_session_mount(se, fuse_opts.mountpoint) == 0); + se = fuse_session_new(&args, &tfs_oper, sizeof(tfs_oper), NULL); + fuse_opt_free_args(&args); + assert(se != NULL); + assert(fuse_set_signal_handlers(se) == 0); + assert(fuse_session_mount(se, fuse_opts.mountpoint) == 0); - /* Start file-system thread */ - assert(pthread_create(&fs_thread, NULL, run_fs, (void *)se) == 0); + /* Start file-system thread */ + assert(pthread_create(&fs_thread, NULL, run_fs, (void *)se) == 0); - /* Write test data */ - test_fs(fuse_opts.mountpoint); - free(fuse_opts.mountpoint); + /* Write test data */ + test_fs(fuse_opts.mountpoint); + free(fuse_opts.mountpoint); - /* Stop file system */ - fuse_session_exit(se); - fuse_session_unmount(se); - assert(pthread_join(fs_thread, NULL) == 0); + /* Stop file system */ + fuse_session_exit(se); + fuse_session_unmount(se); + assert(pthread_join(fs_thread, NULL) == 0); - assert(got_write == 1); + assert(got_write == 1); - /* + /* * when writeback cache is enabled, kernel side can merge requests, but * memory pressure, system 'sync' might trigger data flushes before - flush * might happen in between write syscalls - merging subpage writes into @@ -293,19 +302,18 @@ int main(int argc, char *argv[]) { * Though we can expect that that at least some (but maybe all) write * system calls can be merged. */ - if (options.writeback) - assert(write_cnt < WRITE_SYSCALLS); - else - assert(write_cnt == WRITE_SYSCALLS); + if (options.writeback) + assert(write_cnt < WRITE_SYSCALLS); + else + assert(write_cnt == WRITE_SYSCALLS); - fuse_remove_signal_handlers(se); - fuse_session_destroy(se); + fuse_remove_signal_handlers(se); + fuse_session_destroy(se); - printf("Test completed successfully.\n"); - return 0; + printf("Test completed successfully.\n"); + return 0; } - /** * Local Variables: * mode: c diff --git a/test/util.py b/test/util.py deleted file mode 100644 index a421e7213..000000000 --- a/test/util.py +++ /dev/null @@ -1,209 +0,0 @@ -#!/usr/bin/env python3 -import subprocess -import pytest -import os -import stat -import time -from os.path import join as pjoin -import sys -import re -import itertools -from packaging import version -import logging - -basename = pjoin(os.path.dirname(__file__), '..') - -def parse_kernel_version(release): - # Extract the first three numbers from the kernel version string - match = re.match(r'^(\d+\.\d+\.\d+)', release) - if match: - return version.parse(match.group(1)) - return version.parse('0') - -def get_printcap(): - cmdline = base_cmdline + [ pjoin(basename, 'example', 'printcap') ] - proc = subprocess.Popen(cmdline, stdout=subprocess.PIPE, - universal_newlines=True) - (stdout, _) = proc.communicate(30) - assert proc.returncode == 0 - - proto = None - caps = set() - for line in stdout.split('\n'): - if line.startswith('\t'): - caps.add(line.strip()) - continue - - hit = re.match(r'Protocol version: (\d+)\.(\d+)$', line) - if hit: - proto = (int(hit.group(1)), int(hit.group(2))) - - return (proto, caps) - -def test_printcap(): - get_printcap() - -def wait_for_mount(mount_process, mnt_dir, - test_fn=os.path.ismount): - elapsed = 0 - while elapsed < 30: - if test_fn(mnt_dir): - return True - if mount_process.poll() is not None: - pytest.fail('file system process terminated prematurely') - time.sleep(0.1) - elapsed += 0.1 - pytest.fail("mountpoint failed to come up") - -def cleanup(mount_process, mnt_dir): - # Don't bother trying Valgrind if things already went wrong - - if 'bsd' in sys.platform or 'dragonfly' in sys.platform: - cmd = [ 'umount', '-f', mnt_dir ] - else: - cmd = [pjoin(basename, 'util', 'fusermount3'), - '-z', '-u', mnt_dir] - subprocess.call(cmd, stdout=subprocess.DEVNULL, - stderr=subprocess.STDOUT) - mount_process.terminate() - try: - mount_process.wait(1) - except subprocess.TimeoutExpired: - mount_process.kill() - -def umount(mount_process, mnt_dir): - logger = logging.getLogger(__name__) - logger.debug(f"Unmounting {mnt_dir}") - - if 'bsd' in sys.platform or 'dragonfly' in sys.platform: - cmdline = [ 'umount', mnt_dir ] - logger.debug("Using BSD-style umount command") - else: - logger.debug("Using fusermount3 for unmounting") - # fusermount3 will be setuid root, so we can only trace it with - # valgrind if we're root - if os.getuid() == 0: - cmdline = base_cmdline - logger.debug("Running as root, using valgrind if configured") - else: - cmdline = [] - logger.debug("Not running as root, skipping valgrind for fusermount3") - cmdline = cmdline + [ pjoin(basename, 'util', 'fusermount3'), - '-z', '-u', mnt_dir ] - - logger.debug(f"Unmount command: {' '.join(cmdline)}") - try: - result = subprocess.run(cmdline, capture_output=True, text=True, check=True) - if result.stdout: - logger.debug(f"Unmount command stdout: {result.stdout}") - if result.stderr: - logger.debug(f"Unmount command stderr: {result.stderr}") - except subprocess.CalledProcessError as e: - logger.error(f"Unmount command failed with return code {e.returncode}\nStdout: {e.stdout}\nStderr: {e.stderr}") - raise - - if not os.path.ismount(mnt_dir): - logger.debug(f"{mnt_dir} is no longer a mount point") - else: - logger.warning(f"{mnt_dir} is still a mount point after unmount command") - - # Give mount process a little while to terminate. Popen.wait(timeout) - # was only added in 3.3... - elapsed = 0 - while elapsed < 30: - code = mount_process.poll() - if code is not None: - if code == 0: - return - logger.error(f"File system process terminated with code {code}") - pytest.fail(f'file system process terminated with code {code}') - time.sleep(0.1) - elapsed += 0.1 - logger.error("Mount process did not terminate within 30 seconds") - pytest.fail('mount process did not terminate') - - -def safe_sleep(secs): - '''Like time.sleep(), but sleep for at least *secs* - - `time.sleep` may sleep less than the given period if a signal is - received. This function ensures that we sleep for at least the - desired time. - ''' - - now = time.time() - end = now + secs - while now < end: - time.sleep(end - now) - now = time.time() - -def fuse_test_marker(): - '''Return a pytest.marker that indicates FUSE availability - - If system/user/environment does not support FUSE, return - a `pytest.mark.skip` object with more details. If FUSE is - supported, return `pytest.mark.uses_fuse()`. - ''' - - skip = lambda x: pytest.mark.skip(reason=x) - - if 'bsd' in sys.platform or 'dragonfly' in sys.platform: - return pytest.mark.uses_fuse() - - with subprocess.Popen(['which', 'fusermount3'], stdout=subprocess.PIPE, - universal_newlines=True) as which: - fusermount_path = which.communicate()[0].strip() - - if not fusermount_path or which.returncode != 0: - return skip("Can't find fusermount executable") - - if not os.path.exists('/dev/fuse'): - return skip("FUSE kernel module does not seem to be loaded") - - if os.getuid() == 0: - return pytest.mark.uses_fuse() - - mode = os.stat(fusermount_path).st_mode - if mode & stat.S_ISUID == 0: - return skip('fusermount executable not setuid, and we are not root.') - - try: - fd = os.open('/dev/fuse', os.O_RDWR) - except OSError as exc: - return skip('Unable to open /dev/fuse: %s' % exc.strerror) - else: - os.close(fd) - - return pytest.mark.uses_fuse() - -def powerset(iterable): - s = list(iterable) - return itertools.chain.from_iterable( - itertools.combinations(s, r) for r in range(len(s)+1)) - -def create_tmpdir(mnt_dir): - if not os.path.exists(mnt_dir): - print("makedirs: '" + mnt_dir + "'") - os.makedirs(mnt_dir) - else: - print("mnt_dir exists: '" + mnt_dir + "'") - -# Use valgrind if requested -if os.environ.get('TEST_WITH_VALGRIND', 'no').lower().strip() \ - not in ('no', 'false', '0'): - base_cmdline = [ 'valgrind', '-q', '--' ] -else: - base_cmdline = [] - -# Try to use local fusermount3 -os.environ['PATH'] = '%s:%s' % (pjoin(basename, 'util'), os.environ['PATH']) -# Put example binaries on PATH -os.environ['PATH'] = '%s:%s' % (pjoin(basename, 'example'), os.environ['PATH']) - -try: - (fuse_proto, fuse_caps) = get_printcap() -except: - # Rely on test to raise error - fuse_proto = (0,0) - fuse_caps = set() - diff --git a/test/wrong_command.c b/test/wrong_command.c deleted file mode 100644 index 8b563c980..000000000 --- a/test/wrong_command.c +++ /dev/null @@ -1,16 +0,0 @@ -#include <stdio.h> - -int main(void) { -#ifdef MESON_IS_SUBPROJECT - fprintf(stderr, "libfuse tests were skipped because it's a meson subproject.\n" - "If you wish to run them try:\n" - "'cd <srcdir>/subprojects/libfuse && meson . build && cd build && python3 -m pytest test/' instead"); - return 77; /* report as a skipped test */ -#else - fprintf(stderr, "\x1B[31m\e[1m" - "This is not the command you are looking for.\n" - "You probably want to run 'python3 -m pytest test/' instead" - "\e[0m\n"); - return 1; -#endif -} diff --git a/util/fusermount.c b/util/fusermount.c index dd241a050..11b912b47 100644 --- a/util/fusermount.c +++ b/util/fusermount.c @@ -3,7 +3,7 @@ Copyright (C) 2001-2007 Miklos Szeredi <miklos@szeredi.hu> This program can be distributed under the terms of the GNU GPLv2. - See the file COPYING. + See the file GPL2.txt. */ /* This program does the mounting and unmounting of FUSE filesystems */ @@ -36,11 +36,18 @@ #include <stdbool.h> #include <sys/vfs.h> -#ifdef HAVE_CLOSE_RANGE +#if defined HAVE_CLOSE_RANGE && defined linux #include <linux/close_range.h> #endif +#if defined HAVE_LISTMOUNT +#include <linux/mount.h> +#include <syscall.h> +#include <stdint.h> +#endif + #define FUSE_COMMFD_ENV "_FUSE_COMMFD" +#define FUSE_KERN_DEVICE_ENV "FUSE_KERN_DEVICE" #define FUSE_DEV "/dev/fuse" @@ -565,7 +572,7 @@ static int unmount_fuse(const char *mnt, int quiet, int lazy) return res; } -static int count_fuse_fs(void) +static int count_fuse_fs_mtab(void) { struct mntent *entp; int count = 0; @@ -585,6 +592,72 @@ static int count_fuse_fs(void) return count; } +#ifdef HAVE_LISTMOUNT +static int count_fuse_fs_ls_mnt(void) +{ + #define SMBUF_SIZE 1024 + #define MNT_ID_LEN 128 + + int fuse_count = 0; + int n_mounts = 0; + int ret = 0; + uint64_t mnt_ids[MNT_ID_LEN]; + unsigned char smbuf[SMBUF_SIZE]; + struct mnt_id_req req = { + .size = sizeof(struct mnt_id_req), + }; + struct statmount *sm; + + for (;;) { + req.mnt_id = LSMT_ROOT; + + n_mounts = syscall(SYS_listmount, &req, &mnt_ids, MNT_ID_LEN, 0); + if (n_mounts == -1) { + if (errno != ENOSYS) { + fprintf(stderr, "%s: failed to list mounts: %s\n", progname, + strerror(errno)); + } + return -1; + } + + for (int i = 0; i < n_mounts; i++) { + req.mnt_id = mnt_ids[i]; + req.param = STATMOUNT_FS_TYPE; + ret = syscall(SYS_statmount, &req, &smbuf, SMBUF_SIZE, 0); + if (ret) { + if (errno == ENOENT) + continue; + + fprintf(stderr, "%s: failed to stat mount %lld: %s\n", progname, + req.mnt_id, strerror(errno)); + return -1; + } + + sm = (struct statmount *)smbuf; + if (sm->mask & STATMOUNT_FS_TYPE && + strcmp(&sm->str[sm->fs_type], "fuse") == 0) + fuse_count++; + } + + if (n_mounts < MNT_ID_LEN) + break; + req.param = mnt_ids[MNT_ID_LEN - 1]; + } + return fuse_count; +} + +static int count_fuse_fs(void) +{ + int count = count_fuse_fs_ls_mnt(); + + return count >= 0 ? count : count_fuse_fs_mtab(); +} +#else +static int count_fuse_fs(void) +{ + return count_fuse_fs_mtab(); +} +#endif #else /* IGNORE_MTAB */ static int count_fuse_fs(void) @@ -612,11 +685,19 @@ static int unmount_fuse(const char *mnt, int quiet, int lazy) static void strip_line(char *line) { char *s = strchr(line, '#'); + size_t len; + if (s != NULL) s[0] = '\0'; - for (s = line + strlen(line) - 1; - s >= line && isspace((unsigned char) *s); s--); - s[1] = '\0'; + /* + * Count down rather than walk a pointer back: an empty or all-blank + * line would form line - 1, which is undefined behaviour even though + * the store that follows lands inside the buffer. + */ + len = strlen(line); + while (len > 0 && isspace((unsigned char) line[len - 1])) + len--; + line[len] = '\0'; for (s = line; isspace((unsigned char) *s); s++); if (s != line) memmove(line, s, strlen(s)+1); @@ -627,8 +708,14 @@ static void parse_line(char *line, int linenum) int tmp; if (strcmp(line, "user_allow_other") == 0) user_allow_other = 1; - else if (sscanf(line, "mount_max = %i", &tmp) == 1) - mount_max = tmp; + else if (sscanf(line, "mount_max = %i", &tmp) == 1) { + if (tmp < -1) + fprintf(stderr, + "%s: invalid mount_max = %i in %s at line %i\n", + progname, tmp, FUSE_CONF, linenum); + else + mount_max = tmp; + } else if(line[0]) fprintf(stderr, "%s: unknown parameter in %s at line %i: '%s'\n", @@ -779,7 +866,7 @@ static int get_mnt_opts(int flags, char *opts, char **mnt_optsp) return -1; /* remove comma from end of opts*/ l = strlen(*mnt_optsp); - if ((*mnt_optsp)[l-1] == ',') + if (l > 0 && (*mnt_optsp)[l-1] == ',') (*mnt_optsp)[l-1] = '\0'; if (getuid() != 0) { const char *user = get_user_name(); @@ -1018,83 +1105,111 @@ static int do_mount(const char *mnt, const char **typep, mode_t rootmode, return -1; } +/* + * Resolve the caller-supplied mountpoint exactly once and hand back a + * descriptor for the inode it named. O_NOFOLLOW refuses a symlink outright for + * an unprivileged caller; root keeps following them, as it always has. + */ +static int pin_mountpoint(const char *mnt, bool is_root, struct stat *stbuf) +{ + int open_flags = O_PATH | O_CLOEXEC; + int fd; + + if (!is_root) + open_flags |= O_NOFOLLOW; + + fd = open(mnt, open_flags); + if (fd == -1) { + fprintf(stderr, "%s: failed to access mountpoint %s: %s\n", + progname, mnt, strerror(errno)); + return -1; + } + + if (fstat(fd, stbuf) == -1) { + fprintf(stderr, "%s: failed to access mountpoint %s: %s\n", + progname, mnt, strerror(errno)); + close(fd); + return -1; + } + + return fd; +} + static int check_perm(const char **mntp, struct stat *stbuf, int *mountpoint_fd) { int res; + int fd; const char *mnt = *mntp; - const char *origmnt = mnt; + const bool is_root = getuid() == 0; struct statfs fs_buf; size_t i; - res = lstat(mnt, stbuf); - if (res == -1) { - fprintf(stderr, "%s: failed to access mountpoint %s: %s\n", - progname, mnt, strerror(errno)); + fd = pin_mountpoint(mnt, is_root, stbuf); + if (fd == -1) return -1; - } /* No permission checking is done for root */ - if (getuid() == 0) + if (is_root) { + *mountpoint_fd = fd; return 0; + } if (S_ISDIR(stbuf->st_mode)) { - res = chdir(mnt); + res = fchdir(fd); if (res == -1) { fprintf(stderr, "%s: failed to chdir to mountpoint: %s\n", progname, strerror(errno)); - return -1; - } - mnt = *mntp = "."; - res = lstat(mnt, stbuf); - if (res == -1) { - fprintf(stderr, - "%s: failed to access mountpoint %s: %s\n", - progname, origmnt, strerror(errno)); - return -1; + goto out_close; } + /* + * The pinned directory is the CWD, so "." names it without + * going through the caller-supplied path a second time. + */ + *mntp = "."; if ((stbuf->st_mode & S_ISVTX) && stbuf->st_uid != getuid()) { fprintf(stderr, "%s: mountpoint %s not owned by user\n", - progname, origmnt); - return -1; + progname, mnt); + res = -1; + goto out_close; } - res = access(mnt, W_OK); + res = access(*mntp, W_OK); if (res == -1) { fprintf(stderr, "%s: user has no write access to mountpoint %s\n", - progname, origmnt); - return -1; + progname, mnt); + goto out_close; } } else if (S_ISREG(stbuf->st_mode)) { static char procfile[256]; - *mountpoint_fd = open(mnt, O_WRONLY); - if (*mountpoint_fd == -1) { + int wfd; + + snprintf(procfile, sizeof(procfile), "/proc/self/fd/%i", fd); + + /* + * Reopening the pinned inode through its magic link both tests + * write access and yields the descriptor mount(2) is pointed + * at, so the caller's path is never resolved again. + */ + wfd = open(procfile, O_WRONLY); + if (wfd == -1) { fprintf(stderr, "%s: failed to open %s: %s\n", progname, mnt, strerror(errno)); - return -1; - } - res = fstat(*mountpoint_fd, stbuf); - if (res == -1) { - fprintf(stderr, - "%s: failed to access mountpoint %s: %s\n", - progname, mnt, strerror(errno)); - return -1; - } - if (!S_ISREG(stbuf->st_mode)) { - fprintf(stderr, - "%s: mountpoint %s is no longer a regular file\n", - progname, mnt); - return -1; + res = -1; + goto out_close; } + close(fd); + fd = wfd; - sprintf(procfile, "/proc/self/fd/%i", *mountpoint_fd); + snprintf(procfile, sizeof(procfile), "/proc/self/fd/%i", fd); *mntp = procfile; } else { fprintf(stderr, "%s: mountpoint %s is not a directory or a regular file\n", progname, mnt); - return -1; + res = -1; + goto out_close; } /* Do not permit mounting over anything in procfs - it has a couple @@ -1106,7 +1221,8 @@ static int check_perm(const char **mntp, struct stat *stbuf, int *mountpoint_fd) if (statfs(*mntp, &fs_buf)) { fprintf(stderr, "%s: failed to access mountpoint %s: %s\n", progname, mnt, strerror(errno)); - return -1; + res = -1; + goto out_close; } /* Define permitted filesystems for the mount target. This was @@ -1146,6 +1262,7 @@ static int check_perm(const char **mntp, struct stat *stbuf, int *mountpoint_fd) 0x73717368 /* SQUASHFS_MAGIC */, 0x01021994 /* TMPFS_MAGIC */, 0x24051905 /* UBIFS_SUPER_MAGIC */, + 0x18031977 /* WEKAFS_SUPER_MAGIC */, #if __SIZEOF_LONG__ > 4 0x736675005346544e /* UFSD */, #endif @@ -1154,65 +1271,45 @@ static int check_perm(const char **mntp, struct stat *stbuf, int *mountpoint_fd) 0x858458f6 /* RAMFS_MAGIC */, }; for (i = 0; i < sizeof(f_type_whitelist)/sizeof(f_type_whitelist[0]); i++) { - if (f_type_whitelist[i] == fs_buf.f_type) + if (f_type_whitelist[i] == fs_buf.f_type) { + *mountpoint_fd = fd; return 0; + } } fprintf(stderr, "%s: mounting over filesystem type %#010lx is forbidden\n", progname, (unsigned long)fs_buf.f_type); - return -1; -} + res = -1; -static int try_open(const char *dev, char **devp, int silent) -{ - int fd = open(dev, O_RDWR); - if (fd != -1) { - *devp = strdup(dev); - if (*devp == NULL) { - fprintf(stderr, "%s: failed to allocate memory\n", - progname); - close(fd); - fd = -1; - } - } else if (errno == ENODEV || - errno == ENOENT)/* check for ENOENT too, for the udev case */ - return -2; - else if (!silent) { - fprintf(stderr, "%s: failed to open %s: %s\n", progname, dev, - strerror(errno)); - } - return fd; +out_close: + close(fd); + return res; } -static int try_open_fuse_device(char **devp) +static int open_fuse_device(const char *dev) { int fd; drop_privs(); - fd = try_open(FUSE_DEV, devp, 0); + fd = open(dev, O_RDWR); + if (fd == -1) { + if (errno == ENODEV || errno == ENOENT)/* check for ENOENT too, for the udev case */ + fprintf(stderr, + "%s: fuse device %s not found. Kernel module not loaded?\n", + progname, dev); + else + fprintf(stderr, + "%s: failed to open %s: %s\n", progname, dev, strerror(errno)); + } restore_privs(); return fd; } -static int open_fuse_device(char **devp) -{ - int fd = try_open_fuse_device(devp); - if (fd >= -1) - return fd; - - fprintf(stderr, - "%s: fuse device not found, try 'modprobe fuse' first\n", - progname); - - return -1; -} - - static int mount_fuse(const char *mnt, const char *opts, const char **type) { int res; int fd; - char *dev; + const char *dev = getenv(FUSE_KERN_DEVICE_ENV) ?: FUSE_DEV; struct stat stbuf; char *source = NULL; char *mnt_opts = NULL; @@ -1221,7 +1318,7 @@ static int mount_fuse(const char *mnt, const char *opts, const char **type) char *do_mount_opts = NULL; char *x_opts = NULL; - fd = open_fuse_device(&dev); + fd = open_fuse_device(dev); if (fd == -1) return -1; @@ -1292,7 +1389,6 @@ static int mount_fuse(const char *mnt, const char *opts, const char **type) out_free: free(source); free(mnt_opts); - free(dev); free(x_opts); free(do_mount_opts); @@ -1342,11 +1438,9 @@ static int send_fd(int sock_fd, int fd) /* Helper for should_auto_unmount * - * fusermount typically has the s-bit set - initial open of `mnt` was as root - * and got EACCESS as 'allow_other' was not specified. - * Try opening `mnt` again with uid and guid of the calling process. + * Try opening `mnt` with uid and gid of the calling process. */ -static int recheck_ENOTCONN_as_owner(const char *mnt) +static int check_ENOTCONN_as_owner(const char *mnt) { int pid = fork(); if(pid == -1) { @@ -1365,7 +1459,7 @@ static int recheck_ENOTCONN_as_owner(const char *mnt) } int fd = open(mnt, O_RDONLY); - if(fd == -1 && errno == ENOTCONN) + if (fd == -1 && (errno == ENOTCONN || errno == ECONNABORTED)) _exit(EXIT_SUCCESS); else _exit(EXIT_FAILURE); @@ -1401,7 +1495,6 @@ static int should_auto_unmount(const char *mnt, const char *type) char *copy; const char *last; int result = 0; - int fd; copy = strdup(mnt); if (copy == NULL) { @@ -1414,23 +1507,7 @@ static int should_auto_unmount(const char *mnt, const char *type) if (check_is_mount(last, mnt, type) == -1) goto out; - fd = open(mnt, O_RDONLY); - - if (fd != -1) { - close(fd); - } else { - switch(errno) { - case ENOTCONN: - result = 1; - break; - case EACCES: - result = recheck_ENOTCONN_as_owner(mnt); - break; - default: - result = 0; - break; - } - } + result = check_ENOTCONN_as_owner(mnt); out: free(copy); return result; @@ -1534,14 +1611,15 @@ int main(int argc, char *argv[]) static const struct option long_opts[] = { {"unmount", no_argument, NULL, 'u'}, - // Note: auto-unmount deliberately does not have a short version. - // It's meant for internal use by mount.c's setup_auto_unmount. - {"auto-unmount", no_argument, NULL, 'U'}, {"lazy", no_argument, NULL, 'z'}, {"quiet", no_argument, NULL, 'q'}, {"help", no_argument, NULL, 'h'}, {"version", no_argument, NULL, 'V'}, {"options", required_argument, NULL, 'o'}, + // Note: auto-unmount and comm-fd don't have short versions. + // They'ne meant for internal use by mount.c + {"auto-unmount", no_argument, NULL, 'U'}, + {"comm-fd", required_argument, NULL, 'c'}, {0, 0, 0, 0}}; progname = strdup(argc > 0 ? argv[0] : "fusermount"); @@ -1573,6 +1651,9 @@ int main(int argc, char *argv[]) auto_unmount = 1; setup_auto_unmount_only = 1; break; + case 'c': + commfd = optarg; + break; case 'z': lazy = 1; break; @@ -1619,7 +1700,8 @@ int main(int argc, char *argv[]) if (!setup_auto_unmount_only && unmount) goto do_unmount; - commfd = getenv(FUSE_COMMFD_ENV); + if(commfd == NULL) + commfd = getenv(FUSE_COMMFD_ENV); if (commfd == NULL) { fprintf(stderr, "%s: old style mounting not supported\n", progname); @@ -1637,7 +1719,12 @@ int main(int argc, char *argv[]) { struct stat statbuf; - fstat(cfd, &statbuf); + if (fstat(cfd, &statbuf) == -1) { + fprintf(stderr, + "%s: fstat of comm fd %li failed: %s\n", + progname, cfd, strerror(errno)); + goto err_out; + } if(!S_ISSOCK(statbuf.st_mode)) { fprintf(stderr, "%s: file descriptor %li is not a socket, can't send fuse fd\n", @@ -1655,7 +1742,7 @@ int main(int argc, char *argv[]) res = send_fd(cfd, fd); if (res != 0) { - umount2(mnt, MNT_DETACH); /* lazy umount */ + unmount_fuse(mnt, 1, 1); /* lazy umount */ goto err_out; } close(fd); @@ -1710,7 +1797,7 @@ int main(int argc, char *argv[]) if (geteuid() == 0) res = unmount_fuse(mnt, quiet, lazy); else { - res = umount2(mnt, lazy ? UMOUNT_DETACH : 0); + res = umount2(mnt, (lazy ? UMOUNT_DETACH : 0) | UMOUNT_NOFOLLOW); if (res == -1 && !quiet) fprintf(stderr, "%s: failed to unmount %s: %s\n", diff --git a/util/meson.build b/util/meson.build index 0e4b1cce9..6678a8b9d 100644 --- a/util/meson.build +++ b/util/meson.build @@ -1,6 +1,7 @@ fuseconf_path = join_paths(get_option('prefix'), get_option('sysconfdir'), 'fuse.conf') -executable('fusermount3', ['fusermount.c', '../lib/mount_util.c', '../lib/util.c'], +executable('fusermount3', ['fusermount.c', '../lib/mount_util.c', + '../lib/fuse_log.c', '../lib/util.c'], include_directories: include_dirs, install: true, install_dir: get_option('bindir'), diff --git a/util/mount.fuse.c b/util/mount.fuse.c index b98fb2a65..7dfff1b54 100644 --- a/util/mount.fuse.c +++ b/util/mount.fuse.c @@ -3,7 +3,7 @@ Copyright (C) 2001-2007 Miklos Szeredi <miklos@szeredi.hu> This program can be distributed under the terms of the GNU GPLv2. - See the file COPYING. + See the file GPL2.txt. */ #include "fuse_config.h" @@ -131,7 +131,7 @@ static int prepare_fuse_fd(const char *mountpoint, const char* subtype, } flags = fcntl(fuse_fd, F_GETFD); - if (flags == -1 || fcntl(fuse_fd, F_SETFD, flags & ~FD_CLOEXEC) == 1) { + if (flags == -1 || fcntl(fuse_fd, F_SETFD, flags & ~FD_CLOEXEC) == -1) { fprintf(stderr, "%s: Failed to clear CLOEXEC: %s\n", progname, strerror(errno)); exit(1); diff --git a/util/parse-backtrace.sh b/util/parse-backtrace.sh index 259ed2abe..3db96f8e1 100755 --- a/util/parse-backtrace.sh +++ b/util/parse-backtrace.sh @@ -67,7 +67,7 @@ parse_glibc_trace() line=$(echo $line line | sed -e 's/.*://') # parse lines like - # /usr/lib/libfused.so.1(+0x1c0ef) [0x7fca6061c0ef] + # /usr/lib/libfuse3.so.3(+0x1c0ef) [0x7fca6061c0ef] filename=$(echo $line | awk '{print $1}' | sed -e 's/(.*$//') if [ -z "${filename}" ]; then