From 358761fe3541eaade23fb3c20dcbd11ab46ef3de Mon Sep 17 00:00:00 2001 From: Bernd Schubert Date: Wed, 1 Jan 2025 23:02:52 +0100 Subject: [PATCH 001/310] Update ChangeLog.rst for 3.17 (#1085) Signed-off-by: Bernd Schubert --- ChangeLog.rst | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/ChangeLog.rst b/ChangeLog.rst index 8fd8cd34e..3bf401d99 100644 --- a/ChangeLog.rst +++ b/ChangeLog.rst @@ -1,12 +1,38 @@ -libfuse 3.17 (unreleased) -======================== +libfuse 3.17 (2024-01-01) +========================= +* 3.11 and 3.14.2 introduced ABI incompatibilities, the ABI is restored + to 3.10, .so version was increased since there were releases with + the incompatible ABI + +* The libfuse version a program was compiled against is now encoded into + that program, using inlined functions in fuse_lowlevel.h and fuse.h * Allows to handle fatal signals and to print a backtrace. - New public function: fuse_set_fail_signal_handlers() + New API function: fuse_set_fail_signal_handlers() + * Allows fuse_log() messages to be send to syslog instead of stderr - New public functions: fuse_log_enable_syslog() and fuse_log_close_syslog() + New API functions: fuse_log_enable_syslog() and fuse_log_close_syslog() + * Handle buffer misalignment for FUSE_WRITE +* Added support for filesystem passthrough read/write of files when + FUSE_PASSTHROUGH capability is enabled + New API functions: fuse_passthrough_open() and fuse_passthrough_close(), + also see example/passthrough_hp.cc + +* Added fmask and dmask options to high-level API + - dmask: umask applied to directories + - fmask: umask applied to non-directories + +* Added FUSE_FILL_DIR_DEFAULTS enum to support C++ programs using + fuse_fill_dir_t function + +* Added support for FUSE_CAP_HANDLE_KILLPRIV_V2 + +Fixes: +* Fixed compilation failure on FreeBSD (mount_bsd.c now points to correct + header) + libfuse 3.16.2 (2023-10-10) =========================== From e8bf8ad2e33c15e7da72842639c5ddade6dcf546 Mon Sep 17 00:00:00 2001 From: Bernd Schubert Date: Wed, 1 Jan 2025 21:41:17 +0100 Subject: [PATCH 002/310] Run github workflow actions on release branches Running on the 'master' is not enough, actions also need to run on release branches. Signed-off-by: Bernd Schubert --- .github/workflows/abicheck.yml | 3 ++- .github/workflows/codeql.yml | 8 ++++++-- .github/workflows/codespell.yml | 8 ++++++-- .github/workflows/pr-ci.yml | 2 ++ 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/.github/workflows/abicheck.yml b/.github/workflows/abicheck.yml index 23859d0b9..c75d912cf 100644 --- a/.github/workflows/abicheck.yml +++ b/.github/workflows/abicheck.yml @@ -5,10 +5,11 @@ on: push: branches: - master + - '[0-9]+.[0-9]+' # This will match branches like 3.17, 3.18, 4.0, etc. pull_request: branches: - master - + - '[0-9]+.[0-9]+' permissions: contents: read diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 1cccb5f13..b4c6fc175 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -13,9 +13,13 @@ name: "CodeQL Advanced" on: push: - branches: [ "master" ] + branches: + - master + - '[0-9]+.[0-9]+' # This will match branches like 3.17, 3.18, 4.0, etc. pull_request: - branches: [ "master" ] + branches: + - master + - '[0-9]+.[0-9]+' jobs: analyze: diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml index 472bf6a8c..045bb207f 100644 --- a/.github/workflows/codespell.yml +++ b/.github/workflows/codespell.yml @@ -3,9 +3,13 @@ name: Codespell on: push: - branches: [master] + branches: + - master + - '[0-9]+.[0-9]+' # This will match branches like 3.17, 3.18, 4.0, etc. pull_request: - branches: [master] + branches: + - master + - '[0-9]+.[0-9]+' permissions: contents: read diff --git a/.github/workflows/pr-ci.yml b/.github/workflows/pr-ci.yml index 54d30ac59..bec616621 100644 --- a/.github/workflows/pr-ci.yml +++ b/.github/workflows/pr-ci.yml @@ -4,9 +4,11 @@ on: push: branches: - master + - '[0-9]+.[0-9]+' # This will match branches like 3.17, 3.18, 4.0, etc. pull_request: branches: - master + - '[0-9]+.[0-9]+' permissions: contents: read From 94e269a8a5c50b6bb595261abb9d6cffe3380acb Mon Sep 17 00:00:00 2001 From: Bernd Schubert Date: Wed, 1 Jan 2025 22:53:38 +0100 Subject: [PATCH 003/310] Add a github action to check for "Signed-off-by" Signed-off-by: Bernd Schubert --- .github/workflows/dco.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/workflows/dco.yml diff --git a/.github/workflows/dco.yml b/.github/workflows/dco.yml new file mode 100644 index 000000000..1a2121d63 --- /dev/null +++ b/.github/workflows/dco.yml @@ -0,0 +1,28 @@ +name: DCO Check (Developer Certificate of Origin) + +on: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + check-dco: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + - name: Check DCO + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + run: | + echo "Checking DCO for PR #${{ github.event.pull_request.number }}" + commits=$(gh pr view ${{ github.event.pull_request.number }} --json commits --jq '.commits[].oid') + for commit in $commits + do + if ! git log --format='%B' -n 1 $commit | grep -q "Signed-off-by:"; then + echo "Commit $commit is missing Signed-off-by line" + exit 1 + fi + done + echo "All commits have Signed-off-by lines" From e156075b1566e085d8e56cc79c94b2f016628198 Mon Sep 17 00:00:00 2001 From: Bernd Schubert Date: Wed, 1 Jan 2025 22:58:45 +0100 Subject: [PATCH 004/310] Add a checkpatch.pl github workflow Signed-off-by: Bernd Schubert --- .github/workflows/checkpatch.yml | 22 + checkpatch.pl | 7820 ++++++++++++++++++++++++++++++ 2 files changed, 7842 insertions(+) create mode 100644 .github/workflows/checkpatch.yml create mode 100755 checkpatch.pl diff --git a/.github/workflows/checkpatch.yml b/.github/workflows/checkpatch.yml new file mode 100644 index 000000000..9a3cd1b7f --- /dev/null +++ b/.github/workflows/checkpatch.yml @@ -0,0 +1,22 @@ +name: Checkpatch + +on: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + checkpatch: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y perl + - name: Run checkpatch.pl + run: | + git fetch origin ${{ github.base_ref }} + base_commit=$(git merge-base FETCH_HEAD ${{ github.event.pull_request.head.sha }}) + ./checkpatch.pl --no-tree -g $base_commit diff --git a/checkpatch.pl b/checkpatch.pl new file mode 100755 index 000000000..9eed3683a --- /dev/null +++ b/checkpatch.pl @@ -0,0 +1,7820 @@ +#!/usr/bin/env perl +# SPDX-License-Identifier: GPL-2.0 +# +# (c) 2001, Dave Jones. (the file handling bit) +# (c) 2005, Joel Schopp (the ugly bit) +# (c) 2007,2008, Andy Whitcroft (new conditions, test suite) +# (c) 2008-2010 Andy Whitcroft +# (c) 2010-2018 Joe Perches + +use strict; +use warnings; +use POSIX; +use File::Basename; +use Cwd 'abs_path'; +use Term::ANSIColor qw(:constants); +use Encode qw(decode encode); + +my $P = $0; +my $D = dirname(abs_path($P)); + +my $V = '0.32'; + +use Getopt::Long qw(:config no_auto_abbrev); + +my $quiet = 0; +my $verbose = 0; +my %verbose_messages = (); +my %verbose_emitted = (); +my $tree = 1; +my $chk_signoff = 1; +my $chk_fixes_tag = 1; +my $chk_patch = 1; +my $tst_only; +my $emacs = 0; +my $terse = 0; +my $showfile = 0; +my $file = 0; +my $git = 0; +my %git_commits = (); +my $check = 0; +my $check_orig = 0; +my $summary = 1; +my $mailback = 0; +my $summary_file = 0; +my $show_types = 0; +my $list_types = 0; +my $fix = 0; +my $fix_inplace = 0; +my $root; +my $gitroot = $ENV{'GIT_DIR'}; +$gitroot = ".git" if !defined($gitroot); +my %debug; +my %camelcase = (); +my %use_type = (); +my @use = (); +my %ignore_type = (); +my @ignore = (); +my $help = 0; +my $configuration_file = ".checkpatch.conf"; +my $max_line_length = 100; +my $ignore_perl_version = 0; +my $minimum_perl_version = 5.10.0; +my $min_conf_desc_length = 4; +my $spelling_file = "$D/spelling.txt"; +my $codespell = 0; +my $codespellfile = "/usr/share/codespell/dictionary.txt"; +my $user_codespellfile = ""; +my $conststructsfile = "$D/const_structs.checkpatch"; +my $docsfile = "$D/../Documentation/dev-tools/checkpatch.rst"; +my $typedefsfile; +my $color = "auto"; +my $allow_c99_comments = 1; # Can be overridden by --ignore C99_COMMENT_TOLERANCE +# git output parsing needs US English output, so first set backtick child process LANGUAGE +my $git_command ='export LANGUAGE=en_US.UTF-8; git'; +my $tabsize = 8; +my ${CONFIG_} = "CONFIG_"; + +my %maybe_linker_symbol; # for externs in c exceptions, when seen in *vmlinux.lds.h + +sub help { + my ($exitcode) = @_; + + print << "EOM"; +Usage: $P [OPTION]... [FILE]... +Version: $V + +Options: + -q, --quiet quiet + -v, --verbose verbose mode + --no-tree run without a kernel tree + --no-signoff do not check for 'Signed-off-by' line + --no-fixes-tag do not check for 'Fixes:' tag + --patch treat FILE as patchfile (default) + --emacs emacs compile window format + --terse one line per report + --showfile emit diffed file position, not input file position + -g, --git treat FILE as a single commit or git revision range + single git commit with: + + ^ + ~n + multiple git commits with: + .. + ... + - + git merges are ignored + -f, --file treat FILE as regular source file + --subjective, --strict enable more subjective tests + --list-types list the possible message types + --types TYPE(,TYPE2...) show only these comma separated message types + --ignore TYPE(,TYPE2...) ignore various comma separated message types + --show-types show the specific message type in the output + --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 + --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 + --mailback only produce a report in case of warnings/errors + --summary-file include the filename in summary + --debug KEY=[0|1] turn on/off debugging of KEY, where KEY is one of + 'values', 'possible', 'type', and 'attr' (default + is all off) + --test-only=WORD report only warnings/errors containing WORD + literally + --fix EXPERIMENTAL - may create horrible results + If correctable single-line errors exist, create + ".EXPERIMENTAL-checkpatch-fixes" + with potential errors corrected to the preferred + checkpatch style + --fix-inplace EXPERIMENTAL - may create horrible results + Is the same as --fix, but overwrites the input + file. It's your fault if there's no backup or git + --ignore-perl-version override checking of perl version. expect + runtime errors. + --codespell Use the codespell dictionary for spelling/typos + (default:$codespellfile) + --codespellfile Use this codespell dictionary + --typedefsfile Read additional types from this file + --color[=WHEN] Use colors 'always', 'never', or only when output + is a terminal ('auto'). Default is 'auto'. + --kconfig-prefix=WORD use WORD as a prefix for Kconfig symbols (default + ${CONFIG_}) + -h, --help, --version display this help and exit + +When FILE is - read standard input. +EOM + + exit($exitcode); +} + +sub uniq { + my %seen; + return grep { !$seen{$_}++ } @_; +} + +sub list_types { + my ($exitcode) = @_; + + my $count = 0; + + local $/ = undef; + + open(my $script, '<', abs_path($P)) or + die "$P: Can't read '$P' $!\n"; + + my $text = <$script>; + close($script); + + my %types = (); + # Also catch when type or level is passed through a variable + while ($text =~ /(?:(\bCHK|\bWARN|\bERROR|&\{\$msg_level})\s*\(|\$msg_type\s*=)\s*"([^"]+)"/g) { + if (defined($1)) { + if (exists($types{$2})) { + $types{$2} .= ",$1" if ($types{$2} ne $1); + } else { + $types{$2} = $1; + } + } else { + $types{$2} = "UNDETERMINED"; + } + } + + print("#\tMessage type\n\n"); + if ($color) { + print(" ( Color coding: "); + print(RED . "ERROR" . RESET); + print(" | "); + print(YELLOW . "WARNING" . RESET); + print(" | "); + print(GREEN . "CHECK" . RESET); + print(" | "); + print("Multiple levels / Undetermined"); + print(" )\n\n"); + } + + foreach my $type (sort keys %types) { + my $orig_type = $type; + if ($color) { + my $level = $types{$type}; + if ($level eq "ERROR") { + $type = RED . $type . RESET; + } elsif ($level eq "WARN") { + $type = YELLOW . $type . RESET; + } elsif ($level eq "CHK") { + $type = GREEN . $type . RESET; + } + } + print(++$count . "\t" . $type . "\n"); + if ($verbose && exists($verbose_messages{$orig_type})) { + my $message = $verbose_messages{$orig_type}; + $message =~ s/\n/\n\t/g; + print("\t" . $message . "\n\n"); + } + } + + exit($exitcode); +} + +my $conf = which_conf($configuration_file); +if (-f $conf) { + my @conf_args; + open(my $conffile, '<', "$conf") + or warn "$P: Can't find a readable $configuration_file file $!\n"; + + while (<$conffile>) { + my $line = $_; + + $line =~ s/\s*\n?$//g; + $line =~ s/^\s*//g; + $line =~ s/\s+/ /g; + + next if ($line =~ m/^\s*#/); + next if ($line =~ m/^\s*$/); + + my @words = split(" ", $line); + foreach my $word (@words) { + last if ($word =~ m/^#/); + push (@conf_args, $word); + } + } + close($conffile); + unshift(@ARGV, @conf_args) if @conf_args; +} + +sub load_docs { + open(my $docs, '<', "$docsfile") + or warn "$P: Can't read the documentation file $docsfile $!\n"; + + my $type = ''; + my $desc = ''; + my $in_desc = 0; + + while (<$docs>) { + chomp; + my $line = $_; + $line =~ s/\s+$//; + + if ($line =~ /^\s*\*\*(.+)\*\*$/) { + if ($desc ne '') { + $verbose_messages{$type} = trim($desc); + } + $type = $1; + $desc = ''; + $in_desc = 1; + } elsif ($in_desc) { + if ($line =~ /^(?:\s{4,}|$)/) { + $line =~ s/^\s{4}//; + $desc .= $line; + $desc .= "\n"; + } else { + $verbose_messages{$type} = trim($desc); + $type = ''; + $desc = ''; + $in_desc = 0; + } + } + } + + if ($desc ne '') { + $verbose_messages{$type} = trim($desc); + } + close($docs); +} + +# Perl's Getopt::Long allows options to take optional arguments after a space. +# Prevent --color by itself from consuming other arguments +foreach (@ARGV) { + if ($_ eq "--color" || $_ eq "-color") { + $_ = "--color=$color"; + } +} + +GetOptions( + 'q|quiet+' => \$quiet, + 'v|verbose!' => \$verbose, + 'tree!' => \$tree, + 'signoff!' => \$chk_signoff, + 'fixes-tag!' => \$chk_fixes_tag, + 'patch!' => \$chk_patch, + 'emacs!' => \$emacs, + 'terse!' => \$terse, + 'showfile!' => \$showfile, + 'f|file!' => \$file, + 'g|git!' => \$git, + 'subjective!' => \$check, + 'strict!' => \$check, + 'ignore=s' => \@ignore, + 'types=s' => \@use, + 'show-types!' => \$show_types, + 'list-types!' => \$list_types, + 'max-line-length=i' => \$max_line_length, + 'min-conf-desc-length=i' => \$min_conf_desc_length, + 'tab-size=i' => \$tabsize, + 'root=s' => \$root, + 'summary!' => \$summary, + 'mailback!' => \$mailback, + 'summary-file!' => \$summary_file, + 'fix!' => \$fix, + 'fix-inplace!' => \$fix_inplace, + 'ignore-perl-version!' => \$ignore_perl_version, + 'debug=s' => \%debug, + 'test-only=s' => \$tst_only, + 'codespell!' => \$codespell, + 'codespellfile=s' => \$user_codespellfile, + 'typedefsfile=s' => \$typedefsfile, + 'color=s' => \$color, + 'no-color' => \$color, #keep old behaviors of -nocolor + 'nocolor' => \$color, #keep old behaviors of -nocolor + 'kconfig-prefix=s' => \${CONFIG_}, + 'h|help' => \$help, + 'version' => \$help +) or $help = 2; + +if ($user_codespellfile) { + # Use the user provided codespell file unconditionally + $codespellfile = $user_codespellfile; +} elsif (!(-f $codespellfile)) { + # If /usr/share/codespell/dictionary.txt is not present, try to find it + # under codespell's install directory: /data/dictionary.txt + if (($codespell || $help) && which("python3") ne "") { + my $python_codespell_dict = << "EOF"; + +import os.path as op +import codespell_lib +codespell_dir = op.dirname(codespell_lib.__file__) +codespell_file = op.join(codespell_dir, 'data', 'dictionary.txt') +print(codespell_file, end='') +EOF + + my $codespell_dict = `python3 -c "$python_codespell_dict" 2> /dev/null`; + $codespellfile = $codespell_dict if (-f $codespell_dict); + } +} + +# $help is 1 if either -h, --help or --version is passed as option - exitcode: 0 +# $help is 2 if invalid option is passed - exitcode: 1 +help($help - 1) if ($help); + +die "$P: --git cannot be used with --file or --fix\n" if ($git && ($file || $fix)); +die "$P: --verbose cannot be used with --terse\n" if ($verbose && $terse); + +if ($color =~ /^[01]$/) { + $color = !$color; +} elsif ($color =~ /^always$/i) { + $color = 1; +} elsif ($color =~ /^never$/i) { + $color = 0; +} elsif ($color =~ /^auto$/i) { + $color = (-t STDOUT); +} else { + die "$P: Invalid color mode: $color\n"; +} + +load_docs() if ($verbose); +list_types(0) if ($list_types); + +$fix = 1 if ($fix_inplace); +$check_orig = $check; + +my $exit = 0; + +my $perl_version_ok = 1; +if ($^V && $^V lt $minimum_perl_version) { + $perl_version_ok = 0; + printf "$P: requires at least perl version %vd\n", $minimum_perl_version; + exit(1) if (!$ignore_perl_version); +} + +#if no filenames are given, push '-' to read patch from stdin +if ($#ARGV < 0) { + push(@ARGV, '-'); +} + +# skip TAB size 1 to avoid additional checks on $tabsize - 1 +die "$P: Invalid TAB size: $tabsize\n" if ($tabsize < 2); + +sub hash_save_array_words { + my ($hashRef, $arrayRef) = @_; + + my @array = split(/,/, join(',', @$arrayRef)); + foreach my $word (@array) { + $word =~ s/\s*\n?$//g; + $word =~ s/^\s*//g; + $word =~ s/\s+/ /g; + $word =~ tr/[a-z]/[A-Z]/; + + next if ($word =~ m/^\s*#/); + next if ($word =~ m/^\s*$/); + + $hashRef->{$word}++; + } +} + +sub hash_show_words { + my ($hashRef, $prefix) = @_; + + if (keys %$hashRef) { + print "\nNOTE: $prefix message types:"; + foreach my $word (sort keys %$hashRef) { + print " $word"; + } + print "\n"; + } +} + +hash_save_array_words(\%ignore_type, \@ignore); +hash_save_array_words(\%use_type, \@use); + +my $dbg_values = 0; +my $dbg_possible = 0; +my $dbg_type = 0; +my $dbg_attr = 0; +for my $key (keys %debug) { + ## no critic + eval "\${dbg_$key} = '$debug{$key}';"; + die "$@" if ($@); +} + +my $rpt_cleaners = 0; + +if ($terse) { + $emacs = 1; + $quiet++; +} + +if ($tree) { + if (defined $root) { + if (!top_of_kernel_tree($root)) { + die "$P: $root: --root does not point at a valid tree\n"; + } + } else { + if (top_of_kernel_tree('.')) { + $root = '.'; + } elsif ($0 =~ m@(.*)/scripts/[^/]*$@ && + top_of_kernel_tree($1)) { + $root = $1; + } + } + + if (!defined $root) { + print "Must be run from the top-level dir. of a kernel tree\n"; + exit(2); + } +} + +my $emitted_corrupt = 0; + +our $Ident = qr{ + [A-Za-z_][A-Za-z\d_]* + (?:\s*\#\#\s*[A-Za-z_][A-Za-z\d_]*)* + }x; +our $Storage = qr{extern|static|asmlinkage}; +our $Sparse = qr{ + __user| + __kernel| + __force| + __iomem| + __must_check| + __kprobes| + __ref| + __refconst| + __refdata| + __rcu| + __private + }x; +our $InitAttributePrefix = qr{__(?:mem|cpu|dev|net_|)}; +our $InitAttributeData = qr{$InitAttributePrefix(?:initdata\b)}; +our $InitAttributeConst = qr{$InitAttributePrefix(?:initconst\b)}; +our $InitAttributeInit = qr{$InitAttributePrefix(?:init\b)}; +our $InitAttribute = qr{$InitAttributeData|$InitAttributeConst|$InitAttributeInit}; + +# Notes to $Attribute: +# We need \b after 'init' otherwise 'initconst' will cause a false positive in a check +our $Attribute = qr{ + const| + volatile| + __percpu| + __nocast| + __safe| + __bitwise| + __packed__| + __packed2__| + __naked| + __maybe_unused| + __always_unused| + __noreturn| + __used| + __cold| + __pure| + __noclone| + __deprecated| + __read_mostly| + __ro_after_init| + __kprobes| + $InitAttribute| + __aligned\s*\(.*\)| + ____cacheline_aligned| + ____cacheline_aligned_in_smp| + ____cacheline_internodealigned_in_smp| + __weak| + __alloc_size\s*\(\s*\d+\s*(?:,\s*\d+\s*)?\) + }x; +our $Modifier; +our $Inline = qr{inline|__always_inline|noinline|__inline|__inline__}; +our $Member = qr{->$Ident|\.$Ident|\[[^]]*\]}; +our $Lval = qr{$Ident(?:$Member)*}; + +our $Int_type = qr{(?i)llu|ull|ll|lu|ul|l|u}; +our $Binary = qr{(?i)0b[01]+$Int_type?}; +our $Hex = qr{(?i)0x[0-9a-f]+$Int_type?}; +our $Int = qr{[0-9]+$Int_type?}; +our $Octal = qr{0[0-7]+$Int_type?}; +our $String = qr{(?:\b[Lu])?"[X\t]*"}; +our $Float_hex = qr{(?i)0x[0-9a-f]+p-?[0-9]+[fl]?}; +our $Float_dec = qr{(?i)(?:[0-9]+\.[0-9]*|[0-9]*\.[0-9]+)(?:e-?[0-9]+)?[fl]?}; +our $Float_int = qr{(?i)[0-9]+e-?[0-9]+[fl]?}; +our $Float = qr{$Float_hex|$Float_dec|$Float_int}; +our $Constant = qr{$Float|$Binary|$Octal|$Hex|$Int}; +our $Assignment = qr{\*\=|/=|%=|\+=|-=|<<=|>>=|&=|\^=|\|=|=}; +our $Compare = qr{<=|>=|==|!=|<|(?}; +our $Arithmetic = qr{\+|-|\*|\/|%}; +our $Operators = qr{ + <=|>=|==|!=| + =>|->|<<|>>|<|>|!|~| + &&|\|\||,|\^|\+\+|--|&|\||$Arithmetic + }x; + +our $c90_Keywords = qr{do|for|while|if|else|return|goto|continue|switch|default|case|break}x; + +our $BasicType; +our $NonptrType; +our $NonptrTypeMisordered; +our $NonptrTypeWithAttr; +our $Type; +our $TypeMisordered; +our $Declare; +our $DeclareMisordered; + +our $NON_ASCII_UTF8 = qr{ + [\xC2-\xDF][\x80-\xBF] # non-overlong 2-byte + | \xE0[\xA0-\xBF][\x80-\xBF] # excluding overlongs + | [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2} # straight 3-byte + | \xED[\x80-\x9F][\x80-\xBF] # excluding surrogates + | \xF0[\x90-\xBF][\x80-\xBF]{2} # planes 1-3 + | [\xF1-\xF3][\x80-\xBF]{3} # planes 4-15 + | \xF4[\x80-\x8F][\x80-\xBF]{2} # plane 16 +}x; + +our $UTF8 = qr{ + [\x09\x0A\x0D\x20-\x7E] # ASCII + | $NON_ASCII_UTF8 +}x; + +our $typeC99Typedefs = qr{(?:__)?(?:[us]_?)?int_?(?:8|16|32|64)_t}; +our $typeOtherOSTypedefs = qr{(?x: + u_(?:char|short|int|long) | # bsd + u(?:nchar|short|int|long) # sysv +)}; +our $typeKernelTypedefs = qr{(?x: + (?:__)?(?:u|s|be|le)(?:8|16|32|64)| + atomic_t +)}; +our $typeStdioTypedefs = qr{(?x: + FILE +)}; +our $typeTypedefs = qr{(?x: + $typeC99Typedefs\b| + $typeOtherOSTypedefs\b| + $typeKernelTypedefs\b| + $typeStdioTypedefs\b +)}; + +our $zero_initializer = qr{(?:(?:0[xX])?0+$Int_type?|NULL|false)\b}; + +our $logFunctions = qr{(?x: + printk(?:_ratelimited|_once|_deferred_once|_deferred|)| + (?:[a-z0-9]+_){1,2}(?:printk|emerg|alert|crit|err|warning|warn|notice|info|debug|dbg|vdbg|devel|cont|WARN)(?:_ratelimited|_once|)| + TP_printk| + WARN(?:_RATELIMIT|_ONCE|)| + panic| + MODULE_[A-Z_]+| + seq_vprintf|seq_printf|seq_puts +)}; + +our $allocFunctions = qr{(?x: + (?:(?:devm_)? + (?:kv|k|v)[czm]alloc(?:_array)?(?:_node)? | + kstrdup(?:_const)? | + kmemdup(?:_nul)?) | + (?:\w+)?alloc_skb(?:_ip_align)? | + # dev_alloc_skb/netdev_alloc_skb, et al + dma_alloc_coherent +)}; + +our $signature_tags = qr{(?xi: + Signed-off-by:| + Co-developed-by:| + Acked-by:| + Tested-by:| + Reviewed-by:| + Reported-by:| + Suggested-by:| + To:| + Cc: +)}; + +our @link_tags = qw(Link Closes); + +#Create a search and print patterns for all these strings to be used directly below +our $link_tags_search = ""; +our $link_tags_print = ""; +foreach my $entry (@link_tags) { + if ($link_tags_search ne "") { + $link_tags_search .= '|'; + $link_tags_print .= ' or '; + } + $entry .= ':'; + $link_tags_search .= $entry; + $link_tags_print .= "'$entry'"; +} +$link_tags_search = "(?:${link_tags_search})"; + +our $tracing_logging_tags = qr{(?xi: + [=-]*> | + <[=-]* | + \[ | + \] | + start | + called | + entered | + entry | + enter | + in | + inside | + here | + begin | + exit | + end | + done | + leave | + completed | + out | + return | + [\.\!:\s]* +)}; + +sub edit_distance_min { + my (@arr) = @_; + my $len = scalar @arr; + if ((scalar @arr) < 1) { + # if underflow, return + return; + } + my $min = $arr[0]; + for my $i (0 .. ($len-1)) { + if ($arr[$i] < $min) { + $min = $arr[$i]; + } + } + return $min; +} + +sub get_edit_distance { + my ($str1, $str2) = @_; + $str1 = lc($str1); + $str2 = lc($str2); + $str1 =~ s/-//g; + $str2 =~ s/-//g; + my $len1 = length($str1); + my $len2 = length($str2); + # two dimensional array storing minimum edit distance + my @distance; + for my $i (0 .. $len1) { + for my $j (0 .. $len2) { + if ($i == 0) { + $distance[$i][$j] = $j; + } elsif ($j == 0) { + $distance[$i][$j] = $i; + } elsif (substr($str1, $i-1, 1) eq substr($str2, $j-1, 1)) { + $distance[$i][$j] = $distance[$i - 1][$j - 1]; + } else { + my $dist1 = $distance[$i][$j - 1]; #insert distance + my $dist2 = $distance[$i - 1][$j]; # remove + my $dist3 = $distance[$i - 1][$j - 1]; #replace + $distance[$i][$j] = 1 + edit_distance_min($dist1, $dist2, $dist3); + } + } + } + return $distance[$len1][$len2]; +} + +sub find_standard_signature { + my ($sign_off) = @_; + my @standard_signature_tags = ( + 'Signed-off-by:', 'Co-developed-by:', 'Acked-by:', 'Tested-by:', + 'Reviewed-by:', 'Reported-by:', 'Suggested-by:' + ); + foreach my $signature (@standard_signature_tags) { + return $signature if (get_edit_distance($sign_off, $signature) <= 2); + } + + return ""; +} + +our $obsolete_archives = qr{(?xi: + \Qfreedesktop.org/archives/dri-devel\E | + \Qlists.infradead.org\E | + \Qlkml.org\E | + \Qmail-archive.com\E | + \Qmailman.alsa-project.org/pipermail\E | + \Qmarc.info\E | + \Qozlabs.org/pipermail\E | + \Qspinics.net\E +)}; + +our @typeListMisordered = ( + qr{char\s+(?:un)?signed}, + qr{int\s+(?:(?:un)?signed\s+)?short\s}, + qr{int\s+short(?:\s+(?:un)?signed)}, + qr{short\s+int(?:\s+(?:un)?signed)}, + qr{(?:un)?signed\s+int\s+short}, + qr{short\s+(?:un)?signed}, + qr{long\s+int\s+(?:un)?signed}, + qr{int\s+long\s+(?:un)?signed}, + qr{long\s+(?:un)?signed\s+int}, + qr{int\s+(?:un)?signed\s+long}, + qr{int\s+(?:un)?signed}, + qr{int\s+long\s+long\s+(?:un)?signed}, + qr{long\s+long\s+int\s+(?:un)?signed}, + qr{long\s+long\s+(?:un)?signed\s+int}, + qr{long\s+long\s+(?:un)?signed}, + qr{long\s+(?:un)?signed}, +); + +our @typeList = ( + qr{void}, + qr{(?:(?:un)?signed\s+)?char}, + qr{(?:(?:un)?signed\s+)?short\s+int}, + qr{(?:(?:un)?signed\s+)?short}, + qr{(?:(?:un)?signed\s+)?int}, + qr{(?:(?:un)?signed\s+)?long\s+int}, + qr{(?:(?:un)?signed\s+)?long\s+long\s+int}, + qr{(?:(?:un)?signed\s+)?long\s+long}, + qr{(?:(?:un)?signed\s+)?long}, + qr{(?:un)?signed}, + qr{float}, + qr{double}, + qr{bool}, + qr{struct\s+$Ident}, + qr{union\s+$Ident}, + qr{enum\s+$Ident}, + qr{${Ident}_t}, + qr{${Ident}_handler}, + qr{${Ident}_handler_fn}, + @typeListMisordered, +); + +our $C90_int_types = qr{(?x: + long\s+long\s+int\s+(?:un)?signed| + long\s+long\s+(?:un)?signed\s+int| + long\s+long\s+(?:un)?signed| + (?:(?:un)?signed\s+)?long\s+long\s+int| + (?:(?:un)?signed\s+)?long\s+long| + int\s+long\s+long\s+(?:un)?signed| + int\s+(?:(?:un)?signed\s+)?long\s+long| + + long\s+int\s+(?:un)?signed| + long\s+(?:un)?signed\s+int| + long\s+(?:un)?signed| + (?:(?:un)?signed\s+)?long\s+int| + (?:(?:un)?signed\s+)?long| + int\s+long\s+(?:un)?signed| + int\s+(?:(?:un)?signed\s+)?long| + + int\s+(?:un)?signed| + (?:(?:un)?signed\s+)?int +)}; + +our @typeListFile = (); +our @typeListWithAttr = ( + @typeList, + qr{struct\s+$InitAttribute\s+$Ident}, + qr{union\s+$InitAttribute\s+$Ident}, +); + +our @modifierList = ( + qr{fastcall}, +); +our @modifierListFile = (); + +our @mode_permission_funcs = ( + ["module_param", 3], + ["module_param_(?:array|named|string)", 4], + ["module_param_array_named", 5], + ["debugfs_create_(?:file|u8|u16|u32|u64|x8|x16|x32|x64|size_t|atomic_t|bool|blob|regset32|u32_array)", 2], + ["proc_create(?:_data|)", 2], + ["(?:CLASS|DEVICE|SENSOR|SENSOR_DEVICE|IIO_DEVICE)_ATTR", 2], + ["IIO_DEV_ATTR_[A-Z_]+", 1], + ["SENSOR_(?:DEVICE_|)ATTR_2", 2], + ["SENSOR_TEMPLATE(?:_2|)", 3], + ["__ATTR", 2], +); + +my $word_pattern = '\b[A-Z]?[a-z]{2,}\b'; + +#Create a search pattern for all these functions to speed up a loop below +our $mode_perms_search = ""; +foreach my $entry (@mode_permission_funcs) { + $mode_perms_search .= '|' if ($mode_perms_search ne ""); + $mode_perms_search .= $entry->[0]; +} +$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", +); + +#Create a search pattern for all these strings to speed up a loop below +our $deprecated_apis_search = ""; +foreach my $entry (keys %deprecated_apis) { + $deprecated_apis_search .= '|' if ($deprecated_apis_search ne ""); + $deprecated_apis_search .= $entry; +} +$deprecated_apis_search = "(?:${deprecated_apis_search})"; + +our $mode_perms_world_writable = qr{ + S_IWUGO | + S_IWOTH | + S_IRWXUGO | + S_IALLUGO | + 0[0-7][0-7][2367] +}x; + +our %mode_permission_string_types = ( + "S_IRWXU" => 0700, + "S_IRUSR" => 0400, + "S_IWUSR" => 0200, + "S_IXUSR" => 0100, + "S_IRWXG" => 0070, + "S_IRGRP" => 0040, + "S_IWGRP" => 0020, + "S_IXGRP" => 0010, + "S_IRWXO" => 0007, + "S_IROTH" => 0004, + "S_IWOTH" => 0002, + "S_IXOTH" => 0001, + "S_IRWXUGO" => 0777, + "S_IRUGO" => 0444, + "S_IWUGO" => 0222, + "S_IXUGO" => 0111, +); + +#Create a search pattern for all these strings to speed up a loop below +our $mode_perms_string_search = ""; +foreach my $entry (keys %mode_permission_string_types) { + $mode_perms_string_search .= '|' if ($mode_perms_string_search ne ""); + $mode_perms_string_search .= $entry; +} +our $single_mode_perms_string_search = "(?:${mode_perms_string_search})"; +our $multi_mode_perms_string_search = qr{ + ${single_mode_perms_string_search} + (?:\s*\|\s*${single_mode_perms_string_search})* +}x; + +sub perms_to_octal { + my ($string) = @_; + + return trim($string) if ($string =~ /^\s*0[0-7]{3,3}\s*$/); + + my $val = ""; + my $oval = ""; + my $to = 0; + my $curpos = 0; + my $lastpos = 0; + while ($string =~ /\b(($single_mode_perms_string_search)\b(?:\s*\|\s*)?\s*)/g) { + $curpos = pos($string); + my $match = $2; + my $omatch = $1; + last if ($lastpos > 0 && ($curpos - length($omatch) != $lastpos)); + $lastpos = $curpos; + $to |= $mode_permission_string_types{$match}; + $val .= '\s*\|\s*' if ($val ne ""); + $val .= $match; + $oval .= $omatch; + } + $oval =~ s/^\s*\|\s*//; + $oval =~ s/\s*\|\s*$//; + return sprintf("%04o", $to); +} + +our $allowed_asm_includes = qr{(?x: + irq| + memory| + time| + reboot +)}; +# memory.h: ARM has a custom one + +# Load common spelling mistakes and build regular expression list. +my $misspellings; +my %spelling_fix; + +if (open(my $spelling, '<', $spelling_file)) { + while (<$spelling>) { + my $line = $_; + + $line =~ s/\s*\n?$//g; + $line =~ s/^\s*//g; + + next if ($line =~ m/^\s*#/); + next if ($line =~ m/^\s*$/); + + my ($suspect, $fix) = split(/\|\|/, $line); + + $spelling_fix{$suspect} = $fix; + } + close($spelling); +} else { + warn "No typos will be found - file '$spelling_file': $!\n"; +} + +if ($codespell) { + if (open(my $spelling, '<', $codespellfile)) { + while (<$spelling>) { + my $line = $_; + + $line =~ s/\s*\n?$//g; + $line =~ s/^\s*//g; + + next if ($line =~ m/^\s*#/); + next if ($line =~ m/^\s*$/); + next if ($line =~ m/, disabled/i); + + $line =~ s/,.*$//; + + my ($suspect, $fix) = split(/->/, $line); + + $spelling_fix{$suspect} = $fix; + } + close($spelling); + } else { + warn "No codespell typos will be found - file '$codespellfile': $!\n"; + } +} + +$misspellings = join("|", sort keys %spelling_fix) if keys %spelling_fix; + +sub read_words { + my ($wordsRef, $file) = @_; + + if (open(my $words, '<', $file)) { + while (<$words>) { + my $line = $_; + + $line =~ s/\s*\n?$//g; + $line =~ s/^\s*//g; + + next if ($line =~ m/^\s*#/); + next if ($line =~ m/^\s*$/); + if ($line =~ /\s/) { + print("$file: '$line' invalid - ignored\n"); + next; + } + + $$wordsRef .= '|' if (defined $$wordsRef); + $$wordsRef .= $line; + } + close($file); + return 1; + } + + return 0; +} + +my $const_structs; +if (show_type("CONST_STRUCT")) { + read_words(\$const_structs, $conststructsfile) + or warn "No structs that should be const will be found - file '$conststructsfile': $!\n"; +} + +if (defined($typedefsfile)) { + my $typeOtherTypedefs; + read_words(\$typeOtherTypedefs, $typedefsfile) + or warn "No additional types will be considered - file '$typedefsfile': $!\n"; + $typeTypedefs .= '|' . $typeOtherTypedefs if (defined $typeOtherTypedefs); +} + +sub build_types { + my $mods = "(?x: \n" . join("|\n ", (@modifierList, @modifierListFile)) . "\n)"; + my $all = "(?x: \n" . join("|\n ", (@typeList, @typeListFile)) . "\n)"; + my $Misordered = "(?x: \n" . join("|\n ", @typeListMisordered) . "\n)"; + my $allWithAttr = "(?x: \n" . join("|\n ", @typeListWithAttr) . "\n)"; + $Modifier = qr{(?:$Attribute|$Sparse|$mods)}; + $BasicType = qr{ + (?:$typeTypedefs\b)| + (?:${all}\b) + }x; + $NonptrType = qr{ + (?:$Modifier\s+|const\s+)* + (?: + (?:typeof|__typeof__)\s*\([^\)]*\)| + (?:$typeTypedefs\b)| + (?:${all}\b) + ) + (?:\s+$Modifier|\s+const)* + }x; + $NonptrTypeMisordered = qr{ + (?:$Modifier\s+|const\s+)* + (?: + (?:${Misordered}\b) + ) + (?:\s+$Modifier|\s+const)* + }x; + $NonptrTypeWithAttr = qr{ + (?:$Modifier\s+|const\s+)* + (?: + (?:typeof|__typeof__)\s*\([^\)]*\)| + (?:$typeTypedefs\b)| + (?:${allWithAttr}\b) + ) + (?:\s+$Modifier|\s+const)* + }x; + $Type = qr{ + $NonptrType + (?:(?:\s|\*|\[\])+\s*const|(?:\s|\*\s*(?:const\s*)?|\[\])+|(?:\s*\[\s*\])+){0,4} + (?:\s+$Inline|\s+$Modifier)* + }x; + $TypeMisordered = qr{ + $NonptrTypeMisordered + (?:(?:\s|\*|\[\])+\s*const|(?:\s|\*\s*(?:const\s*)?|\[\])+|(?:\s*\[\s*\])+){0,4} + (?:\s+$Inline|\s+$Modifier)* + }x; + $Declare = qr{(?:$Storage\s+(?:$Inline\s+)?)?$Type}; + $DeclareMisordered = qr{(?:$Storage\s+(?:$Inline\s+)?)?$TypeMisordered}; +} +build_types(); + +our $Typecast = qr{\s*(\(\s*$NonptrType\s*\)){0,1}\s*}; + +# Using $balanced_parens, $LvalOrFunc, or $FuncArg +# requires at least perl version v5.10.0 +# Any use must be runtime checked with $^V + +our $balanced_parens = qr/(\((?:[^\(\)]++|(?-1))*\))/; +our $LvalOrFunc = qr{((?:[\&\*]\s*)?$Lval)\s*($balanced_parens{0,1})\s*}; +our $FuncArg = qr{$Typecast{0,1}($LvalOrFunc|$Constant|$String)}; + +our $declaration_macros = qr{(?x: + (?:$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*\( +)}; + +our %allow_repeated_words = ( + add => '', + added => '', + bad => '', + be => '', +); + +sub deparenthesize { + my ($string) = @_; + return "" if (!defined($string)); + + while ($string =~ /^\s*\(.*\)\s*$/) { + $string =~ s@^\s*\(\s*@@; + $string =~ s@\s*\)\s*$@@; + } + + $string =~ s@\s+@ @g; + + return $string; +} + +sub seed_camelcase_file { + my ($file) = @_; + + return if (!(-f $file)); + + local $/; + + open(my $include_file, '<', "$file") + or warn "$P: Can't read '$file' $!\n"; + my $text = <$include_file>; + close($include_file); + + my @lines = split('\n', $text); + + foreach my $line (@lines) { + next if ($line !~ /(?:[A-Z][a-z]|[a-z][A-Z])/); + if ($line =~ /^[ \t]*(?:#[ \t]*define|typedef\s+$Type)\s+(\w*(?:[A-Z][a-z]|[a-z][A-Z])\w*)/) { + $camelcase{$1} = 1; + } elsif ($line =~ /^\s*$Declare\s+(\w*(?:[A-Z][a-z]|[a-z][A-Z])\w*)\s*[\(\[,;]/) { + $camelcase{$1} = 1; + } elsif ($line =~ /^\s*(?:union|struct|enum)\s+(\w*(?:[A-Z][a-z]|[a-z][A-Z])\w*)\s*[;\{]/) { + $camelcase{$1} = 1; + } + } +} + +our %maintained_status = (); + +sub is_maintained_obsolete { + my ($filename) = @_; + + return 0 if (!$tree || !(-e "$root/scripts/get_maintainer.pl")); + + if (!exists($maintained_status{$filename})) { + $maintained_status{$filename} = `perl $root/scripts/get_maintainer.pl --status --nom --nol --nogit --nogit-fallback -f $filename 2>&1`; + } + + return $maintained_status{$filename} =~ /obsolete/i; +} + +sub is_SPDX_License_valid { + my ($license) = @_; + + return 1 if (!$tree || which("python3") eq "" || !(-x "$root/scripts/spdxcheck.py") || !(-e "$gitroot")); + + my $root_path = abs_path($root); + my $status = `cd "$root_path"; echo "$license" | scripts/spdxcheck.py -`; + return 0 if ($status ne ""); + return 1; +} + +my $camelcase_seeded = 0; +sub seed_camelcase_includes { + return if ($camelcase_seeded); + + my $files; + my $camelcase_cache = ""; + my @include_files = (); + + $camelcase_seeded = 1; + + if (-e "$gitroot") { + my $git_last_include_commit = `${git_command} log --no-merges --pretty=format:"%h%n" -1 -- include`; + chomp $git_last_include_commit; + $camelcase_cache = ".checkpatch-camelcase.git.$git_last_include_commit"; + } else { + my $last_mod_date = 0; + $files = `find $root/include -name "*.h"`; + @include_files = split('\n', $files); + foreach my $file (@include_files) { + my $date = POSIX::strftime("%Y%m%d%H%M", + localtime((stat $file)[9])); + $last_mod_date = $date if ($last_mod_date < $date); + } + $camelcase_cache = ".checkpatch-camelcase.date.$last_mod_date"; + } + + if ($camelcase_cache ne "" && -f $camelcase_cache) { + open(my $camelcase_file, '<', "$camelcase_cache") + or warn "$P: Can't read '$camelcase_cache' $!\n"; + while (<$camelcase_file>) { + chomp; + $camelcase{$_} = 1; + } + close($camelcase_file); + + return; + } + + if (-e "$gitroot") { + $files = `${git_command} ls-files "include/*.h"`; + @include_files = split('\n', $files); + } + + foreach my $file (@include_files) { + seed_camelcase_file($file); + } + + if ($camelcase_cache ne "") { + unlink glob ".checkpatch-camelcase.*"; + open(my $camelcase_file, '>', "$camelcase_cache") + or warn "$P: Can't write '$camelcase_cache' $!\n"; + foreach (sort { lc($a) cmp lc($b) } keys(%camelcase)) { + print $camelcase_file ("$_\n"); + } + close($camelcase_file); + } +} + +sub git_is_single_file { + my ($filename) = @_; + + return 0 if ((which("git") eq "") || !(-e "$gitroot")); + + my $output = `${git_command} ls-files -- $filename 2>/dev/null`; + my $count = $output =~ tr/\n//; + return $count eq 1 && $output =~ m{^${filename}$}; +} + +sub git_commit_info { + my ($commit, $id, $desc) = @_; + + return ($id, $desc) if ((which("git") eq "") || !(-e "$gitroot")); + + my $output = `${git_command} log --no-color --format='%H %s' -1 $commit 2>&1`; + $output =~ s/^\s*//gm; + my @lines = split("\n", $output); + + return ($id, $desc) if ($#lines < 0); + + if ($lines[0] =~ /^error: short SHA1 $commit is ambiguous/) { +# Maybe one day convert this block of bash into something that returns +# all matching commit ids, but it's very slow... +# +# echo "checking commits $1..." +# git rev-list --remotes | grep -i "^$1" | +# while read line ; do +# git log --format='%H %s' -1 $line | +# echo "commit $(cut -c 1-12,41-)" +# done + } elsif ($lines[0] =~ /^fatal: ambiguous argument '$commit': unknown revision or path not in the working tree\./ || + $lines[0] =~ /^fatal: bad object $commit/) { + $id = undef; + } else { + $id = substr($lines[0], 0, 12); + $desc = substr($lines[0], 41); + } + + return ($id, $desc); +} + +$chk_signoff = 0 if ($file); +$chk_fixes_tag = 0 if ($file); + +my @rawlines = (); +my @lines = (); +my @fixed = (); +my @fixed_inserted = (); +my @fixed_deleted = (); +my $fixlinenr = -1; + +# If input is git commits, extract all commits from the commit expressions. +# For example, HEAD-3 means we need check 'HEAD, HEAD~1, HEAD~2'. +die "$P: No git repository found\n" if ($git && !-e "$gitroot"); + +if ($git) { + my @commits = (); + foreach my $commit_expr (@ARGV) { + my $git_range; + if ($commit_expr =~ m/^(.*)-(\d+)$/) { + $git_range = "-$2 $1"; + } elsif ($commit_expr =~ m/\.\./) { + $git_range = "$commit_expr"; + } else { + $git_range = "-1 $commit_expr"; + } + my $lines = `${git_command} log --no-color --no-merges --pretty=format:'%H %s' $git_range`; + foreach my $line (split(/\n/, $lines)) { + $line =~ /^([0-9a-fA-F]{40,40}) (.*)$/; + next if (!defined($1) || !defined($2)); + my $sha1 = $1; + my $subject = $2; + unshift(@commits, $sha1); + $git_commits{$sha1} = $subject; + } + } + die "$P: no git commits after extraction!\n" if (@commits == 0); + @ARGV = @commits; +} + +my $vname; +$allow_c99_comments = !defined $ignore_type{"C99_COMMENT_TOLERANCE"}; +for my $filename (@ARGV) { + my $FILE; + my $is_git_file = git_is_single_file($filename); + my $oldfile = $file; + $file = 1 if ($is_git_file); + if ($git) { + open($FILE, '-|', "git format-patch -M --stdout -1 $filename") || + die "$P: $filename: git format-patch failed - $!\n"; + } elsif ($file) { + open($FILE, '-|', "diff -u /dev/null $filename") || + die "$P: $filename: diff failed - $!\n"; + } elsif ($filename eq '-') { + open($FILE, '<&STDIN'); + } else { + open($FILE, '<', "$filename") || + die "$P: $filename: open failed - $!\n"; + } + if ($filename eq '-') { + $vname = 'Your patch'; + } elsif ($git) { + $vname = "Commit " . substr($filename, 0, 12) . ' ("' . $git_commits{$filename} . '")'; + } else { + $vname = $filename; + } + while (<$FILE>) { + chomp; + push(@rawlines, $_); + $vname = qq("$1") if ($filename eq '-' && $_ =~ m/^Subject:\s+(.+)/i); + } + close($FILE); + + if ($#ARGV > 0 && $quiet == 0) { + print '-' x length($vname) . "\n"; + print "$vname\n"; + print '-' x length($vname) . "\n"; + } + + if (!process($filename)) { + $exit = 1; + } + @rawlines = (); + @lines = (); + @fixed = (); + @fixed_inserted = (); + @fixed_deleted = (); + $fixlinenr = -1; + @modifierListFile = (); + @typeListFile = (); + build_types(); + $file = $oldfile if ($is_git_file); +} + +if (!$quiet) { + hash_show_words(\%use_type, "Used"); + hash_show_words(\%ignore_type, "Ignored"); + + if (!$perl_version_ok) { + print << "EOM" + +NOTE: perl $^V is not modern enough to detect all possible issues. + An upgrade to at least perl $minimum_perl_version is suggested. +EOM + } + if ($exit) { + print << "EOM" + +NOTE: If any of the errors are false positives, please report + them to the maintainer, see CHECKPATCH in MAINTAINERS. +EOM + } +} + +exit($exit); + +sub top_of_kernel_tree { + my ($root) = @_; + + my @tree_check = ( + "COPYING", "CREDITS", "Kbuild", "MAINTAINERS", "Makefile", + "README", "Documentation", "arch", "include", "drivers", + "fs", "init", "ipc", "kernel", "lib", "scripts", + ); + + foreach my $check (@tree_check) { + if (! -e $root . '/' . $check) { + return 0; + } + } + return 1; +} + +sub parse_email { + my ($formatted_email) = @_; + + my $name = ""; + my $quoted = ""; + my $name_comment = ""; + my $address = ""; + my $comment = ""; + + if ($formatted_email =~ /^(.*)<(\S+\@\S+)>(.*)$/) { + $name = $1; + $address = $2; + $comment = $3 if defined $3; + } elsif ($formatted_email =~ /^\s*<(\S+\@\S+)>(.*)$/) { + $address = $1; + $comment = $2 if defined $2; + } elsif ($formatted_email =~ /(\S+\@\S+)(.*)$/) { + $address = $1; + $comment = $2 if defined $2; + $formatted_email =~ s/\Q$address\E.*$//; + $name = $formatted_email; + $name = trim($name); + $name =~ s/^\"|\"$//g; + # If there's a name left after stripping spaces and + # leading quotes, and the address doesn't have both + # leading and trailing angle brackets, the address + # is invalid. ie: + # "joe smith joe@smith.com" bad + # "joe smith ]+>$/) { + $name = ""; + $address = ""; + $comment = ""; + } + } + + # Extract comments from names excluding quoted parts + # "John D. (Doe)" - Do not extract + if ($name =~ s/\"(.+)\"//) { + $quoted = $1; + } + while ($name =~ s/\s*($balanced_parens)\s*/ /) { + $name_comment .= trim($1); + } + $name =~ s/^[ \"]+|[ \"]+$//g; + $name = trim("$quoted $name"); + + $address = trim($address); + $address =~ s/^\<|\>$//g; + $comment = trim($comment); + + if ($name =~ /[^\w \-]/i) { ##has "must quote" chars + $name =~ s/(?"; + } + $formatted_email .= "$comment"; + return $formatted_email; +} + +sub reformat_email { + my ($email) = @_; + + my ($email_name, $name_comment, $email_address, $comment) = parse_email($email); + return format_email($email_name, $name_comment, $email_address, $comment); +} + +sub same_email_addresses { + my ($email1, $email2) = @_; + + my ($email1_name, $name1_comment, $email1_address, $comment1) = parse_email($email1); + my ($email2_name, $name2_comment, $email2_address, $comment2) = parse_email($email2); + + return $email1_name eq $email2_name && + $email1_address eq $email2_address && + $name1_comment eq $name2_comment && + $comment1 eq $comment2; +} + +sub which { + my ($bin) = @_; + + foreach my $path (split(/:/, $ENV{PATH})) { + if (-e "$path/$bin") { + return "$path/$bin"; + } + } + + return ""; +} + +sub which_conf { + my ($conf) = @_; + + foreach my $path (split(/:/, ".:$ENV{HOME}:.scripts")) { + if (-e "$path/$conf") { + return "$path/$conf"; + } + } + + return ""; +} + +sub expand_tabs { + my ($str) = @_; + + my $res = ''; + my $n = 0; + for my $c (split(//, $str)) { + if ($c eq "\t") { + $res .= ' '; + $n++; + for (; ($n % $tabsize) != 0; $n++) { + $res .= ' '; + } + next; + } + $res .= $c; + $n++; + } + + return $res; +} +sub copy_spacing { + (my $res = shift) =~ tr/\t/ /c; + return $res; +} + +sub line_stats { + my ($line) = @_; + + # Drop the diff line leader and expand tabs + $line =~ s/^.//; + $line = expand_tabs($line); + + # Pick the indent from the front of the line. + my ($white) = ($line =~ /^(\s*)/); + + return (length($line), length($white)); +} + +my $sanitise_quote = ''; + +sub sanitise_line_reset { + my ($in_comment) = @_; + + if ($in_comment) { + $sanitise_quote = '*/'; + } else { + $sanitise_quote = ''; + } +} +sub sanitise_line { + my ($line) = @_; + + my $res = ''; + my $l = ''; + + my $qlen = 0; + my $off = 0; + my $c; + + # Always copy over the diff marker. + $res = substr($line, 0, 1); + + for ($off = 1; $off < length($line); $off++) { + $c = substr($line, $off, 1); + + # Comments we are whacking completely including the begin + # and end, all to $;. + if ($sanitise_quote eq '' && substr($line, $off, 2) eq '/*') { + $sanitise_quote = '*/'; + + substr($res, $off, 2, "$;$;"); + $off++; + next; + } + if ($sanitise_quote eq '*/' && substr($line, $off, 2) eq '*/') { + $sanitise_quote = ''; + substr($res, $off, 2, "$;$;"); + $off++; + next; + } + if ($sanitise_quote eq '' && substr($line, $off, 2) eq '//') { + $sanitise_quote = '//'; + + substr($res, $off, 2, $sanitise_quote); + $off++; + next; + } + + # A \ in a string means ignore the next character. + if (($sanitise_quote eq "'" || $sanitise_quote eq '"') && + $c eq "\\") { + substr($res, $off, 2, 'XX'); + $off++; + next; + } + # Regular quotes. + if ($c eq "'" || $c eq '"') { + if ($sanitise_quote eq '') { + $sanitise_quote = $c; + + substr($res, $off, 1, $c); + next; + } elsif ($sanitise_quote eq $c) { + $sanitise_quote = ''; + } + } + + #print "c<$c> SQ<$sanitise_quote>\n"; + if ($off != 0 && $sanitise_quote eq '*/' && $c ne "\t") { + substr($res, $off, 1, $;); + } elsif ($off != 0 && $sanitise_quote eq '//' && $c ne "\t") { + substr($res, $off, 1, $;); + } elsif ($off != 0 && $sanitise_quote && $c ne "\t") { + substr($res, $off, 1, 'X'); + } else { + substr($res, $off, 1, $c); + } + } + + if ($sanitise_quote eq '//') { + $sanitise_quote = ''; + } + + # The pathname on a #include may be surrounded by '<' and '>'. + if ($res =~ /^.\s*\#\s*include\s+\<(.*)\>/) { + my $clean = 'X' x length($1); + $res =~ s@\<.*\>@<$clean>@; + + # The whole of a #error is a string. + } elsif ($res =~ /^.\s*\#\s*(?:error|warning)\s+(.*)\b/) { + my $clean = 'X' x length($1); + $res =~ s@(\#\s*(?:error|warning)\s+).*@$1$clean@; + } + + if ($allow_c99_comments && $res =~ m@(//.*$)@) { + my $match = $1; + $res =~ s/\Q$match\E/"$;" x length($match)/e; + } + + return $res; +} + +sub get_quoted_string { + my ($line, $rawline) = @_; + + return "" if (!defined($line) || !defined($rawline)); + return "" if ($line !~ m/($String)/g); + return substr($rawline, $-[0], $+[0] - $-[0]); +} + +sub ctx_statement_block { + my ($linenr, $remain, $off) = @_; + my $line = $linenr - 1; + my $blk = ''; + my $soff = $off; + my $coff = $off - 1; + my $coff_set = 0; + + my $loff = 0; + + my $type = ''; + my $level = 0; + my @stack = (); + my $p; + my $c; + my $len = 0; + + my $remainder; + while (1) { + @stack = (['', 0]) if ($#stack == -1); + + #warn "CSB: blk<$blk> remain<$remain>\n"; + # If we are about to drop off the end, pull in more + # context. + if ($off >= $len) { + for (; $remain > 0; $line++) { + last if (!defined $lines[$line]); + next if ($lines[$line] =~ /^-/); + $remain--; + $loff = $len; + $blk .= $lines[$line] . "\n"; + $len = length($blk); + $line++; + last; + } + # Bail if there is no further context. + #warn "CSB: blk<$blk> off<$off> len<$len>\n"; + if ($off >= $len) { + last; + } + if ($level == 0 && substr($blk, $off) =~ /^.\s*#\s*define/) { + $level++; + $type = '#'; + } + } + $p = $c; + $c = substr($blk, $off, 1); + $remainder = substr($blk, $off); + + #warn "CSB: c<$c> type<$type> level<$level> remainder<$remainder> coff_set<$coff_set>\n"; + + # Handle nested #if/#else. + if ($remainder =~ /^#\s*(?:ifndef|ifdef|if)\s/) { + push(@stack, [ $type, $level ]); + } elsif ($remainder =~ /^#\s*(?:else|elif)\b/) { + ($type, $level) = @{$stack[$#stack - 1]}; + } elsif ($remainder =~ /^#\s*endif\b/) { + ($type, $level) = @{pop(@stack)}; + } + + # Statement ends at the ';' or a close '}' at the + # outermost level. + if ($level == 0 && $c eq ';') { + last; + } + + # An else is really a conditional as long as its not else if + if ($level == 0 && $coff_set == 0 && + (!defined($p) || $p =~ /(?:\s|\}|\+)/) && + $remainder =~ /^(else)(?:\s|{)/ && + $remainder !~ /^else\s+if\b/) { + $coff = $off + length($1) - 1; + $coff_set = 1; + #warn "CSB: mark coff<$coff> soff<$soff> 1<$1>\n"; + #warn "[" . substr($blk, $soff, $coff - $soff + 1) . "]\n"; + } + + if (($type eq '' || $type eq '(') && $c eq '(') { + $level++; + $type = '('; + } + if ($type eq '(' && $c eq ')') { + $level--; + $type = ($level != 0)? '(' : ''; + + if ($level == 0 && $coff < $soff) { + $coff = $off; + $coff_set = 1; + #warn "CSB: mark coff<$coff>\n"; + } + } + if (($type eq '' || $type eq '{') && $c eq '{') { + $level++; + $type = '{'; + } + if ($type eq '{' && $c eq '}') { + $level--; + $type = ($level != 0)? '{' : ''; + + if ($level == 0) { + if (substr($blk, $off + 1, 1) eq ';') { + $off++; + } + last; + } + } + # Preprocessor commands end at the newline unless escaped. + if ($type eq '#' && $c eq "\n" && $p ne "\\") { + $level--; + $type = ''; + $off++; + last; + } + $off++; + } + # We are truly at the end, so shuffle to the next line. + if ($off == $len) { + $loff = $len + 1; + $line++; + $remain--; + } + + my $statement = substr($blk, $soff, $off - $soff + 1); + my $condition = substr($blk, $soff, $coff - $soff + 1); + + #warn "STATEMENT<$statement>\n"; + #warn "CONDITION<$condition>\n"; + + #print "coff<$coff> soff<$off> loff<$loff>\n"; + + return ($statement, $condition, + $line, $remain + 1, $off - $loff + 1, $level); +} + +sub statement_lines { + my ($stmt) = @_; + + # Strip the diff line prefixes and rip blank lines at start and end. + $stmt =~ s/(^|\n)./$1/g; + $stmt =~ s/^\s*//; + $stmt =~ s/\s*$//; + + my @stmt_lines = ($stmt =~ /\n/g); + + return $#stmt_lines + 2; +} + +sub statement_rawlines { + my ($stmt) = @_; + + my @stmt_lines = ($stmt =~ /\n/g); + + return $#stmt_lines + 2; +} + +sub statement_block_size { + my ($stmt) = @_; + + $stmt =~ s/(^|\n)./$1/g; + $stmt =~ s/^\s*{//; + $stmt =~ s/}\s*$//; + $stmt =~ s/^\s*//; + $stmt =~ s/\s*$//; + + my @stmt_lines = ($stmt =~ /\n/g); + my @stmt_statements = ($stmt =~ /;/g); + + my $stmt_lines = $#stmt_lines + 2; + my $stmt_statements = $#stmt_statements + 1; + + if ($stmt_lines > $stmt_statements) { + return $stmt_lines; + } else { + return $stmt_statements; + } +} + +sub ctx_statement_full { + my ($linenr, $remain, $off) = @_; + my ($statement, $condition, $level); + + my (@chunks); + + # Grab the first conditional/block pair. + ($statement, $condition, $linenr, $remain, $off, $level) = + ctx_statement_block($linenr, $remain, $off); + #print "F: c<$condition> s<$statement> remain<$remain>\n"; + push(@chunks, [ $condition, $statement ]); + if (!($remain > 0 && $condition =~ /^\s*(?:\n[+-])?\s*(?:if|else|do)\b/s)) { + return ($level, $linenr, @chunks); + } + + # Pull in the following conditional/block pairs and see if they + # could continue the statement. + for (;;) { + ($statement, $condition, $linenr, $remain, $off, $level) = + ctx_statement_block($linenr, $remain, $off); + #print "C: c<$condition> s<$statement> remain<$remain>\n"; + last if (!($remain > 0 && $condition =~ /^(?:\s*\n[+-])*\s*(?:else|do)\b/s)); + #print "C: push\n"; + push(@chunks, [ $condition, $statement ]); + } + + return ($level, $linenr, @chunks); +} + +sub ctx_block_get { + my ($linenr, $remain, $outer, $open, $close, $off) = @_; + my $line; + my $start = $linenr - 1; + my $blk = ''; + my @o; + my @c; + my @res = (); + + my $level = 0; + my @stack = ($level); + for ($line = $start; $remain > 0; $line++) { + next if ($rawlines[$line] =~ /^-/); + $remain--; + + $blk .= $rawlines[$line]; + + # Handle nested #if/#else. + if ($lines[$line] =~ /^.\s*#\s*(?:ifndef|ifdef|if)\s/) { + push(@stack, $level); + } elsif ($lines[$line] =~ /^.\s*#\s*(?:else|elif)\b/) { + $level = $stack[$#stack - 1]; + } elsif ($lines[$line] =~ /^.\s*#\s*endif\b/) { + $level = pop(@stack); + } + + foreach my $c (split(//, $lines[$line])) { + ##print "C<$c>L<$level><$open$close>O<$off>\n"; + if ($off > 0) { + $off--; + next; + } + + if ($c eq $close && $level > 0) { + $level--; + last if ($level == 0); + } elsif ($c eq $open) { + $level++; + } + } + + if (!$outer || $level <= 1) { + push(@res, $rawlines[$line]); + } + + last if ($level == 0); + } + + return ($level, @res); +} +sub ctx_block_outer { + my ($linenr, $remain) = @_; + + my ($level, @r) = ctx_block_get($linenr, $remain, 1, '{', '}', 0); + return @r; +} +sub ctx_block { + my ($linenr, $remain) = @_; + + my ($level, @r) = ctx_block_get($linenr, $remain, 0, '{', '}', 0); + return @r; +} +sub ctx_statement { + my ($linenr, $remain, $off) = @_; + + my ($level, @r) = ctx_block_get($linenr, $remain, 0, '(', ')', $off); + return @r; +} +sub ctx_block_level { + my ($linenr, $remain) = @_; + + return ctx_block_get($linenr, $remain, 0, '{', '}', 0); +} +sub ctx_statement_level { + my ($linenr, $remain, $off) = @_; + + return ctx_block_get($linenr, $remain, 0, '(', ')', $off); +} + +sub ctx_locate_comment { + my ($first_line, $end_line) = @_; + + # If c99 comment on the current line, or the line before or after + my ($current_comment) = ($rawlines[$end_line - 1] =~ m@^\+.*(//.*$)@); + return $current_comment if (defined $current_comment); + ($current_comment) = ($rawlines[$end_line - 2] =~ m@^[\+ ].*(//.*$)@); + return $current_comment if (defined $current_comment); + ($current_comment) = ($rawlines[$end_line] =~ m@^[\+ ].*(//.*$)@); + return $current_comment if (defined $current_comment); + + # Catch a comment on the end of the line itself. + ($current_comment) = ($rawlines[$end_line - 1] =~ m@.*(/\*.*\*/)\s*(?:\\\s*)?$@); + return $current_comment if (defined $current_comment); + + # Look through the context and try and figure out if there is a + # comment. + my $in_comment = 0; + $current_comment = ''; + for (my $linenr = $first_line; $linenr < $end_line; $linenr++) { + my $line = $rawlines[$linenr - 1]; + #warn " $line\n"; + if ($linenr == $first_line and $line =~ m@^.\s*\*@) { + $in_comment = 1; + } + if ($line =~ m@/\*@) { + $in_comment = 1; + } + if (!$in_comment && $current_comment ne '') { + $current_comment = ''; + } + $current_comment .= $line . "\n" if ($in_comment); + if ($line =~ m@\*/@) { + $in_comment = 0; + } + } + + chomp($current_comment); + return($current_comment); +} +sub ctx_has_comment { + my ($first_line, $end_line) = @_; + my $cmt = ctx_locate_comment($first_line, $end_line); + + ##print "LINE: $rawlines[$end_line - 1 ]\n"; + ##print "CMMT: $cmt\n"; + + return ($cmt ne ''); +} + +sub raw_line { + my ($linenr, $cnt) = @_; + + my $offset = $linenr - 1; + $cnt++; + + my $line; + while ($cnt) { + $line = $rawlines[$offset++]; + next if (defined($line) && $line =~ /^-/); + $cnt--; + } + + return $line; +} + +sub get_stat_real { + my ($linenr, $lc) = @_; + + my $stat_real = raw_line($linenr, 0); + for (my $count = $linenr + 1; $count <= $lc; $count++) { + $stat_real = $stat_real . "\n" . raw_line($count, 0); + } + + return $stat_real; +} + +sub get_stat_here { + my ($linenr, $cnt, $here) = @_; + + my $herectx = $here . "\n"; + for (my $n = 0; $n < $cnt; $n++) { + $herectx .= raw_line($linenr, $n) . "\n"; + } + + return $herectx; +} + +sub cat_vet { + my ($vet) = @_; + my ($res, $coded); + + $res = ''; + while ($vet =~ /([^[:cntrl:]]*)([[:cntrl:]]|$)/g) { + $res .= $1; + if ($2 ne '') { + $coded = sprintf("^%c", unpack('C', $2) + 64); + $res .= $coded; + } + } + $res =~ s/$/\$/; + + return $res; +} + +my $av_preprocessor = 0; +my $av_pending; +my @av_paren_type; +my $av_pend_colon; + +sub annotate_reset { + $av_preprocessor = 0; + $av_pending = '_'; + @av_paren_type = ('E'); + $av_pend_colon = 'O'; +} + +sub annotate_values { + my ($stream, $type) = @_; + + my $res; + my $var = '_' x length($stream); + my $cur = $stream; + + print "$stream\n" if ($dbg_values > 1); + + while (length($cur)) { + @av_paren_type = ('E') if ($#av_paren_type < 0); + print " <" . join('', @av_paren_type) . + "> <$type> <$av_pending>" if ($dbg_values > 1); + if ($cur =~ /^(\s+)/o) { + print "WS($1)\n" if ($dbg_values > 1); + if ($1 =~ /\n/ && $av_preprocessor) { + $type = pop(@av_paren_type); + $av_preprocessor = 0; + } + + } elsif ($cur =~ /^(\(\s*$Type\s*)\)/ && $av_pending eq '_') { + print "CAST($1)\n" if ($dbg_values > 1); + push(@av_paren_type, $type); + $type = 'c'; + + } elsif ($cur =~ /^($Type)\s*(?:$Ident|,|\)|\(|\s*$)/) { + print "DECLARE($1)\n" if ($dbg_values > 1); + $type = 'T'; + + } elsif ($cur =~ /^($Modifier)\s*/) { + print "MODIFIER($1)\n" if ($dbg_values > 1); + $type = 'T'; + + } elsif ($cur =~ /^(\#\s*define\s*$Ident)(\(?)/o) { + print "DEFINE($1,$2)\n" if ($dbg_values > 1); + $av_preprocessor = 1; + push(@av_paren_type, $type); + if ($2 ne '') { + $av_pending = 'N'; + } + $type = 'E'; + + } elsif ($cur =~ /^(\#\s*(?:undef\s*$Ident|include\b))/o) { + print "UNDEF($1)\n" if ($dbg_values > 1); + $av_preprocessor = 1; + push(@av_paren_type, $type); + + } elsif ($cur =~ /^(\#\s*(?:ifdef|ifndef|if))/o) { + print "PRE_START($1)\n" if ($dbg_values > 1); + $av_preprocessor = 1; + + push(@av_paren_type, $type); + push(@av_paren_type, $type); + $type = 'E'; + + } elsif ($cur =~ /^(\#\s*(?:else|elif))/o) { + print "PRE_RESTART($1)\n" if ($dbg_values > 1); + $av_preprocessor = 1; + + push(@av_paren_type, $av_paren_type[$#av_paren_type]); + + $type = 'E'; + + } elsif ($cur =~ /^(\#\s*(?:endif))/o) { + print "PRE_END($1)\n" if ($dbg_values > 1); + + $av_preprocessor = 1; + + # Assume all arms of the conditional end as this + # one does, and continue as if the #endif was not here. + pop(@av_paren_type); + push(@av_paren_type, $type); + $type = 'E'; + + } elsif ($cur =~ /^(\\\n)/o) { + print "PRECONT($1)\n" if ($dbg_values > 1); + + } elsif ($cur =~ /^(__attribute__)\s*\(?/o) { + print "ATTR($1)\n" if ($dbg_values > 1); + $av_pending = $type; + $type = 'N'; + + } elsif ($cur =~ /^(sizeof)\s*(\()?/o) { + print "SIZEOF($1)\n" if ($dbg_values > 1); + if (defined $2) { + $av_pending = 'V'; + } + $type = 'N'; + + } elsif ($cur =~ /^(if|while|for)\b/o) { + print "COND($1)\n" if ($dbg_values > 1); + $av_pending = 'E'; + $type = 'N'; + + } elsif ($cur =~/^(case)/o) { + print "CASE($1)\n" if ($dbg_values > 1); + $av_pend_colon = 'C'; + $type = 'N'; + + } elsif ($cur =~/^(return|else|goto|typeof|__typeof__)\b/o) { + print "KEYWORD($1)\n" if ($dbg_values > 1); + $type = 'N'; + + } elsif ($cur =~ /^(\()/o) { + print "PAREN('$1')\n" if ($dbg_values > 1); + push(@av_paren_type, $av_pending); + $av_pending = '_'; + $type = 'N'; + + } elsif ($cur =~ /^(\))/o) { + my $new_type = pop(@av_paren_type); + if ($new_type ne '_') { + $type = $new_type; + print "PAREN('$1') -> $type\n" + if ($dbg_values > 1); + } else { + print "PAREN('$1')\n" if ($dbg_values > 1); + } + + } elsif ($cur =~ /^($Ident)\s*\(/o) { + print "FUNC($1)\n" if ($dbg_values > 1); + $type = 'V'; + $av_pending = 'V'; + + } elsif ($cur =~ /^($Ident\s*):(?:\s*\d+\s*(,|=|;))?/) { + if (defined $2 && $type eq 'C' || $type eq 'T') { + $av_pend_colon = 'B'; + } elsif ($type eq 'E') { + $av_pend_colon = 'L'; + } + print "IDENT_COLON($1,$type>$av_pend_colon)\n" if ($dbg_values > 1); + $type = 'V'; + + } elsif ($cur =~ /^($Ident|$Constant)/o) { + print "IDENT($1)\n" if ($dbg_values > 1); + $type = 'V'; + + } elsif ($cur =~ /^($Assignment)/o) { + print "ASSIGN($1)\n" if ($dbg_values > 1); + $type = 'N'; + + } elsif ($cur =~/^(;|{|})/) { + print "END($1)\n" if ($dbg_values > 1); + $type = 'E'; + $av_pend_colon = 'O'; + + } elsif ($cur =~/^(,)/) { + print "COMMA($1)\n" if ($dbg_values > 1); + $type = 'C'; + + } elsif ($cur =~ /^(\?)/o) { + print "QUESTION($1)\n" if ($dbg_values > 1); + $type = 'N'; + + } elsif ($cur =~ /^(:)/o) { + print "COLON($1,$av_pend_colon)\n" if ($dbg_values > 1); + + substr($var, length($res), 1, $av_pend_colon); + if ($av_pend_colon eq 'C' || $av_pend_colon eq 'L') { + $type = 'E'; + } else { + $type = 'N'; + } + $av_pend_colon = 'O'; + + } elsif ($cur =~ /^(\[)/o) { + print "CLOSE($1)\n" if ($dbg_values > 1); + $type = 'N'; + + } elsif ($cur =~ /^(-(?![->])|\+(?!\+)|\*|\&\&|\&)/o) { + my $variant; + + print "OPV($1)\n" if ($dbg_values > 1); + if ($type eq 'V') { + $variant = 'B'; + } else { + $variant = 'U'; + } + + substr($var, length($res), 1, $variant); + $type = 'N'; + + } elsif ($cur =~ /^($Operators)/o) { + print "OP($1)\n" if ($dbg_values > 1); + if ($1 ne '++' && $1 ne '--') { + $type = 'N'; + } + + } elsif ($cur =~ /(^.)/o) { + print "C($1)\n" if ($dbg_values > 1); + } + if (defined $1) { + $cur = substr($cur, length($1)); + $res .= $type x length($1); + } + } + + return ($res, $var); +} + +sub possible { + my ($possible, $line) = @_; + my $notPermitted = qr{(?: + ^(?: + $Modifier| + $Storage| + $Type| + DEFINE_\S+ + )$| + ^(?: + goto| + return| + case| + else| + asm|__asm__| + do| + \#| + \#\#| + )(?:\s|$)| + ^(?:typedef|struct|enum)\b + )}x; + warn "CHECK<$possible> ($line)\n" if ($dbg_possible > 2); + if ($possible !~ $notPermitted) { + # Check for modifiers. + $possible =~ s/\s*$Storage\s*//g; + $possible =~ s/\s*$Sparse\s*//g; + if ($possible =~ /^\s*$/) { + + } elsif ($possible =~ /\s/) { + $possible =~ s/\s*$Type\s*//g; + for my $modifier (split(' ', $possible)) { + if ($modifier !~ $notPermitted) { + warn "MODIFIER: $modifier ($possible) ($line)\n" if ($dbg_possible); + push(@modifierListFile, $modifier); + } + } + + } else { + warn "POSSIBLE: $possible ($line)\n" if ($dbg_possible); + push(@typeListFile, $possible); + } + build_types(); + } else { + warn "NOTPOSS: $possible ($line)\n" if ($dbg_possible > 1); + } +} + +my $prefix = ''; + +sub show_type { + my ($type) = @_; + + $type =~ tr/[a-z]/[A-Z]/; + + return defined $use_type{$type} if (scalar keys %use_type > 0); + + return !defined $ignore_type{$type}; +} + +sub report { + my ($level, $type, $msg) = @_; + + if (!show_type($type) || + (defined $tst_only && $msg !~ /\Q$tst_only\E/)) { + return 0; + } + my $output = ''; + if ($color) { + if ($level eq 'ERROR') { + $output .= RED; + } elsif ($level eq 'WARNING') { + $output .= YELLOW; + } else { + $output .= GREEN; + } + } + $output .= $prefix . $level . ':'; + if ($show_types) { + $output .= BLUE if ($color); + $output .= "$type:"; + } + $output .= RESET if ($color); + $output .= ' ' . $msg . "\n"; + + if ($showfile) { + my @lines = split("\n", $output, -1); + splice(@lines, 1, 1); + $output = join("\n", @lines); + } + + if ($terse) { + $output = (split('\n', $output))[0] . "\n"; + } + + if ($verbose && exists($verbose_messages{$type}) && + !exists($verbose_emitted{$type})) { + $output .= $verbose_messages{$type} . "\n\n"; + $verbose_emitted{$type} = 1; + } + + push(our @report, $output); + + return 1; +} + +sub report_dump { + our @report; +} + +sub fixup_current_range { + my ($lineRef, $offset, $length) = @_; + + if ($$lineRef =~ /^\@\@ -\d+,\d+ \+(\d+),(\d+) \@\@/) { + my $o = $1; + my $l = $2; + my $no = $o + $offset; + my $nl = $l + $length; + $$lineRef =~ s/\+$o,$l \@\@/\+$no,$nl \@\@/; + } +} + +sub fix_inserted_deleted_lines { + my ($linesRef, $insertedRef, $deletedRef) = @_; + + my $range_last_linenr = 0; + my $delta_offset = 0; + + my $old_linenr = 0; + my $new_linenr = 0; + + my $next_insert = 0; + my $next_delete = 0; + + my @lines = (); + + my $inserted = @{$insertedRef}[$next_insert++]; + my $deleted = @{$deletedRef}[$next_delete++]; + + foreach my $old_line (@{$linesRef}) { + my $save_line = 1; + my $line = $old_line; #don't modify the array + if ($line =~ /^(?:\+\+\+|\-\-\-)\s+\S+/) { #new filename + $delta_offset = 0; + } elsif ($line =~ /^\@\@ -\d+,\d+ \+\d+,\d+ \@\@/) { #new hunk + $range_last_linenr = $new_linenr; + fixup_current_range(\$line, $delta_offset, 0); + } + + while (defined($deleted) && ${$deleted}{'LINENR'} == $old_linenr) { + $deleted = @{$deletedRef}[$next_delete++]; + $save_line = 0; + fixup_current_range(\$lines[$range_last_linenr], $delta_offset--, -1); + } + + while (defined($inserted) && ${$inserted}{'LINENR'} == $old_linenr) { + push(@lines, ${$inserted}{'LINE'}); + $inserted = @{$insertedRef}[$next_insert++]; + $new_linenr++; + fixup_current_range(\$lines[$range_last_linenr], $delta_offset++, 1); + } + + if ($save_line) { + push(@lines, $line); + $new_linenr++; + } + + $old_linenr++; + } + + return @lines; +} + +sub fix_insert_line { + my ($linenr, $line) = @_; + + my $inserted = { + LINENR => $linenr, + LINE => $line, + }; + push(@fixed_inserted, $inserted); +} + +sub fix_delete_line { + my ($linenr, $line) = @_; + + my $deleted = { + LINENR => $linenr, + LINE => $line, + }; + + push(@fixed_deleted, $deleted); +} + +sub ERROR { + my ($type, $msg) = @_; + + if (report("ERROR", $type, $msg)) { + our $clean = 0; + our $cnt_error++; + return 1; + } + return 0; +} +sub WARN { + my ($type, $msg) = @_; + + if (report("WARNING", $type, $msg)) { + our $clean = 0; + our $cnt_warn++; + return 1; + } + return 0; +} +sub CHK { + my ($type, $msg) = @_; + + if ($check && report("CHECK", $type, $msg)) { + our $clean = 0; + our $cnt_chk++; + return 1; + } + return 0; +} + +sub check_absolute_file { + my ($absolute, $herecurr) = @_; + my $file = $absolute; + + ##print "absolute<$absolute>\n"; + + # See if any suffix of this path is a path within the tree. + while ($file =~ s@^[^/]*/@@) { + if (-f "$root/$file") { + ##print "file<$file>\n"; + last; + } + } + if (! -f _) { + return 0; + } + + # It is, so see if the prefix is acceptable. + my $prefix = $absolute; + substr($prefix, -length($file)) = ''; + + ##print "prefix<$prefix>\n"; + if ($prefix ne ".../") { + WARN("USE_RELATIVE_PATH", + "use relative pathname instead of absolute in changelog text\n" . $herecurr); + } +} + +sub trim { + my ($string) = @_; + + $string =~ s/^\s+|\s+$//g; + + return $string; +} + +sub ltrim { + my ($string) = @_; + + $string =~ s/^\s+//; + + return $string; +} + +sub rtrim { + my ($string) = @_; + + $string =~ s/\s+$//; + + return $string; +} + +sub string_find_replace { + my ($string, $find, $replace) = @_; + + $string =~ s/$find/$replace/g; + + return $string; +} + +sub tabify { + my ($leading) = @_; + + my $source_indent = $tabsize; + my $max_spaces_before_tab = $source_indent - 1; + my $spaces_to_tab = " " x $source_indent; + + #convert leading spaces to tabs + 1 while $leading =~ s@^([\t]*)$spaces_to_tab@$1\t@g; + #Remove spaces before a tab + 1 while $leading =~ s@^([\t]*)( {1,$max_spaces_before_tab})\t@$1\t@g; + + return "$leading"; +} + +sub pos_last_openparen { + my ($line) = @_; + + my $pos = 0; + + my $opens = $line =~ tr/\(/\(/; + my $closes = $line =~ tr/\)/\)/; + + my $last_openparen = 0; + + if (($opens == 0) || ($closes >= $opens)) { + return -1; + } + + my $len = length($line); + + for ($pos = 0; $pos < $len; $pos++) { + my $string = substr($line, $pos); + if ($string =~ /^($FuncArg|$balanced_parens)/) { + $pos += length($1) - 1; + } elsif (substr($line, $pos, 1) eq '(') { + $last_openparen = $pos; + } elsif (index($string, '(') == -1) { + last; + } + } + + return length(expand_tabs(substr($line, 0, $last_openparen))) + 1; +} + +sub get_raw_comment { + my ($line, $rawline) = @_; + my $comment = ''; + + for my $i (0 .. (length($line) - 1)) { + if (substr($line, $i, 1) eq "$;") { + $comment .= substr($rawline, $i, 1); + } + } + + return $comment; +} + +sub exclude_global_initialisers { + my ($realfile) = @_; + + # Do not check for BPF programs (tools/testing/selftests/bpf/progs/*.c, samples/bpf/*_kern.c, *.bpf.c). + return $realfile =~ m@^tools/testing/selftests/bpf/progs/.*\.c$@ || + $realfile =~ m@^samples/bpf/.*_kern\.c$@ || + $realfile =~ m@/bpf/.*\.bpf\.c$@; +} + +sub process { + my $filename = shift; + + my $linenr=0; + my $prevline=""; + my $prevrawline=""; + my $stashline=""; + my $stashrawline=""; + + my $length; + my $indent; + my $previndent=0; + my $stashindent=0; + + our $clean = 1; + my $signoff = 0; + my $fixes_tag = 0; + my $is_revert = 0; + my $needs_fixes_tag = ""; + my $author = ''; + my $authorsignoff = 0; + my $author_sob = ''; + my $is_patch = 0; + my $is_binding_patch = -1; + my $in_header_lines = $file ? 0 : 1; + my $in_commit_log = 0; #Scanning lines before patch + my $has_patch_separator = 0; #Found a --- line + my $has_commit_log = 0; #Encountered lines before patch + my $commit_log_lines = 0; #Number of commit log lines + my $commit_log_possible_stack_dump = 0; + my $commit_log_long_line = 0; + my $commit_log_has_diff = 0; + my $reported_maintainer_file = 0; + my $non_utf8_charset = 0; + + my $last_git_commit_id_linenr = -1; + + my $last_blank_line = 0; + my $last_coalesced_string_linenr = -1; + + our @report = (); + our $cnt_lines = 0; + our $cnt_error = 0; + our $cnt_warn = 0; + our $cnt_chk = 0; + + # Trace the real file/line as we go. + my $realfile = ''; + my $realline = 0; + my $realcnt = 0; + my $here = ''; + my $context_function; #undef'd unless there's a known function + my $in_comment = 0; + my $comment_edge = 0; + my $first_line = 0; + my $p1_prefix = ''; + + my $prev_values = 'E'; + + # suppression flags + my %suppress_ifbraces; + my %suppress_whiletrailers; + my %suppress_export; + my $suppress_statement = 0; + + my %signatures = (); + + # Pre-scan the patch sanitizing the lines. + # Pre-scan the patch looking for any __setup documentation. + # + my @setup_docs = (); + my $setup_docs = 0; + + my $camelcase_file_seeded = 0; + + my $checklicenseline = 1; + + sanitise_line_reset(); + my $line; + foreach my $rawline (@rawlines) { + $linenr++; + $line = $rawline; + + push(@fixed, $rawline) if ($fix); + + if ($rawline=~/^\+\+\+\s+(\S+)/) { + $setup_docs = 0; + if ($1 =~ m@Documentation/admin-guide/kernel-parameters.txt$@) { + $setup_docs = 1; + } + #next; + } + if ($rawline =~ /^\@\@ -\d+(?:,\d+)? \+(\d+)(,(\d+))? \@\@/) { + $realline=$1-1; + if (defined $2) { + $realcnt=$3+1; + } else { + $realcnt=1+1; + } + $in_comment = 0; + + # Guestimate if this is a continuing comment. Run + # the context looking for a comment "edge". If this + # edge is a close comment then we must be in a comment + # at context start. + my $edge; + my $cnt = $realcnt; + for (my $ln = $linenr + 1; $cnt > 0; $ln++) { + next if (defined $rawlines[$ln - 1] && + $rawlines[$ln - 1] =~ /^-/); + $cnt--; + #print "RAW<$rawlines[$ln - 1]>\n"; + last if (!defined $rawlines[$ln - 1]); + if ($rawlines[$ln - 1] =~ m@(/\*|\*/)@ && + $rawlines[$ln - 1] !~ m@"[^"]*(?:/\*|\*/)[^"]*"@) { + ($edge) = $1; + last; + } + } + if (defined $edge && $edge eq '*/') { + $in_comment = 1; + } + + # Guestimate if this is a continuing comment. If this + # is the start of a diff block and this line starts + # ' *' then it is very likely a comment. + if (!defined $edge && + $rawlines[$linenr] =~ m@^.\s*(?:\*\*+| \*)(?:\s|$)@) + { + $in_comment = 1; + } + + ##print "COMMENT:$in_comment edge<$edge> $rawline\n"; + sanitise_line_reset($in_comment); + + } elsif ($realcnt && $rawline =~ /^(?:\+| |$)/) { + # Standardise the strings and chars within the input to + # simplify matching -- only bother with positive lines. + $line = sanitise_line($rawline); + } + push(@lines, $line); + + if ($realcnt > 1) { + $realcnt-- if ($line =~ /^(?:\+| |$)/); + } else { + $realcnt = 0; + } + + #print "==>$rawline\n"; + #print "-->$line\n"; + + if ($setup_docs && $line =~ /^\+/) { + push(@setup_docs, $line); + } + } + + $prefix = ''; + + $realcnt = 0; + $linenr = 0; + $fixlinenr = -1; + foreach my $line (@lines) { + $linenr++; + $fixlinenr++; + my $sline = $line; #copy of $line + $sline =~ s/$;/ /g; #with comments as spaces + + my $rawline = $rawlines[$linenr - 1]; + my $raw_comment = get_raw_comment($line, $rawline); + +# check if it's a mode change, rename or start of a patch + if (!$in_commit_log && + ($line =~ /^ mode change [0-7]+ => [0-7]+ \S+\s*$/ || + ($line =~ /^rename (?:from|to) \S+\s*$/ || + $line =~ /^diff --git a\/[\w\/\.\_\-]+ b\/\S+\s*$/))) { + $is_patch = 1; + } + +#extract the line range in the file after the patch is applied + if (!$in_commit_log && + $line =~ /^\@\@ -\d+(?:,\d+)? \+(\d+)(,(\d+))? \@\@(.*)/) { + my $context = $4; + $is_patch = 1; + $first_line = $linenr + 1; + $realline=$1-1; + if (defined $2) { + $realcnt=$3+1; + } else { + $realcnt=1+1; + } + annotate_reset(); + $prev_values = 'E'; + + %suppress_ifbraces = (); + %suppress_whiletrailers = (); + %suppress_export = (); + $suppress_statement = 0; + if ($context =~ /\b(\w+)\s*\(/) { + $context_function = $1; + } else { + undef $context_function; + } + next; + +# track the line number as we move through the hunk, note that +# new versions of GNU diff omit the leading space on completely +# blank context lines so we need to count that too. + } elsif ($line =~ /^( |\+|$)/) { + $realline++; + $realcnt-- if ($realcnt != 0); + + # Measure the line length and indent. + ($length, $indent) = line_stats($rawline); + + # Track the previous line. + ($prevline, $stashline) = ($stashline, $line); + ($previndent, $stashindent) = ($stashindent, $indent); + ($prevrawline, $stashrawline) = ($stashrawline, $rawline); + + #warn "line<$line>\n"; + + } elsif ($realcnt == 1) { + $realcnt--; + } + + my $hunk_line = ($realcnt != 0); + + $here = "#$linenr: " if (!$file); + $here = "#$realline: " if ($file); + + my $found_file = 0; + # extract the filename as it passes + if ($line =~ /^diff --git.*?(\S+)$/) { + $realfile = $1; + $realfile =~ s@^([^/]*)/@@ if (!$file); + $in_commit_log = 0; + $found_file = 1; + } elsif ($line =~ /^\+\+\+\s+(\S+)/) { + $realfile = $1; + $realfile =~ s@^([^/]*)/@@ if (!$file); + $in_commit_log = 0; + + $p1_prefix = $1; + if (!$file && $tree && $p1_prefix ne '' && + -e "$root/$p1_prefix") { + WARN("PATCH_PREFIX", + "patch prefix '$p1_prefix' exists, appears to be a -p0 patch\n"); + } + + 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"); + } + $found_file = 1; + } + +#make up the handle for any error we report on this line + if ($showfile) { + $prefix = "$realfile:$realline: " + } elsif ($emacs) { + if ($file) { + $prefix = "$filename:$realline: "; + } else { + $prefix = "$filename:$linenr: "; + } + } + + if ($found_file) { + if (is_maintained_obsolete($realfile)) { + WARN("OBSOLETE", + "$realfile is marked as 'obsolete' in the MAINTAINERS hierarchy. No unnecessary modifications please.\n"); + } + if ($realfile =~ m@^(?:drivers/net/|net/|drivers/staging/)@) { + $check = 1; + } else { + $check = $check_orig; + } + $checklicenseline = 1; + + if ($realfile !~ /^MAINTAINERS/) { + my $last_binding_patch = $is_binding_patch; + + $is_binding_patch = () = $realfile =~ m@^(?:Documentation/devicetree/|include/dt-bindings/)@; + + if (($last_binding_patch != -1) && + ($last_binding_patch ^ $is_binding_patch)) { + WARN("DT_SPLIT_BINDING_PATCH", + "DT binding docs and includes should be a separate patch. See: Documentation/devicetree/bindings/submitting-patches.rst\n"); + } + } + + next; + } + + $here .= "FILE: $realfile:$realline:" if ($realcnt != 0); + + my $hereline = "$here\n$rawline\n"; + my $herecurr = "$here\n$rawline\n"; + my $hereprev = "$here\n$prevrawline\n$rawline\n"; + + $cnt_lines++ if ($realcnt != 0); + +# Verify the existence of a commit log if appropriate +# 2 is used because a $signature is counted in $commit_log_lines + if ($in_commit_log) { + if ($line !~ /^\s*$/) { + $commit_log_lines++; #could be a $signature + } + } elsif ($has_commit_log && $commit_log_lines < 2) { + WARN("COMMIT_MESSAGE", + "Missing commit description - Add an appropriate one\n"); + $commit_log_lines = 2; #warn only once + } + +# Check if the commit log has what seems like a diff which can confuse patch + if ($in_commit_log && !$commit_log_has_diff && + (($line =~ m@^\s+diff\b.*a/([\w/]+)@ && + $line =~ m@^\s+diff\b.*a/[\w/]+\s+b/$1\b@) || + $line =~ m@^\s*(?:\-\-\-\s+a/|\+\+\+\s+b/)@ || + $line =~ m/^\s*\@\@ \-\d+,\d+ \+\d+,\d+ \@\@/)) { + ERROR("DIFF_IN_COMMIT_MSG", + "Avoid using diff content in the commit message - patch(1) might not work\n" . $herecurr); + $commit_log_has_diff = 1; + } + +# Check for incorrect file permissions + if ($line =~ /^new (file )?mode.*[7531]\d{0,2}$/) { + my $permhere = $here . "FILE: $realfile\n"; + if ($realfile !~ m@scripts/@ && + $realfile !~ /\.(py|pl|awk|sh)$/) { + ERROR("EXECUTE_PERMISSIONS", + "do not set execute permissions for source files\n" . $permhere); + } + } + +# Check the patch for a From: + if (decode("MIME-Header", $line) =~ /^From:\s*(.*)/) { + $author = $1; + my $curline = $linenr; + while(defined($rawlines[$curline]) && ($rawlines[$curline++] =~ /^[ \t]\s*(.*)/)) { + $author .= $1; + } + $author = encode("utf8", $author) if ($line =~ /=\?utf-8\?/i); + $author =~ s/"//g; + $author = reformat_email($author); + } + +# Check the patch for a signoff: + if ($line =~ /^\s*signed-off-by:\s*(.*)/i) { + $signoff++; + $in_commit_log = 0; + if ($author ne '' && $authorsignoff != 1) { + if (same_email_addresses($1, $author)) { + $authorsignoff = 1; + } else { + my $ctx = $1; + my ($email_name, $email_comment, $email_address, $comment1) = parse_email($ctx); + my ($author_name, $author_comment, $author_address, $comment2) = parse_email($author); + + if (lc $email_address eq lc $author_address && $email_name eq $author_name) { + $author_sob = $ctx; + $authorsignoff = 2; + } elsif (lc $email_address eq lc $author_address) { + $author_sob = $ctx; + $authorsignoff = 3; + } elsif ($email_name eq $author_name) { + $author_sob = $ctx; + $authorsignoff = 4; + + my $address1 = $email_address; + my $address2 = $author_address; + + if ($address1 =~ /(\S+)\+\S+(\@.*)/) { + $address1 = "$1$2"; + } + if ($address2 =~ /(\S+)\+\S+(\@.*)/) { + $address2 = "$1$2"; + } + if ($address1 eq $address2) { + $authorsignoff = 5; + } + } + } + } + } + +# Check for patch separator + if ($line =~ /^---$/) { + $has_patch_separator = 1; + $in_commit_log = 0; + } + +# Check if MAINTAINERS is being updated. If so, there's probably no need to +# emit the "does MAINTAINERS need updating?" message on file add/move/delete + if ($line =~ /^\s*MAINTAINERS\s*\|/) { + $reported_maintainer_file = 1; + } + +# Check signature styles + if (!$in_header_lines && + $line =~ /^(\s*)([a-z0-9_-]+by:|$signature_tags)(\s*)(.*)/i) { + my $space_before = $1; + my $sign_off = $2; + my $space_after = $3; + my $email = $4; + my $ucfirst_sign_off = ucfirst(lc($sign_off)); + + if ($sign_off !~ /$signature_tags/) { + my $suggested_signature = find_standard_signature($sign_off); + if ($suggested_signature eq "") { + WARN("BAD_SIGN_OFF", + "Non-standard signature: $sign_off\n" . $herecurr); + } else { + if (WARN("BAD_SIGN_OFF", + "Non-standard signature: '$sign_off' - perhaps '$suggested_signature'?\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ s/$sign_off/$suggested_signature/; + } + } + } + if (defined $space_before && $space_before ne "") { + if (WARN("BAD_SIGN_OFF", + "Do not use whitespace before $ucfirst_sign_off\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] = + "$ucfirst_sign_off $email"; + } + } + if ($sign_off =~ /-by:$/i && $sign_off ne $ucfirst_sign_off) { + if (WARN("BAD_SIGN_OFF", + "'$ucfirst_sign_off' is the preferred signature form\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] = + "$ucfirst_sign_off $email"; + } + + } + if (!defined $space_after || $space_after ne " ") { + if (WARN("BAD_SIGN_OFF", + "Use a single space after $ucfirst_sign_off\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] = + "$ucfirst_sign_off $email"; + } + } + + my ($email_name, $name_comment, $email_address, $comment) = parse_email($email); + my $suggested_email = format_email(($email_name, $name_comment, $email_address, $comment)); + if ($suggested_email eq "") { + ERROR("BAD_SIGN_OFF", + "Unrecognized email address: '$email'\n" . $herecurr); + } else { + my $dequoted = $suggested_email; + $dequoted =~ s/^"//; + $dequoted =~ s/" 1) { + WARN("BAD_SIGN_OFF", + "Use a single name comment in email: '$email'\n" . $herecurr); + } + + + # stable@vger.kernel.org or stable@kernel.org shouldn't + # have an email name. In addition comments should strictly + # begin with a # + if ($email =~ /^.*stable\@(?:vger\.)?kernel\.org/i) { + if (($comment ne "" && $comment !~ /^#.+/) || + ($email_name ne "")) { + my $cur_name = $email_name; + my $new_comment = $comment; + $cur_name =~ s/[a-zA-Z\s\-\"]+//g; + + # Remove brackets enclosing comment text + # and # from start of comments to get comment text + $new_comment =~ s/^\((.*)\)$/$1/; + $new_comment =~ s/^\[(.*)\]$/$1/; + $new_comment =~ s/^[\s\#]+|\s+$//g; + + $new_comment = trim("$new_comment $cur_name") if ($cur_name ne $new_comment); + $new_comment = " # $new_comment" if ($new_comment ne ""); + my $new_email = "$email_address$new_comment"; + + if (WARN("BAD_STABLE_ADDRESS_STYLE", + "Invalid email format for stable: '$email', prefer '$new_email'\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ s/\Q$email\E/$new_email/; + } + } + } elsif ($comment ne "" && $comment !~ /^(?:#.+|\(.+\))$/) { + my $new_comment = $comment; + + # Extract comment text from within brackets or + # c89 style /*...*/ comments + $new_comment =~ s/^\[(.*)\]$/$1/; + $new_comment =~ s/^\/\*(.*)\*\/$/$1/; + + $new_comment = trim($new_comment); + $new_comment =~ s/^[^\w]$//; # Single lettered comment with non word character is usually a typo + $new_comment = "($new_comment)" if ($new_comment ne ""); + my $new_email = format_email($email_name, $name_comment, $email_address, $new_comment); + + if (WARN("BAD_SIGN_OFF", + "Unexpected content after email: '$email', should be: '$new_email'\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ s/\Q$email\E/$new_email/; + } + } + } + +# Check for duplicate signatures + my $sig_nospace = $line; + $sig_nospace =~ s/\s//g; + $sig_nospace = lc($sig_nospace); + if (defined $signatures{$sig_nospace}) { + WARN("BAD_SIGN_OFF", + "Duplicate signature\n" . $herecurr); + } else { + $signatures{$sig_nospace} = 1; + } + +# Check Co-developed-by: immediately followed by Signed-off-by: with same name and email + if ($sign_off =~ /^co-developed-by:$/i) { + if ($email eq $author) { + WARN("BAD_SIGN_OFF", + "Co-developed-by: should not be used to attribute nominal patch author '$author'\n" . $herecurr); + } + if (!defined $lines[$linenr]) { + WARN("BAD_SIGN_OFF", + "Co-developed-by: must be immediately followed by Signed-off-by:\n" . $herecurr); + } elsif ($rawlines[$linenr] !~ /^signed-off-by:\s*(.*)/i) { + WARN("BAD_SIGN_OFF", + "Co-developed-by: must be immediately followed by Signed-off-by:\n" . $herecurr . $rawlines[$linenr] . "\n"); + } elsif ($1 ne $email) { + WARN("BAD_SIGN_OFF", + "Co-developed-by and Signed-off-by: name/email do not match\n" . $herecurr . $rawlines[$linenr] . "\n"); + } + } + +# check if Reported-by: is followed by a Closes: tag + if ($sign_off =~ /^reported(?:|-and-tested)-by:$/i) { + if (!defined $lines[$linenr]) { + WARN("BAD_REPORTED_BY_LINK", + "Reported-by: should be immediately followed by Closes: with a URL to the report\n" . $herecurr . "\n"); + } elsif ($rawlines[$linenr] !~ /^closes:\s*/i) { + WARN("BAD_REPORTED_BY_LINK", + "Reported-by: should be immediately followed by Closes: with a URL to the report\n" . $herecurr . $rawlines[$linenr] . "\n"); + } + } + } + +# These indicate a bug fix + if (!$in_header_lines && !$is_patch && + $line =~ /^This reverts commit/) { + $is_revert = 1; + } + + if (!$in_header_lines && !$is_patch && + $line =~ /((?:(?:BUG: K.|UB)SAN: |Call Trace:|stable\@|syzkaller))/) { + $needs_fixes_tag = $1; + } + +# Check Fixes: styles is correct + if (!$in_header_lines && + $line =~ /^\s*(fixes:?)\s*(?:commit\s*)?([0-9a-f]{5,40})(?:\s*($balanced_parens))?/i) { + my $tag = $1; + my $orig_commit = $2; + my $title; + my $title_has_quotes = 0; + $fixes_tag = 1; + if (defined $3) { + # Always strip leading/trailing parens then double quotes if existing + $title = substr($3, 1, -1); + if ($title =~ /^".*"$/) { + $title = substr($title, 1, -1); + $title_has_quotes = 1; + } + } else { + $title = "commit title" + } + + + 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_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 (WARN("BAD_FIXES_TAG", + "Please use correct Fixes: style 'Fixes: <12 chars of sha1> (\"\")' - ie: 'Fixes: $cid (\"$ctitle\")'\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] = "Fixes: $cid (\"$ctitle\")"; + } + } + } + +# Check email subject for common tools that don't need to be mentioned + if ($in_header_lines && + $line =~ /^Subject:.*\b(?:checkpatch|sparse|smatch)\b[^:]/i) { + WARN("EMAIL_SUBJECT", + "A patch subject line should describe the change not the tool that found it\n" . $herecurr); + } + +# Check for Gerrit Change-Ids not in any patch context + if ($realfile eq '' && !$has_patch_separator && $line =~ /^\s*change-id:/i) { + if (ERROR("GERRIT_CHANGE_ID", + "Remove Gerrit Change-Id's before submitting upstream\n" . $herecurr) && + $fix) { + fix_delete_line($fixlinenr, $rawline); + } + } + +# Check if the commit log is in a possible stack dump + if ($in_commit_log && !$commit_log_possible_stack_dump && + ($line =~ /^\s*(?:WARNING:|BUG:)/ || + $line =~ /^\s*\[\s*\d+\.\d{6,6}\s*\]/ || + # timestamp + $line =~ /^\s*\[\<[0-9a-fA-F]{8,}\>\]/) || + $line =~ /^(?:\s+\w+:\s+[0-9a-fA-F]+){3,3}/ || + $line =~ /^\s*\#\d+\s*\[[0-9a-fA-F]+\]\s*\w+ at [0-9a-fA-F]+/) { + # stack dump address styles + $commit_log_possible_stack_dump = 1; + } + +# Check for line lengths > 75 in commit log, warn once + if ($in_commit_log && !$commit_log_long_line && + length($line) > 75 && + !($line =~ /^\s*[a-zA-Z0-9_\/\.]+\s+\|\s+\d+/ || + # file delta changes + $line =~ /^\s*(?:[\w\.\-\+]*\/)++[\w\.\-\+]+:/ || + # filename then : + $line =~ /^\s*(?:Fixes:|$link_tags_search|$signature_tags)/i || + # A Fixes:, link or signature tag line + $commit_log_possible_stack_dump)) { + WARN("COMMIT_LOG_LONG_LINE", + "Prefer a maximum 75 chars per line (possible unwrapped commit description?)\n" . $herecurr); + $commit_log_long_line = 1; + } + +# Reset possible stack dump if a blank line is found + if ($in_commit_log && $commit_log_possible_stack_dump && + $line =~ /^\s*$/) { + $commit_log_possible_stack_dump = 0; + } + +# Check for odd tags before a URI/URL + if ($in_commit_log && + $line =~ /^\s*(\w+:)\s*http/ && $1 !~ /^$link_tags_search$/) { + if ($1 =~ /^v(?:ersion)?\d+/i) { + WARN("COMMIT_LOG_VERSIONING", + "Patch version information should be after the --- line\n" . $herecurr); + } else { + WARN("COMMIT_LOG_USE_LINK", + "Unknown link reference '$1', use $link_tags_print instead\n" . $herecurr); + } + } + +# Check for misuse of the link tags + if ($in_commit_log && + $line =~ /^\s*(\w+:)\s*(\S+)/) { + my $tag = $1; + my $value = $2; + if ($tag =~ /^$link_tags_search$/ && $value !~ m{^https?://}) { + WARN("COMMIT_LOG_WRONG_LINK", + "'$tag' should be followed by a public http(s) link\n" . $herecurr); + } + } + +# Check for lines starting with a # + if ($in_commit_log && $line =~ /^#/) { + if (WARN("COMMIT_COMMENT_SYMBOL", + "Commit log lines starting with '#' are dropped by git as comments\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ s/^/ /; + } + } + +# 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") +# with the commit subject '("' prefix and '")' suffix +# This is a fairly compilicated block as it tests for what appears to be +# bare SHA-1 hash with minimum length of 5. It also avoids several types of +# possible SHA-1 matches. +# A commit match can span multiple lines so this block attempts to find a +# complete typical commit on a maximum of 3 lines + if ($perl_version_ok && + $in_commit_log && !$commit_log_possible_stack_dump && + $line !~ /^\s*(?:Link|Patchwork|http|https|BugLink|base-commit):/i && + $line !~ /^This reverts commit [0-9a-f]{7,40}/ && + (($line =~ /\bcommit\s+[0-9a-f]{5,}\b/i || + ($line =~ /\bcommit\s*$/i && defined($rawlines[$linenr]) && $rawlines[$linenr] =~ /^\s*[0-9a-f]{5,}\b/i)) || + ($line =~ /(?:\s|^)[0-9a-f]{12,40}(?:[\s"'\(\[]|$)/i && + $line !~ /[\<\[][0-9a-f]{12,40}[\>\]]/i && + $line !~ /\bfixes:\s*[0-9a-f]{12,40}/i))) { + my $init_char = "c"; + my $orig_commit = ""; + my $short = 1; + my $long = 0; + my $case = 1; + my $space = 1; + my $id = '0123456789ab'; + my $orig_desc = "commit description"; + my $description = ""; + my $herectx = $herecurr; + my $has_parens = 0; + my $has_quotes = 0; + + my $input = $line; + if ($line =~ /(?:\bcommit\s+[0-9a-f]{5,}|\bcommit\s*$)/i) { + for (my $n = 0; $n < 2; $n++) { + if ($input =~ /\bcommit\s+[0-9a-f]{5,}\s*($balanced_parens)/i) { + $orig_desc = $1; + $has_parens = 1; + # Always strip leading/trailing parens then double quotes if existing + $orig_desc = substr($orig_desc, 1, -1); + if ($orig_desc =~ /^".*"$/) { + $orig_desc = substr($orig_desc, 1, -1); + $has_quotes = 1; + } + last; + } + last if ($#lines < $linenr + $n); + $input .= " " . trim($rawlines[$linenr + $n]); + $herectx .= "$rawlines[$linenr + $n]\n"; + } + $herectx = $herecurr if (!$has_parens); + } + + if ($input =~ /\b(c)ommit\s+([0-9a-f]{5,})\b/i) { + $init_char = $1; + $orig_commit = lc($2); + $short = 0 if ($input =~ /\bcommit\s+[0-9a-f]{12,40}/i); + $long = 1 if ($input =~ /\bcommit\s+[0-9a-f]{41,}/i); + $space = 0 if ($input =~ /\bcommit [0-9a-f]/i); + $case = 0 if ($input =~ /\b[Cc]ommit\s+[0-9a-f]{5,40}[^A-F]/); + } elsif ($input =~ /\b([0-9a-f]{12,40})\b/i) { + $orig_commit = lc($1); + } + + ($id, $description) = git_commit_info($orig_commit, + $id, $orig_desc); + + if (defined($id) && + ($short || $long || $space || $case || ($orig_desc ne $description) || !$has_quotes) && + $last_git_commit_id_linenr != $linenr - 1) { + ERROR("GIT_COMMIT_ID", + "Please use git commit description style 'commit <12+ chars of sha1> (\"<title line>\")' - ie: '${init_char}ommit $id (\"$description\")'\n" . $herectx); + } + #don't report the next line if this line ends in commit and the sha1 hash is the next line + $last_git_commit_id_linenr = $linenr if ($line =~ /\bcommit\s*$/i); + } + +# Check for mailing list archives other than lore.kernel.org + if ($rawline =~ m{http.*\b$obsolete_archives}) { + WARN("PREFER_LORE_ARCHIVE", + "Use lore.kernel.org archive links when possible - see https://lore.kernel.org/lists.html\n" . $herecurr); + } + +# Check for added, moved or deleted files + if (!$reported_maintainer_file && !$in_commit_log && + ($line =~ /^(?:new|deleted) file mode\s*\d+\s*$/ || + $line =~ /^rename (?:from|to) [\w\/\.\-]+\s*$/ || + ($line =~ /\{\s*([\w\/\.\-]*)\s*\=\>\s*([\w\/\.\-]*)\s*\}/ && + (defined($1) || defined($2))))) { + $is_patch = 1; + $reported_maintainer_file = 1; + WARN("FILE_PATH_CHANGES", + "added, moved or deleted file(s), does MAINTAINERS need updating?\n" . $herecurr); + } + +# Check for adding new DT bindings not in schema format + if (!$in_commit_log && + ($line =~ /^new file mode\s*\d+\s*$/) && + ($realfile =~ m@^Documentation/devicetree/bindings/.*\.txt$@)) { + WARN("DT_SCHEMA_BINDING_PATCH", + "DT bindings should be in DT schema format. See: Documentation/devicetree/bindings/writing-schema.rst\n"); + } + +# Check for wrappage within a valid hunk of the file + if ($realcnt != 0 && $line !~ m{^(?:\+|-| |\\ No newline|$)}) { + ERROR("CORRUPTED_PATCH", + "patch seems to be corrupt (line wrapped?)\n" . + $herecurr) if (!$emitted_corrupt++); + } + +# UTF-8 regex found at http://www.w3.org/International/questions/qa-forms-utf-8.en.php + if (($realfile =~ /^$/ || $line =~ /^\+/) && + $rawline !~ m/^$UTF8*$/) { + my ($utf8_prefix) = ($rawline =~ /^($UTF8*)/); + + my $blank = copy_spacing($rawline); + my $ptr = substr($blank, 0, length($utf8_prefix)) . "^"; + my $hereptr = "$hereline$ptr\n"; + + CHK("INVALID_UTF8", + "Invalid UTF-8, patch and commit message should be encoded in UTF-8\n" . $hereptr); + } + +# Check if it's the start of a commit log +# (not a header line and we haven't seen the patch filename) + if ($in_header_lines && $realfile =~ /^$/ && + !($rawline =~ /^\s+(?:\S|$)/ || + $rawline =~ /^(?:commit\b|from\b|[\w-]+:)/i)) { + $in_header_lines = 0; + $in_commit_log = 1; + $has_commit_log = 1; + } + +# Check if there is UTF-8 in a commit log when a mail header has explicitly +# declined it, i.e defined some charset where it is missing. + if ($in_header_lines && + $rawline =~ /^Content-Type:.+charset="(.+)".*$/ && + $1 !~ /utf-8/i) { + $non_utf8_charset = 1; + } + + if ($in_commit_log && $non_utf8_charset && $realfile =~ /^$/ && + $rawline =~ /$NON_ASCII_UTF8/) { + WARN("UTF8_BEFORE_PATCH", + "8-bit UTF-8 used in possible commit log\n" . $herecurr); + } + +# Check for absolute kernel paths in commit message + if ($tree && $in_commit_log) { + while ($line =~ m{(?:^|\s)(/\S*)}g) { + my $file = $1; + + if ($file =~ m{^(.*?)(?::\d+)+:?$} && + check_absolute_file($1, $herecurr)) { + # + } else { + check_absolute_file($file, $herecurr); + } + } + } + +# Check for various typo / spelling mistakes + if (defined($misspellings) && + ($in_commit_log || $line =~ /^(?:\+|Subject:)/i)) { + while ($rawline =~ /(?:^|[^\w\-'`])($misspellings)(?:[^\w\-'`]|$)/gi) { + my $typo = $1; + my $blank = copy_spacing($rawline); + my $ptr = substr($blank, 0, $-[1]) . "^" x length($typo); + my $hereptr = "$hereline$ptr\n"; + my $typo_fix = $spelling_fix{lc($typo)}; + $typo_fix = ucfirst($typo_fix) if ($typo =~ /^[A-Z]/); + $typo_fix = uc($typo_fix) if ($typo =~ /^[A-Z]+$/); + my $msg_level = \&WARN; + $msg_level = \&CHK if ($file); + if (&{$msg_level}("TYPO_SPELLING", + "'$typo' may be misspelled - perhaps '$typo_fix'?\n" . $hereptr) && + $fix) { + $fixed[$fixlinenr] =~ s/(^|[^A-Za-z@])($typo)($|[^A-Za-z@])/$1$typo_fix$3/; + } + } + } + +# check for invalid commit id + if ($in_commit_log && $line =~ /(^fixes:|\bcommit)\s+([0-9a-f]{6,40})\b/i) { + my $id; + my $description; + ($id, $description) = git_commit_info($2, undef, undef); + if (!defined($id)) { + WARN("UNKNOWN_COMMIT_ID", + "Unknown commit id '$2', maybe rebased or not pulled?\n" . $herecurr); + } + } + +# check for repeated words separated by a single space +# avoid false positive from list command eg, '-rw-r--r-- 1 root root' + if (($rawline =~ /^\+/ || $in_commit_log) && + $rawline !~ /[bcCdDlMnpPs\?-][rwxsStT-]{9}/) { + pos($rawline) = 1 if (!$in_commit_log); + while ($rawline =~ /\b($word_pattern) (?=($word_pattern))/g) { + + my $first = $1; + my $second = $2; + my $start_pos = $-[1]; + my $end_pos = $+[2]; + if ($first =~ /(?:struct|union|enum)/) { + pos($rawline) += length($first) + length($second) + 1; + next; + } + + next if (lc($first) ne lc($second)); + next if ($first eq 'long'); + + # check for character before and after the word matches + my $start_char = ''; + my $end_char = ''; + $start_char = substr($rawline, $start_pos - 1, 1) if ($start_pos > ($in_commit_log ? 0 : 1)); + $end_char = substr($rawline, $end_pos, 1) if ($end_pos < length($rawline)); + + next if ($start_char =~ /^\S$/); + next if (index(" \t.,;?!", $end_char) == -1); + + # avoid repeating hex occurrences like 'ff ff fe 09 ...' + if ($first =~ /\b[0-9a-f]{2,}\b/i) { + next if (!exists($allow_repeated_words{lc($first)})); + } + + if (WARN("REPEATED_WORD", + "Possible repeated word: '$first'\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ s/\b$first $second\b/$first/; + } + } + + # if it's a repeated word on consecutive lines in a comment block + if ($prevline =~ /$;+\s*$/ && + $prevrawline =~ /($word_pattern)\s*$/) { + my $last_word = $1; + if ($rawline =~ /^\+\s*\*\s*$last_word /) { + if (WARN("REPEATED_WORD", + "Possible repeated word: '$last_word'\n" . $hereprev) && + $fix) { + $fixed[$fixlinenr] =~ s/(\+\s*\*\s*)$last_word /$1/; + } + } + } + } + +# ignore non-hunk lines and lines being removed + next if (!$hunk_line || $line =~ /^-/); + +#trailing whitespace + if ($line =~ /^\+.*\015/) { + my $herevet = "$here\n" . cat_vet($rawline) . "\n"; + if (ERROR("DOS_LINE_ENDINGS", + "DOS line endings\n" . $herevet) && + $fix) { + $fixed[$fixlinenr] =~ s/[\s\015]+$//; + } + } elsif ($rawline =~ /^\+.*\S\s+$/ || $rawline =~ /^\+\s+$/) { + my $herevet = "$here\n" . cat_vet($rawline) . "\n"; + if (ERROR("TRAILING_WHITESPACE", + "trailing whitespace\n" . $herevet) && + $fix) { + $fixed[$fixlinenr] =~ s/\s+$//; + } + + $rpt_cleaners = 1; + } + +# Check for FSF mailing addresses. + if ($rawline =~ /\bwrite to the Free/i || + $rawline =~ /\b675\s+Mass\s+Ave/i || + $rawline =~ /\b59\s+Temple\s+Pl/i || + $rawline =~ /\b51\s+Franklin\s+St/i) { + my $herevet = "$here\n" . cat_vet($rawline) . "\n"; + my $msg_level = \&ERROR; + $msg_level = \&CHK if ($file); + &{$msg_level}("FSF_MAILING_ADDRESS", + "Do not include the paragraph about writing to the Free Software Foundation's mailing address from the sample GPL notice. The FSF has changed addresses in the past, and may do so again. Linux already includes a copy of the GPL.\n" . $herevet) + } + +# check for Kconfig help text having a real description +# Only applies when adding the entry originally, after that we do not have +# sufficient context to determine whether it is indeed long enough. + if ($realfile =~ /Kconfig/ && + # 'choice' is usually the last thing on the line (though + # Kconfig supports named choices), so use a word boundary + # (\b) rather than a whitespace character (\s) + $line =~ /^\+\s*(?:config|menuconfig|choice)\b/) { + my $ln = $linenr; + my $needs_help = 0; + my $has_help = 0; + my $help_length = 0; + while (defined $lines[$ln]) { + my $f = $lines[$ln++]; + + next if ($f =~ /^-/); + last if ($f !~ /^[\+ ]/); # !patch context + + if ($f =~ /^\+\s*(?:bool|tristate|prompt)\s*["']/) { + $needs_help = 1; + next; + } + if ($f =~ /^\+\s*help\s*$/) { + $has_help = 1; + next; + } + + $f =~ s/^.//; # strip patch context [+ ] + $f =~ s/#.*//; # strip # directives + $f =~ s/^\s+//; # strip leading blanks + next if ($f =~ /^$/); # skip blank lines + + # At the end of this Kconfig block: + # This only checks context lines in the patch + # and so hopefully shouldn't trigger false + # positives, even though some of these are + # common words in help texts + if ($f =~ /^(?:config|menuconfig|choice|endchoice| + if|endif|menu|endmenu|source)\b/x) { + last; + } + $help_length++ if ($has_help); + } + if ($needs_help && + $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"); + } + } + +# check MAINTAINERS entries + if ($realfile =~ /^MAINTAINERS$/) { +# check MAINTAINERS entries for the right form + if ($rawline =~ /^\+[A-Z]:/ && + $rawline !~ /^\+[A-Z]:\t\S/) { + if (WARN("MAINTAINERS_STYLE", + "MAINTAINERS entries use one tab after TYPE:\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ s/^(\+[A-Z]):\s*/$1:\t/; + } + } +# check MAINTAINERS entries for the right ordering too + my $preferred_order = 'MRLSWQBCPTFXNK'; + if ($rawline =~ /^\+[A-Z]:/ && + $prevrawline =~ /^[\+ ][A-Z]:/) { + $rawline =~ /^\+([A-Z]):\s*(.*)/; + my $cur = $1; + my $curval = $2; + $prevrawline =~ /^[\+ ]([A-Z]):\s*(.*)/; + my $prev = $1; + my $prevval = $2; + my $curindex = index($preferred_order, $cur); + my $previndex = index($preferred_order, $prev); + if ($curindex < 0) { + WARN("MAINTAINERS_STYLE", + "Unknown MAINTAINERS entry type: '$cur'\n" . $herecurr); + } else { + if ($previndex >= 0 && $curindex < $previndex) { + WARN("MAINTAINERS_STYLE", + "Misordered MAINTAINERS entry - list '$cur:' before '$prev:'\n" . $hereprev); + } elsif ((($prev eq 'F' && $cur eq 'F') || + ($prev eq 'X' && $cur eq 'X')) && + ($prevval cmp $curval) > 0) { + WARN("MAINTAINERS_STYLE", + "Misordered MAINTAINERS entry - list file patterns in alphabetic order\n" . $hereprev); + } + } + } + } + + 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*\"/) || + ($realfile =~ /\.[ch]$/ && $line =~ /^\+.*\.compatible\s*=\s*\"/))) { + + my @compats = $rawline =~ /\"([a-zA-Z0-9\-\,\.\+_]+)\"/g; + + my $dt_path = $root . "/Documentation/devicetree/bindings/"; + my $vp_file = $dt_path . "vendor-prefixes.yaml"; + + foreach my $compat (@compats) { + my $compat2 = $compat; + $compat2 =~ s/\,[a-zA-Z0-9]*\-/\,<\.\*>\-/; + my $compat3 = $compat; + $compat3 =~ s/\,([a-z]*)[0-9]*\-/\,$1<\.\*>\-/; + `grep -Erq "$compat|$compat2|$compat3" $dt_path`; + if ( $? >> 8 ) { + WARN("UNDOCUMENTED_DT_STRING", + "DT compatible string \"$compat\" appears un-documented -- check $dt_path\n" . $herecurr); + } + + next if $compat !~ /^([a-zA-Z0-9\-]+)\,/; + my $vendor = $1; + `grep -Eq "\\"\\^\Q$vendor\E,\\.\\*\\":" $vp_file`; + if ( $? >> 8 ) { + WARN("UNDOCUMENTED_DT_STRING", + "DT compatible string vendor \"$vendor\" appears un-documented -- check $vp_file\n" . $herecurr); + } + } + } + +# check for using SPDX license tag at beginning of files + if ($realline == $checklicenseline) { + if ($rawline =~ /^[ \+]\s*\#\!\s*\//) { + $checklicenseline = 2; + } elsif ($rawline =~ /^\+/) { + my $comment = ""; + if ($realfile =~ /\.(h|s|S)$/) { + $comment = '/*'; + } elsif ($realfile =~ /\.(c|rs|dts|dtsi)$/) { + $comment = '//'; + } elsif (($checklicenseline == 2) || $realfile =~ /\.(sh|pl|py|awk|tc|yaml)$/) { + $comment = '#'; + } elsif ($realfile =~ /\.rst$/) { + $comment = '..'; + } + +# check SPDX comment style for .[chsS] files + if ($realfile =~ /\.[chsS]$/ && + $rawline =~ /SPDX-License-Identifier:/ && + $rawline !~ m@^\+\s*\Q$comment\E\s*@) { + WARN("SPDX_LICENSE_TAG", + "Improper SPDX comment style for '$realfile', please use '$comment' instead\n" . $herecurr); + } + + if ($comment !~ /^$/ && + $rawline !~ m@^\+\Q$comment\E SPDX-License-Identifier: @) { + WARN("SPDX_LICENSE_TAG", + "Missing or malformed SPDX-License-Identifier tag in line $checklicenseline\n" . $herecurr); + } elsif ($rawline =~ /(SPDX-License-Identifier: .*)/) { + my $spdx_license = $1; + if (!is_SPDX_License_valid($spdx_license)) { + WARN("SPDX_LICENSE_TAG", + "'$spdx_license' is not supported in LICENSES/...\n" . $herecurr); + } + if ($realfile =~ m@^Documentation/devicetree/bindings/@ && + $spdx_license !~ /GPL-2\.0(?:-only)? OR BSD-2-Clause/) { + my $msg_level = \&WARN; + $msg_level = \&CHK if ($file); + if (&{$msg_level}("SPDX_LICENSE_TAG", + + "DT binding documents should be licensed (GPL-2.0-only OR BSD-2-Clause)\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ s/SPDX-License-Identifier: .*/SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)/; + } + } + if ($realfile =~ m@^include/dt-bindings/@ && + $spdx_license !~ /GPL-2\.0(?:-only)? OR \S+/) { + WARN("SPDX_LICENSE_TAG", + "DT binding headers should be licensed (GPL-2.0-only OR .*)\n" . $herecurr); + } + } + } + } + +# check for embedded filenames + if ($rawline =~ /^\+.*\b\Q$realfile\E\b/) { + WARN("EMBEDDED_FILENAME", + "It's generally not useful to have the filename in the file\n" . $herecurr); + } + +# check we are in a valid source file if not then ignore this hunk + next if ($realfile !~ /\.(h|c|rs|s|S|sh|dtsi|dts)$/); + +# check for using SPDX-License-Identifier on the wrong line number + if ($realline != $checklicenseline && + $rawline =~ /\bSPDX-License-Identifier:/ && + substr($line, @-, @+ - @-) eq "$;" x (@+ - @-)) { + WARN("SPDX_LICENSE_TAG", + "Misplaced SPDX-License-Identifier tag - use line $checklicenseline instead\n" . $herecurr); + } + +# line length limit (with some exclusions) +# +# There are a few types of lines that may extend beyond $max_line_length: +# logging functions like pr_info that end in a string +# lines with a single string +# #defines that are a single string +# lines with an RFC3986 like URL +# +# There are 3 different line length message types: +# LONG_LINE_COMMENT a comment starts before but extends beyond $max_line_length +# LONG_LINE_STRING a string starts before but extends beyond $max_line_length +# LONG_LINE all other lines longer than $max_line_length +# +# if LONG_LINE is ignored, the other 2 types are also ignored +# + + if ($line =~ /^\+/ && $length > $max_line_length) { + my $msg_type = "LONG_LINE"; + + # Check the allowed long line types first + + # logging functions that end in a string that starts + # before $max_line_length + if ($line =~ /^\+\s*$logFunctions\s*\(\s*(?:(?:KERN_\S+\s*|[^"]*))?($String\s*(?:|,|\)\s*;)\s*)$/ && + length(expand_tabs(substr($line, 1, length($line) - length($1) - 1))) <= $max_line_length) { + $msg_type = ""; + + # lines with only strings (w/ possible termination) + # #defines with only strings + } elsif ($line =~ /^\+\s*$String\s*(?:\s*|,|\)\s*;)\s*$/ || + $line =~ /^\+\s*#\s*define\s+\w+\s+$String$/) { + $msg_type = ""; + + # More special cases + } elsif ($line =~ /^\+.*\bEFI_GUID\s*\(/ || + $line =~ /^\+\s*(?:\w+)?\s*DEFINE_PER_CPU/) { + $msg_type = ""; + + # URL ($rawline is used in case the URL is in a comment) + } elsif ($rawline =~ /^\+.*\b[a-z][\w\.\+\-]*:\/\/\S+/i) { + $msg_type = ""; + + # Otherwise set the alternate message types + + # a comment starts before $max_line_length + } elsif ($line =~ /($;[\s$;]*)$/ && + length(expand_tabs(substr($line, 1, length($line) - length($1) - 1))) <= $max_line_length) { + $msg_type = "LONG_LINE_COMMENT" + + # a quoted string starts before $max_line_length + } elsif ($sline =~ /\s*($String(?:\s*(?:\\|,\s*|\)\s*;\s*))?)$/ && + length(expand_tabs(substr($line, 1, length($line) - length($1) - 1))) <= $max_line_length) { + $msg_type = "LONG_LINE_STRING" + } + + if ($msg_type ne "" && + show_type("LONG_LINE") && show_type($msg_type)) { + my $msg_level = \&WARN; + $msg_level = \&CHK if ($file); + &{$msg_level}($msg_type, + "line length of $length exceeds $max_line_length columns\n" . $herecurr); + } + } + +# check for adding lines without a newline. + if ($line =~ /^\+/ && defined $lines[$linenr] && $lines[$linenr] =~ /^\\ No newline at end of file/) { + if (WARN("MISSING_EOF_NEWLINE", + "adding a line without newline at end of file\n" . $herecurr) && + $fix) { + fix_delete_line($fixlinenr+1, "No newline at end of file"); + } + } + +# check for .L prefix local symbols in .S files + if ($realfile =~ /\.S$/ && + $line =~ /^\+\s*(?:[A-Z]+_)?SYM_[A-Z]+_(?:START|END)(?:_[A-Z_]+)?\s*\(\s*\.L/) { + WARN("AVOID_L_PREFIX", + "Avoid using '.L' prefixed local symbol names for denoting a range of code via 'SYM_*_START/END' annotations; see Documentation/core-api/asm-annotations.rst\n" . $herecurr); + } + +# check we are in a valid source file C or perl if not then ignore this hunk + next if ($realfile !~ /\.(h|c|pl|dtsi|dts)$/); + +# at the beginning of a line any tabs must come first and anything +# more than $tabsize must use tabs. + if ($rawline =~ /^\+\s* \t\s*\S/ || + $rawline =~ /^\+\s* \s*/) { + my $herevet = "$here\n" . cat_vet($rawline) . "\n"; + $rpt_cleaners = 1; + if (ERROR("CODE_INDENT", + "code indent should use tabs where possible\n" . $herevet) && + $fix) { + $fixed[$fixlinenr] =~ s/^\+([ \t]+)/"\+" . tabify($1)/e; + } + } + +# check for space before tabs. + if ($rawline =~ /^\+/ && $rawline =~ / \t/) { + my $herevet = "$here\n" . cat_vet($rawline) . "\n"; + if (WARN("SPACE_BEFORE_TAB", + "please, no space before tabs\n" . $herevet) && + $fix) { + while ($fixed[$fixlinenr] =~ + s/(^\+.*) {$tabsize,$tabsize}\t/$1\t\t/) {} + while ($fixed[$fixlinenr] =~ + s/(^\+.*) +\t/$1\t/) {} + } + } + +# check for assignments on the start of a line + if ($sline =~ /^\+\s+($Assignment)[^=]/) { + my $operator = $1; + if (CHK("ASSIGNMENT_CONTINUATIONS", + "Assignment operator '$1' should be on the previous line\n" . $hereprev) && + $fix && $prevrawline =~ /^\+/) { + # add assignment operator to the previous line, remove from current line + $fixed[$fixlinenr - 1] .= " $operator"; + $fixed[$fixlinenr] =~ s/\Q$operator\E\s*//; + } + } + +# check for && or || at the start of a line + if ($rawline =~ /^\+\s*(&&|\|\|)/) { + my $operator = $1; + if (CHK("LOGICAL_CONTINUATIONS", + "Logical continuations should be on the previous line\n" . $hereprev) && + $fix && $prevrawline =~ /^\+/) { + # insert logical operator at last non-comment, non-whitepsace char on previous line + $prevline =~ /[\s$;]*$/; + my $line_end = substr($prevrawline, $-[0]); + $fixed[$fixlinenr - 1] =~ s/\Q$line_end\E$/ $operator$line_end/; + $fixed[$fixlinenr] =~ s/\Q$operator\E\s*//; + } + } + +# check indentation starts on a tab stop + if ($perl_version_ok && + $sline =~ /^\+\t+( +)(?:$c90_Keywords\b|\{\s*$|\}\s*(?:else\b|while\b|\s*$)|$Declare\s*$Ident\s*[;=])/) { + my $indent = length($1); + if ($indent % $tabsize) { + if (WARN("TABSTOP", + "Statements should start on a tabstop\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ s@(^\+\t+) +@$1 . "\t" x ($indent/$tabsize)@e; + } + } + } + +# check multi-line statement indentation matches previous line + if ($perl_version_ok && + $prevline =~ /^\+([ \t]*)((?:$c90_Keywords(?:\s+if)\s*)|(?:$Declare\s*)?(?:$Ident|\(\s*\*\s*$Ident\s*\))\s*|(?:\*\s*)*$Lval\s*=\s*$Ident\s*)\(.*(\&\&|\|\||,)\s*$/) { + $prevline =~ /^\+(\t*)(.*)$/; + my $oldindent = $1; + my $rest = $2; + + my $pos = pos_last_openparen($rest); + if ($pos >= 0) { + $line =~ /^(\+| )([ \t]*)/; + my $newindent = $2; + + my $goodtabindent = $oldindent . + "\t" x ($pos / $tabsize) . + " " x ($pos % $tabsize); + my $goodspaceindent = $oldindent . " " x $pos; + + if ($newindent ne $goodtabindent && + $newindent ne $goodspaceindent) { + + if (CHK("PARENTHESIS_ALIGNMENT", + "Alignment should match open parenthesis\n" . $hereprev) && + $fix && $line =~ /^\+/) { + $fixed[$fixlinenr] =~ + s/^\+[ \t]*/\+$goodtabindent/; + } + } + } + } + +# check for space after cast like "(int) foo" or "(struct foo) bar" +# avoid checking a few false positives: +# "sizeof(<type>)" or "__alignof__(<type>)" +# function pointer declarations like "(*foo)(int) = bar;" +# structure definitions like "(struct foo) { 0 };" +# multiline macros that define functions +# known attributes or the __attribute__ keyword + if ($line =~ /^\+(.*)\(\s*$Type\s*\)([ \t]++)((?![={]|\\$|$Attribute|__attribute__))/ && + (!defined($1) || $1 !~ /\b(?:sizeof|__alignof__)\s*$/)) { + if (CHK("SPACING", + "No space is necessary after a cast\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ + s/(\(\s*$Type\s*\))[ \t]+/$1/; + } + } + +# Block comments use * on subsequent lines + if ($prevline =~ /$;[ \t]*$/ && #ends in comment + $prevrawline =~ /^\+.*?\/\*/ && #starting /* + $prevrawline !~ /\*\/[ \t]*$/ && #no trailing */ + $rawline =~ /^\+/ && #line is new + $rawline !~ /^\+[ \t]*\*/) { #no leading * + WARN("BLOCK_COMMENT_STYLE", + "Block comments use * on subsequent lines\n" . $hereprev); + } + +# Block comments use */ on trailing lines + if ($rawline !~ m@^\+[ \t]*\*/[ \t]*$@ && #trailing */ + $rawline !~ m@^\+.*/\*.*\*/[ \t]*$@ && #inline /*...*/ + $rawline !~ m@^\+.*\*{2,}/[ \t]*$@ && #trailing **/ + $rawline =~ m@^\+[ \t]*.+\*\/[ \t]*$@) { #non blank */ + WARN("BLOCK_COMMENT_STYLE", + "Block comments use a trailing */ on a separate line\n" . $herecurr); + } + +# Block comment * alignment + if ($prevline =~ /$;[ \t]*$/ && #ends in comment + $line =~ /^\+[ \t]*$;/ && #leading comment + $rawline =~ /^\+[ \t]*\*/ && #leading * + (($prevrawline =~ /^\+.*?\/\*/ && #leading /* + $prevrawline !~ /\*\/[ \t]*$/) || #no trailing */ + $prevrawline =~ /^\+[ \t]*\*/)) { #leading * + my $oldindent; + $prevrawline =~ m@^\+([ \t]*/?)\*@; + if (defined($1)) { + $oldindent = expand_tabs($1); + } else { + $prevrawline =~ m@^\+(.*/?)\*@; + $oldindent = expand_tabs($1); + } + $rawline =~ m@^\+([ \t]*)\*@; + my $newindent = $1; + $newindent = expand_tabs($newindent); + if (length($oldindent) ne length($newindent)) { + WARN("BLOCK_COMMENT_STYLE", + "Block comments should align the * on each line\n" . $hereprev); + } + } + +# check for missing blank lines after struct/union declarations +# with exceptions for various attributes and macros + if ($prevline =~ /^[\+ ]};?\s*$/ && + $line =~ /^\+/ && + !($line =~ /^\+\s*$/ || + $line =~ /^\+\s*(?:EXPORT_SYMBOL|early_param|ALLOW_ERROR_INJECTION)/ || + $line =~ /^\+\s*MODULE_/i || + $line =~ /^\+\s*\#\s*(?:end|elif|else)/ || + $line =~ /^\+[a-z_]*init/ || + $line =~ /^\+\s*(?:static\s+)?[A-Z_]*ATTR/ || + $line =~ /^\+\s*DECLARE/ || + $line =~ /^\+\s*builtin_[\w_]*driver/ || + $line =~ /^\+\s*__setup/)) { + if (CHK("LINE_SPACING", + "Please use a blank line after function/struct/union/enum declarations\n" . $hereprev) && + $fix) { + fix_insert_line($fixlinenr, "\+"); + } + } + +# check for multiple consecutive blank lines + if ($prevline =~ /^[\+ ]\s*$/ && + $line =~ /^\+\s*$/ && + $last_blank_line != ($linenr - 1)) { + if (CHK("LINE_SPACING", + "Please don't use multiple blank lines\n" . $hereprev) && + $fix) { + fix_delete_line($fixlinenr, $rawline); + } + + $last_blank_line = $linenr; + } + +# check for missing blank lines after declarations +# (declarations must have the same indentation and not be at the start of line) + if (($prevline =~ /\+(\s+)\S/) && $sline =~ /^\+$1\S/) { + # use temporaries + my $sl = $sline; + my $pl = $prevline; + # remove $Attribute/$Sparse uses to simplify comparisons + $sl =~ s/\b(?:$Attribute|$Sparse)\b//g; + $pl =~ s/\b(?:$Attribute|$Sparse)\b//g; + if (($pl =~ /^\+\s+$Declare\s*$Ident\s*[=,;:\[]/ || + # function pointer declarations + $pl =~ /^\+\s+$Declare\s*\(\s*\*\s*$Ident\s*\)\s*[=,;:\[\(]/ || + # foo bar; where foo is some local typedef or #define + $pl =~ /^\+\s+$Ident(?:\s+|\s*\*\s*)$Ident\s*[=,;\[]/ || + # known declaration macros + $pl =~ /^\+\s+$declaration_macros/) && + # for "else if" which can look like "$Ident $Ident" + !($pl =~ /^\+\s+$c90_Keywords\b/ || + # other possible extensions of declaration lines + $pl =~ /(?:$Compare|$Assignment|$Operators)\s*$/ || + # not starting a section or a macro "\" extended line + $pl =~ /(?:\{\s*|\\)$/) && + # looks like a declaration + !($sl =~ /^\+\s+$Declare\s*$Ident\s*[=,;:\[]/ || + # function pointer declarations + $sl =~ /^\+\s+$Declare\s*\(\s*\*\s*$Ident\s*\)\s*[=,;:\[\(]/ || + # foo bar; where foo is some local typedef or #define + $sl =~ /^\+\s+$Ident(?:\s+|\s*\*\s*)$Ident\s*[=,;\[]/ || + # known declaration macros + $sl =~ /^\+\s+$declaration_macros/ || + # start of struct or union or enum + $sl =~ /^\+\s+(?:static\s+)?(?:const\s+)?(?:union|struct|enum|typedef)\b/ || + # start or end of block or continuation of declaration + $sl =~ /^\+\s+(?:$|[\{\}\.\#\"\?\:\(\[])/ || + # bitfield continuation + $sl =~ /^\+\s+$Ident\s*:\s*\d+\s*[,;]/ || + # other possible extensions of declaration lines + $sl =~ /^\+\s+\(?\s*(?:$Compare|$Assignment|$Operators)/)) { + if (WARN("LINE_SPACING", + "Missing a blank line after declarations\n" . $hereprev) && + $fix) { + fix_insert_line($fixlinenr, "\+"); + } + } + } + +# check for spaces at the beginning of a line. +# Exceptions: +# 1) within comments +# 2) indented preprocessor commands +# 3) hanging labels + if ($rawline =~ /^\+ / && $line !~ /^\+ *(?:$;|#|$Ident:)/) { + my $herevet = "$here\n" . cat_vet($rawline) . "\n"; + if (WARN("LEADING_SPACE", + "please, no spaces at the start of a line\n" . $herevet) && + $fix) { + $fixed[$fixlinenr] =~ s/^\+([ \t]+)/"\+" . tabify($1)/e; + } + } + +# check we are in a valid C source file if not then ignore this hunk + next if ($realfile !~ /\.(h|c)$/); + +# check for unusual line ending [ or ( + if ($line =~ /^\+.*([\[\(])\s*$/) { + CHK("OPEN_ENDED_LINE", + "Lines should not end with a '$1'\n" . $herecurr); + } + +# check if this appears to be the start function declaration, save the name + if ($sline =~ /^\+\{\s*$/ && + $prevline =~ /^\+(?:(?:(?:$Storage|$Inline)\s*)*\s*$Type\s*)?($Ident)\(/) { + $context_function = $1; + } + +# check if this appears to be the end of function declaration + if ($sline =~ /^\+\}\s*$/) { + undef $context_function; + } + +# check indentation of any line with a bare else +# (but not if it is a multiple line "if (foo) return bar; else return baz;") +# if the previous line is a break or return and is indented 1 tab more... + if ($sline =~ /^\+([\t]+)(?:}[ \t]*)?else(?:[ \t]*{)?\s*$/) { + my $tabs = length($1) + 1; + if ($prevline =~ /^\+\t{$tabs,$tabs}break\b/ || + ($prevline =~ /^\+\t{$tabs,$tabs}return\b/ && + defined $lines[$linenr] && + $lines[$linenr] !~ /^[ \+]\t{$tabs,$tabs}return/)) { + WARN("UNNECESSARY_ELSE", + "else is not generally useful after a break or return\n" . $hereprev); + } + } + +# check indentation of a line with a break; +# if the previous line is a goto, return or break +# and is indented the same # of tabs + if ($sline =~ /^\+([\t]+)break\s*;\s*$/) { + my $tabs = $1; + if ($prevline =~ /^\+$tabs(goto|return|break)\b/) { + if (WARN("UNNECESSARY_BREAK", + "break is not useful after a $1\n" . $hereprev) && + $fix) { + fix_delete_line($fixlinenr, $rawline); + } + } + } + +# check for RCS/CVS revision markers + if ($rawline =~ /^\+.*\$(Revision|Log|Id)(?:\$|)/) { + WARN("CVS_KEYWORD", + "CVS style keyword markers, these will _not_ be updated\n". $herecurr); + } + +# check for old HOTPLUG __dev<foo> section markings + if ($line =~ /\b(__dev(init|exit)(data|const|))\b/) { + WARN("HOTPLUG_SECTION", + "Using $1 is unnecessary\n" . $herecurr); + } + +# Check for potential 'bare' types + my ($stat, $cond, $line_nr_next, $remain_next, $off_next, + $realline_next); +#print "LINE<$line>\n"; + if ($linenr > $suppress_statement && + $realcnt && $sline =~ /.\s*\S/) { + ($stat, $cond, $line_nr_next, $remain_next, $off_next) = + ctx_statement_block($linenr, $realcnt, 0); + $stat =~ s/\n./\n /g; + $cond =~ s/\n./\n /g; + +#print "linenr<$linenr> <$stat>\n"; + # If this statement has no statement boundaries within + # it there is no point in retrying a statement scan + # until we hit end of it. + my $frag = $stat; $frag =~ s/;+\s*$//; + if ($frag !~ /(?:{|;)/) { +#print "skip<$line_nr_next>\n"; + $suppress_statement = $line_nr_next; + } + + # Find the real next line. + $realline_next = $line_nr_next; + if (defined $realline_next && + (!defined $lines[$realline_next - 1] || + substr($lines[$realline_next - 1], $off_next) =~ /^\s*$/)) { + $realline_next++; + } + + my $s = $stat; + $s =~ s/{.*$//s; + + # Ignore goto labels. + if ($s =~ /$Ident:\*$/s) { + + # Ignore functions being called + } elsif ($s =~ /^.\s*$Ident\s*\(/s) { + + } elsif ($s =~ /^.\s*else\b/s) { + + # declarations always start with types + } elsif ($prev_values eq 'E' && $s =~ /^.\s*(?:$Storage\s+)?(?:$Inline\s+)?(?:const\s+)?((?:\s*$Ident)+?)\b(?:\s+$Sparse)?\s*\**\s*(?:$Ident|\(\*[^\)]*\))(?:\s*$Modifier)?\s*(?:;|=|,|\()/s) { + my $type = $1; + $type =~ s/\s+/ /g; + possible($type, "A:" . $s); + + # definitions in global scope can only start with types + } elsif ($s =~ /^.(?:$Storage\s+)?(?:$Inline\s+)?(?:const\s+)?($Ident)\b\s*(?!:)/s) { + possible($1, "B:" . $s); + } + + # any (foo ... *) is a pointer cast, and foo is a type + while ($s =~ /\(($Ident)(?:\s+$Sparse)*[\s\*]+\s*\)/sg) { + possible($1, "C:" . $s); + } + + # Check for any sort of function declaration. + # int foo(something bar, other baz); + # void (*store_gdt)(x86_descr_ptr *); + if ($prev_values eq 'E' && $s =~ /^(.(?:typedef\s*)?(?:(?:$Storage|$Inline)\s*)*\s*$Type\s*(?:\b$Ident|\(\*\s*$Ident\))\s*)\(/s) { + my ($name_len) = length($1); + + my $ctx = $s; + substr($ctx, 0, $name_len + 1, ''); + $ctx =~ s/\)[^\)]*$//; + + for my $arg (split(/\s*,\s*/, $ctx)) { + if ($arg =~ /^(?:const\s+)?($Ident)(?:\s+$Sparse)*\s*\**\s*(:?\b$Ident)?$/s || $arg =~ /^($Ident)$/s) { + + possible($1, "D:" . $s); + } + } + } + + } + +# +# Checks which may be anchored in the context. +# + +# Check for switch () and associated case and default +# statements should be at the same indent. + if ($line=~/\bswitch\s*\(.*\)/) { + my $err = ''; + my $sep = ''; + my @ctx = ctx_block_outer($linenr, $realcnt); + shift(@ctx); + for my $ctx (@ctx) { + my ($clen, $cindent) = line_stats($ctx); + if ($ctx =~ /^\+\s*(case\s+|default:)/ && + $indent != $cindent) { + $err .= "$sep$ctx\n"; + $sep = ''; + } else { + $sep = "[...]\n"; + } + } + if ($err ne '') { + ERROR("SWITCH_CASE_INDENT_LEVEL", + "switch and case should be at the same indent\n$hereline$err"); + } + } + +# if/while/etc brace do not go on next line, unless defining a do while loop, +# or if that brace on the next line is for something else + if ($line =~ /(.*)\b((?:if|while|for|switch|(?:[a-z_]+|)for_each[a-z_]+)\s*\(|do\b|else\b)/ && $line !~ /^.\s*\#/) { + my $pre_ctx = "$1$2"; + + my ($level, @ctx) = ctx_statement_level($linenr, $realcnt, 0); + + if ($line =~ /^\+\t{6,}/) { + WARN("DEEP_INDENTATION", + "Too many leading tabs - consider code refactoring\n" . $herecurr); + } + + my $ctx_cnt = $realcnt - $#ctx - 1; + my $ctx = join("\n", @ctx); + + my $ctx_ln = $linenr; + my $ctx_skip = $realcnt; + + while ($ctx_skip > $ctx_cnt || ($ctx_skip == $ctx_cnt && + defined $lines[$ctx_ln - 1] && + $lines[$ctx_ln - 1] =~ /^-/)) { + ##print "SKIP<$ctx_skip> CNT<$ctx_cnt>\n"; + $ctx_skip-- if (!defined $lines[$ctx_ln - 1] || $lines[$ctx_ln - 1] !~ /^-/); + $ctx_ln++; + } + + #print "realcnt<$realcnt> ctx_cnt<$ctx_cnt>\n"; + #print "pre<$pre_ctx>\nline<$line>\nctx<$ctx>\nnext<$lines[$ctx_ln - 1]>\n"; + + if ($ctx !~ /{\s*/ && defined($lines[$ctx_ln - 1]) && $lines[$ctx_ln - 1] =~ /^\+\s*{/) { + ERROR("OPEN_BRACE", + "that open brace { should be on the previous line\n" . + "$here\n$ctx\n$rawlines[$ctx_ln - 1]\n"); + } + if ($level == 0 && $pre_ctx !~ /}\s*while\s*\($/ && + $ctx =~ /\)\s*\;\s*$/ && + defined $lines[$ctx_ln - 1]) + { + my ($nlength, $nindent) = line_stats($lines[$ctx_ln - 1]); + if ($nindent > $indent) { + WARN("TRAILING_SEMICOLON", + "trailing semicolon indicates no statements, indent implies otherwise\n" . + "$here\n$ctx\n$rawlines[$ctx_ln - 1]\n"); + } + } + } + +# Check relative indent for conditionals and blocks. + if ($line =~ /\b(?:(?:if|while|for|(?:[a-z_]+|)for_each[a-z_]+)\s*\(|(?:do|else)\b)/ && $line !~ /^.\s*#/ && $line !~ /\}\s*while\s*/) { + ($stat, $cond, $line_nr_next, $remain_next, $off_next) = + ctx_statement_block($linenr, $realcnt, 0) + if (!defined $stat); + my ($s, $c) = ($stat, $cond); + + substr($s, 0, length($c), ''); + + # remove inline comments + $s =~ s/$;/ /g; + $c =~ s/$;/ /g; + + # Find out how long the conditional actually is. + my @newlines = ($c =~ /\n/gs); + my $cond_lines = 1 + $#newlines; + + # Make sure we remove the line prefixes as we have + # none on the first line, and are going to readd them + # where necessary. + $s =~ s/\n./\n/gs; + while ($s =~ /\n\s+\\\n/) { + $cond_lines += $s =~ s/\n\s+\\\n/\n/g; + } + + # We want to check the first line inside the block + # starting at the end of the conditional, so remove: + # 1) any blank line termination + # 2) any opening brace { on end of the line + # 3) any do (...) { + my $continuation = 0; + my $check = 0; + $s =~ s/^.*\bdo\b//; + $s =~ s/^\s*{//; + if ($s =~ s/^\s*\\//) { + $continuation = 1; + } + if ($s =~ s/^\s*?\n//) { + $check = 1; + $cond_lines++; + } + + # Also ignore a loop construct at the end of a + # preprocessor statement. + if (($prevline =~ /^.\s*#\s*define\s/ || + $prevline =~ /\\\s*$/) && $continuation == 0) { + $check = 0; + } + + my $cond_ptr = -1; + $continuation = 0; + while ($cond_ptr != $cond_lines) { + $cond_ptr = $cond_lines; + + # If we see an #else/#elif then the code + # is not linear. + if ($s =~ /^\s*\#\s*(?:else|elif)/) { + $check = 0; + } + + # Ignore: + # 1) blank lines, they should be at 0, + # 2) preprocessor lines, and + # 3) labels. + if ($continuation || + $s =~ /^\s*?\n/ || + $s =~ /^\s*#\s*?/ || + $s =~ /^\s*$Ident\s*:/) { + $continuation = ($s =~ /^.*?\\\n/) ? 1 : 0; + if ($s =~ s/^.*?\n//) { + $cond_lines++; + } + } + } + + my (undef, $sindent) = line_stats("+" . $s); + my $stat_real = raw_line($linenr, $cond_lines); + + # Check if either of these lines are modified, else + # this is not this patch's fault. + if (!defined($stat_real) || + $stat !~ /^\+/ && $stat_real !~ /^\+/) { + $check = 0; + } + if (defined($stat_real) && $cond_lines > 1) { + $stat_real = "[...]\n$stat_real"; + } + + #print "line<$line> prevline<$prevline> indent<$indent> sindent<$sindent> check<$check> continuation<$continuation> s<$s> cond_lines<$cond_lines> stat_real<$stat_real> stat<$stat>\n"; + + if ($check && $s ne '' && + (($sindent % $tabsize) != 0 || + ($sindent < $indent) || + ($sindent == $indent && + ($s !~ /^\s*(?:\}|\{|else\b)/)) || + ($sindent > $indent + $tabsize))) { + WARN("SUSPECT_CODE_INDENT", + "suspect code indent for conditional statements ($indent, $sindent)\n" . $herecurr . "$stat_real\n"); + } + } + + # Track the 'values' across context and added lines. + my $opline = $line; $opline =~ s/^./ /; + my ($curr_values, $curr_vars) = + annotate_values($opline . "\n", $prev_values); + $curr_values = $prev_values . $curr_values; + if ($dbg_values) { + my $outline = $opline; $outline =~ s/\t/ /g; + print "$linenr > .$outline\n"; + print "$linenr > $curr_values\n"; + print "$linenr > $curr_vars\n"; + } + $prev_values = substr($curr_values, -1); + +#ignore lines not being added + next if ($line =~ /^[^\+]/); + +# check for self assignments used to avoid compiler warnings +# e.g.: int foo = foo, *bar = NULL; +# struct foo bar = *(&(bar)); + if ($line =~ /^\+\s*(?:$Declare)?([A-Za-z_][A-Za-z\d_]*)\s*=/) { + my $var = $1; + if ($line =~ /^\+\s*(?:$Declare)?$var\s*=\s*(?:$var|\*\s*\(?\s*&\s*\(?\s*$var\s*\)?\s*\)?)\s*[;,]/) { + WARN("SELF_ASSIGNMENT", + "Do not use self-assignments to avoid compiler warnings\n" . $herecurr); + } + } + +# check for dereferences that span multiple lines + if ($prevline =~ /^\+.*$Lval\s*(?:\.|->)\s*$/ && + $line =~ /^\+\s*(?!\#\s*(?!define\s+|if))\s*$Lval/) { + $prevline =~ /($Lval\s*(?:\.|->))\s*$/; + my $ref = $1; + $line =~ /^.\s*($Lval)/; + $ref .= $1; + $ref =~ s/\s//g; + WARN("MULTILINE_DEREFERENCE", + "Avoid multiple line dereference - prefer '$ref'\n" . $hereprev); + } + +# check for declarations of signed or unsigned without int + while ($line =~ m{\b($Declare)\s*(?!char\b|short\b|int\b|long\b)\s*($Ident)?\s*[=,;\[\)\(]}g) { + my $type = $1; + my $var = $2; + $var = "" if (!defined $var); + if ($type =~ /^(?:(?:$Storage|$Inline|$Attribute)\s+)*((?:un)?signed)((?:\s*\*)*)\s*$/) { + my $sign = $1; + my $pointer = $2; + + $pointer = "" if (!defined $pointer); + + if (WARN("UNSPECIFIED_INT", + "Prefer '" . trim($sign) . " int" . rtrim($pointer) . "' to bare use of '$sign" . rtrim($pointer) . "'\n" . $herecurr) && + $fix) { + my $decl = trim($sign) . " int "; + my $comp_pointer = $pointer; + $comp_pointer =~ s/\s//g; + $decl .= $comp_pointer; + $decl = rtrim($decl) if ($var eq ""); + $fixed[$fixlinenr] =~ s@\b$sign\s*\Q$pointer\E\s*$var\b@$decl$var@; + } + } + } + +# TEST: allow direct testing of the type matcher. + if ($dbg_type) { + if ($line =~ /^.\s*$Declare\s*$/) { + ERROR("TEST_TYPE", + "TEST: is type\n" . $herecurr); + } elsif ($dbg_type > 1 && $line =~ /^.+($Declare)/) { + ERROR("TEST_NOT_TYPE", + "TEST: is not type ($1 is)\n". $herecurr); + } + next; + } +# TEST: allow direct testing of the attribute matcher. + if ($dbg_attr) { + if ($line =~ /^.\s*$Modifier\s*$/) { + ERROR("TEST_ATTR", + "TEST: is attr\n" . $herecurr); + } elsif ($dbg_attr > 1 && $line =~ /^.+($Modifier)/) { + ERROR("TEST_NOT_ATTR", + "TEST: is not attr ($1 is)\n". $herecurr); + } + next; + } + +# check for initialisation to aggregates open brace on the next line + if ($line =~ /^.\s*{/ && + $prevline =~ /(?:^|[^=])=\s*$/) { + if (ERROR("OPEN_BRACE", + "that open brace { should be on the previous line\n" . $hereprev) && + $fix && $prevline =~ /^\+/ && $line =~ /^\+/) { + fix_delete_line($fixlinenr - 1, $prevrawline); + fix_delete_line($fixlinenr, $rawline); + my $fixedline = $prevrawline; + $fixedline =~ s/\s*=\s*$/ = {/; + fix_insert_line($fixlinenr, $fixedline); + $fixedline = $line; + $fixedline =~ s/^(.\s*)\{\s*/$1/; + fix_insert_line($fixlinenr, $fixedline); + } + } + +# +# Checks which are anchored on the added line. +# + +# check for malformed paths in #include statements (uses RAW line) + if ($rawline =~ m{^.\s*\#\s*include\s+[<"](.*)[">]}) { + my $path = $1; + if ($path =~ m{//}) { + ERROR("MALFORMED_INCLUDE", + "malformed #include filename\n" . $herecurr); + } + if ($path =~ "^uapi/" && $realfile =~ m@\binclude/uapi/@) { + ERROR("UAPI_INCLUDE", + "No #include in ...include/uapi/... should use a uapi/ path prefix\n" . $herecurr); + } + } + +# no C99 // comments + if ($line =~ m{//}) { + if (ERROR("C99_COMMENTS", + "do not use C99 // comments\n" . $herecurr) && + $fix) { + my $line = $fixed[$fixlinenr]; + if ($line =~ /\/\/(.*)$/) { + my $comment = trim($1); + $fixed[$fixlinenr] =~ s@\/\/(.*)$@/\* $comment \*/@; + } + } + } + # Remove C99 comments. + $line =~ s@//.*@@; + $opline =~ s@//.*@@; + +# EXPORT_SYMBOL should immediately follow the thing it is exporting, consider +# the whole statement. +#print "APW <$lines[$realline_next - 1]>\n"; + if (defined $realline_next && + exists $lines[$realline_next - 1] && + !defined $suppress_export{$realline_next} && + ($lines[$realline_next - 1] =~ /EXPORT_SYMBOL.*\((.*)\)/)) { + # Handle definitions which produce identifiers with + # a prefix: + # XXX(foo); + # EXPORT_SYMBOL(something_foo); + my $name = $1; + $name =~ s/^\s*($Ident).*/$1/; + if ($stat =~ /^(?:.\s*}\s*\n)?.([A-Z_]+)\s*\(\s*($Ident)/ && + $name =~ /^${Ident}_$2/) { +#print "FOO C name<$name>\n"; + $suppress_export{$realline_next} = 1; + + } elsif ($stat !~ /(?: + \n.}\s*$| + ^.DEFINE_$Ident\(\Q$name\E\)| + ^.DECLARE_$Ident\(\Q$name\E\)| + ^.LIST_HEAD\(\Q$name\E\)| + ^.(?:$Storage\s+)?$Type\s*\(\s*\*\s*\Q$name\E\s*\)\s*\(| + \b\Q$name\E(?:\s+$Attribute)*\s*(?:;|=|\[|\() + )/x) { +#print "FOO A<$lines[$realline_next - 1]> stat<$stat> name<$name>\n"; + $suppress_export{$realline_next} = 2; + } else { + $suppress_export{$realline_next} = 1; + } + } + if (!defined $suppress_export{$linenr} && + $prevline =~ /^.\s*$/ && + ($line =~ /EXPORT_SYMBOL.*\((.*)\)/)) { +#print "FOO B <$lines[$linenr - 1]>\n"; + $suppress_export{$linenr} = 2; + } + if (defined $suppress_export{$linenr} && + $suppress_export{$linenr} == 2) { + WARN("EXPORT_SYMBOL", + "EXPORT_SYMBOL(foo); should immediately follow its function/variable\n" . $herecurr); + } + +# check for global initialisers. + if ($line =~ /^\+$Type\s*$Ident(?:\s+$Modifier)*\s*=\s*($zero_initializer)\s*;/ && + !exclude_global_initialisers($realfile)) { + if (ERROR("GLOBAL_INITIALISERS", + "do not initialise globals to $1\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ s/(^.$Type\s*$Ident(?:\s+$Modifier)*)\s*=\s*$zero_initializer\s*;/$1;/; + } + } +# check for static initialisers. + if ($line =~ /^\+.*\bstatic\s.*=\s*($zero_initializer)\s*;/) { + if (ERROR("INITIALISED_STATIC", + "do not initialise statics to $1\n" . + $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ s/(\bstatic\s.*?)\s*=\s*$zero_initializer\s*;/$1;/; + } + } + +# check for misordered declarations of char/short/int/long with signed/unsigned + while ($sline =~ m{(\b$TypeMisordered\b)}g) { + my $tmp = trim($1); + WARN("MISORDERED_TYPE", + "type '$tmp' should be specified in [[un]signed] [short|int|long|long long] order\n" . $herecurr); + } + +# check for unnecessary <signed> int declarations of short/long/long long + while ($sline =~ m{\b($TypeMisordered(\s*\*)*|$C90_int_types)\b}g) { + my $type = trim($1); + next if ($type !~ /\bint\b/); + next if ($type !~ /\b(?:short|long\s+long|long)\b/); + my $new_type = $type; + $new_type =~ s/\b\s*int\s*\b/ /; + $new_type =~ s/\b\s*(?:un)?signed\b\s*/ /; + $new_type =~ s/^const\s+//; + $new_type = "unsigned $new_type" if ($type =~ /\bunsigned\b/); + $new_type = "const $new_type" if ($type =~ /^const\b/); + $new_type =~ s/\s+/ /g; + $new_type = trim($new_type); + if (WARN("UNNECESSARY_INT", + "Prefer '$new_type' over '$type' as the int is unnecessary\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ s/\b\Q$type\E\b/$new_type/; + } + } + +# check for static const char * arrays. + if ($line =~ /\bstatic\s+const\s+char\s*\*\s*(\w+)\s*\[\s*\]\s*=\s*/) { + WARN("STATIC_CONST_CHAR_ARRAY", + "static const char * array should probably be static const char * const\n" . + $herecurr); + } + +# check for initialized const char arrays that should be static const + if ($line =~ /^\+\s*const\s+(char|unsigned\s+char|_*u8|(?:[us]_)?int8_t)\s+\w+\s*\[\s*(?:\w+\s*)?\]\s*=\s*"/) { + if (WARN("STATIC_CONST_CHAR_ARRAY", + "const array should probably be static const\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ s/(^.\s*)const\b/${1}static const/; + } + } + +# check for static char foo[] = "bar" declarations. + if ($line =~ /\bstatic\s+char\s+(\w+)\s*\[\s*\]\s*=\s*"/) { + WARN("STATIC_CONST_CHAR_ARRAY", + "static char array declaration should probably be static const char\n" . + $herecurr); + } + +# check for const <foo> const where <foo> is not a pointer or array type + if ($sline =~ /\bconst\s+($BasicType)\s+const\b/) { + my $found = $1; + if ($sline =~ /\bconst\s+\Q$found\E\s+const\b\s*\*/) { + WARN("CONST_CONST", + "'const $found const *' should probably be 'const $found * const'\n" . $herecurr); + } elsif ($sline !~ /\bconst\s+\Q$found\E\s+const\s+\w+\s*\[/) { + WARN("CONST_CONST", + "'const $found const' should probably be 'const $found'\n" . $herecurr); + } + } + +# check for const static or static <non ptr type> const declarations +# prefer 'static const <foo>' over 'const static <foo>' and 'static <foo> const' + if ($sline =~ /^\+\s*const\s+static\s+($Type)\b/ || + $sline =~ /^\+\s*static\s+($BasicType)\s+const\b/) { + if (WARN("STATIC_CONST", + "Move const after static - use 'static const $1'\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ s/\bconst\s+static\b/static const/; + $fixed[$fixlinenr] =~ s/\bstatic\s+($BasicType)\s+const\b/static const $1/; + } + } + +# check for non-global char *foo[] = {"bar", ...} declarations. + if ($line =~ /^.\s+(?:static\s+|const\s+)?char\s+\*\s*\w+\s*\[\s*\]\s*=\s*\{/) { + WARN("STATIC_CONST_CHAR_ARRAY", + "char * array declaration might be better as static const\n" . + $herecurr); + } + +# check for sizeof(foo)/sizeof(foo[0]) that could be ARRAY_SIZE(foo) + if ($line =~ m@\bsizeof\s*\(\s*($Lval)\s*\)@) { + my $array = $1; + if ($line =~ m@\b(sizeof\s*\(\s*\Q$array\E\s*\)\s*/\s*sizeof\s*\(\s*\Q$array\E\s*\[\s*0\s*\]\s*\))@) { + my $array_div = $1; + if (WARN("ARRAY_SIZE", + "Prefer ARRAY_SIZE($array)\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ s/\Q$array_div\E/ARRAY_SIZE($array)/; + } + } + } + +# check for function declarations without arguments like "int foo()" + if ($line =~ /(\b$Type\s*$Ident)\s*\(\s*\)/) { + if (ERROR("FUNCTION_WITHOUT_ARGS", + "Bad function definition - $1() should probably be $1(void)\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ s/(\b($Type)\s+($Ident))\s*\(\s*\)/$2 $3(void)/; + } + } + +# check for new typedefs, only function parameters and sparse annotations +# make sense. + if ($line =~ /\btypedef\s/ && + $line !~ /\btypedef\s+$Type\s*\(\s*\*?$Ident\s*\)\s*\(/ && + $line !~ /\btypedef\s+$Type\s+$Ident\s*\(/ && + $line !~ /\b$typeTypedefs\b/ && + $line !~ /\b__bitwise\b/) { + WARN("NEW_TYPEDEFS", + "do not add new typedefs\n" . $herecurr); + } + +# * goes on variable not on type + # (char*[ const]) + while ($line =~ m{(\($NonptrType(\s*(?:$Modifier\b\s*|\*\s*)+)\))}g) { + #print "AA<$1>\n"; + my ($ident, $from, $to) = ($1, $2, $2); + + # Should start with a space. + $to =~ s/^(\S)/ $1/; + # Should not end with a space. + $to =~ s/\s+$//; + # '*'s should not have spaces between. + while ($to =~ s/\*\s+\*/\*\*/) { + } + +## print "1: from<$from> to<$to> ident<$ident>\n"; + if ($from ne $to) { + if (ERROR("POINTER_LOCATION", + "\"(foo$from)\" should be \"(foo$to)\"\n" . $herecurr) && + $fix) { + my $sub_from = $ident; + my $sub_to = $ident; + $sub_to =~ s/\Q$from\E/$to/; + $fixed[$fixlinenr] =~ + s@\Q$sub_from\E@$sub_to@; + } + } + } + while ($line =~ m{(\b$NonptrType(\s*(?:$Modifier\b\s*|\*\s*)+)($Ident))}g) { + #print "BB<$1>\n"; + my ($match, $from, $to, $ident) = ($1, $2, $2, $3); + + # Should start with a space. + $to =~ s/^(\S)/ $1/; + # Should not end with a space. + $to =~ s/\s+$//; + # '*'s should not have spaces between. + while ($to =~ s/\*\s+\*/\*\*/) { + } + # Modifiers should have spaces. + $to =~ s/(\b$Modifier$)/$1 /; + +## print "2: from<$from> to<$to> ident<$ident>\n"; + if ($from ne $to && $ident !~ /^$Modifier$/) { + if (ERROR("POINTER_LOCATION", + "\"foo${from}bar\" should be \"foo${to}bar\"\n" . $herecurr) && + $fix) { + + my $sub_from = $match; + my $sub_to = $match; + $sub_to =~ s/\Q$from\E/$to/; + $fixed[$fixlinenr] =~ + s@\Q$sub_from\E@$sub_to@; + } + } + } + +# 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*\(/) { + my $msg_level = \&WARN; + $msg_level = \&CHK if ($file); + &{$msg_level}("AVOID_BUG", + "Do not crash the kernel unless it is absolutely unavoidable--use WARN_ON_ONCE() plus recovery code (if feasible) instead of BUG() or variants\n" . $herecurr); + } + +# avoid LINUX_VERSION_CODE + if ($line =~ /\bLINUX_VERSION_CODE\b/) { + WARN("LINUX_VERSION_CODE", + "LINUX_VERSION_CODE should be avoided, code should be for the version to which it is merged\n" . $herecurr); + } + +# check for uses of printk_ratelimit + if ($line =~ /\bprintk_ratelimit\s*\(/) { + WARN("PRINTK_RATELIMITED", + "Prefer printk_ratelimited or pr_<level>_ratelimited to printk_ratelimit\n" . $herecurr); + } + +# printk should use KERN_* levels + if ($line =~ /\bprintk\s*\(\s*(?!KERN_[A-Z]+\b)/) { + WARN("PRINTK_WITHOUT_KERN_LEVEL", + "printk() should include KERN_<LEVEL> facility level\n" . $herecurr); + } + +# prefer variants of (subsystem|netdev|dev|pr)_<level> to printk(KERN_<LEVEL> + if ($line =~ /\b(printk(_once|_ratelimited)?)\s*\(\s*KERN_([A-Z]+)/) { + my $printk = $1; + my $modifier = $2; + my $orig = $3; + $modifier = "" if (!defined($modifier)); + my $level = lc($orig); + $level = "warn" if ($level eq "warning"); + my $level2 = $level; + $level2 = "dbg" if ($level eq "debug"); + $level .= $modifier; + $level2 .= $modifier; + WARN("PREFER_PR_LEVEL", + "Prefer [subsystem eg: netdev]_$level2([subsystem]dev, ... then dev_$level2(dev, ... then pr_$level(... to $printk(KERN_$orig ...\n" . $herecurr); + } + +# prefer dev_<level> to dev_printk(KERN_<LEVEL> + if ($line =~ /\bdev_printk\s*\(\s*KERN_([A-Z]+)/) { + my $orig = $1; + my $level = lc($orig); + $level = "warn" if ($level eq "warning"); + $level = "dbg" if ($level eq "debug"); + WARN("PREFER_DEV_LEVEL", + "Prefer dev_$level(... to dev_printk(KERN_$orig, ...\n" . $herecurr); + } + +# trace_printk should not be used in production code. + if ($line =~ /\b(trace_printk|trace_puts|ftrace_vprintk)\s*\(/) { + WARN("TRACE_PRINTK", + "Do not use $1() in production code (this can be ignored if built only with a debug config option)\n" . $herecurr); + } + +# ENOSYS means "bad syscall nr" and nothing else. This will have a small +# number of false positives, but assembly files are not checked, so at +# least the arch entry code will not trigger this warning. + if ($line =~ /\bENOSYS\b/) { + WARN("ENOSYS", + "ENOSYS means 'invalid syscall nr' and nothing else\n" . $herecurr); + } + +# ENOTSUPP is not a standard error code and should be avoided in new patches. +# Folks usually mean EOPNOTSUPP (also called ENOTSUP), when they type ENOTSUPP. +# Similarly to ENOSYS warning a small number of false positives is expected. + if (!$file && $line =~ /\bENOTSUPP\b/) { + if (WARN("ENOTSUPP", + "ENOTSUPP is not a SUSV4 error code, prefer EOPNOTSUPP\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ s/\bENOTSUPP\b/EOPNOTSUPP/; + } + } + +# function brace can't be on same line, except for #defines of do while, +# or if closed on same line + if ($perl_version_ok && + $sline =~ /$Type\s*$Ident\s*$balanced_parens\s*\{/ && + $sline !~ /\#\s*define\b.*do\s*\{/ && + $sline !~ /}/) { + if (ERROR("OPEN_BRACE", + "open brace '{' following function definitions go on the next line\n" . $herecurr) && + $fix) { + fix_delete_line($fixlinenr, $rawline); + my $fixed_line = $rawline; + $fixed_line =~ /(^..*$Type\s*$Ident\(.*\)\s*)\{(.*)$/; + my $line1 = $1; + my $line2 = $2; + fix_insert_line($fixlinenr, ltrim($line1)); + fix_insert_line($fixlinenr, "\+{"); + if ($line2 !~ /^\s*$/) { + fix_insert_line($fixlinenr, "\+\t" . trim($line2)); + } + } + } + +# open braces for enum, union and struct go on the same line. + if ($line =~ /^.\s*{/ && + $prevline =~ /^.\s*(?:typedef\s+)?(enum|union|struct)(?:\s+$Ident)?\s*$/) { + if (ERROR("OPEN_BRACE", + "open brace '{' following $1 go on the same line\n" . $hereprev) && + $fix && $prevline =~ /^\+/ && $line =~ /^\+/) { + fix_delete_line($fixlinenr - 1, $prevrawline); + fix_delete_line($fixlinenr, $rawline); + my $fixedline = rtrim($prevrawline) . " {"; + fix_insert_line($fixlinenr, $fixedline); + $fixedline = $rawline; + $fixedline =~ s/^(.\s*)\{\s*/$1\t/; + if ($fixedline !~ /^\+\s*$/) { + fix_insert_line($fixlinenr, $fixedline); + } + } + } + +# missing space after union, struct or enum definition + if ($line =~ /^.\s*(?:typedef\s+)?(enum|union|struct)(?:\s+$Ident){1,2}[=\{]/) { + if (WARN("SPACING", + "missing space after $1 definition\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ + s/^(.\s*(?:typedef\s+)?(?:enum|union|struct)(?:\s+$Ident){1,2})([=\{])/$1 $2/; + } + } + +# Function pointer declarations +# check spacing between type, funcptr, and args +# canonical declaration is "type (*funcptr)(args...)" + if ($line =~ /^.\s*($Declare)\((\s*)\*(\s*)($Ident)(\s*)\)(\s*)\(/) { + my $declare = $1; + my $pre_pointer_space = $2; + my $post_pointer_space = $3; + my $funcname = $4; + my $post_funcname_space = $5; + my $pre_args_space = $6; + +# the $Declare variable will capture all spaces after the type +# so check it for a missing trailing missing space but pointer return types +# don't need a space so don't warn for those. + my $post_declare_space = ""; + if ($declare =~ /(\s+)$/) { + $post_declare_space = $1; + $declare = rtrim($declare); + } + if ($declare !~ /\*$/ && $post_declare_space =~ /^$/) { + WARN("SPACING", + "missing space after return type\n" . $herecurr); + $post_declare_space = " "; + } + +# unnecessary space "type (*funcptr)(args...)" +# This test is not currently implemented because these declarations are +# equivalent to +# int foo(int bar, ...) +# and this is form shouldn't/doesn't generate a checkpatch warning. +# +# elsif ($declare =~ /\s{2,}$/) { +# WARN("SPACING", +# "Multiple spaces after return type\n" . $herecurr); +# } + +# unnecessary space "type ( *funcptr)(args...)" + if (defined $pre_pointer_space && + $pre_pointer_space =~ /^\s/) { + WARN("SPACING", + "Unnecessary space after function pointer open parenthesis\n" . $herecurr); + } + +# unnecessary space "type (* funcptr)(args...)" + if (defined $post_pointer_space && + $post_pointer_space =~ /^\s/) { + WARN("SPACING", + "Unnecessary space before function pointer name\n" . $herecurr); + } + +# unnecessary space "type (*funcptr )(args...)" + if (defined $post_funcname_space && + $post_funcname_space =~ /^\s/) { + WARN("SPACING", + "Unnecessary space after function pointer name\n" . $herecurr); + } + +# unnecessary space "type (*funcptr) (args...)" + if (defined $pre_args_space && + $pre_args_space =~ /^\s/) { + WARN("SPACING", + "Unnecessary space before function pointer arguments\n" . $herecurr); + } + + if (show_type("SPACING") && $fix) { + $fixed[$fixlinenr] =~ + s/^(.\s*)$Declare\s*\(\s*\*\s*$Ident\s*\)\s*\(/$1 . $declare . $post_declare_space . '(*' . $funcname . ')('/ex; + } + } + +# check for spacing round square brackets; allowed: +# 1. with a type on the left -- int [] a; +# 2. at the beginning of a line for slice initialisers -- [0...10] = 5, +# 3. inside a curly brace -- = { [0...10] = 5 } + while ($line =~ /(.*?\s)\[/g) { + my ($where, $prefix) = ($-[1], $1); + if ($prefix !~ /$Type\s+$/ && + ($where != 0 || $prefix !~ /^.\s+$/) && + $prefix !~ /[{,:]\s+$/) { + if (ERROR("BRACKET_SPACE", + "space prohibited before open square bracket '['\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ + s/^(\+.*?)\s+\[/$1\[/; + } + } + } + +# check for spaces between functions and their parentheses. + while ($line =~ /($Ident)\s+\(/g) { + my $name = $1; + my $ctx_before = substr($line, 0, $-[1]); + my $ctx = "$ctx_before$name"; + + # Ignore those directives where spaces _are_ permitted. + if ($name =~ /^(?: + if|for|while|switch|return|case| + volatile|__volatile__| + __attribute__|format|__extension__| + asm|__asm__|scoped_guard)$/x) + { + # cpp #define statements have non-optional spaces, ie + # if there is a space between the name and the open + # parenthesis it is simply not a parameter group. + } elsif ($ctx_before =~ /^.\s*\#\s*define\s*$/) { + + # cpp #elif statement condition may start with a ( + } elsif ($ctx =~ /^.\s*\#\s*elif\s*$/) { + + # If this whole things ends with a type its most + # likely a typedef for a function. + } elsif ($ctx =~ /$Type$/) { + + } else { + if (WARN("SPACING", + "space prohibited between function name and open parenthesis '('\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ + s/\b$name\s+\(/$name\(/; + } + } + } + +# Check operator spacing. + if (!($line=~/\#\s*include/)) { + my $fixed_line = ""; + my $line_fixed = 0; + + my $ops = qr{ + <<=|>>=|<=|>=|==|!=| + \+=|-=|\*=|\/=|%=|\^=|\|=|&=| + =>|->|<<|>>|<|>|=|!|~| + &&|\|\||,|\^|\+\+|--|&|\||\+|-|\*|\/|%| + \?:|\?|: + }x; + my @elements = split(/($ops|;)/, $opline); + +## print("element count: <" . $#elements . ">\n"); +## foreach my $el (@elements) { +## print("el: <$el>\n"); +## } + + my @fix_elements = (); + my $off = 0; + + foreach my $el (@elements) { + push(@fix_elements, substr($rawline, $off, length($el))); + $off += length($el); + } + + $off = 0; + + my $blank = copy_spacing($opline); + my $last_after = -1; + + for (my $n = 0; $n < $#elements; $n += 2) { + + my $good = $fix_elements[$n] . $fix_elements[$n + 1]; + +## print("n: <$n> good: <$good>\n"); + + $off += length($elements[$n]); + + # Pick up the preceding and succeeding characters. + my $ca = substr($opline, 0, $off); + my $cc = ''; + if (length($opline) >= ($off + length($elements[$n + 1]))) { + $cc = substr($opline, $off + length($elements[$n + 1])); + } + my $cb = "$ca$;$cc"; + + my $a = ''; + $a = 'V' if ($elements[$n] ne ''); + $a = 'W' if ($elements[$n] =~ /\s$/); + $a = 'C' if ($elements[$n] =~ /$;$/); + $a = 'B' if ($elements[$n] =~ /(\[|\()$/); + $a = 'O' if ($elements[$n] eq ''); + $a = 'E' if ($ca =~ /^\s*$/); + + my $op = $elements[$n + 1]; + + my $c = ''; + if (defined $elements[$n + 2]) { + $c = 'V' if ($elements[$n + 2] ne ''); + $c = 'W' if ($elements[$n + 2] =~ /^\s/); + $c = 'C' if ($elements[$n + 2] =~ /^$;/); + $c = 'B' if ($elements[$n + 2] =~ /^(\)|\]|;)/); + $c = 'O' if ($elements[$n + 2] eq ''); + $c = 'E' if ($elements[$n + 2] =~ /^\s*\\$/); + } else { + $c = 'E'; + } + + my $ctx = "${a}x${c}"; + + my $at = "(ctx:$ctx)"; + + my $ptr = substr($blank, 0, $off) . "^"; + my $hereptr = "$hereline$ptr\n"; + + # Pull out the value of this operator. + my $op_type = substr($curr_values, $off + 1, 1); + + # Get the full operator variant. + my $opv = $op . substr($curr_vars, $off, 1); + + # Ignore operators passed as parameters. + if ($op_type ne 'V' && + $ca =~ /\s$/ && $cc =~ /^\s*[,\)]/) { + +# # Ignore comments +# } elsif ($op =~ /^$;+$/) { + + # ; should have either the end of line or a space or \ after it + } elsif ($op eq ';') { + if ($ctx !~ /.x[WEBC]/ && + $cc !~ /^\\/ && $cc !~ /^;/) { + if (ERROR("SPACING", + "space required after that '$op' $at\n" . $hereptr)) { + $good = $fix_elements[$n] . trim($fix_elements[$n + 1]) . " "; + $line_fixed = 1; + } + } + + # // is a comment + } elsif ($op eq '//') { + + # : when part of a bitfield + } elsif ($opv eq ':B') { + # skip the bitfield test for now + + # No spaces for: + # -> + } elsif ($op eq '->') { + if ($ctx =~ /Wx.|.xW/) { + if (ERROR("SPACING", + "spaces prohibited around that '$op' $at\n" . $hereptr)) { + $good = rtrim($fix_elements[$n]) . trim($fix_elements[$n + 1]); + if (defined $fix_elements[$n + 2]) { + $fix_elements[$n + 2] =~ s/^\s+//; + } + $line_fixed = 1; + } + } + + # , must not have a space before and must have a space on the right. + } elsif ($op eq ',') { + my $rtrim_before = 0; + my $space_after = 0; + if ($ctx =~ /Wx./) { + if (ERROR("SPACING", + "space prohibited before that '$op' $at\n" . $hereptr)) { + $line_fixed = 1; + $rtrim_before = 1; + } + } + if ($ctx !~ /.x[WEC]/ && $cc !~ /^}/) { + if (ERROR("SPACING", + "space required after that '$op' $at\n" . $hereptr)) { + $line_fixed = 1; + $last_after = $n; + $space_after = 1; + } + } + if ($rtrim_before || $space_after) { + if ($rtrim_before) { + $good = rtrim($fix_elements[$n]) . trim($fix_elements[$n + 1]); + } else { + $good = $fix_elements[$n] . trim($fix_elements[$n + 1]); + } + if ($space_after) { + $good .= " "; + } + } + + # '*' as part of a type definition -- reported already. + } elsif ($opv eq '*_') { + #warn "'*' is part of type\n"; + + # unary operators should have a space before and + # none after. May be left adjacent to another + # unary operator, or a cast + } elsif ($op eq '!' || $op eq '~' || + $opv eq '*U' || $opv eq '-U' || + $opv eq '&U' || $opv eq '&&U') { + if ($ctx !~ /[WEBC]x./ && $ca !~ /(?:\)|!|~|\*|-|\&|\||\+\+|\-\-|\{)$/) { + if (ERROR("SPACING", + "space required before that '$op' $at\n" . $hereptr)) { + if ($n != $last_after + 2) { + $good = $fix_elements[$n] . " " . ltrim($fix_elements[$n + 1]); + $line_fixed = 1; + } + } + } + if ($op eq '*' && $cc =~/\s*$Modifier\b/) { + # A unary '*' may be const + + } elsif ($ctx =~ /.xW/) { + if (ERROR("SPACING", + "space prohibited after that '$op' $at\n" . $hereptr)) { + $good = $fix_elements[$n] . rtrim($fix_elements[$n + 1]); + if (defined $fix_elements[$n + 2]) { + $fix_elements[$n + 2] =~ s/^\s+//; + } + $line_fixed = 1; + } + } + + # unary ++ and unary -- are allowed no space on one side. + } elsif ($op eq '++' or $op eq '--') { + if ($ctx !~ /[WEOBC]x[^W]/ && $ctx !~ /[^W]x[WOBEC]/) { + if (ERROR("SPACING", + "space required one side of that '$op' $at\n" . $hereptr)) { + $good = $fix_elements[$n] . trim($fix_elements[$n + 1]) . " "; + $line_fixed = 1; + } + } + if ($ctx =~ /Wx[BE]/ || + ($ctx =~ /Wx./ && $cc =~ /^;/)) { + if (ERROR("SPACING", + "space prohibited before that '$op' $at\n" . $hereptr)) { + $good = rtrim($fix_elements[$n]) . trim($fix_elements[$n + 1]); + $line_fixed = 1; + } + } + if ($ctx =~ /ExW/) { + if (ERROR("SPACING", + "space prohibited after that '$op' $at\n" . $hereptr)) { + $good = $fix_elements[$n] . trim($fix_elements[$n + 1]); + if (defined $fix_elements[$n + 2]) { + $fix_elements[$n + 2] =~ s/^\s+//; + } + $line_fixed = 1; + } + } + + # << and >> may either have or not have spaces both sides + } elsif ($op eq '<<' or $op eq '>>' or + $op eq '&' or $op eq '^' or $op eq '|' or + $op eq '+' or $op eq '-' or + $op eq '*' or $op eq '/' or + $op eq '%') + { + if ($check) { + if (defined $fix_elements[$n + 2] && $ctx !~ /[EW]x[EW]/) { + if (CHK("SPACING", + "spaces preferred around that '$op' $at\n" . $hereptr)) { + $good = rtrim($fix_elements[$n]) . " " . trim($fix_elements[$n + 1]) . " "; + $fix_elements[$n + 2] =~ s/^\s+//; + $line_fixed = 1; + } + } elsif (!defined $fix_elements[$n + 2] && $ctx !~ /Wx[OE]/) { + if (CHK("SPACING", + "space preferred before that '$op' $at\n" . $hereptr)) { + $good = rtrim($fix_elements[$n]) . " " . trim($fix_elements[$n + 1]); + $line_fixed = 1; + } + } + } elsif ($ctx =~ /Wx[^WCE]|[^WCE]xW/) { + if (ERROR("SPACING", + "need consistent spacing around '$op' $at\n" . $hereptr)) { + $good = rtrim($fix_elements[$n]) . " " . trim($fix_elements[$n + 1]) . " "; + if (defined $fix_elements[$n + 2]) { + $fix_elements[$n + 2] =~ s/^\s+//; + } + $line_fixed = 1; + } + } + + # A colon needs no spaces before when it is + # terminating a case value or a label. + } elsif ($opv eq ':C' || $opv eq ':L') { + if ($ctx =~ /Wx./ and $realfile !~ m@.*\.lds\.h$@) { + if (ERROR("SPACING", + "space prohibited before that '$op' $at\n" . $hereptr)) { + $good = rtrim($fix_elements[$n]) . trim($fix_elements[$n + 1]); + $line_fixed = 1; + } + } + + # All the others need spaces both sides. + } elsif ($ctx !~ /[EWC]x[CWE]/) { + my $ok = 0; + + # Ignore email addresses <foo@bar> + if (($op eq '<' && + $cc =~ /^\S+\@\S+>/) || + ($op eq '>' && + $ca =~ /<\S+\@\S+$/)) + { + $ok = 1; + } + + # for asm volatile statements + # ignore a colon with another + # colon immediately before or after + if (($op eq ':') && + ($ca =~ /:$/ || $cc =~ /^:/)) { + $ok = 1; + } + + # messages are ERROR, but ?: are CHK + if ($ok == 0) { + my $msg_level = \&ERROR; + $msg_level = \&CHK if (($op eq '?:' || $op eq '?' || $op eq ':') && $ctx =~ /VxV/); + + if (&{$msg_level}("SPACING", + "spaces required around that '$op' $at\n" . $hereptr)) { + $good = rtrim($fix_elements[$n]) . " " . trim($fix_elements[$n + 1]) . " "; + if (defined $fix_elements[$n + 2]) { + $fix_elements[$n + 2] =~ s/^\s+//; + } + $line_fixed = 1; + } + } + } + $off += length($elements[$n + 1]); + +## print("n: <$n> GOOD: <$good>\n"); + + $fixed_line = $fixed_line . $good; + } + + if (($#elements % 2) == 0) { + $fixed_line = $fixed_line . $fix_elements[$#elements]; + } + + if ($fix && $line_fixed && $fixed_line ne $fixed[$fixlinenr]) { + $fixed[$fixlinenr] = $fixed_line; + } + + + } + +# check for whitespace before a non-naked semicolon + if ($line =~ /^\+.*\S\s+;\s*$/) { + if (WARN("SPACING", + "space prohibited before semicolon\n" . $herecurr) && + $fix) { + 1 while $fixed[$fixlinenr] =~ + s/^(\+.*\S)\s+;/$1;/; + } + } + +# check for multiple assignments + if ($line =~ /^.\s*$Lval\s*=\s*$Lval\s*=(?!=)/) { + CHK("MULTIPLE_ASSIGNMENTS", + "multiple assignments should be avoided\n" . $herecurr); + } + +## # check for multiple declarations, allowing for a function declaration +## # continuation. +## if ($line =~ /^.\s*$Type\s+$Ident(?:\s*=[^,{]*)?\s*,\s*$Ident.*/ && +## $line !~ /^.\s*$Type\s+$Ident(?:\s*=[^,{]*)?\s*,\s*$Type\s*$Ident.*/) { +## +## # Remove any bracketed sections to ensure we do not +## # falsely report the parameters of functions. +## my $ln = $line; +## while ($ln =~ s/\([^\(\)]*\)//g) { +## } +## if ($ln =~ /,/) { +## WARN("MULTIPLE_DECLARATION", +## "declaring multiple variables together should be avoided\n" . $herecurr); +## } +## } + +#need space before brace following if, while, etc + if (($line =~ /\(.*\)\{/ && $line !~ /\($Type\)\{/) || + $line =~ /\b(?:else|do)\{/) { + if (ERROR("SPACING", + "space required before the open brace '{'\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ s/^(\+.*(?:do|else|\)))\{/$1 {/; + } + } + +## # check for blank lines before declarations +## if ($line =~ /^.\t+$Type\s+$Ident(?:\s*=.*)?;/ && +## $prevrawline =~ /^.\s*$/) { +## WARN("SPACING", +## "No blank lines before declarations\n" . $hereprev); +## } +## + +# closing brace should have a space following it when it has anything +# on the line + if ($line =~ /}(?!(?:,|;|\)|\}))\S/) { + if (ERROR("SPACING", + "space required after that close brace '}'\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ + s/}((?!(?:,|;|\)))\S)/} $1/; + } + } + +# check spacing on square brackets + if ($line =~ /\[\s/ && $line !~ /\[\s*$/) { + if (ERROR("SPACING", + "space prohibited after that open square bracket '['\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ + s/\[\s+/\[/; + } + } + if ($line =~ /\s\]/) { + if (ERROR("SPACING", + "space prohibited before that close square bracket ']'\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ + s/\s+\]/\]/; + } + } + +# check spacing on parentheses + if ($line =~ /\(\s/ && $line !~ /\(\s*(?:\\)?$/ && + $line !~ /for\s*\(\s+;/) { + if (ERROR("SPACING", + "space prohibited after that open parenthesis '('\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ + s/\(\s+/\(/; + } + } + if ($line =~ /(\s+)\)/ && $line !~ /^.\s*\)/ && + $line !~ /for\s*\(.*;\s+\)/ && + $line !~ /:\s+\)/) { + if (ERROR("SPACING", + "space prohibited before that close parenthesis ')'\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ + s/\s+\)/\)/; + } + } + +# check unnecessary parentheses around addressof/dereference single $Lvals +# ie: &(foo->bar) should be &foo->bar and *(foo->bar) should be *foo->bar + + while ($line =~ /(?:[^&]&\s*|\*)\(\s*($Ident\s*(?:$Member\s*)+)\s*\)/g) { + my $var = $1; + if (CHK("UNNECESSARY_PARENTHESES", + "Unnecessary parentheses around $var\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ s/\(\s*\Q$var\E\s*\)/$var/; + } + } + +# check for unnecessary parentheses around function pointer uses +# ie: (foo->bar)(); should be foo->bar(); +# but not "if (foo->bar) (" to avoid some false positives + if ($line =~ /(\bif\s*|)(\(\s*$Ident\s*(?:$Member\s*)+\))[ \t]*\(/ && $1 !~ /^if/) { + my $var = $2; + if (CHK("UNNECESSARY_PARENTHESES", + "Unnecessary parentheses around function pointer $var\n" . $herecurr) && + $fix) { + my $var2 = deparenthesize($var); + $var2 =~ s/\s//g; + $fixed[$fixlinenr] =~ s/\Q$var\E/$var2/; + } + } + +# check for unnecessary parentheses around comparisons in if uses +# when !drivers/staging or command-line uses --strict + if (($realfile !~ m@^(?:drivers/staging/)@ || $check_orig) && + $perl_version_ok && defined($stat) && + $stat =~ /(^.\s*if\s*($balanced_parens))/) { + my $if_stat = $1; + my $test = substr($2, 1, -1); + my $herectx; + while ($test =~ /(?:^|[^\w\&\!\~])+\s*\(\s*([\&\!\~]?\s*$Lval\s*(?:$Compare\s*$FuncArg)?)\s*\)/g) { + my $match = $1; + # avoid parentheses around potential macro args + next if ($match =~ /^\s*\w+\s*$/); + if (!defined($herectx)) { + $herectx = $here . "\n"; + my $cnt = statement_rawlines($if_stat); + for (my $n = 0; $n < $cnt; $n++) { + my $rl = raw_line($linenr, $n); + $herectx .= $rl . "\n"; + last if $rl =~ /^[ \+].*\{/; + } + } + CHK("UNNECESSARY_PARENTHESES", + "Unnecessary parentheses around '$match'\n" . $herectx); + } + } + +# check that goto labels aren't indented (allow a single space indentation) +# and ignore bitfield definitions like foo:1 +# Strictly, labels can have whitespace after the identifier and before the : +# but this is not allowed here as many ?: uses would appear to be labels + if ($sline =~ /^.\s+[A-Za-z_][A-Za-z\d_]*:(?!\s*\d+)/ && + $sline !~ /^. [A-Za-z\d_][A-Za-z\d_]*:/ && + $sline !~ /^.\s+default:/) { + if (WARN("INDENTED_LABEL", + "labels should not be indented\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ + s/^(.)\s+/$1/; + } + } + +# check if a statement with a comma should be two statements like: +# foo = bar(), /* comma should be semicolon */ +# bar = baz(); + if (defined($stat) && + $stat =~ /^\+\s*(?:$Lval\s*$Assignment\s*)?$FuncArg\s*,\s*(?:$Lval\s*$Assignment\s*)?$FuncArg\s*;\s*$/) { + my $cnt = statement_rawlines($stat); + my $herectx = get_stat_here($linenr, $cnt, $here); + WARN("SUSPECT_COMMA_SEMICOLON", + "Possible comma where semicolon could be used\n" . $herectx); + } + +# return is not a function + if (defined($stat) && $stat =~ /^.\s*return(\s*)\(/s) { + my $spacing = $1; + if ($perl_version_ok && + $stat =~ /^.\s*return\s*($balanced_parens)\s*;\s*$/) { + my $value = $1; + $value = deparenthesize($value); + if ($value =~ m/^\s*$FuncArg\s*(?:\?|$)/) { + ERROR("RETURN_PARENTHESES", + "return is not a function, parentheses are not required\n" . $herecurr); + } + } elsif ($spacing !~ /\s+/) { + ERROR("SPACING", + "space required before the open parenthesis '('\n" . $herecurr); + } + } + +# unnecessary return in a void function +# at end-of-function, with the previous line a single leading tab, then return; +# and the line before that not a goto label target like "out:" + if ($sline =~ /^[ \+]}\s*$/ && + $prevline =~ /^\+\treturn\s*;\s*$/ && + $linenr >= 3 && + $lines[$linenr - 3] =~ /^[ +]/ && + $lines[$linenr - 3] !~ /^[ +]\s*$Ident\s*:/) { + WARN("RETURN_VOID", + "void function return statements are not generally useful\n" . $hereprev); + } + +# if statements using unnecessary parentheses - ie: if ((foo == bar)) + if ($perl_version_ok && + $line =~ /\bif\s*((?:\(\s*){2,})/) { + my $openparens = $1; + my $count = $openparens =~ tr@\(@\(@; + my $msg = ""; + if ($line =~ /\bif\s*(?:\(\s*){$count,$count}$LvalOrFunc\s*($Compare)\s*$LvalOrFunc(?:\s*\)){$count,$count}/) { + my $comp = $4; #Not $1 because of $LvalOrFunc + $msg = " - maybe == should be = ?" if ($comp eq "=="); + WARN("UNNECESSARY_PARENTHESES", + "Unnecessary parentheses$msg\n" . $herecurr); + } + } + +# comparisons with a constant or upper case identifier on the left +# avoid cases like "foo + BAR < baz" +# only fix matches surrounded by parentheses to avoid incorrect +# conversions like "FOO < baz() + 5" being "misfixed" to "baz() > FOO + 5" + if ($perl_version_ok && + $line =~ /^\+(.*)\b($Constant|[A-Z_][A-Z0-9_]*)\s*($Compare)\s*($LvalOrFunc)/) { + my $lead = $1; + my $const = $2; + my $comp = $3; + my $to = $4; + my $newcomp = $comp; + if ($lead !~ /(?:$Operators|\.)\s*$/ && + $to !~ /^(?:Constant|[A-Z_][A-Z0-9_]*)$/ && + WARN("CONSTANT_COMPARISON", + "Comparisons should place the constant on the right side of the test\n" . $herecurr) && + $fix) { + if ($comp eq "<") { + $newcomp = ">"; + } elsif ($comp eq "<=") { + $newcomp = ">="; + } elsif ($comp eq ">") { + $newcomp = "<"; + } elsif ($comp eq ">=") { + $newcomp = "<="; + } + $fixed[$fixlinenr] =~ s/\(\s*\Q$const\E\s*$Compare\s*\Q$to\E\s*\)/($to $newcomp $const)/; + } + } + +# Return of what appears to be an errno should normally be negative + if ($sline =~ /\breturn(?:\s*\(+\s*|\s+)(E[A-Z]+)(?:\s*\)+\s*|\s*)[;:,]/) { + my $name = $1; + if ($name ne 'EOF' && $name ne 'ERROR' && $name !~ /^EPOLL/) { + WARN("USE_NEGATIVE_ERRNO", + "return of an errno should typically be negative (ie: return -$1)\n" . $herecurr); + } + } + +# Need a space before open parenthesis after if, while etc + if ($line =~ /\b(if|while|for|switch)\(/) { + if (ERROR("SPACING", + "space required before the open parenthesis '('\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ + s/\b(if|while|for|switch)\(/$1 \(/; + } + } + +# Check for illegal assignment in if conditional -- and check for trailing +# statements after the conditional. + if ($line =~ /do\s*(?!{)/) { + ($stat, $cond, $line_nr_next, $remain_next, $off_next) = + ctx_statement_block($linenr, $realcnt, 0) + if (!defined $stat); + my ($stat_next) = ctx_statement_block($line_nr_next, + $remain_next, $off_next); + $stat_next =~ s/\n./\n /g; + ##print "stat<$stat> stat_next<$stat_next>\n"; + + if ($stat_next =~ /^\s*while\b/) { + # If the statement carries leading newlines, + # then count those as offsets. + my ($whitespace) = + ($stat_next =~ /^((?:\s*\n[+-])*\s*)/s); + my $offset = + statement_rawlines($whitespace) - 1; + + $suppress_whiletrailers{$line_nr_next + + $offset} = 1; + } + } + if (!defined $suppress_whiletrailers{$linenr} && + defined($stat) && defined($cond) && + $line =~ /\b(?:if|while|for)\s*\(/ && $line !~ /^.\s*#/) { + my ($s, $c) = ($stat, $cond); + my $fixed_assign_in_if = 0; + + if ($c =~ /\bif\s*\(.*[^<>!=]=[^=].*/s) { + if (ERROR("ASSIGN_IN_IF", + "do not use assignment in if condition\n" . $herecurr) && + $fix && $perl_version_ok) { + if ($rawline =~ /^\+(\s+)if\s*\(\s*(\!)?\s*\(\s*(($Lval)\s*=\s*$LvalOrFunc)\s*\)\s*(?:($Compare)\s*($FuncArg))?\s*\)\s*(\{)?\s*$/) { + my $space = $1; + my $not = $2; + my $statement = $3; + my $assigned = $4; + my $test = $8; + my $against = $9; + my $brace = $15; + fix_delete_line($fixlinenr, $rawline); + fix_insert_line($fixlinenr, "$space$statement;"); + my $newline = "${space}if ("; + $newline .= '!' if defined($not); + $newline .= '(' if (defined $not && defined($test) && defined($against)); + $newline .= "$assigned"; + $newline .= " $test $against" if (defined($test) && defined($against)); + $newline .= ')' if (defined $not && defined($test) && defined($against)); + $newline .= ')'; + $newline .= " {" if (defined($brace)); + fix_insert_line($fixlinenr + 1, $newline); + $fixed_assign_in_if = 1; + } + } + } + + # Find out what is on the end of the line after the + # conditional. + substr($s, 0, length($c), ''); + $s =~ s/\n.*//g; + $s =~ s/$;//g; # Remove any comments + if (length($c) && $s !~ /^\s*{?\s*\\*\s*$/ && + $c !~ /}\s*while\s*/) + { + # Find out how long the conditional actually is. + my @newlines = ($c =~ /\n/gs); + my $cond_lines = 1 + $#newlines; + my $stat_real = ''; + + $stat_real = raw_line($linenr, $cond_lines) + . "\n" if ($cond_lines); + if (defined($stat_real) && $cond_lines > 1) { + $stat_real = "[...]\n$stat_real"; + } + + if (ERROR("TRAILING_STATEMENTS", + "trailing statements should be on next line\n" . $herecurr . $stat_real) && + !$fixed_assign_in_if && + $cond_lines == 0 && + $fix && $perl_version_ok && + $fixed[$fixlinenr] =~ /^\+(\s*)((?:if|while|for)\s*$balanced_parens)\s*(.*)$/) { + my $indent = $1; + my $test = $2; + my $rest = rtrim($4); + if ($rest =~ /;$/) { + $fixed[$fixlinenr] = "\+$indent$test"; + fix_insert_line($fixlinenr + 1, "$indent\t$rest"); + } + } + } + } + +# Check for bitwise tests written as boolean + if ($line =~ / + (?: + (?:\[|\(|\&\&|\|\|) + \s*0[xX][0-9]+\s* + (?:\&\&|\|\|) + | + (?:\&\&|\|\|) + \s*0[xX][0-9]+\s* + (?:\&\&|\|\||\)|\]) + )/x) + { + WARN("HEXADECIMAL_BOOLEAN_TEST", + "boolean test with hexadecimal, perhaps just 1 \& or \|?\n" . $herecurr); + } + +# if and else should not have general statements after it + if ($line =~ /^.\s*(?:}\s*)?else\b(.*)/) { + my $s = $1; + $s =~ s/$;//g; # Remove any comments + if ($s !~ /^\s*(?:\sif|(?:{|)\s*\\?\s*$)/) { + ERROR("TRAILING_STATEMENTS", + "trailing statements should be on next line\n" . $herecurr); + } + } +# if should not continue a brace + if ($line =~ /}\s*if\b/) { + ERROR("TRAILING_STATEMENTS", + "trailing statements should be on next line (or did you mean 'else if'?)\n" . + $herecurr); + } +# case and default should not have general statements after them + if ($line =~ /^.\s*(?:case\s*.*|default\s*):/g && + $line !~ /\G(?: + (?:\s*$;*)(?:\s*{)?(?:\s*$;*)(?:\s*\\)?\s*$| + \s*return\s+ + )/xg) + { + ERROR("TRAILING_STATEMENTS", + "trailing statements should be on next line\n" . $herecurr); + } + + # Check for }<nl>else {, these must be at the same + # indent level to be relevant to each other. + if ($prevline=~/}\s*$/ and $line=~/^.\s*else\s*/ && + $previndent == $indent) { + if (ERROR("ELSE_AFTER_BRACE", + "else should follow close brace '}'\n" . $hereprev) && + $fix && $prevline =~ /^\+/ && $line =~ /^\+/) { + fix_delete_line($fixlinenr - 1, $prevrawline); + fix_delete_line($fixlinenr, $rawline); + my $fixedline = $prevrawline; + $fixedline =~ s/}\s*$//; + if ($fixedline !~ /^\+\s*$/) { + fix_insert_line($fixlinenr, $fixedline); + } + $fixedline = $rawline; + $fixedline =~ s/^(.\s*)else/$1} else/; + fix_insert_line($fixlinenr, $fixedline); + } + } + + if ($prevline=~/}\s*$/ and $line=~/^.\s*while\s*/ && + $previndent == $indent) { + my ($s, $c) = ctx_statement_block($linenr, $realcnt, 0); + + # Find out what is on the end of the line after the + # conditional. + substr($s, 0, length($c), ''); + $s =~ s/\n.*//g; + + if ($s =~ /^\s*;/) { + if (ERROR("WHILE_AFTER_BRACE", + "while should follow close brace '}'\n" . $hereprev) && + $fix && $prevline =~ /^\+/ && $line =~ /^\+/) { + fix_delete_line($fixlinenr - 1, $prevrawline); + fix_delete_line($fixlinenr, $rawline); + my $fixedline = $prevrawline; + my $trailing = $rawline; + $trailing =~ s/^\+//; + $trailing = trim($trailing); + $fixedline =~ s/}\s*$/} $trailing/; + fix_insert_line($fixlinenr, $fixedline); + } + } + } + +#Specific variable tests + while ($line =~ m{($Constant|$Lval)}g) { + my $var = $1; + +#CamelCase + if ($var !~ /^$Constant$/ && + $var =~ /[A-Z][a-z]|[a-z][A-Z]/ && +#Ignore some autogenerated defines and enum values + $var !~ /^(?:[A-Z]+_){1,5}[A-Z]{1,3}[a-z]/ && +#Ignore Page<foo> variants + $var !~ /^(?:Clear|Set|TestClear|TestSet|)Page[A-Z]/ && +#Ignore ETHTOOL_LINK_MODE_<foo> variants + $var !~ /^ETHTOOL_LINK_MODE_/ && +#Ignore SI style variants like nS, mV and dB +#(ie: max_uV, regulator_min_uA_show, RANGE_mA_VALUE) + $var !~ /^(?:[a-z0-9_]*|[A-Z0-9_]*)?_?[a-z][A-Z](?:_[a-z0-9_]+|_[A-Z0-9_]+)?$/ && +#Ignore some three character SI units explicitly, like MiB and KHz + $var !~ /^(?:[a-z_]*?)_?(?:[KMGT]iB|[KMGT]?Hz)(?:_[a-z_]+)?$/) { + while ($var =~ m{\b($Ident)}g) { + my $word = $1; + next if ($word !~ /[A-Z][a-z]|[a-z][A-Z]/); + if ($check) { + seed_camelcase_includes(); + if (!$file && !$camelcase_file_seeded) { + seed_camelcase_file($realfile); + $camelcase_file_seeded = 1; + } + } + if (!defined $camelcase{$word}) { + $camelcase{$word} = 1; + CHK("CAMELCASE", + "Avoid CamelCase: <$word>\n" . $herecurr); + } + } + } + } + +#no spaces allowed after \ in define + if ($line =~ /\#\s*define.*\\\s+$/) { + if (WARN("WHITESPACE_AFTER_LINE_CONTINUATION", + "Whitespace after \\ makes next lines useless\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ s/\s+$//; + } + } + +# warn if <asm/foo.h> is #included and <linux/foo.h> is available and includes +# itself <asm/foo.h> (uses RAW line) + if ($tree && $rawline =~ m{^.\s*\#\s*include\s*\<asm\/(.*)\.h\>}) { + my $file = "$1.h"; + my $checkfile = "include/linux/$file"; + if (-f "$root/$checkfile" && + $realfile ne $checkfile && + $1 !~ /$allowed_asm_includes/) + { + my $asminclude = `grep -Ec "#include\\s+<asm/$file>" $root/$checkfile`; + if ($asminclude > 0) { + if ($realfile =~ m{^arch/}) { + CHK("ARCH_INCLUDE_LINUX", + "Consider using #include <linux/$file> instead of <asm/$file>\n" . $herecurr); + } else { + WARN("INCLUDE_LINUX", + "Use #include <linux/$file> instead of <asm/$file>\n" . $herecurr); + } + } + } + } + +# 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 + if ($realfile !~ m@/vmlinux.lds.h$@ && + $line =~ /^.\s*\#\s*define\s*$Ident(\()?/) { + my $ln = $linenr; + my $cnt = $realcnt; + my ($off, $dstat, $dcond, $rest); + my $ctx = ''; + my $has_flow_statement = 0; + my $has_arg_concat = 0; + ($dstat, $dcond, $ln, $cnt, $off) = + ctx_statement_block($linenr, $realcnt, 0); + $ctx = $dstat; + #print "dstat<$dstat> dcond<$dcond> cnt<$cnt> off<$off>\n"; + #print "LINE<$lines[$ln-1]> len<" . length($lines[$ln-1]) . "\n"; + + $has_flow_statement = 1 if ($ctx =~ /\b(goto|return)\b/); + $has_arg_concat = 1 if ($ctx =~ /\#\#/ && $ctx !~ /\#\#\s*(?:__VA_ARGS__|args)\b/); + + $dstat =~ s/^.\s*\#\s*define\s+$Ident(\([^\)]*\))?\s*//; + my $define_args = $1; + my $define_stmt = $dstat; + my @def_args = (); + + if (defined $define_args && $define_args ne "") { + $define_args = substr($define_args, 1, length($define_args) - 2); + $define_args =~ s/\s*//g; + $define_args =~ s/\\\+?//g; + @def_args = split(",", $define_args); + } + + $dstat =~ s/$;//g; + $dstat =~ s/\\\n.//g; + $dstat =~ s/^\s*//s; + $dstat =~ s/\s*$//s; + + # Flatten any parentheses and braces + while ($dstat =~ s/\([^\(\)]*\)/1u/ || + $dstat =~ s/\{[^\{\}]*\}/1u/ || + $dstat =~ s/.\[[^\[\]]*\]/1u/) + { + } + + # Flatten any obvious string concatenation. + while ($dstat =~ s/($String)\s*$Ident/$1/ || + $dstat =~ s/$Ident\s*($String)/$1/) + { + } + + # Make asm volatile uses seem like a generic function + $dstat =~ s/\b_*asm_*\s+_*volatile_*\b/asm_volatile/g; + + my $exceptions = qr{ + $Declare| + module_param_named| + MODULE_PARM_DESC| + DECLARE_PER_CPU| + DEFINE_PER_CPU| + __typeof__\(| + union| + struct| + \.$Ident\s*=\s*| + ^\"|\"$| + ^\[ + }x; + #print "REST<$rest> dstat<$dstat> ctx<$ctx>\n"; + + $ctx =~ s/\n*$//; + my $stmt_cnt = statement_rawlines($ctx); + my $herectx = get_stat_here($linenr, $stmt_cnt, $here); + + if ($dstat ne '' && + $dstat !~ /^(?:$Ident|-?$Constant),$/ && # 10, // foo(), + $dstat !~ /^(?:$Ident|-?$Constant);$/ && # foo(); + $dstat !~ /^[!~-]?(?:$Lval|$Constant)$/ && # 10 // foo() // !foo // ~foo // -foo // foo->bar // foo.bar->baz + $dstat !~ /^'X'$/ && $dstat !~ /^'XX'$/ && # character constants + $dstat !~ /$exceptions/ && + $dstat !~ /^\.$Ident\s*=/ && # .foo = + $dstat !~ /^(?:\#\s*$Ident|\#\s*$Constant)\s*$/ && # stringification #foo + $dstat !~ /^case\b/ && # case ... + $dstat !~ /^do\s*$Constant\s*while\s*$Constant;?$/ && # do {...} while (...); // do {...} while (...) + $dstat !~ /^while\s*$Constant\s*$Constant\s*$/ && # while (...) {...} + $dstat !~ /^for\s*$Constant$/ && # for (...) + $dstat !~ /^for\s*$Constant\s+(?:$Ident|-?$Constant)$/ && # for (...) bar() + $dstat !~ /^do\s*{/ && # do {... + $dstat !~ /^\(\{/ && # ({... + $ctx !~ /^.\s*#\s*define\s+TRACE_(?:SYSTEM|INCLUDE_FILE|INCLUDE_PATH)\b/) + { + if ($dstat =~ /^\s*if\b/) { + 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"); + } else { + ERROR("COMPLEX_MACRO", + "Macros with complex values should be enclosed in parentheses\n" . "$herectx"); + } + + } + + # Make $define_stmt single line, comment-free, etc + my @stmt_array = split('\n', $define_stmt); + my $first = 1; + $define_stmt = ""; + foreach my $l (@stmt_array) { + $l =~ s/\\$//; + if ($first) { + $define_stmt = $l; + $first = 0; + } elsif ($l =~ /^[\+ ]/) { + $define_stmt .= substr($l, 1); + } + } + $define_stmt =~ s/$;//g; + $define_stmt =~ s/\s+/ /g; + $define_stmt = trim($define_stmt); + +# check if any macro arguments are reused (ignore '...' and 'type') + foreach my $arg (@def_args) { + next if ($arg =~ /\.\.\./); + next if ($arg =~ /^type$/i); + my $tmp_stmt = $define_stmt; + $tmp_stmt =~ s/\b(__must_be_array|offsetof|sizeof|sizeof_field|__stringify|typeof|__typeof__|__builtin\w+|typecheck\s*\(\s*$Type\s*,|\#+)\s*\(*\s*$arg\s*\)*\b//g; + $tmp_stmt =~ s/\#+\s*$arg\b//g; + $tmp_stmt =~ s/\b$arg\s*\#\#//g; + my $use_cnt = () = $tmp_stmt =~ /\b$arg\b/g; + if ($use_cnt > 1) { + CHK("MACRO_ARG_REUSE", + "Macro argument reuse '$arg' - possible side-effects?\n" . "$herectx"); + } +# check if any macro arguments may have other precedence issues + if ($tmp_stmt =~ m/($Operators)?\s*\b$arg\b\s*($Operators)?/m && + ((defined($1) && $1 ne ',') || + (defined($2) && $2 ne ','))) { + CHK("MACRO_ARG_PRECEDENCE", + "Macro argument '$arg' may be better as '($arg)' to avoid precedence issues\n" . "$herectx"); + } + +# check if this is an unused argument + if ($define_stmt !~ /\b$arg\b/) { + WARN("MACRO_ARG_UNUSED", + "Argument '$arg' is not used in function-like macro\n" . "$herectx"); + } + } + +# check for macros with flow control, but without ## concatenation +# ## concatenation is commonly a macro that defines a function so ignore those + if ($has_flow_statement && !$has_arg_concat) { + my $cnt = statement_rawlines($ctx); + my $herectx = get_stat_here($linenr, $cnt, $here); + + WARN("MACRO_WITH_FLOW_CONTROL", + "Macros with flow control statements should be avoided\n" . "$herectx"); + } + +# check for line continuations outside of #defines, preprocessor #, and asm + + } elsif ($realfile =~ m@/vmlinux.lds.h$@) { + $line =~ s/(\w+)/$maybe_linker_symbol{$1}++/ge; + #print "REAL: $realfile\nln: $line\nkeys:", sort keys %maybe_linker_symbol; + } else { + if ($prevline !~ /^..*\\$/ && + $line !~ /^\+\s*\#.*\\$/ && # preprocessor + $line !~ /^\+.*\b(__asm__|asm)\b.*\\$/ && # asm + $line =~ /^\+.*\\$/) { + WARN("LINE_CONTINUATIONS", + "Avoid unnecessary line continuations\n" . $herecurr); + } + } + +# do {} while (0) macro tests: +# single-statement macros do not need to be enclosed in do while (0) loop, +# macro should not end with a semicolon + if ($perl_version_ok && + $realfile !~ m@/vmlinux.lds.h$@ && + $line =~ /^.\s*\#\s*define\s+$Ident(\()?/) { + my $ln = $linenr; + my $cnt = $realcnt; + my ($off, $dstat, $dcond, $rest); + my $ctx = ''; + ($dstat, $dcond, $ln, $cnt, $off) = + ctx_statement_block($linenr, $realcnt, 0); + $ctx = $dstat; + + $dstat =~ s/\\\n.//g; + $dstat =~ s/$;/ /g; + + if ($dstat =~ /^\+\s*#\s*define\s+$Ident\s*${balanced_parens}\s*do\s*{(.*)\s*}\s*while\s*\(\s*0\s*\)\s*([;\s]*)\s*$/) { + my $stmts = $2; + my $semis = $3; + + $ctx =~ s/\n*$//; + my $cnt = statement_rawlines($ctx); + my $herectx = get_stat_here($linenr, $cnt, $here); + + if (($stmts =~ tr/;/;/) == 1 && + $stmts !~ /^\s*(if|while|for|switch)\b/) { + WARN("SINGLE_STATEMENT_DO_WHILE_MACRO", + "Single statement macros should not use a do {} while (0) loop\n" . "$herectx"); + } + if (defined $semis && $semis ne "") { + WARN("DO_WHILE_MACRO_WITH_TRAILING_SEMICOLON", + "do {} while (0) macros should not be semicolon terminated\n" . "$herectx"); + } + } elsif ($dstat =~ /^\+\s*#\s*define\s+$Ident.*;\s*$/) { + $ctx =~ s/\n*$//; + my $cnt = statement_rawlines($ctx); + my $herectx = get_stat_here($linenr, $cnt, $here); + + WARN("TRAILING_SEMICOLON", + "macros should not use a trailing semicolon\n" . "$herectx"); + } + } + +# check for redundant bracing round if etc + if ($line =~ /(^.*)\bif\b/ && $1 !~ /else\s*$/) { + my ($level, $endln, @chunks) = + ctx_statement_full($linenr, $realcnt, 1); + #print "chunks<$#chunks> linenr<$linenr> endln<$endln> level<$level>\n"; + #print "APW: <<$chunks[1][0]>><<$chunks[1][1]>>\n"; + if ($#chunks > 0 && $level == 0) { + my @allowed = (); + my $allow = 0; + my $seen = 0; + my $herectx = $here . "\n"; + my $ln = $linenr - 1; + for my $chunk (@chunks) { + my ($cond, $block) = @{$chunk}; + + # If the condition carries leading newlines, then count those as offsets. + my ($whitespace) = ($cond =~ /^((?:\s*\n[+-])*\s*)/s); + my $offset = statement_rawlines($whitespace) - 1; + + $allowed[$allow] = 0; + #print "COND<$cond> whitespace<$whitespace> offset<$offset>\n"; + + # We have looked at and allowed this specific line. + $suppress_ifbraces{$ln + $offset} = 1; + + $herectx .= "$rawlines[$ln + $offset]\n[...]\n"; + $ln += statement_rawlines($block) - 1; + + substr($block, 0, length($cond), ''); + + $seen++ if ($block =~ /^\s*{/); + + #print "cond<$cond> block<$block> allowed<$allowed[$allow]>\n"; + if (statement_lines($cond) > 1) { + #print "APW: ALLOWED: cond<$cond>\n"; + $allowed[$allow] = 1; + } + if ($block =~/\b(?:if|for|while)\b/) { + #print "APW: ALLOWED: block<$block>\n"; + $allowed[$allow] = 1; + } + if (statement_block_size($block) > 1) { + #print "APW: ALLOWED: lines block<$block>\n"; + $allowed[$allow] = 1; + } + $allow++; + } + if ($seen) { + my $sum_allowed = 0; + foreach (@allowed) { + $sum_allowed += $_; + } + if ($sum_allowed == 0) { + WARN("BRACES", + "braces {} are not necessary for any arm of this statement\n" . $herectx); + } elsif ($sum_allowed != $allow && + $seen != $allow) { + CHK("BRACES", + "braces {} should be used on all arms of this statement\n" . $herectx); + } + } + } + } + if (!defined $suppress_ifbraces{$linenr - 1} && + $line =~ /\b(if|while|for|else)\b/) { + my $allowed = 0; + + # Check the pre-context. + if (substr($line, 0, $-[0]) =~ /(\}\s*)$/) { + #print "APW: ALLOWED: pre<$1>\n"; + $allowed = 1; + } + + my ($level, $endln, @chunks) = + ctx_statement_full($linenr, $realcnt, $-[0]); + + # Check the condition. + my ($cond, $block) = @{$chunks[0]}; + #print "CHECKING<$linenr> cond<$cond> block<$block>\n"; + if (defined $cond) { + substr($block, 0, length($cond), ''); + } + if (statement_lines($cond) > 1) { + #print "APW: ALLOWED: cond<$cond>\n"; + $allowed = 1; + } + if ($block =~/\b(?:if|for|while)\b/) { + #print "APW: ALLOWED: block<$block>\n"; + $allowed = 1; + } + if (statement_block_size($block) > 1) { + #print "APW: ALLOWED: lines block<$block>\n"; + $allowed = 1; + } + # Check the post-context. + if (defined $chunks[1]) { + my ($cond, $block) = @{$chunks[1]}; + if (defined $cond) { + substr($block, 0, length($cond), ''); + } + if ($block =~ /^\s*\{/) { + #print "APW: ALLOWED: chunk-1 block<$block>\n"; + $allowed = 1; + } + } + if ($level == 0 && $block =~ /^\s*\{/ && !$allowed) { + my $cnt = statement_rawlines($block); + my $herectx = get_stat_here($linenr, $cnt, $here); + + WARN("BRACES", + "braces {} are not necessary for single statement blocks\n" . $herectx); + } + } + +# check for single line unbalanced braces + if ($sline =~ /^.\s*\}\s*else\s*$/ || + $sline =~ /^.\s*else\s*\{\s*$/) { + CHK("BRACES", "Unbalanced braces around else statement\n" . $herecurr); + } + +# check for unnecessary blank lines around braces + if (($line =~ /^.\s*}\s*$/ && $prevrawline =~ /^.\s*$/)) { + if (CHK("BRACES", + "Blank lines aren't necessary before a close brace '}'\n" . $hereprev) && + $fix && $prevrawline =~ /^\+/) { + fix_delete_line($fixlinenr - 1, $prevrawline); + } + } + if (($rawline =~ /^.\s*$/ && $prevline =~ /^..*{\s*$/)) { + if (CHK("BRACES", + "Blank lines aren't necessary after an open brace '{'\n" . $hereprev) && + $fix) { + fix_delete_line($fixlinenr, $rawline); + } + } + +# no volatiles please + my $asm_volatile = qr{\b(__asm__|asm)\s+(__volatile__|volatile)\b}; + if ($line =~ /\bvolatile\b/ && $line !~ /$asm_volatile/) { + WARN("VOLATILE", + "Use of volatile is usually wrong: see Documentation/process/volatile-considered-harmful.rst\n" . $herecurr); + } + +# Check for user-visible strings broken across lines, which breaks the ability +# to grep for the string. Make exceptions when the previous string ends in a +# newline (multiple lines in one string constant) or '\t', '\r', ';', or '{' +# (common in inline assembly) or is a octal \123 or hexadecimal \xaf value + if ($line =~ /^\+\s*$String/ && + $prevline =~ /"\s*$/ && + $prevrawline !~ /(?:\\(?:[ntr]|[0-7]{1,3}|x[0-9a-fA-F]{1,2})|;\s*|\{\s*)"\s*$/) { + if (WARN("SPLIT_STRING", + "quoted string split across lines\n" . $hereprev) && + $fix && + $prevrawline =~ /^\+.*"\s*$/ && + $last_coalesced_string_linenr != $linenr - 1) { + my $extracted_string = get_quoted_string($line, $rawline); + my $comma_close = ""; + if ($rawline =~ /\Q$extracted_string\E(\s*\)\s*;\s*$|\s*,\s*)/) { + $comma_close = $1; + } + + fix_delete_line($fixlinenr - 1, $prevrawline); + fix_delete_line($fixlinenr, $rawline); + my $fixedline = $prevrawline; + $fixedline =~ s/"\s*$//; + $fixedline .= substr($extracted_string, 1) . trim($comma_close); + fix_insert_line($fixlinenr - 1, $fixedline); + $fixedline = $rawline; + $fixedline =~ s/\Q$extracted_string\E\Q$comma_close\E//; + if ($fixedline !~ /\+\s*$/) { + fix_insert_line($fixlinenr, $fixedline); + } + $last_coalesced_string_linenr = $linenr; + } + } + +# check for missing a space in a string concatenation + if ($prevrawline =~ /[^\\]\w"$/ && $rawline =~ /^\+[\t ]+"\w/) { + WARN('MISSING_SPACE', + "break quoted strings at a space character\n" . $hereprev); + } + +# check for an embedded function name in a string when the function is known +# This does not work very well for -f --file checking as it depends on patch +# context providing the function name or a single line form for in-file +# function declarations + if ($line =~ /^\+.*$String/ && + defined($context_function) && + get_quoted_string($line, $rawline) =~ /\b$context_function\b/ && + length(get_quoted_string($line, $rawline)) != (length($context_function) + 2)) { + WARN("EMBEDDED_FUNCTION_NAME", + "Prefer using '\"%s...\", __func__' to using '$context_function', this function's name, in a string\n" . $herecurr); + } + +# check for unnecessary function tracing like uses +# This does not use $logFunctions because there are many instances like +# 'dprintk(FOO, "%s()\n", __func__);' which do not match $logFunctions + if ($rawline =~ /^\+.*\([^"]*"$tracing_logging_tags{0,3}%s(?:\s*\(\s*\)\s*)?$tracing_logging_tags{0,3}(?:\\n)?"\s*,\s*__func__\s*\)\s*;/) { + if (WARN("TRACING_LOGGING", + "Unnecessary ftrace-like logging - prefer using ftrace\n" . $herecurr) && + $fix) { + fix_delete_line($fixlinenr, $rawline); + } + } + +# check for spaces before a quoted newline + if ($rawline =~ /^.*\".*\s\\n/) { + if (WARN("QUOTED_WHITESPACE_BEFORE_NEWLINE", + "unnecessary whitespace before a quoted newline\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ s/^(\+.*\".*)\s+\\n/$1\\n/; + } + + } + +# concatenated string without spaces between elements + if ($line =~ /$String[A-Z_]/ || + ($line =~ /([A-Za-z0-9_]+)$String/ && $1 !~ /^[Lu]$/)) { + if (CHK("CONCATENATED_STRING", + "Concatenated strings should use spaces between elements\n" . $herecurr) && + $fix) { + while ($line =~ /($String)/g) { + my $extracted_string = substr($rawline, $-[0], $+[0] - $-[0]); + $fixed[$fixlinenr] =~ s/\Q$extracted_string\E([A-Za-z0-9_])/$extracted_string $1/; + $fixed[$fixlinenr] =~ s/([A-Za-z0-9_])\Q$extracted_string\E/$1 $extracted_string/; + } + } + } + +# uncoalesced string fragments + if ($line =~ /$String\s*[Lu]?"/) { + if (WARN("STRING_FRAGMENTS", + "Consecutive strings are generally better as a single string\n" . $herecurr) && + $fix) { + while ($line =~ /($String)(?=\s*")/g) { + my $extracted_string = substr($rawline, $-[0], $+[0] - $-[0]); + $fixed[$fixlinenr] =~ s/\Q$extracted_string\E\s*"/substr($extracted_string, 0, -1)/e; + } + } + } + +# check for non-standard and hex prefixed decimal printf formats + my $show_L = 1; #don't show the same defect twice + my $show_Z = 1; + while ($line =~ /(?:^|")([X\t]*)(?:"|$)/g) { + my $string = substr($rawline, $-[1], $+[1] - $-[1]); + $string =~ s/%%/__/g; + # check for %L + if ($show_L && $string =~ /%[\*\d\.\$]*L([diouxX])/) { + WARN("PRINTF_L", + "\%L$1 is non-standard C, use %ll$1\n" . $herecurr); + $show_L = 0; + } + # check for %Z + if ($show_Z && $string =~ /%[\*\d\.\$]*Z([diouxX])/) { + WARN("PRINTF_Z", + "%Z$1 is non-standard C, use %z$1\n" . $herecurr); + $show_Z = 0; + } + # check for 0x<decimal> + if ($string =~ /0x%[\*\d\.\$\Llzth]*[diou]/) { + ERROR("PRINTF_0XDECIMAL", + "Prefixing 0x with decimal output is defective\n" . $herecurr); + } + } + +# check for line continuations in quoted strings with odd counts of " + if ($rawline =~ /\\$/ && $sline =~ tr/"/"/ % 2) { + WARN("LINE_CONTINUATIONS", + "Avoid line continuations in quoted strings\n" . $herecurr); + } + +# warn about #if 0 + if ($line =~ /^.\s*\#\s*if\s+0\b/) { + WARN("IF_0", + "Consider removing the code enclosed by this #if 0 and its #endif\n" . $herecurr); + } + +# warn about #if 1 + if ($line =~ /^.\s*\#\s*if\s+1\b/) { + WARN("IF_1", + "Consider removing the #if 1 and its #endif\n" . $herecurr); + } + +# check for needless "if (<foo>) fn(<foo>)" uses + if ($prevline =~ /\bif\s*\(\s*($Lval)\s*\)/) { + my $tested = quotemeta($1); + my $expr = '\s*\(\s*' . $tested . '\s*\)\s*;'; + if ($line =~ /\b(kfree|usb_free_urb|debugfs_remove(?:_recursive)?|(?:kmem_cache|mempool|dma_pool)_destroy)$expr/) { + my $func = $1; + if (WARN('NEEDLESS_IF', + "$func(NULL) is safe and this check is probably not required\n" . $hereprev) && + $fix) { + my $do_fix = 1; + my $leading_tabs = ""; + my $new_leading_tabs = ""; + if ($lines[$linenr - 2] =~ /^\+(\t*)if\s*\(\s*$tested\s*\)\s*$/) { + $leading_tabs = $1; + } else { + $do_fix = 0; + } + if ($lines[$linenr - 1] =~ /^\+(\t+)$func\s*\(\s*$tested\s*\)\s*;\s*$/) { + $new_leading_tabs = $1; + if (length($leading_tabs) + 1 ne length($new_leading_tabs)) { + $do_fix = 0; + } + } else { + $do_fix = 0; + } + if ($do_fix) { + fix_delete_line($fixlinenr - 1, $prevrawline); + $fixed[$fixlinenr] =~ s/^\+$new_leading_tabs/\+$leading_tabs/; + } + } + } + } + +# check for unnecessary "Out of Memory" messages + if ($line =~ /^\+.*\b$logFunctions\s*\(/ && + $prevline =~ /^[ \+]\s*if\s*\(\s*(\!\s*|NULL\s*==\s*)?($Lval)(\s*==\s*NULL\s*)?\s*\)/ && + (defined $1 || defined $3) && + $linenr > 3) { + my $testval = $2; + my $testline = $lines[$linenr - 3]; + + my ($s, $c) = ctx_statement_block($linenr - 3, $realcnt, 0); +# print("line: <$line>\nprevline: <$prevline>\ns: <$s>\nc: <$c>\n\n\n"); + + if ($s =~ /(?:^|\n)[ \+]\s*(?:$Type\s*)?\Q$testval\E\s*=\s*(?:\([^\)]*\)\s*)?\s*$allocFunctions\s*\(/ && + $s !~ /\b__GFP_NOWARN\b/ ) { + WARN("OOM_MESSAGE", + "Possible unnecessary 'out of memory' message\n" . $hereprev); + } + } + +# check for logging functions with KERN_<LEVEL> + if ($line !~ /printk(?:_ratelimited|_once)?\s*\(/ && + $line =~ /\b$logFunctions\s*\(.*\b(KERN_[A-Z]+)\b/) { + my $level = $1; + if (WARN("UNNECESSARY_KERN_LEVEL", + "Possible unnecessary $level\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ s/\s*$level\s*//; + } + } + +# check for logging continuations + if ($line =~ /\bprintk\s*\(\s*KERN_CONT\b|\bpr_cont\s*\(/) { + WARN("LOGGING_CONTINUATION", + "Avoid logging continuation uses where feasible\n" . $herecurr); + } + +# check for unnecessary use of %h[xudi] and %hh[xudi] in logging functions + if (defined $stat && + $line =~ /\b$logFunctions\s*\(/ && + index($stat, '"') >= 0) { + my $lc = $stat =~ tr@\n@@; + $lc = $lc + $linenr; + my $stat_real = get_stat_real($linenr, $lc); + pos($stat_real) = index($stat_real, '"'); + while ($stat_real =~ /[^\"%]*(%[\#\d\.\*\-]*(h+)[idux])/g) { + my $pspec = $1; + my $h = $2; + my $lineoff = substr($stat_real, 0, $-[1]) =~ tr@\n@@; + if (WARN("UNNECESSARY_MODIFIER", + "Integer promotion: Using '$h' in '$pspec' is unnecessary\n" . "$here\n$stat_real\n") && + $fix && $fixed[$fixlinenr + $lineoff] =~ /^\+/) { + my $nspec = $pspec; + $nspec =~ s/h//g; + $fixed[$fixlinenr + $lineoff] =~ s/\Q$pspec\E/$nspec/; + } + } + } + +# check for mask then right shift without a parentheses + if ($perl_version_ok && + $line =~ /$LvalOrFunc\s*\&\s*($LvalOrFunc)\s*>>/ && + $4 !~ /^\&/) { # $LvalOrFunc may be &foo, ignore if so + WARN("MASK_THEN_SHIFT", + "Possible precedence defect with mask then right shift - may need parentheses\n" . $herecurr); + } + +# check for pointer comparisons to NULL + if ($perl_version_ok) { + while ($line =~ /\b$LvalOrFunc\s*(==|\!=)\s*NULL\b/g) { + my $val = $1; + my $equal = "!"; + $equal = "" if ($4 eq "!="); + if (CHK("COMPARISON_TO_NULL", + "Comparison to NULL could be written \"${equal}${val}\"\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ s/\b\Q$val\E\s*(?:==|\!=)\s*NULL\b/$equal$val/; + } + } + } + +# check for bad placement of section $InitAttribute (e.g.: __initdata) + if ($line =~ /(\b$InitAttribute\b)/) { + my $attr = $1; + if ($line =~ /^\+\s*static\s+(?:const\s+)?(?:$attr\s+)?($NonptrTypeWithAttr)\s+(?:$attr\s+)?($Ident(?:\[[^]]*\])?)\s*[=;]/) { + my $ptr = $1; + my $var = $2; + if ((($ptr =~ /\b(union|struct)\s+$attr\b/ && + ERROR("MISPLACED_INIT", + "$attr should be placed after $var\n" . $herecurr)) || + ($ptr !~ /\b(union|struct)\s+$attr\b/ && + WARN("MISPLACED_INIT", + "$attr should be placed after $var\n" . $herecurr))) && + $fix) { + $fixed[$fixlinenr] =~ s/(\bstatic\s+(?:const\s+)?)(?:$attr\s+)?($NonptrTypeWithAttr)\s+(?:$attr\s+)?($Ident(?:\[[^]]*\])?)\s*([=;])\s*/"$1" . trim(string_find_replace($2, "\\s*$attr\\s*", " ")) . " " . trim(string_find_replace($3, "\\s*$attr\\s*", "")) . " $attr" . ("$4" eq ";" ? ";" : " = ")/e; + } + } + } + +# check for $InitAttributeData (ie: __initdata) with const + if ($line =~ /\bconst\b/ && $line =~ /($InitAttributeData)/) { + my $attr = $1; + $attr =~ /($InitAttributePrefix)(.*)/; + my $attr_prefix = $1; + my $attr_type = $2; + if (ERROR("INIT_ATTRIBUTE", + "Use of const init definition must use ${attr_prefix}initconst\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ + s/$InitAttributeData/${attr_prefix}initconst/; + } + } + +# check for $InitAttributeConst (ie: __initconst) without const + if ($line !~ /\bconst\b/ && $line =~ /($InitAttributeConst)/) { + my $attr = $1; + if (ERROR("INIT_ATTRIBUTE", + "Use of $attr requires a separate use of const\n" . $herecurr) && + $fix) { + my $lead = $fixed[$fixlinenr] =~ + /(^\+\s*(?:static\s+))/; + $lead = rtrim($1); + $lead = "$lead " if ($lead !~ /^\+$/); + $lead = "${lead}const "; + $fixed[$fixlinenr] =~ s/(^\+\s*(?:static\s+))/$lead/; + } + } + +# check for __read_mostly with const non-pointer (should just be const) + if ($line =~ /\b__read_mostly\b/ && + $line =~ /($Type)\s*$Ident/ && $1 !~ /\*\s*$/ && $1 =~ /\bconst\b/) { + if (ERROR("CONST_READ_MOSTLY", + "Invalid use of __read_mostly with const type\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ s/\s+__read_mostly\b//; + } + } + +# don't use __constant_<foo> functions outside of include/uapi/ + if ($realfile !~ m@^include/uapi/@ && + $line =~ /(__constant_(?:htons|ntohs|[bl]e(?:16|32|64)_to_cpu|cpu_to_[bl]e(?:16|32|64)))\s*\(/) { + my $constant_func = $1; + my $func = $constant_func; + $func =~ s/^__constant_//; + if (WARN("CONSTANT_CONVERSION", + "$constant_func should be $func\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ s/\b$constant_func\b/$func/g; + } + } + +# prefer usleep_range over udelay + if ($line =~ /\budelay\s*\(\s*(\d+)\s*\)/) { + my $delay = $1; + # ignore udelay's < 10, however + if (! ($delay < 10) ) { + CHK("USLEEP_RANGE", + "usleep_range is preferred over udelay; see function description of usleep_range() and udelay().\n" . $herecurr); + } + if ($delay > 2000) { + WARN("LONG_UDELAY", + "long udelay - prefer mdelay; see function description of mdelay().\n" . $herecurr); + } + } + +# warn about unexpectedly long msleep's + if ($line =~ /\bmsleep\s*\((\d+)\);/) { + if ($1 < 20) { + WARN("MSLEEP", + "msleep < 20ms can sleep for up to 20ms; see function description of msleep().\n" . $herecurr); + } + } + +# check for comparisons of jiffies + if ($line =~ /\bjiffies\s*$Compare|$Compare\s*jiffies\b/) { + WARN("JIFFIES_COMPARISON", + "Comparing jiffies is almost always wrong; prefer time_after, time_before and friends\n" . $herecurr); + } + +# check for comparisons of get_jiffies_64() + if ($line =~ /\bget_jiffies_64\s*\(\s*\)\s*$Compare|$Compare\s*get_jiffies_64\s*\(\s*\)/) { + WARN("JIFFIES_COMPARISON", + "Comparing get_jiffies_64() is almost always wrong; prefer time_after64, time_before64 and friends\n" . $herecurr); + } + +# warn about #ifdefs in C files +# if ($line =~ /^.\s*\#\s*if(|n)def/ && ($realfile =~ /\.c$/)) { +# print "#ifdef in C files should be avoided\n"; +# print "$herecurr"; +# $clean = 0; +# } + +# warn about spacing in #ifdefs + if ($line =~ /^.\s*\#\s*(ifdef|ifndef|elif)\s\s+/) { + if (ERROR("SPACING", + "exactly one space required after that #$1\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ + s/^(.\s*\#\s*(ifdef|ifndef|elif))\s{2,}/$1 /; + } + + } + +# check for spinlock_t definitions without a comment. + if ($line =~ /^.\s*(struct\s+mutex|spinlock_t)\s+\S+;/ || + $line =~ /^.\s*(DEFINE_MUTEX)\s*\(/) { + my $which = $1; + if (!ctx_has_comment($first_line, $linenr)) { + CHK("UNCOMMENTED_DEFINITION", + "$1 definition without comment\n" . $herecurr); + } + } +# check for memory barriers without a comment. + + my $barriers = qr{ + mb| + rmb| + wmb + }x; + my $barrier_stems = qr{ + mb__before_atomic| + mb__after_atomic| + store_release| + load_acquire| + store_mb| + (?:$barriers) + }x; + my $all_barriers = qr{ + (?:$barriers)| + smp_(?:$barrier_stems)| + virt_(?:$barrier_stems) + }x; + + if ($line =~ /\b(?:$all_barriers)\s*\(/) { + if (!ctx_has_comment($first_line, $linenr)) { + WARN("MEMORY_BARRIER", + "memory barrier without comment\n" . $herecurr); + } + } + + my $underscore_smp_barriers = qr{__smp_(?:$barrier_stems)}x; + + if ($realfile !~ m@^include/asm-generic/@ && + $realfile !~ m@/barrier\.h$@ && + $line =~ m/\b(?:$underscore_smp_barriers)\s*\(/ && + $line !~ m/^.\s*\#\s*define\s+(?:$underscore_smp_barriers)\s*\(/) { + WARN("MEMORY_BARRIER", + "__smp memory barriers shouldn't be used outside barrier.h and asm-generic\n" . $herecurr); + } + +# check for waitqueue_active without a comment. + if ($line =~ /\bwaitqueue_active\s*\(/) { + if (!ctx_has_comment($first_line, $linenr)) { + WARN("WAITQUEUE_ACTIVE", + "waitqueue_active without comment\n" . $herecurr); + } + } + +# check for data_race without a comment. + if ($line =~ /\bdata_race\s*\(/) { + if (!ctx_has_comment($first_line, $linenr)) { + WARN("DATA_RACE", + "data_race 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", + "architecture specific defines should be avoided\n" . $herecurr); + } + +# check that the storage class is not after a type + if ($line =~ /\b($Type)\s+($Storage)\b/) { + WARN("STORAGE_CLASS", + "storage class '$2' should be located before type '$1'\n" . $herecurr); + } +# Check that the storage class is at the beginning of a declaration + if ($line =~ /\b$Storage\b/ && + $line !~ /^.\s*$Storage/ && + $line =~ /^.\s*(.+?)\$Storage\s/ && + $1 !~ /[\,\)]\s*$/) { + WARN("STORAGE_CLASS", + "storage class should be at the beginning of the declaration\n" . $herecurr); + } + +# check the location of the inline attribute, that it is between +# storage class and type. + if ($line =~ /\b$Type\s+$Inline\b/ || + $line =~ /\b$Inline\s+$Storage\b/) { + ERROR("INLINE_LOCATION", + "inline keyword should sit between storage class and type\n" . $herecurr); + } + +# Check for __inline__ and __inline, prefer inline + if ($realfile !~ m@\binclude/uapi/@ && + $line =~ /\b(__inline__|__inline)\b/) { + if (WARN("INLINE", + "plain inline is preferred over $1\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ s/\b(__inline__|__inline)\b/inline/; + + } + } + +# Check for compiler attributes + if ($realfile !~ m@\binclude/uapi/@ && + $rawline =~ /\b__attribute__\s*\(\s*($balanced_parens)\s*\)/) { + my $attr = $1; + $attr =~ s/\s*\(\s*(.*)\)\s*/$1/; + + my %attr_list = ( + "alias" => "__alias", + "aligned" => "__aligned", + "always_inline" => "__always_inline", + "assume_aligned" => "__assume_aligned", + "cold" => "__cold", + "const" => "__attribute_const__", + "copy" => "__copy", + "designated_init" => "__designated_init", + "externally_visible" => "__visible", + "format" => "printf|scanf", + "gnu_inline" => "__gnu_inline", + "malloc" => "__malloc", + "mode" => "__mode", + "no_caller_saved_registers" => "__no_caller_saved_registers", + "noclone" => "__noclone", + "noinline" => "noinline", + "nonstring" => "__nonstring", + "noreturn" => "__noreturn", + "packed" => "__packed", + "pure" => "__pure", + "section" => "__section", + "used" => "__used", + "weak" => "__weak" + ); + + while ($attr =~ /\s*(\w+)\s*(${balanced_parens})?/g) { + my $orig_attr = $1; + my $params = ''; + $params = $2 if defined($2); + my $curr_attr = $orig_attr; + $curr_attr =~ s/^[\s_]+|[\s_]+$//g; + if (exists($attr_list{$curr_attr})) { + my $new = $attr_list{$curr_attr}; + if ($curr_attr eq "format" && $params) { + $params =~ /^\s*\(\s*(\w+)\s*,\s*(.*)/; + $new = "__$1\($2"; + } else { + $new = "$new$params"; + } + if (WARN("PREFER_DEFINED_ATTRIBUTE_MACRO", + "Prefer $new over __attribute__(($orig_attr$params))\n" . $herecurr) && + $fix) { + my $remove = "\Q$orig_attr\E" . '\s*' . "\Q$params\E" . '(?:\s*,\s*)?'; + $fixed[$fixlinenr] =~ s/$remove//; + $fixed[$fixlinenr] =~ s/\b__attribute__/$new __attribute__/; + $fixed[$fixlinenr] =~ s/\}\Q$new\E/} $new/; + $fixed[$fixlinenr] =~ s/ __attribute__\s*\(\s*\(\s*\)\s*\)//; + } + } + } + + # Check for __attribute__ unused, prefer __always_unused or __maybe_unused + if ($attr =~ /^_*unused/) { + WARN("PREFER_DEFINED_ATTRIBUTE_MACRO", + "__always_unused or __maybe_unused is preferred over __attribute__((__unused__))\n" . $herecurr); + } + } + +# Check for __attribute__ weak, or __weak declarations (may have link issues) + if ($perl_version_ok && + $line =~ /(?:$Declare|$DeclareMisordered)\s*$Ident\s*$balanced_parens\s*(?:$Attribute)?\s*;/ && + ($line =~ /\b__attribute__\s*\(\s*\(.*\bweak\b/ || + $line =~ /\b__weak\b/)) { + ERROR("WEAK_DECLARATION", + "Using weak declarations can have unintended link defects\n" . $herecurr); + } + +# check for c99 types like uint8_t used outside of uapi/ and tools/ + if ($realfile !~ m@\binclude/uapi/@ && + $realfile !~ m@\btools/@ && + $line =~ /\b($Declare)\s*$Ident\s*[=;,\[]/) { + my $type = $1; + if ($type =~ /\b($typeC99Typedefs)\b/) { + $type = $1; + my $kernel_type = 'u'; + $kernel_type = 's' if ($type =~ /^_*[si]/); + $type =~ /(\d+)/; + $kernel_type .= $1; + if (CHK("PREFER_KERNEL_TYPES", + "Prefer kernel type '$kernel_type' over '$type'\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ s/\b$type\b/$kernel_type/; + } + } + } + +# check for cast of C90 native int or longer types constants + if ($line =~ /(\(\s*$C90_int_types\s*\)\s*)($Constant)\b/) { + my $cast = $1; + my $const = $2; + my $suffix = ""; + my $newconst = $const; + $newconst =~ s/${Int_type}$//; + $suffix .= 'U' if ($cast =~ /\bunsigned\b/); + if ($cast =~ /\blong\s+long\b/) { + $suffix .= 'LL'; + } elsif ($cast =~ /\blong\b/) { + $suffix .= 'L'; + } + if (WARN("TYPECAST_INT_CONSTANT", + "Unnecessary typecast of c90 int constant - '$cast$const' could be '$const$suffix'\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ s/\Q$cast\E$const\b/$newconst$suffix/; + } + } + +# check for sizeof(&) + if ($line =~ /\bsizeof\s*\(\s*\&/) { + WARN("SIZEOF_ADDRESS", + "sizeof(& should be avoided\n" . $herecurr); + } + +# check for sizeof without parenthesis + if ($line =~ /\bsizeof\s+((?:\*\s*|)$Lval|$Type(?:\s+$Lval|))/) { + if (WARN("SIZEOF_PARENTHESIS", + "sizeof $1 should be sizeof($1)\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ s/\bsizeof\s+((?:\*\s*|)$Lval|$Type(?:\s+$Lval|))/"sizeof(" . trim($1) . ")"/ex; + } + } + +# check for struct spinlock declarations + if ($line =~ /^.\s*\bstruct\s+spinlock\s+\w+\s*;/) { + WARN("USE_SPINLOCK_T", + "struct spinlock should be spinlock_t\n" . $herecurr); + } + +# check for seq_printf uses that could be seq_puts + if ($sline =~ /\bseq_printf\s*\(.*"\s*\)\s*;\s*$/) { + my $fmt = get_quoted_string($line, $rawline); + $fmt =~ s/%%//g; + if ($fmt !~ /%/) { + if (WARN("PREFER_SEQ_PUTS", + "Prefer seq_puts to seq_printf\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ s/\bseq_printf\b/seq_puts/; + } + } + } + +# check for vsprintf extension %p<foo> misuses + if ($perl_version_ok && + defined $stat && + $stat =~ /^\+(?![^\{]*\{\s*).*\b(\w+)\s*\(.*$String\s*,/s && + $1 !~ /^_*volatile_*$/) { + my $stat_real; + + my $lc = $stat =~ tr@\n@@; + $lc = $lc + $linenr; + for (my $count = $linenr; $count <= $lc; $count++) { + my $specifier; + my $extension; + my $qualifier; + my $bad_specifier = ""; + my $fmt = get_quoted_string($lines[$count - 1], raw_line($count, 0)); + $fmt =~ s/%%//g; + + while ($fmt =~ /(\%[\*\d\.]*p(\w)(\w*))/g) { + $specifier = $1; + $extension = $2; + $qualifier = $3; + if ($extension !~ /[4SsBKRraEehMmIiUDdgVCbGNOxtf]/ || + ($extension eq "f" && + defined $qualifier && $qualifier !~ /^w/) || + ($extension eq "4" && + defined $qualifier && $qualifier !~ /^cc/)) { + $bad_specifier = $specifier; + last; + } + if ($extension eq "x" && !defined($stat_real)) { + if (!defined($stat_real)) { + $stat_real = get_stat_real($linenr, $lc); + } + WARN("VSPRINTF_SPECIFIER_PX", + "Using vsprintf specifier '\%px' potentially exposes the kernel memory layout, if you don't really need the address please consider using '\%p'.\n" . "$here\n$stat_real\n"); + } + } + if ($bad_specifier ne "") { + my $stat_real = get_stat_real($linenr, $lc); + my $msg_level = \&WARN; + my $ext_type = "Invalid"; + my $use = ""; + if ($bad_specifier =~ /p[Ff]/) { + $use = " - use %pS instead"; + $use =~ s/pS/ps/ if ($bad_specifier =~ /pf/); + } elsif ($bad_specifier =~ /pA/) { + $use = " - '%pA' is only intended to be used from Rust code"; + $msg_level = \&ERROR; + } + + &{$msg_level}("VSPRINTF_POINTER_EXTENSION", + "$ext_type vsprintf pointer extension '$bad_specifier'$use\n" . "$here\n$stat_real\n"); + } + } + } + +# Check for misused memsets + if ($perl_version_ok && + defined $stat && + $stat =~ /^\+(?:.*?)\bmemset\s*\(\s*$FuncArg\s*,\s*$FuncArg\s*\,\s*$FuncArg\s*\)/) { + + my $ms_addr = $2; + my $ms_val = $7; + my $ms_size = $12; + + if ($ms_size =~ /^(0x|)0$/i) { + ERROR("MEMSET", + "memset to 0's uses 0 as the 2nd argument, not the 3rd\n" . "$here\n$stat\n"); + } elsif ($ms_size =~ /^(0x|)1$/i) { + WARN("MEMSET", + "single byte memset is suspicious. Swapped 2nd/3rd argument?\n" . "$here\n$stat\n"); + } + } + +# Check for memcpy(foo, bar, ETH_ALEN) that could be ether_addr_copy(foo, bar) +# if ($perl_version_ok && +# defined $stat && +# $stat =~ /^\+(?:.*?)\bmemcpy\s*\(\s*$FuncArg\s*,\s*$FuncArg\s*\,\s*ETH_ALEN\s*\)/) { +# if (WARN("PREFER_ETHER_ADDR_COPY", +# "Prefer ether_addr_copy() over memcpy() if the Ethernet addresses are __aligned(2)\n" . "$here\n$stat\n") && +# $fix) { +# $fixed[$fixlinenr] =~ s/\bmemcpy\s*\(\s*$FuncArg\s*,\s*$FuncArg\s*\,\s*ETH_ALEN\s*\)/ether_addr_copy($2, $7)/; +# } +# } + +# Check for memcmp(foo, bar, ETH_ALEN) that could be ether_addr_equal*(foo, bar) +# if ($perl_version_ok && +# defined $stat && +# $stat =~ /^\+(?:.*?)\bmemcmp\s*\(\s*$FuncArg\s*,\s*$FuncArg\s*\,\s*ETH_ALEN\s*\)/) { +# WARN("PREFER_ETHER_ADDR_EQUAL", +# "Prefer ether_addr_equal() or ether_addr_equal_unaligned() over memcmp()\n" . "$here\n$stat\n") +# } + +# check for memset(foo, 0x0, ETH_ALEN) that could be eth_zero_addr +# check for memset(foo, 0xFF, ETH_ALEN) that could be eth_broadcast_addr +# if ($perl_version_ok && +# defined $stat && +# $stat =~ /^\+(?:.*?)\bmemset\s*\(\s*$FuncArg\s*,\s*$FuncArg\s*\,\s*ETH_ALEN\s*\)/) { +# +# my $ms_val = $7; +# +# if ($ms_val =~ /^(?:0x|)0+$/i) { +# if (WARN("PREFER_ETH_ZERO_ADDR", +# "Prefer eth_zero_addr over memset()\n" . "$here\n$stat\n") && +# $fix) { +# $fixed[$fixlinenr] =~ s/\bmemset\s*\(\s*$FuncArg\s*,\s*$FuncArg\s*,\s*ETH_ALEN\s*\)/eth_zero_addr($2)/; +# } +# } elsif ($ms_val =~ /^(?:0xff|255)$/i) { +# if (WARN("PREFER_ETH_BROADCAST_ADDR", +# "Prefer eth_broadcast_addr() over memset()\n" . "$here\n$stat\n") && +# $fix) { +# $fixed[$fixlinenr] =~ s/\bmemset\s*\(\s*$FuncArg\s*,\s*$FuncArg\s*,\s*ETH_ALEN\s*\)/eth_broadcast_addr($2)/; +# } +# } +# } + +# strcpy uses that should likely be strscpy + if ($line =~ /\bstrcpy\s*\(/) { + 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*\(/) { + 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*\(/) { + WARN("STRNCPY", + "Prefer strscpy, strscpy_pad, or __nonstring over strncpy - see: https://github.com/KSPP/linux/issues/90\n" . $herecurr); + } + +# ethtool_sprintf uses that should likely be ethtool_puts + if ($line =~ /\bethtool_sprintf\s*\(\s*$FuncArg\s*,\s*$FuncArg\s*\)/) { + if (WARN("PREFER_ETHTOOL_PUTS", + "Prefer ethtool_puts over ethtool_sprintf with only two arguments\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ s/\bethtool_sprintf\s*\(\s*($FuncArg)\s*,\s*($FuncArg)/ethtool_puts($1, $7)/; + } + } + + # use $rawline because $line loses %s via sanitization and thus we can't match against it. + if ($rawline =~ /\bethtool_sprintf\s*\(\s*$FuncArg\s*,\s*\"\%s\"\s*,\s*$FuncArg\s*\)/) { + if (WARN("PREFER_ETHTOOL_PUTS", + "Prefer ethtool_puts over ethtool_sprintf with standalone \"%s\" specifier\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ s/\bethtool_sprintf\s*\(\s*($FuncArg)\s*,\s*"\%s"\s*,\s*($FuncArg)/ethtool_puts($1, $7)/; + } + } + + +# typecasts on min/max could be min_t/max_t + if ($perl_version_ok && + defined $stat && + $stat =~ /^\+(?:.*?)\b(min|max)\s*\(\s*$FuncArg\s*,\s*$FuncArg\s*\)/) { + if (defined $2 || defined $7) { + my $call = $1; + my $cast1 = deparenthesize($2); + my $arg1 = $3; + my $cast2 = deparenthesize($7); + my $arg2 = $8; + my $cast; + + if ($cast1 ne "" && $cast2 ne "" && $cast1 ne $cast2) { + $cast = "$cast1 or $cast2"; + } elsif ($cast1 ne "") { + $cast = $cast1; + } else { + $cast = $cast2; + } + WARN("MINMAX", + "$call() should probably be ${call}_t($cast, $arg1, $arg2)\n" . "$here\n$stat\n"); + } + } + +# check usleep_range arguments + if ($perl_version_ok && + defined $stat && + $stat =~ /^\+(?:.*?)\busleep_range\s*\(\s*($FuncArg)\s*,\s*($FuncArg)\s*\)/) { + my $min = $1; + my $max = $7; + if ($min eq $max) { + WARN("USLEEP_RANGE", + "usleep_range should not use min == max args; see function description of usleep_range().\n" . "$here\n$stat\n"); + } elsif ($min =~ /^\d+$/ && $max =~ /^\d+$/ && + $min > $max) { + WARN("USLEEP_RANGE", + "usleep_range args reversed, use min then max; see function description of usleep_range().\n" . "$here\n$stat\n"); + } + } + +# check for naked sscanf + if ($perl_version_ok && + defined $stat && + $line =~ /\bsscanf\b/ && + ($stat !~ /$Ident\s*=\s*sscanf\s*$balanced_parens/ && + $stat !~ /\bsscanf\s*$balanced_parens\s*(?:$Compare)/ && + $stat !~ /(?:$Compare)\s*\bsscanf\s*$balanced_parens/)) { + my $lc = $stat =~ tr@\n@@; + $lc = $lc + $linenr; + my $stat_real = get_stat_real($linenr, $lc); + WARN("NAKED_SSCANF", + "unchecked sscanf return value\n" . "$here\n$stat_real\n"); + } + +# check for simple sscanf that should be kstrto<foo> + if ($perl_version_ok && + defined $stat && + $line =~ /\bsscanf\b/) { + my $lc = $stat =~ tr@\n@@; + $lc = $lc + $linenr; + my $stat_real = get_stat_real($linenr, $lc); + if ($stat_real =~ /\bsscanf\b\s*\(\s*$FuncArg\s*,\s*("[^"]+")/) { + my $format = $6; + my $count = $format =~ tr@%@%@; + if ($count == 1 && + $format =~ /^"\%(?i:ll[udxi]|[udxi]ll|ll|[hl]h?[udxi]|[udxi][hl]h?|[hl]h?|[udxi])"$/) { + WARN("SSCANF_TO_KSTRTO", + "Prefer kstrto<type> to single variable sscanf\n" . "$here\n$stat_real\n"); + } + } + } + +# check for new externs in .h files. + if ($realfile =~ /\.h$/ && + $line =~ /^\+\s*(extern\s+)$Type\s*$Ident\s*\(/s) { + if (CHK("AVOID_EXTERNS", + "extern prototypes should be avoided in .h files\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ s/(.*)\bextern\b\s*(.*)/$1$2/; + } + } + +# check for new externs in .c files. + if ($realfile =~ /\.c$/ && defined $stat && + $stat =~ /^.\s*(?:extern\s+)?$Type\s+($Ident)(\s*)\(/s) + { + my $function_name = $1; + my $paren_space = $2; + + my $s = $stat; + if (defined $cond) { + substr($s, 0, length($cond), ''); + } + if ($s =~ /^\s*;/) + { + WARN("AVOID_EXTERNS", + "externs should be avoided in .c files\n" . $herecurr); + } + + if ($paren_space =~ /\n/) { + WARN("FUNCTION_ARGUMENTS", + "arguments for function declarations should follow identifier\n" . $herecurr); + } + + } elsif ($realfile =~ /\.c$/ && defined $stat && + $stat =~ /^\+extern struct\s+(\w+)\s+(\w+)\[\];/) + { + my ($st_type, $st_name) = ($1, $2); + + for my $s (keys %maybe_linker_symbol) { + #print "Linker symbol? $st_name : $s\n"; + goto LIKELY_LINKER_SYMBOL + if $st_name =~ /$s/; + } + WARN("AVOID_EXTERNS", + "found a file-scoped extern type:$st_type name:$st_name in .c file\n" + . "is this a linker symbol ?\n" . $herecurr); + LIKELY_LINKER_SYMBOL: + + } elsif ($realfile =~ /\.c$/ && defined $stat && + $stat =~ /^.\s*extern\s+/) + { + WARN("AVOID_EXTERNS", + "externs should be avoided in .c files\n" . $herecurr); + } + +# check for function declarations that have arguments without identifier names + if (defined $stat && + $stat =~ /^.\s*(?:extern\s+)?$Type\s*(?:$Ident|\(\s*\*\s*$Ident\s*\))\s*\(\s*([^{]+)\s*\)\s*;/s && + $1 ne "void") { + my $args = trim($1); + while ($args =~ m/\s*($Type\s*(?:$Ident|\(\s*\*\s*$Ident?\s*\)\s*$balanced_parens)?)/g) { + my $arg = trim($1); + if ($arg =~ /^$Type$/ && $arg !~ /enum\s+$Ident$/) { + WARN("FUNCTION_ARGUMENTS", + "function definition argument '$arg' should also have an identifier name\n" . $herecurr); + } + } + } + +# check for function definitions + if ($perl_version_ok && + defined $stat && + $stat =~ /^.\s*(?:$Storage\s+)?$Type\s*($Ident)\s*$balanced_parens\s*{/s) { + $context_function = $1; + +# check for multiline function definition with misplaced open brace + my $ok = 0; + my $cnt = statement_rawlines($stat); + my $herectx = $here . "\n"; + for (my $n = 0; $n < $cnt; $n++) { + my $rl = raw_line($linenr, $n); + $herectx .= $rl . "\n"; + $ok = 1 if ($rl =~ /^[ \+]\{/); + $ok = 1 if ($rl =~ /\{/ && $n == 0); + last if $rl =~ /^[ \+].*\{/; + } + if (!$ok) { + ERROR("OPEN_BRACE", + "open brace '{' following function definitions go on the next line\n" . $herectx); + } + } + +# checks for new __setup's + if ($rawline =~ /\b__setup\("([^"]*)"/) { + my $name = $1; + + if (!grep(/$name/, @setup_docs)) { + CHK("UNDOCUMENTED_SETUP", + "__setup appears un-documented -- check Documentation/admin-guide/kernel-parameters.txt\n" . $herecurr); + } + } + +# check for pointless casting of alloc functions + if ($line =~ /\*\s*\)\s*$allocFunctions\b/) { + WARN("UNNECESSARY_CASTS", + "unnecessary cast may hide bugs, see http://c-faq.com/malloc/mallocnocast.html\n" . $herecurr); + } + +# alloc style +# p = alloc(sizeof(struct foo), ...) should be p = alloc(sizeof(*p), ...) + if ($perl_version_ok && + $line =~ /\b($Lval)\s*\=\s*(?:$balanced_parens)?\s*((?:kv|k|v)[mz]alloc(?:_node)?)\s*\(\s*(sizeof\s*\(\s*struct\s+$Lval\s*\))/) { + CHK("ALLOC_SIZEOF_STRUCT", + "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 + 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 $r1 = $a1; + my $r2 = $a2; + if ($a1 =~ /^sizeof\s*\S/) { + $r1 = $a2; + $r2 = $a1; + } + if ($r1 !~ /^sizeof\b/ && $r2 =~ /^sizeof\s*\S/ && + !($r1 =~ /^$Constant$/ || $r1 =~ /^[A-Z_][A-Z0-9_]*$/)) { + my $cnt = statement_rawlines($stat); + my $herectx = get_stat_here($linenr, $cnt, $here); + + if (WARN("ALLOC_WITH_MULTIPLY", + "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; + } + } + } + +# check for krealloc arg reuse + if ($perl_version_ok && + $line =~ /\b($Lval)\s*\=\s*(?:$balanced_parens)?\s*krealloc\s*\(\s*($Lval)\s*,/ && + $1 eq $3) { + WARN("KREALLOC_ARG_REUSE", + "Reusing the krealloc arg is almost always a bug\n" . $herecurr); + } + +# check for alloc argument mismatch + if ($line =~ /\b((?:devm_)?((?:k|kv)?(calloc|malloc_array)(?:_node)?))\s*\(\s*sizeof\b/) { + WARN("ALLOC_ARRAY_ARGS", + "$1 uses number as first arg, sizeof is generally wrong\n" . $herecurr); + } + +# check for multiple semicolons + if ($line =~ /;\s*;\s*$/) { + if (WARN("ONE_SEMICOLON", + "Statements terminations use 1 semicolon\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ s/(\s*;\s*){2,}$/;/g; + } + } + +# check for #defines like: 1 << <digit> that could be BIT(digit), it is not exported to uapi + if ($realfile !~ m@^include/uapi/@ && + $line =~ /#\s*define\s+\w+\s+\(?\s*1\s*([ulUL]*)\s*\<\<\s*(?:\d+|$Ident)\s*\)?/) { + my $ull = ""; + $ull = "_ULL" if (defined($1) && $1 =~ /ll/i); + if (CHK("BIT_MACRO", + "Prefer using the BIT$ull macro\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ s/\(?\s*1\s*[ulUL]*\s*<<\s*(\d+|$Ident)\s*\)?/BIT${ull}($1)/; + } + } + +# check for IS_ENABLED() without CONFIG_<FOO> ($rawline for comments too) + if ($rawline =~ /\bIS_ENABLED\s*\(\s*(\w+)\s*\)/ && $1 !~ /^${CONFIG_}/) { + WARN("IS_ENABLED_CONFIG", + "IS_ENABLED($1) is normally used as IS_ENABLED(${CONFIG_}$1)\n" . $herecurr); + } + +# check for #if defined CONFIG_<FOO> || defined CONFIG_<FOO>_MODULE + if ($line =~ /^\+\s*#\s*if\s+defined(?:\s*\(?\s*|\s+)(${CONFIG_}[A-Z_]+)\s*\)?\s*\|\|\s*defined(?:\s*\(?\s*|\s+)\1_MODULE\s*\)?\s*$/) { + my $config = $1; + if (WARN("PREFER_IS_ENABLED", + "Prefer IS_ENABLED(<FOO>) to ${CONFIG_}<FOO> || ${CONFIG_}<FOO>_MODULE\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] = "\+#if IS_ENABLED($config)"; + } + } + +# check for /* fallthrough */ like comment, prefer fallthrough; + my @fallthroughs = ( + 'fallthrough', + '@fallthrough@', + 'lint -fallthrough[ \t]*', + 'intentional(?:ly)?[ \t]*fall(?:(?:s | |-)[Tt]|t)hr(?:ough|u|ew)', + '(?:else,?\s*)?FALL(?:S | |-)?THR(?:OUGH|U|EW)[ \t.!]*(?:-[^\n\r]*)?', + 'Fall(?:(?:s | |-)[Tt]|t)hr(?:ough|u|ew)[ \t.!]*(?:-[^\n\r]*)?', + 'fall(?:s | |-)?thr(?:ough|u|ew)[ \t.!]*(?:-[^\n\r]*)?', + ); + if ($raw_comment ne '') { + foreach my $ft (@fallthroughs) { + if ($raw_comment =~ /$ft/) { + my $msg_level = \&WARN; + $msg_level = \&CHK if ($file); + &{$msg_level}("PREFER_FALLTHROUGH", + "Prefer 'fallthrough;' over fallthrough comment\n" . $herecurr); + last; + } + } + } + +# check for switch/default statements without a break; + if ($perl_version_ok && + defined $stat && + $stat =~ /^\+[$;\s]*(?:case[$;\s]+\w+[$;\s]*:[$;\s]*|)*[$;\s]*\bdefault[$;\s]*:[$;\s]*;/g) { + my $cnt = statement_rawlines($stat); + my $herectx = get_stat_here($linenr, $cnt, $here); + + WARN("DEFAULT_NO_BREAK", + "switch default: should use break\n" . $herectx); + } + +# check for gcc specific __FUNCTION__ + if ($line =~ /\b__FUNCTION__\b/) { + if (WARN("USE_FUNC", + "__func__ should be used instead of gcc specific __FUNCTION__\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ s/\b__FUNCTION__\b/__func__/g; + } + } + +# check for uses of __DATE__, __TIME__, __TIMESTAMP__ + while ($line =~ /\b(__(?:DATE|TIME|TIMESTAMP)__)\b/g) { + ERROR("DATE_TIME", + "Use of the '$1' macro makes the build non-deterministic\n" . $herecurr); + } + +# check for use of yield() + if ($line =~ /\byield\s*\(\s*\)/) { + WARN("YIELD", + "Using yield() is generally wrong. See yield() kernel-doc (sched/core.c)\n" . $herecurr); + } + +# check for comparisons against true and false + if ($line =~ /\+\s*(.*?)\b(true|false|$Lval)\s*(==|\!=)\s*(true|false|$Lval)\b(.*)$/i) { + my $lead = $1; + my $arg = $2; + my $test = $3; + my $otype = $4; + my $trail = $5; + my $op = "!"; + + ($arg, $otype) = ($otype, $arg) if ($arg =~ /^(?:true|false)$/i); + + my $type = lc($otype); + if ($type =~ /^(?:true|false)$/) { + if (("$test" eq "==" && "$type" eq "true") || + ("$test" eq "!=" && "$type" eq "false")) { + $op = ""; + } + + CHK("BOOL_COMPARISON", + "Using comparison to $otype is error prone\n" . $herecurr); + +## maybe suggesting a correct construct would better +## "Using comparison to $otype is error prone. Perhaps use '${lead}${op}${arg}${trail}'\n" . $herecurr); + + } + } + +# check for semaphores initialized locked + if ($line =~ /^.\s*sema_init.+,\W?0\W?\)/) { + WARN("CONSIDER_COMPLETION", + "consider using a completion\n" . $herecurr); + } + +# recommend kstrto* over simple_strto* and strict_strto* + if ($line =~ /\b((simple|strict)_(strto(l|ll|ul|ull)))\s*\(/) { + WARN("CONSIDER_KSTRTO", + "$1 is obsolete, use k$3 instead\n" . $herecurr); + } + +# check for __initcall(), use device_initcall() explicitly or more appropriate function please + if ($line =~ /^.\s*__initcall\s*\(/) { + WARN("USE_DEVICE_INITCALL", + "please use device_initcall() or more appropriate function instead of __initcall() (see include/linux/init.h)\n" . $herecurr); + } + +# check for spin_is_locked(), suggest lockdep instead + if ($line =~ /\bspin_is_locked\(/) { + WARN("USE_LOCKDEP", + "Where possible, use lockdep_assert_held instead of assertions based on spin_is_locked\n" . $herecurr); + } + +# check for deprecated apis + if ($line =~ /\b($deprecated_apis_search)\b\s*\(/) { + my $deprecated_api = $1; + my $new_api = $deprecated_apis{$deprecated_api}; + WARN("DEPRECATED_API", + "Deprecated use of '$deprecated_api', prefer '$new_api' instead\n" . $herecurr); + } + +# check for various structs that are normally const (ops, kgdb, device_tree) +# and avoid what seem like struct definitions 'struct foo {' + if (defined($const_structs) && + $line !~ /\bconst\b/ && + $line =~ /\bstruct\s+($const_structs)\b(?!\s*\{)/) { + WARN("CONST_STRUCT", + "struct $1 should normally be const\n" . $herecurr); + } + +# use of NR_CPUS is usually wrong +# ignore definitions of NR_CPUS and usage to define arrays as likely right +# ignore designated initializers using NR_CPUS + if ($line =~ /\bNR_CPUS\b/ && + $line !~ /^.\s*\s*#\s*if\b.*\bNR_CPUS\b/ && + $line !~ /^.\s*\s*#\s*define\b.*\bNR_CPUS\b/ && + $line !~ /^.\s*$Declare\s.*\[[^\]]*NR_CPUS[^\]]*\]/ && + $line !~ /\[[^\]]*\.\.\.[^\]]*NR_CPUS[^\]]*\]/ && + $line !~ /\[[^\]]*NR_CPUS[^\]]*\.\.\.[^\]]*\]/ && + $line !~ /^.\s*\.\w+\s*=\s*.*\bNR_CPUS\b/) + { + WARN("NR_CPUS", + "usage of NR_CPUS is often wrong - consider using cpu_possible(), num_possible_cpus(), for_each_possible_cpu(), etc\n" . $herecurr); + } + +# Use of __ARCH_HAS_<FOO> or ARCH_HAVE_<BAR> is wrong. + if ($line =~ /\+\s*#\s*define\s+((?:__)?ARCH_(?:HAS|HAVE)\w*)\b/) { + ERROR("DEFINE_ARCH_HAS", + "#define of '$1' is wrong - use Kconfig variables or standard guards instead\n" . $herecurr); + } + +# likely/unlikely comparisons similar to "(likely(foo) > 0)" + if ($perl_version_ok && + $line =~ /\b((?:un)?likely)\s*\(\s*$FuncArg\s*\)\s*$Compare/) { + WARN("LIKELY_MISUSE", + "Using $1 should generally have parentheses around the comparison\n" . $herecurr); + } + +# return sysfs_emit(foo, fmt, ...) fmt without newline + if ($line =~ /\breturn\s+sysfs_emit\s*\(\s*$FuncArg\s*,\s*($String)/ && + substr($rawline, $-[6], $+[6] - $-[6]) !~ /\\n"$/) { + my $offset = $+[6] - 1; + if (WARN("SYSFS_EMIT", + "return sysfs_emit(...) formats should include a terminating newline\n" . $herecurr) && + $fix) { + substr($fixed[$fixlinenr], $offset, 0) = '\\n'; + } + } + +# check for array definition/declarations that should use flexible arrays instead + if ($sline =~ /^[\+ ]\s*\}(?:\s*__packed)?\s*;\s*$/ && + $prevline =~ /^\+\s*(?:\}(?:\s*__packed\s*)?|$Type)\s*$Ident\s*\[\s*(0|1)\s*\]\s*;\s*$/) { + if (ERROR("FLEXIBLE_ARRAY", + "Use C99 flexible arrays - see https://docs.kernel.org/process/deprecated.html#zero-length-and-one-element-arrays\n" . $hereprev) && + $1 == '0' && $fix) { + $fixed[$fixlinenr - 1] =~ s/\[\s*0\s*\]/[]/; + } + } + +# nested likely/unlikely calls + if ($line =~ /\b(?:(?:un)?likely)\s*\(\s*!?\s*(IS_ERR(?:_OR_NULL|_VALUE)?|WARN)/) { + WARN("LIKELY_MISUSE", + "nested (un)?likely() calls, $1 already uses unlikely() internally\n" . $herecurr); + } + +# whine mightly about in_atomic + if ($line =~ /\bin_atomic\s*\(/) { + if ($realfile =~ m@^drivers/@) { + ERROR("IN_ATOMIC", + "do not use in_atomic in drivers\n" . $herecurr); + } elsif ($realfile !~ m@^kernel/@) { + WARN("IN_ATOMIC", + "use of in_atomic() is incorrect outside core kernel code\n" . $herecurr); + } + } + +# Complain about RCU Tasks Trace used outside of BPF (and of course, RCU). + our $rcu_trace_funcs = qr{(?x: + rcu_read_lock_trace | + rcu_read_lock_trace_held | + rcu_read_unlock_trace | + call_rcu_tasks_trace | + synchronize_rcu_tasks_trace | + rcu_barrier_tasks_trace | + rcu_request_urgent_qs_task + )}; + our $rcu_trace_paths = qr{(?x: + kernel/bpf/ | + include/linux/bpf | + net/bpf/ | + kernel/rcu/ | + include/linux/rcu + )}; + if ($line =~ /\b($rcu_trace_funcs)\s*\(/) { + if ($realfile !~ m{^$rcu_trace_paths}) { + WARN("RCU_TASKS_TRACE", + "use of RCU tasks trace is incorrect outside BPF or core RCU code\n" . $herecurr); + } + } + +# check for lockdep_set_novalidate_class + if ($line =~ /^.\s*lockdep_set_novalidate_class\s*\(/ || + $line =~ /__lockdep_no_validate__\s*\)/ ) { + if ($realfile !~ m@^kernel/lockdep@ && + $realfile !~ m@^include/linux/lockdep@ && + $realfile !~ m@^drivers/base/core@) { + ERROR("LOCKDEP", + "lockdep_no_validate class is reserved for device->mutex.\n" . $herecurr); + } + } + + if ($line =~ /debugfs_create_\w+.*\b$mode_perms_world_writable\b/ || + $line =~ /DEVICE_ATTR.*\b$mode_perms_world_writable\b/) { + WARN("EXPORTED_WORLD_WRITABLE", + "Exporting world writable files is usually an error. Consider more restrictive permissions.\n" . $herecurr); + } + +# check for DEVICE_ATTR uses that could be DEVICE_ATTR_<FOO> +# and whether or not function naming is typical and if +# DEVICE_ATTR permissions uses are unusual too + if ($perl_version_ok && + defined $stat && + $stat =~ /\bDEVICE_ATTR\s*\(\s*(\w+)\s*,\s*\(?\s*(\s*(?:${multi_mode_perms_string_search}|0[0-7]{3,3})\s*)\s*\)?\s*,\s*(\w+)\s*,\s*(\w+)\s*\)/) { + my $var = $1; + my $perms = $2; + my $show = $3; + my $store = $4; + my $octal_perms = perms_to_octal($perms); + if ($show =~ /^${var}_show$/ && + $store =~ /^${var}_store$/ && + $octal_perms eq "0644") { + if (WARN("DEVICE_ATTR_RW", + "Use DEVICE_ATTR_RW\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ s/\bDEVICE_ATTR\s*\(\s*$var\s*,\s*\Q$perms\E\s*,\s*$show\s*,\s*$store\s*\)/DEVICE_ATTR_RW(${var})/; + } + } elsif ($show =~ /^${var}_show$/ && + $store =~ /^NULL$/ && + $octal_perms eq "0444") { + if (WARN("DEVICE_ATTR_RO", + "Use DEVICE_ATTR_RO\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ s/\bDEVICE_ATTR\s*\(\s*$var\s*,\s*\Q$perms\E\s*,\s*$show\s*,\s*NULL\s*\)/DEVICE_ATTR_RO(${var})/; + } + } elsif ($show =~ /^NULL$/ && + $store =~ /^${var}_store$/ && + $octal_perms eq "0200") { + if (WARN("DEVICE_ATTR_WO", + "Use DEVICE_ATTR_WO\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ s/\bDEVICE_ATTR\s*\(\s*$var\s*,\s*\Q$perms\E\s*,\s*NULL\s*,\s*$store\s*\)/DEVICE_ATTR_WO(${var})/; + } + } elsif ($octal_perms eq "0644" || + $octal_perms eq "0444" || + $octal_perms eq "0200") { + my $newshow = "$show"; + $newshow = "${var}_show" if ($show ne "NULL" && $show ne "${var}_show"); + my $newstore = $store; + $newstore = "${var}_store" if ($store ne "NULL" && $store ne "${var}_store"); + my $rename = ""; + if ($show ne $newshow) { + $rename .= " '$show' to '$newshow'"; + } + if ($store ne $newstore) { + $rename .= " '$store' to '$newstore'"; + } + WARN("DEVICE_ATTR_FUNCTIONS", + "Consider renaming function(s)$rename\n" . $herecurr); + } else { + WARN("DEVICE_ATTR_PERMS", + "DEVICE_ATTR unusual permissions '$perms' used\n" . $herecurr); + } + } + +# Mode permission misuses where it seems decimal should be octal +# This uses a shortcut match to avoid unnecessary uses of a slow foreach loop +# o Ignore module_param*(...) uses with a decimal 0 permission as that has a +# specific definition of not visible in sysfs. +# o Ignore proc_create*(...) uses with a decimal 0 permission as that means +# use the default permissions + if ($perl_version_ok && + defined $stat && + $line =~ /$mode_perms_search/) { + foreach my $entry (@mode_permission_funcs) { + my $func = $entry->[0]; + my $arg_pos = $entry->[1]; + + my $lc = $stat =~ tr@\n@@; + $lc = $lc + $linenr; + my $stat_real = get_stat_real($linenr, $lc); + + my $skip_args = ""; + if ($arg_pos > 1) { + $arg_pos--; + $skip_args = "(?:\\s*$FuncArg\\s*,\\s*){$arg_pos,$arg_pos}"; + } + my $test = "\\b$func\\s*\\(${skip_args}($FuncArg(?:\\|\\s*$FuncArg)*)\\s*[,\\)]"; + if ($stat =~ /$test/) { + my $val = $1; + $val = $6 if ($skip_args ne ""); + if (!($func =~ /^(?:module_param|proc_create)/ && $val eq "0") && + (($val =~ /^$Int$/ && $val !~ /^$Octal$/) || + ($val =~ /^$Octal$/ && length($val) ne 4))) { + ERROR("NON_OCTAL_PERMISSIONS", + "Use 4 digit octal (0777) not decimal permissions\n" . "$here\n" . $stat_real); + } + if ($val =~ /^$Octal$/ && (oct($val) & 02)) { + ERROR("EXPORTED_WORLD_WRITABLE", + "Exporting writable files is usually an error. Consider more restrictive permissions.\n" . "$here\n" . $stat_real); + } + } + } + } + +# check for uses of S_<PERMS> that could be octal for readability + while ($line =~ m{\b($multi_mode_perms_string_search)\b}g) { + my $oval = $1; + my $octal = perms_to_octal($oval); + if (WARN("SYMBOLIC_PERMS", + "Symbolic permissions '$oval' are not preferred. Consider using octal permissions '$octal'.\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ s/\Q$oval\E/$octal/; + } + } + +# validate content of MODULE_LICENSE against list from include/linux/module.h + if ($line =~ /\bMODULE_LICENSE\s*\(\s*($String)\s*\)/) { + my $extracted_string = get_quoted_string($line, $rawline); + my $valid_licenses = qr{ + GPL| + GPL\ v2| + GPL\ and\ additional\ rights| + Dual\ BSD/GPL| + Dual\ MIT/GPL| + Dual\ MPL/GPL| + Proprietary + }x; + if ($extracted_string !~ /^"(?:$valid_licenses)"$/x) { + WARN("MODULE_LICENSE", + "unknown module license " . $extracted_string . "\n" . $herecurr); + } + if (!$file && $extracted_string eq '"GPL v2"') { + if (WARN("MODULE_LICENSE", + "Prefer \"GPL\" over \"GPL v2\" - see commit bf7fbeeae6db (\"module: Cure the MODULE_LICENSE \"GPL\" vs. \"GPL v2\" bogosity\")\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ s/\bMODULE_LICENSE\s*\(\s*"GPL v2"\s*\)/MODULE_LICENSE("GPL")/; + } + } + } + +# check for sysctl duplicate constants + if ($line =~ /\.extra[12]\s*=\s*&(zero|one|int_max)\b/) { + WARN("DUPLICATED_SYSCTL_CONST", + "duplicated sysctl range checking value '$1', consider using the shared one in include/linux/sysctl.h\n" . $herecurr); + } + } + + # If we have no input at all, then there is nothing to report on + # so just keep quiet. + if ($#rawlines == -1) { + exit(0); + } + + # In mailback mode only produce a report in the negative, for + # things that appear to be patches. + if ($mailback && ($clean == 1 || !$is_patch)) { + exit(0); + } + + # This is not a patch, and we are in 'no-patch' mode so + # just keep quiet. + if (!$chk_patch && !$is_patch) { + exit(0); + } + + if (!$is_patch && $filename !~ /cover-letter\.patch$/) { + ERROR("NOT_UNIFIED_DIFF", + "Does not appear to be a unified-diff format patch\n"); + } + if ($is_patch && $has_commit_log && $chk_fixes_tag) { + if ($needs_fixes_tag ne "" && !$is_revert && !$fixes_tag) { + WARN("MISSING_FIXES_TAG", + "The commit message has '$needs_fixes_tag', perhaps it also needs a 'Fixes:' tag?\n"); + } + } + if ($is_patch && $has_commit_log && $chk_signoff) { + if ($signoff == 0) { + ERROR("MISSING_SIGN_OFF", + "Missing Signed-off-by: line(s)\n"); + } elsif ($authorsignoff != 1) { + # authorsignoff values: + # 0 -> missing sign off + # 1 -> sign off identical + # 2 -> names and addresses match, comments mismatch + # 3 -> addresses match, names different + # 4 -> names match, addresses different + # 5 -> names match, addresses excluding subaddress details (refer RFC 5233) match + + my $sob_msg = "'From: $author' != 'Signed-off-by: $author_sob'"; + + if ($authorsignoff == 0) { + ERROR("NO_AUTHOR_SIGN_OFF", + "Missing Signed-off-by: line by nominal patch author '$author'\n"); + } elsif ($authorsignoff == 2) { + CHK("FROM_SIGN_OFF_MISMATCH", + "From:/Signed-off-by: email comments mismatch: $sob_msg\n"); + } elsif ($authorsignoff == 3) { + WARN("FROM_SIGN_OFF_MISMATCH", + "From:/Signed-off-by: email name mismatch: $sob_msg\n"); + } elsif ($authorsignoff == 4) { + WARN("FROM_SIGN_OFF_MISMATCH", + "From:/Signed-off-by: email address mismatch: $sob_msg\n"); + } elsif ($authorsignoff == 5) { + WARN("FROM_SIGN_OFF_MISMATCH", + "From:/Signed-off-by: email subaddress mismatch: $sob_msg\n"); + } + } + } + + print report_dump(); + if ($summary && !($clean == 1 && $quiet == 1)) { + print "$filename " if ($summary_file); + print "total: $cnt_error errors, $cnt_warn warnings, " . + (($check)? "$cnt_chk checks, " : "") . + "$cnt_lines lines checked\n"; + } + + if ($quiet == 0) { + # If there were any defects found and not already fixing them + if (!$clean and !$fix) { + print << "EOM" + +NOTE: For some of the reported defects, checkpatch may be able to + mechanically convert to the typical style using --fix or --fix-inplace. +EOM + } + # If there were whitespace errors which cleanpatch can fix + # then suggest that. + if ($rpt_cleaners) { + $rpt_cleaners = 0; + print << "EOM" + +NOTE: Whitespace errors detected. + You may wish to use scripts/cleanpatch or scripts/cleanfile +EOM + } + } + + if ($clean == 0 && $fix && + ("@rawlines" ne "@fixed" || + $#fixed_inserted >= 0 || $#fixed_deleted >= 0)) { + my $newfile = $filename; + $newfile .= ".EXPERIMENTAL-checkpatch-fixes" if (!$fix_inplace); + my $linecount = 0; + my $f; + + @fixed = fix_inserted_deleted_lines(\@fixed, \@fixed_inserted, \@fixed_deleted); + + open($f, '>', $newfile) + or die "$P: Can't open $newfile for write\n"; + foreach my $fixed_line (@fixed) { + $linecount++; + if ($file) { + if ($linecount > 3) { + $fixed_line =~ s/^\+//; + print $f $fixed_line . "\n"; + } + } else { + print $f $fixed_line . "\n"; + } + } + close($f); + + if (!$quiet) { + print << "EOM"; + +Wrote EXPERIMENTAL --fix correction(s) to '$newfile' + +Do _NOT_ trust the results written to this file. +Do _NOT_ submit these changes without inspecting them for correctness. + +This EXPERIMENTAL file is simply a convenience to help rewrite patches. +No warranties, expressed or implied... +EOM + } + } + + if ($quiet == 0) { + print "\n"; + if ($clean == 1) { + print "$vname has no obvious style problems and is ready for submission.\n"; + } else { + print "$vname has style problems, please review.\n"; + } + } + return $clean; +} From 58e7e36deeb899146ecda71f2c2cda000269a305 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Thu, 2 Jan 2025 21:10:11 +0100 Subject: [PATCH 005/310] Update AUTHORS for 3.17 and add script for that Signed-off-by: Bernd Schubert <bernd@bsbernd.com> --- AUTHORS | 28 ++++++++++++++++++++++++++++ dev-docs/extend-authors.sh | 35 +++++++++++++++++++++++++++++++++++ dev-docs/release-process.md | 2 +- 3 files changed, 64 insertions(+), 1 deletion(-) create mode 100755 dev-docs/extend-authors.sh diff --git a/AUTHORS b/AUTHORS index 146dae6a4..9fd3b49ac 100644 --- a/AUTHORS +++ b/AUTHORS @@ -231,3 +231,31 @@ Yuri Per <yuri@acronis.com> Zhansong Gao <zhsgao@hotmail.com> Zhiqiang Liu <liuzhiqiang26@huawei.com> zsugabubus <zsugabubus@users.noreply.github.com> + +# New authors since fuse-3.16.2 +farlongsignal <141166749+farlongsignal@users.noreply.github.com> +yangyun50 <149988609+yangyun50@users.noreply.github.com> +bigbrotherwei <1965867461@qq.com> +Caian Benedicto <2220062+Caian@users.noreply.github.com> +desertwitch <24509509+desertwitch@users.noreply.github.com> +SteveYang <40466358+SteveY4ng@users.noreply.github.com> +FredyVia <942513309@qq.com> +legezywzh <94814730+legezywzh@users.noreply.github.com> +CismonX <admin@cismon.net> +amitgeron <amit.geron@gmail.com> +Bernd Schubert <bernd@bsbernd.com> +Daniel Rosenberg <drosen@google.com> +Horst Birthelmer <hbirthelmer@ddn.com> +Joanne Koong <joannelkoong@gmail.com> +Josef Bacik <josef@toxicpanda.com> +Matthew <matthew@matthew-cash.com> +gandalfs_cat <meow@kittcat.dev> +MJ Harvey <mharvey@jumptrading.com> +Nils <nils@nilsand.re> +Norman Wilson <norman@teach.cs.toronto.edu> +leipeng <peng@topling.cn> +Vladimir Serbinenko <phcoder@gmail.com> +George Hilliard <thirtythreeforty@gmail.com> +Tyler Hall <tylerwhall@gmail.com> +yangyun <yangyun50@huawei.com> +Abhishek <abhi_45645@yahoo.com> diff --git a/dev-docs/extend-authors.sh b/dev-docs/extend-authors.sh new file mode 100755 index 000000000..30e7dff4b --- /dev/null +++ b/dev-docs/extend-authors.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +# Check if a starting tag is provided +if [ $# -eq 0 ]; then + echo "Usage: $0 <starting_tag>" + echo "Example: $0 fuse-3.16.2" + exit 1 +fi + +START_TAG=$1 + +# Extract email addresses from git log +git_emails=$(git log ${START_TAG}..HEAD --format='<%aE>' | sort -u | sed 's/^<//;s/>$//') + +# Extract email addresses from AUTHORS file +authors_emails=$(grep -oP '(?<=<)[^>]+' AUTHORS | sort -u) + +# Find new email addresses (in git_emails but not in authors_emails) +# -1 suppresses lines unique to AUTHORS, -3 suppresses lines common to both +# Result: only lines unique to git_emails (i.e., new authors) +new_emails=$(comm -1 -3 <(echo "$authors_emails") <(echo "$git_emails")) + +# If there are new email addresses, add corresponding authors to the AUTHORS file +if [ -n "$new_emails" ]; then + echo -e "\nNew authors to be added:" + echo -e "\n# New authors since ${START_TAG}" >> AUTHORS + for email in $new_emails; do + author=$(git log -1 --format='%aN <%aE>' --author="$email") + echo "$author" + echo "$author" >> AUTHORS + done + echo "AUTHORS file has been updated." +else + echo "No new authors found since ${START_TAG}." +fi diff --git a/dev-docs/release-process.md b/dev-docs/release-process.md index c466d947a..fe4c225d1 100644 --- a/dev-docs/release-process.md +++ b/dev-docs/release-process.md @@ -10,7 +10,7 @@ Release Process * Create signing key for the next release: `P=fuse-<A.B+1> signify-openbsd -G -n -p signify/$P.pub -s signify/$P.sec; git add signify/$P.pub` * Expire old release signing keys (keep one around just in case) -* Update authors: `git log --all --pretty="format:%an <%aE>" | sort -u >> AUTHORS` +* To update authors run : dev-docs/extend-authors.sh * `git commit --all -m "Released $TAG"` * `git tag $TAG` * Build tarball, `./make_release_tarball.sh` From fb1168669875312155da984dd4915f130bed091e Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Thu, 2 Jan 2025 21:36:13 +0100 Subject: [PATCH 006/310] .github/workflows/codespell.yml: checkpatch.pl needs to be skipped checkpatch.pl has a list of mispelled words and the codespell test fails on that. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> --- .github/workflows/codespell.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml index 045bb207f..3b771c1ea 100644 --- a/.github/workflows/codespell.yml +++ b/.github/workflows/codespell.yml @@ -24,3 +24,5 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Codespell uses: codespell-project/actions-codespell@406322ec52dd7b488e48c1c4b82e2a8b3a1bf630 # v2.1 + with: + skip: checkpatch.pl From fbc7b01e5e92c62fd8673c0b500803023d1e80f7 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Thu, 2 Jan 2025 21:38:30 +0100 Subject: [PATCH 007/310] Fix the checkpatch.pl workflow - The MAINTAINERS test is not valid for libfuse. - Correct the base commit Signed-off-by: Bernd Schubert <bernd@bsbernd.com> --- .github/workflows/checkpatch.yml | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/workflows/checkpatch.yml b/.github/workflows/checkpatch.yml index 9a3cd1b7f..3f0eb9f07 100644 --- a/.github/workflows/checkpatch.yml +++ b/.github/workflows/checkpatch.yml @@ -18,5 +18,14 @@ jobs: - name: Run checkpatch.pl run: | git fetch origin ${{ github.base_ref }} - base_commit=$(git merge-base FETCH_HEAD ${{ github.event.pull_request.head.sha }}) - ./checkpatch.pl --no-tree -g $base_commit + 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 + 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 -g $commit; then + echo "checkpatch.pl found issues in commit $commit - $subject" + exit 1 + fi + done From 9eb6d6236e523587ad2db5e4a679e929d422e6a9 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Thu, 2 Jan 2025 22:45:43 +0100 Subject: [PATCH 008/310] Remove the DCO check Actually checkpatch.pl already checks for "Signed-off-by" and also handles merge commits - no need for duplicate logic. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> --- .github/workflows/dco.yml | 28 ---------------------------- 1 file changed, 28 deletions(-) delete mode 100644 .github/workflows/dco.yml diff --git a/.github/workflows/dco.yml b/.github/workflows/dco.yml deleted file mode 100644 index 1a2121d63..000000000 --- a/.github/workflows/dco.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: DCO Check (Developer Certificate of Origin) - -on: - pull_request: - types: [opened, synchronize, reopened] - -jobs: - check-dco: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - fetch-depth: 0 - - name: Check DCO - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_REPO: ${{ github.repository }} - run: | - echo "Checking DCO for PR #${{ github.event.pull_request.number }}" - commits=$(gh pr view ${{ github.event.pull_request.number }} --json commits --jq '.commits[].oid') - for commit in $commits - do - if ! git log --format='%B' -n 1 $commit | grep -q "Signed-off-by:"; then - echo "Commit $commit is missing Signed-off-by line" - exit 1 - fi - done - echo "All commits have Signed-off-by lines" From aca2557ba40c81c10244a06f29e364a3ece429b6 Mon Sep 17 00:00:00 2001 From: Amir Goldstein <amir73il@gmail.com> Date: Wed, 1 Jan 2025 18:16:43 +0100 Subject: [PATCH 009/310] Fix libfuse build with FUSE_USE_VERSION 30 Signed-off-by: Amir Goldstein <amir73il@gmail.com> --- include/fuse.h | 4 +++- test/test_setattr.c | 3 +++ test/test_write_cache.c | 3 +++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/include/fuse.h b/include/fuse.h index 2fc1b6fa9..dbfa439d9 100644 --- a/include/fuse.h +++ b/include/fuse.h @@ -1013,7 +1013,9 @@ struct fuse *_fuse_new(struct fuse_args *args, #if FUSE_USE_VERSION == 30 struct fuse *_fuse_new_30(struct fuse_args *args, const struct fuse_operations *op, - size_t op_size, void *user_data); + size_t op_size, + struct libfuse_version *version, + void *user_data); static inline struct fuse * fuse_new(struct fuse_args *args, const struct fuse_operations *op, size_t op_size, diff --git a/test/test_setattr.c b/test/test_setattr.c index 8575203d1..ac552644c 100644 --- a/test/test_setattr.c +++ b/test/test_setattr.c @@ -9,6 +9,9 @@ #define FUSE_USE_VERSION 30 +/* Not really needed - just to test build with FUSE_USE_VERSION == 30 */ +#include <fuse.h> + #include <fuse_config.h> #include <fuse_lowlevel.h> #include <stdio.h> diff --git a/test/test_write_cache.c b/test/test_write_cache.c index d3c7af086..0c8fa7e8b 100644 --- a/test/test_write_cache.c +++ b/test/test_write_cache.c @@ -9,6 +9,9 @@ #define FUSE_USE_VERSION 30 +/* Not really needed - just to test build with FUSE_USE_VERSION == 30 */ +#include <fuse.h> + #include <fuse_config.h> #include <fuse_lowlevel.h> #include <stdio.h> From 9b92c988eed637c8186360481b949ffa4397e942 Mon Sep 17 00:00:00 2001 From: Amir Goldstein <amir73il@gmail.com> Date: Fri, 3 Jan 2025 12:57:15 +0100 Subject: [PATCH 010/310] Fix build of memfs_ll without manual meson reconfigure After pulling latest code, memfs_ll build would fail because it builds with C++11. Changing the default of cpp_std in meson.build is not enough to fix this problem even if user runs 'meson setup --reconfigure'. I had to run 'meson setup -Dcpp_std= --reconfigure' to fix the build as mentioned in this meson issue: https://github.com/mesonbuild/meson/issues/8062#issuecomment-1568249672 Signed-off-by: Amir Goldstein <amir73il@gmail.com> --- example/meson.build | 1 + 1 file changed, 1 insertion(+) diff --git a/example/meson.build b/example/meson.build index 5c02cc1f3..b2e896cc4 100644 --- a/example/meson.build +++ b/example/meson.build @@ -37,6 +37,7 @@ if not platform.endswith('bsd') and platform != 'dragonfly' and add_languages('c install: false) executable('memfs_ll', 'memfs_ll.cc', dependencies: [ thread_dep, libfuse_dep ], + cpp_args : '-std=c++17', install: false) endif From 52d4791e83852b485a3b22ab99f22cc37073aa70 Mon Sep 17 00:00:00 2001 From: Amir Goldstein <amir73il@gmail.com> Date: Thu, 2 Jan 2025 20:25:13 +0100 Subject: [PATCH 011/310] example/passthrough: Enable testing of readdirplus without fill offsets passthrough example supports the --plus command line argument to reply to readdirplus with fill_dir_plus and unspecified (0) fill offsets. As explained in this comment: https://github.com/libfuse/libfuse/pull/896#issuecomment-1978917041 passthrough example needs a few more changes to be able to test commit dd95d13a ("fix readdirplus when filler is called with zero offset (#896)) With the changes in this commit, readdirplus without fill offsets can be tested to verify the readdirplus fix above with command line: passthrough --plus -o auto_cache,modules=subdir,subdir=/src /mnt Signed-off-by: Amir Goldstein <amir73il@gmail.com> --- example/passthrough.c | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/example/passthrough.c b/example/passthrough.c index 30a8ad51c..cc93f53ee 100644 --- a/example/passthrough.c +++ b/example/passthrough.c @@ -73,9 +73,11 @@ static void *xmp_init(struct fuse_conn_info *conn, the cache of the associated inode - resulting in an incorrect st_nlink value being reported for any remaining hardlinks to this inode. */ - cfg->entry_timeout = 0; - cfg->attr_timeout = 0; - cfg->negative_timeout = 0; + if (!cfg->auto_cache) { + cfg->entry_timeout = 0; + cfg->attr_timeout = 0; + cfg->negative_timeout = 0; + } return NULL; } @@ -134,9 +136,14 @@ static int xmp_readdir(const char *path, void *buf, fuse_fill_dir_t filler, while ((de = readdir(dp)) != NULL) { struct stat st; - memset(&st, 0, sizeof(st)); - st.st_ino = de->d_ino; - st.st_mode = de->d_type << 12; + if (fill_dir_plus) { + fstatat(dirfd(dp), de->d_name, &st, + AT_SYMLINK_NOFOLLOW); + } else { + memset(&st, 0, sizeof(st)); + st.st_ino = de->d_ino; + st.st_mode = de->d_type << 12; + } if (filler(buf, de->d_name, &st, 0, fill_dir_plus)) break; } From 8d34e367f76cb5d28671dd992e6a568b53845b3a Mon Sep 17 00:00:00 2001 From: Amir Goldstein <amir73il@gmail.com> Date: Thu, 2 Jan 2025 21:14:01 +0100 Subject: [PATCH 012/310] Fix junk readdirplus results when filesystem not filling stat info Commit dd95d13a ("fix readdirplus when filler is called with zero offset (#896)) broke readdirplus with passthrough example command: passthrough -o auto_cache,modules=subdir,subdir=/src /mnt The /src directory looks like this: ~# ls -l /src total 0 drwx------ 3 root root 60 Jan 2 17:51 testdir And the fuse directory looks like this: ~# ls -l /mnt total 0 d--------- 0 root root 0 Jan 1 1970 testdir Because readdir_fill_from_list() ignores the fact that filesystem did not pass the FUSE_FILL_DIR_PLUS flag with valid stat info. Signed-off-by: Amir Goldstein <amir73il@gmail.com> --- lib/fuse.c | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/fuse.c b/lib/fuse.c index a1537afd1..aa5c61104 100644 --- a/lib/fuse.c +++ b/lib/fuse.c @@ -163,6 +163,7 @@ struct node_lru { struct fuse_direntry { struct stat stat; + enum fuse_fill_dir_flags flags; char *name; struct fuse_direntry *next; }; @@ -3437,7 +3438,7 @@ static int extend_contents(struct fuse_dh *dh, unsigned minsize) } static int fuse_add_direntry_to_dh(struct fuse_dh *dh, const char *name, - struct stat *st) + struct stat *st, enum fuse_fill_dir_flags flags) { struct fuse_direntry *de; @@ -3452,6 +3453,7 @@ static int fuse_add_direntry_to_dh(struct fuse_dh *dh, const char *name, free(de); return -1; } + de->flags = flags; de->stat = *st; de->next = NULL; @@ -3529,7 +3531,7 @@ static int fill_dir(void *dh_, const char *name, const struct stat *statp, } else { dh->filled = 1; - if (fuse_add_direntry_to_dh(dh, name, &stbuf) == -1) + if (fuse_add_direntry_to_dh(dh, name, &stbuf, flags) == -1) return 1; } return 0; @@ -3607,7 +3609,7 @@ static int fill_dir_plus(void *dh_, const char *name, const struct stat *statp, } else { dh->filled = 1; - if (fuse_add_direntry_to_dh(dh, name, &e.attr) == -1) + if (fuse_add_direntry_to_dh(dh, name, &e.attr, flags) == -1) return 1; } @@ -3695,7 +3697,8 @@ static int readdir_fill_from_list(fuse_req_t req, struct fuse_dh *dh, .attr = de->stat, }; - if (!is_dot_or_dotdot(de->name)) { + if (de->flags & FUSE_FILL_DIR_PLUS && + !is_dot_or_dotdot(de->name)) { res = do_lookup(dh->fuse, dh->nodeid, de->name, &e); if (res) { From 0636f1fd5bf4c88f0fe04166c906a95a53837c35 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Sat, 4 Jan 2025 16:08:30 +0100 Subject: [PATCH 013/310] Fix fuse_main_real symbols Commit 58f85bfa9b7d ("Add in the libfuse version a program...") forgot to add a fuse_main_real function for libfuse compilations that are not symboled. That is now added in compat.c. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> --- lib/compat.c | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/lib/compat.c b/lib/compat.c index bab3f88c6..ce8a93399 100644 --- a/lib/compat.c +++ b/lib/compat.c @@ -17,12 +17,15 @@ #include "libfuse_config.h" +#include <stddef.h> + struct fuse_args; struct fuse_cmdline_opts; struct fuse_cmdline_opts; struct fuse_session; struct fuse_custom_io; - +struct fuse_operations; +struct fuse_lowlevel_ops; /** * Compatibility ABI symbol for systems that do not support version symboling @@ -36,7 +39,7 @@ struct fuse_custom_io; #undef fuse_parse_cmdline #endif int fuse_parse_cmdline_30(struct fuse_args *args, - struct fuse_cmdline_opts *opts); + struct fuse_cmdline_opts *opts); int fuse_parse_cmdline(struct fuse_args *args, struct fuse_cmdline_opts *opts); int fuse_parse_cmdline(struct fuse_args *args, @@ -55,6 +58,30 @@ int fuse_session_custom_io(struct fuse_session *se, { return fuse_session_custom_io_30(se, io, fd); } + +int fuse_main_real_30(int argc, char *argv[], const struct fuse_operations *op, + size_t op_size, void *user_data); +int fuse_main_real(int argc, char *argv[], const struct fuse_operations *op, + size_t op_size, void *user_data); +int fuse_main_real(int argc, char *argv[], const struct fuse_operations *op, + size_t op_size, void *user_data) +{ + return fuse_main_real_30(argc, argv, op, op_size, user_data); +} + +struct fuse_session *fuse_session_new_30(struct fuse_args *args, + const struct fuse_lowlevel_ops *op, + size_t op_size, void *userdata); +struct fuse_session *fuse_session_new(struct fuse_args *args, + const struct fuse_lowlevel_ops *op, + size_t op_size, void *userdata); +struct fuse_session *fuse_session_new(struct fuse_args *args, + const struct fuse_lowlevel_ops *op, + size_t op_size, void *userdata) +{ + return fuse_session_new_30(args, op, op_size, userdata); +} + #endif From 1c58dc6c0e4001334b20290d24d20cec9f81e638 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Sat, 4 Jan 2025 20:29:18 +0100 Subject: [PATCH 014/310] Avoid global declarion of internal functions that are new in 3.17 _fuse_new() is not supposed to be called by external users outside of internal functions or static inlined functions. This also removes several functions from lib/fuse_versionscript which where added and exported by commit 58f85bfa9b7d ("Add in the libfuse version a program...) as these are libfuse internal only. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> --- include/fuse.h | 25 ++++++++++++++++--------- include/fuse_lowlevel.h | 26 +++++++------------------- lib/fuse.c | 6 ++++++ lib/fuse_lowlevel.c | 13 +++++++++---- lib/fuse_versionscript | 4 ++-- lib/helper.c | 4 ++++ 6 files changed, 44 insertions(+), 34 deletions(-) diff --git a/include/fuse.h b/include/fuse.h index dbfa439d9..c3add843a 100644 --- a/include/fuse.h +++ b/include/fuse.h @@ -76,7 +76,7 @@ enum fuse_fill_dir_flags { * stream. It does not need to be the actual physical position. A * value of zero is reserved to indicate that seeking in directories * is not supported. - * + * * @param buf the buffer passed to the readdir() operation * @param name the file name of the directory entry * @param stbuf file attributes, can be NULL @@ -192,7 +192,7 @@ struct fuse_config { * have to guarantee uniqueness, however some applications * rely on this value being unique for the whole filesystem. * - * Note that this does *not* affect the inode that libfuse + * Note that this does *not* affect the inode that libfuse * and the kernel use internally (also called the "nodeid"). */ int32_t use_ino; @@ -522,9 +522,9 @@ struct fuse_operations { * * Flush is called on each close() of a file descriptor, as opposed to * release which is called on the close of the last file descriptor for - * a file. Under Linux, errors returned by flush() will be passed to + * a file. Under Linux, errors returned by flush() will be passed to * userspace as errors from close(), so flush() is a good place to write - * back any cached dirty data. However, many applications ignore errors + * back any cached dirty data. However, many applications ignore errors * on close(), and on non-Linux systems, close() may succeed even if flush() * returns an error. For these reasons, filesystems should not assume * that errors returned by flush will ever be noticed or even @@ -978,11 +978,6 @@ fuse_main(int argc, char *argv[], const struct fuse_operations *op, */ void fuse_lib_help(struct fuse_args *args); -struct fuse *_fuse_new(struct fuse_args *args, - const struct fuse_operations *op, - size_t op_size, struct libfuse_version *version, - void *user_data); - /** * Create a new FUSE filesystem. * @@ -1021,6 +1016,12 @@ fuse_new(struct fuse_args *args, const struct fuse_operations *op, size_t op_size, void *user_data) { + /* not declared globally, to restrict usage of this function */ + struct fuse *_fuse_new(struct fuse_args *args, + const struct fuse_operations *op, size_t op_size, + struct libfuse_version *version, + void *user_data); + struct libfuse_version version = { .major = FUSE_MAJOR_VERSION, .minor = FUSE_MINOR_VERSION, @@ -1044,6 +1045,12 @@ fuse_new(struct fuse_args *args, .padding = 0 }; + /* not declared globally, to restrict usage of this function */ + struct fuse *_fuse_new(struct fuse_args *args, + const struct fuse_operations *op, size_t op_size, + struct libfuse_version *version, + void *user_data); + return _fuse_new(args, op, op_size, &version, user_data); } #else /* LIBFUSE_BUILT_WITH_VERSIONED_SYMBOLS */ diff --git a/include/fuse_lowlevel.h b/include/fuse_lowlevel.h index 3d398dec5..0fa20390d 100644 --- a/include/fuse_lowlevel.h +++ b/include/fuse_lowlevel.h @@ -2045,26 +2045,8 @@ int fuse_parse_cmdline_312(struct fuse_args *args, #endif #endif -/* - * This should mostly not be called directly, but instead the fuse_session_new() - * macro should be used, which fills in the libfuse version compilation - * is done against automatically. - */ -struct fuse_session *_fuse_session_new_317(struct fuse_args *args, - const struct fuse_lowlevel_ops *op, - size_t op_size, - struct libfuse_version *version, - void *userdata); - /* Do not call this directly, but only through fuse_session_new() */ -#if (defined(LIBFUSE_BUILT_WITH_VERSIONED_SYMBOLS)) -struct fuse_session * -_fuse_session_new(struct fuse_args *args, - const struct fuse_lowlevel_ops *op, - size_t op_size, - struct libfuse_version *version, - void *userdata); -#else +#if (!defined(LIBFUSE_BUILT_WITH_VERSIONED_SYMBOLS)) struct fuse_session * _fuse_session_new_317(struct fuse_args *args, const struct fuse_lowlevel_ops *op, @@ -2116,6 +2098,12 @@ fuse_session_new(struct fuse_args *args, .padding = 0 }; + /* not declared globally, to restrict usage of this function */ + struct fuse_session *_fuse_session_new( + struct fuse_args *args, const struct fuse_lowlevel_ops *op, + size_t op_size, struct libfuse_version *version, + void *userdata); + return _fuse_session_new(args, op, op_size, &version, userdata); } diff --git a/lib/fuse.c b/lib/fuse.c index aa5c61104..8fbc035ba 100644 --- a/lib/fuse.c +++ b/lib/fuse.c @@ -5009,6 +5009,12 @@ struct fuse *_fuse_new_317(struct fuse_args *args, f->conf.readdir_ino = 1; #endif + /* not declared globally, to restrict usage of this function */ + struct fuse_session *_fuse_session_new( + struct fuse_args *args, const struct fuse_lowlevel_ops *op, + size_t op_size, struct libfuse_version *version, + void *userdata); + f->se = _fuse_session_new(args, &llop, sizeof(llop), version, f); if (f->se == NULL) goto out_free_fs; diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index d84c6781e..c993860cd 100644 --- a/lib/fuse_lowlevel.c +++ b/lib/fuse_lowlevel.c @@ -3248,10 +3248,15 @@ int fuse_session_receive_buf_internal(struct fuse_session *se, FUSE_SYMVER("_fuse_session_new_317", "_fuse_session_new@@FUSE_3.17") struct fuse_session *_fuse_session_new_317(struct fuse_args *args, - const struct fuse_lowlevel_ops *op, - size_t op_size, - struct libfuse_version *version, - void *userdata) + const struct fuse_lowlevel_ops *op, + size_t op_size, + struct libfuse_version *version, + void *userdata); +struct fuse_session *_fuse_session_new_317(struct fuse_args *args, + const struct fuse_lowlevel_ops *op, + size_t op_size, + struct libfuse_version *version, + void *userdata) { int err; struct fuse_session *se; diff --git a/lib/fuse_versionscript b/lib/fuse_versionscript index 14cbca142..f20e2b43e 100644 --- a/lib/fuse_versionscript +++ b/lib/fuse_versionscript @@ -189,11 +189,11 @@ FUSE_3.12 { FUSE_3.17 { global: +#if !defined(LIBFUSE_BUILT_WITH_VERSIONED_SYMBOLS) _fuse_session_new_317; - _fuse_new; - _fuse_new_30; _fuse_new_317; fuse_main_real_317; +#endif fuse_passthrough_open; fuse_passthrough_close; fuse_session_custom_io_30; diff --git a/lib/helper.c b/lib/helper.c index e84c857ce..2794e66bb 100644 --- a/lib/helper.c +++ b/lib/helper.c @@ -344,6 +344,10 @@ int fuse_main_real_317(int argc, char *argv[], const struct fuse_operations *op, goto out1; } + struct fuse *_fuse_new(struct fuse_args *args, + const struct fuse_operations *op, size_t op_size, + struct libfuse_version *version, + void *user_data); fuse = _fuse_new(&args, op, op_size, version, user_data); if (fuse == NULL) { res = 3; From a1e5aded5942e78472e6d60846a3f4e769d13680 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Sat, 4 Jan 2025 22:06:31 +0100 Subject: [PATCH 015/310] checkpatch: Disable warning for externs in C files We do actually need these, at least for compat.c. Also disable git commit id warnings, these are doing more harm than good (for example trigger long line warnings when fulfilled). Signed-off-by: Bernd Schubert <bernd@bsbernd.com> --- .github/workflows/checkpatch.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/checkpatch.yml b/.github/workflows/checkpatch.yml index 3f0eb9f07..91db8f2e3 100644 --- a/.github/workflows/checkpatch.yml +++ b/.github/workflows/checkpatch.yml @@ -24,7 +24,7 @@ jobs: git rev-list --reverse $base_commit..HEAD | while read commit; do 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 -g $commit; then + if ! ./checkpatch.pl --no-tree --ignore MAINTAINERS,SPDX_LICENSE_TAG,COMMIT_MESSAGE,FILE_PATH_CHANGES,EMAIL_SUBJECT,AVOID_EXTERNS,GIT_COMMIT_ID -g $commit; then echo "checkpatch.pl found issues in commit $commit - $subject" exit 1 fi From fc95fd5076fd845e496bfbcec1ad9da16534b1c9 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Sat, 4 Jan 2025 23:18:14 +0100 Subject: [PATCH 016/310] Make fuse_main_real() not symboled Addresses https://github.com/libfuse/libfuse/issues/1092 We actually don't need to make fuse_main_real() symboled, as it is not part of the official API. The inlined function now always calls into fuse_main_real_317 and the compat ABI function (which should also be available for dlopen/dlsym) is now always compiled, independent if the compiler/linker support versioned symbols. Additionally, fuse_main_real() is also declared as inlined function and a warning message is created when that function is called. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> --- include/fuse.h | 41 +++++++++++++++++++++++++++++------------ lib/compat.c | 23 +++++++++++------------ lib/fuse_versionscript | 2 +- lib/helper.c | 8 ++++---- 4 files changed, 45 insertions(+), 29 deletions(-) diff --git a/include/fuse.h b/include/fuse.h index c3add843a..969ed9785 100644 --- a/include/fuse.h +++ b/include/fuse.h @@ -877,21 +877,32 @@ struct fuse_context { mode_t umask; }; -#if (defined(LIBFUSE_BUILT_WITH_VERSIONED_SYMBOLS)) /** * The real main function * * Do not call this directly, use fuse_main() */ -int fuse_main_real(int argc, char *argv[], const struct fuse_operations *op, - size_t op_size, struct libfuse_version *version, - void *user_data); -#else -int fuse_main_real_317(int argc, char *argv[], const struct fuse_operations *op, - size_t op_size, struct libfuse_version *version, void *user_data); -#define fuse_main_real(argc, argv, op, op_size, version, user_data) \ - fuse_main_real_317(argc, argv, op, op_size, version, user_data); -#endif +static inline int fuse_main_real(int argc, char *argv[], + const struct fuse_operations *op, + size_t op_size, void *user_data) +{ + struct libfuse_version version = { .major = FUSE_MAJOR_VERSION, + .minor = FUSE_MINOR_VERSION, + .hotfix = FUSE_HOTFIX_VERSION, + .padding = 0 }; + + fuse_log(FUSE_LOG_ERR, + "%s is a libfuse internal function, please use fuse_main()\n", + __func__); + + /* not declared globally, to restrict usage of this function */ + int fuse_main_real_317(int argc, char *argv[], + const struct fuse_operations *op, size_t op_size, + struct libfuse_version *version, + void *user_data); + + return fuse_main_real_317(argc, argv, op, op_size, &version, user_data); +} /** * Main function of FUSE. @@ -957,8 +968,14 @@ fuse_main(int argc, char *argv[], const struct fuse_operations *op, .hotfix = FUSE_HOTFIX_VERSION, .padding = 0 }; - return fuse_main_real(argc, argv, op, sizeof(*(op)), &version, - user_data); + + /* not declared globally, to restrict usage of this function */ + int fuse_main_real_317(int argc, char *argv[], + const struct fuse_operations *op, size_t op_size, + struct libfuse_version *version, + void *user_data); + return fuse_main_real_317(argc, argv, op, sizeof(*(op)), &version, + user_data); } /* ----------------------------------------------------------- * diff --git a/lib/compat.c b/lib/compat.c index ce8a93399..685229920 100644 --- a/lib/compat.c +++ b/lib/compat.c @@ -18,6 +18,7 @@ #include "libfuse_config.h" #include <stddef.h> +#include <stdint.h> struct fuse_args; struct fuse_cmdline_opts; @@ -59,16 +60,6 @@ int fuse_session_custom_io(struct fuse_session *se, return fuse_session_custom_io_30(se, io, fd); } -int fuse_main_real_30(int argc, char *argv[], const struct fuse_operations *op, - size_t op_size, void *user_data); -int fuse_main_real(int argc, char *argv[], const struct fuse_operations *op, - size_t op_size, void *user_data); -int fuse_main_real(int argc, char *argv[], const struct fuse_operations *op, - size_t op_size, void *user_data) -{ - return fuse_main_real_30(argc, argv, op, op_size, user_data); -} - struct fuse_session *fuse_session_new_30(struct fuse_args *args, const struct fuse_lowlevel_ops *op, size_t op_size, void *userdata); @@ -82,6 +73,14 @@ struct fuse_session *fuse_session_new(struct fuse_args *args, return fuse_session_new_30(args, op, op_size, userdata); } -#endif - +#endif /* LIBFUSE_BUILT_WITH_VERSIONED_SYMBOLS */ +int fuse_main_real_30(int argc, char *argv[], const struct fuse_operations *op, + size_t op_size, void *user_data); +int fuse_main_real(int argc, char *argv[], const struct fuse_operations *op, + size_t op_size, void *user_data); +int fuse_main_real(int argc, char *argv[], const struct fuse_operations *op, + size_t op_size, void *user_data) +{ + return fuse_main_real_30(argc, argv, op, op_size, user_data); +} diff --git a/lib/fuse_versionscript b/lib/fuse_versionscript index f20e2b43e..78df3f029 100644 --- a/lib/fuse_versionscript +++ b/lib/fuse_versionscript @@ -189,10 +189,10 @@ FUSE_3.12 { FUSE_3.17 { global: + fuse_main_real_317; #if !defined(LIBFUSE_BUILT_WITH_VERSIONED_SYMBOLS) _fuse_session_new_317; _fuse_new_317; - fuse_main_real_317; #endif fuse_passthrough_open; fuse_passthrough_close; diff --git a/lib/helper.c b/lib/helper.c index 2794e66bb..660f029cd 100644 --- a/lib/helper.c +++ b/lib/helper.c @@ -304,9 +304,10 @@ int fuse_daemonize(int foreground) return 0; } +/* Not symboled, as not part of the official API */ int fuse_main_real_317(int argc, char *argv[], const struct fuse_operations *op, - size_t op_size, struct libfuse_version *version, void *user_data); -FUSE_SYMVER("fuse_main_real_317", "fuse_main_real@@FUSE_3.17") + size_t op_size, struct libfuse_version *version, + void *user_data); int fuse_main_real_317(int argc, char *argv[], const struct fuse_operations *op, size_t op_size, struct libfuse_version *version, void *user_data) { @@ -400,14 +401,13 @@ int fuse_main_real_317(int argc, char *argv[], const struct fuse_operations *op, return res; } +/* Not symboled, as not part of the official API */ int fuse_main_real_30(int argc, char *argv[], const struct fuse_operations *op, size_t op_size, void *user_data); -FUSE_SYMVER("fuse_main_real_30", "fuse_main_real@FUSE_3.0") int fuse_main_real_30(int argc, char *argv[], const struct fuse_operations *op, size_t op_size, void *user_data) { struct libfuse_version version = { 0 }; - return fuse_main_real_317(argc, argv, op, op_size, &version, user_data); } From 33c0ce14cfee5a7251dc2542948f1a98d775edc4 Mon Sep 17 00:00:00 2001 From: Zegang <zegang.luo@qq.com> Date: Wed, 8 Jan 2025 14:50:30 +0800 Subject: [PATCH 017/310] Identify the FD hold by parent process Issue: There is no directly way to get the FD hold by parent process which asked do fuse mount. Use Case: For auto_unmount case, identify the FD can easy to close the FD and make automatically unmount manually and explicitly. The FD[1] can be got via getenv(FUSE_COMMFD2_ENV). One potential use case is to satisfy FD-Leak checks. Solution: Add an extra env _FUSE_COMMFD2 to store the FD. This will provide a easy way to get the FD via FUSE_COMMFD2_ENV. Signed-off-by: Zegang Luo <zegang.luo@qq.com> --- lib/mount.c | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/lib/mount.c b/lib/mount.c index aedd9b92e..7dd727ce2 100644 --- a/lib/mount.c +++ b/lib/mount.c @@ -48,6 +48,7 @@ #define FUSERMOUNT_PROG "fusermount3" #define FUSE_COMMFD_ENV "_FUSE_COMMFD" +#define FUSE_COMMFD2_ENV "_FUSE_COMMFD2" #ifndef MS_DIRSYNC #define MS_DIRSYNC 128 @@ -367,6 +368,14 @@ static int setup_auto_unmount(const char *mountpoint, int quiet) 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, @@ -431,6 +440,14 @@ static int fuse_mount_fusermount(const char *mountpoint, struct mount_opts *mo, 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, From 6f73db5e4adcb1eaaf9e22a8453090b1ff09ad80 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Mon, 13 Jan 2025 15:50:19 +0100 Subject: [PATCH 018/310] Update doxygen/comments for fuse_reply_open/fuse_reply_create Comments for fuse_reply_open and fuse_reply_create and with that doxygen had not been updated for parallel_direct_writes and others. Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- include/fuse_lowlevel.h | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/include/fuse_lowlevel.h b/include/fuse_lowlevel.h index 0fa20390d..f8213cae1 100644 --- a/include/fuse_lowlevel.h +++ b/include/fuse_lowlevel.h @@ -1381,7 +1381,8 @@ int fuse_reply_entry(fuse_req_t req, const struct fuse_entry_param *e); * Reply with a directory entry and open parameters * * currently the following members of 'fi' are used: - * fh, direct_io, keep_cache + * fh, direct_io, keep_cache, cache_readdir, nonseekable, noflush, + * parallel_direct_writes * * Possible requests: * create @@ -1440,7 +1441,8 @@ int fuse_passthrough_close(fuse_req_t req, int backing_id); * Reply with open parameters * * currently the following members of 'fi' are used: - * fh, direct_io, keep_cache + * fh, direct_io, keep_cache, cache_readdir, nonseekable, noflush, + * parallel_direct_writes, * * Possible requests: * open, opendir From 0244e90a9f9d904b210ed26c04a4a3d8b69753b9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jan 2025 22:40:13 +0000 Subject: [PATCH 019/310] build(deps): bump github/codeql-action from 3.28.0 to 3.28.1 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.28.0 to 3.28.1. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/48ab28a6f5dbc2a99bf1e0131198dd8f1df78169...b6a472f63d85b9c78a3ac5e89422239fc15e9b3c) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index b4c6fc175..9c4c4d91e 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -53,7 +53,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0 + uses: github/codeql-action/init@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -78,7 +78,7 @@ jobs: meson compile -C build - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0 + uses: github/codeql-action/analyze@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1 with: category: "/language:${{matrix.language}}" From cf56672c8a39db594949588277009a721ef7eeae Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Fri, 24 Jan 2025 11:15:41 +0100 Subject: [PATCH 020/310] codespell: Add 're-used' and 're-using' to the ignore list These seem to be fine and exist in the code and also seem to be common English (there are debates which spelling is right). Signed-off-by: Bernd Schubert <bernd@bsbernd.com> --- .codespellrc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.codespellrc b/.codespellrc index 6bf9cccce..cbd941f1a 100644 --- a/.codespellrc +++ b/.codespellrc @@ -8,4 +8,6 @@ skip = .git,*.pdf,*.svg,AUTHORS # - alse: used in regex # - siz: wanted short # - fiter: variable -ignore-words-list = alse,siz,fiter +# - re-used: intentional hyphenation +# - re-using: intentional hyphenation +ignore-words-list = alse,siz,fiter,re-used,re-using From 48d12f86e73e456b53defc4a8a98ed4db8a54b39 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Fri, 24 Jan 2025 11:28:52 +0100 Subject: [PATCH 021/310] checkpatch: Ignore ENOSYS_SYSCALL This ENOSYS is the fuse protocol return code to tell fuse client/kernel that an operation is not supported, warning on it is not right. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> --- .github/workflows/checkpatch.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/checkpatch.yml b/.github/workflows/checkpatch.yml index 91db8f2e3..1adada4c5 100644 --- a/.github/workflows/checkpatch.yml +++ b/.github/workflows/checkpatch.yml @@ -24,7 +24,7 @@ jobs: git rev-list --reverse $base_commit..HEAD | while read commit; do 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 -g $commit; then + 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 echo "checkpatch.pl found issues in commit $commit - $subject" exit 1 fi From da3c7e6216a929beac902991004c500993d87d95 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Fri, 24 Jan 2025 11:58:44 +0100 Subject: [PATCH 022/310] abidiff: Don't warn about added symbols So far we only want it to warn about changed symbols. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> --- .github/workflows/abicheck.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/abicheck.yml b/.github/workflows/abicheck.yml index c75d912cf..526fb4296 100644 --- a/.github/workflows/abicheck.yml +++ b/.github/workflows/abicheck.yml @@ -57,7 +57,8 @@ jobs: meson compile -C build - name: Run abidiff - run: abidiff + run: abidiff + --no-added-syms --headers-dir1 previous/include/ --headers-dir2 current/include/ previous/build/lib/libfuse3.so From 43ec53d6a16363184a95e59627c3f54d5568d8d1 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Mon, 27 Jan 2025 21:33:43 +0100 Subject: [PATCH 023/310] lib: Set thread names Main worker threads: fuse_worker high level clean up threads: "fuse_prune_nodes" Signed-off-by: Bernd Schubert <bernd@bsbernd.com> --- lib/fuse.c | 4 ++++ lib/fuse_loop_mt.c | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/lib/fuse.c b/lib/fuse.c index 8fbc035ba..5d57d2428 100644 --- a/lib/fuse.c +++ b/lib/fuse.c @@ -9,6 +9,8 @@ See the file COPYING.LIB */ +#define _GNU_SOURCE + #include "fuse_config.h" #include "fuse_i.h" #include "fuse_lowlevel.h" @@ -4890,6 +4892,8 @@ static void *fuse_prune_nodes(void *fuse) struct fuse *f = fuse; int sleep_time; + pthread_setname_np(pthread_self(), "fuse_prune_nodes"); + while(1) { sleep_time = fuse_clean_cache(f); sleep(sleep_time); diff --git a/lib/fuse_loop_mt.c b/lib/fuse_loop_mt.c index 0e79b499a..95316f7fd 100644 --- a/lib/fuse_loop_mt.c +++ b/lib/fuse_loop_mt.c @@ -8,6 +8,8 @@ See the file COPYING.LIB. */ +#define _GNU_SOURCE + #include "fuse_config.h" #include "fuse_lowlevel.h" #include "fuse_misc.h" @@ -130,6 +132,8 @@ static void *fuse_do_work(void *data) struct fuse_worker *w = (struct fuse_worker *) data; struct fuse_mt *mt = w->mt; + pthread_setname_np(pthread_self(), "fuse_worker"); + while (!fuse_session_exited(mt->se)) { int isforget = 0; int res; From ba7beb21f63e91e22e877017ec6b359b5f8ece00 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Jan 2025 22:28:21 +0000 Subject: [PATCH 024/310] build(deps): bump github/codeql-action from 3.28.1 to 3.28.6 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.28.1 to 3.28.6. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/b6a472f63d85b9c78a3ac5e89422239fc15e9b3c...17a820bf2e43b47be2c72b39cc905417bc1ab6d0) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 9c4c4d91e..ef3126c52 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -53,7 +53,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1 + uses: github/codeql-action/init@17a820bf2e43b47be2c72b39cc905417bc1ab6d0 # v3.28.6 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -78,7 +78,7 @@ jobs: meson compile -C build - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1 + uses: github/codeql-action/analyze@17a820bf2e43b47be2c72b39cc905417bc1ab6d0 # v3.28.6 with: category: "/language:${{matrix.language}}" From 64de51de4909eb7b68e982ab9922eb24dda4899e Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Wed, 29 Jan 2025 10:46:18 +0100 Subject: [PATCH 025/310] github actions: Add an include-what-you-need check Probably lots of issues right now, so let's fix it step by step by only checking modified files - new PRs should fix their modified files. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> --- .github/workflows/iwyi-check.yml | 51 ++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 .github/workflows/iwyi-check.yml diff --git a/.github/workflows/iwyi-check.yml b/.github/workflows/iwyi-check.yml new file mode 100644 index 000000000..3751f35de --- /dev/null +++ b/.github/workflows/iwyi-check.yml @@ -0,0 +1,51 @@ +# check for uneeded header includes of modified files +# False positives can be avoided with +# #include "some_include.h" // IWYU pragma: keep + +name: IWYU Check + +on: + pull_request: + branches: [ main ] + paths: + - '**.cpp' + - '**.hpp' + - '**.c' + - '**.h' + +jobs: + iwyu-check: + name: Include What You Use Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + + - name: Install IWYU + run: | + sudo apt-get update + sudo apt-get install -y iwyu + + - name: Get changed files + id: changed-files + run: | + git fetch origin ${{ github.base_ref }} + base_commit=$(git merge-base FETCH_HEAD ${{ github.event.pull_request.head.sha }}) + changed_files=$(git diff --name-only $base_commit HEAD | grep -E '\.(cpp|hpp|c|h)$' || true) + echo "files=$changed_files" >> $GITHUB_OUTPUT + + - name: Run IWYU checks on changed files + if: steps.changed-files.outputs.files != '' + run: | + echo "${{ steps.changed-files.outputs.files }}" | while read -r file; do + if [ -f "$file" ]; then + echo "Checking $file..." + iwyu -Xiwyu --mapping_file=iwyu.imp "$file" 2>&1 || true + fi + done | tee iwyu_output.txt + if grep -q "should add these lines:" iwyu_output.txt || \ + grep -q "should remove these lines:" iwyu_output.txt; then + echo "IWYU checks failed. Please fix the includes in the affected files." + exit 1 + fi From 999aee6c0d6bf1ac67c3b89472f32fd1d384cf44 Mon Sep 17 00:00:00 2001 From: Luis Henriques <luis@igalia.com> Date: Fri, 31 Jan 2025 15:26:20 +0000 Subject: [PATCH 026/310] fuse_lowlevel.c: fix possible 64 bits value truncation Because conn.want_ext is a uint64_t, copying it into a uint32_t may result in truncating it's value. This patch fixes a bug in do_init() where the 32 bits copy is again converted into a 64 bits value, because it will be used in convert_to_conn_want_ext(). Signed-off-by: Luis Henriques <luis@igalia.com> --- lib/fuse_lowlevel.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index c993860cd..36baaa65b 100644 --- a/lib/fuse_lowlevel.c +++ b/lib/fuse_lowlevel.c @@ -2178,7 +2178,7 @@ void do_init(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) se->got_init = 1; if (se->op.init) { - uint32_t want_ext_default = se->conn.want_ext; + uint64_t want_ext_default = se->conn.want_ext; int rc; // Apply the first 32 bits of capable_ext to capable From 7f86f3be7148b15b71b63357813c66dd32177cf6 Mon Sep 17 00:00:00 2001 From: Luis Henriques <luis@igalia.com> Date: Fri, 31 Jan 2025 16:03:54 +0000 Subject: [PATCH 027/310] fuse_lowlevel.c: drop incorrect comment from convert_to_conn_want_ext() The comment is probably a leftover from older versions, as the application of the 32 bits of conn.capable_ext to conn.capable is done before function convert_to_conn_want_ext() is invoked. Signed-off-by: Luis Henriques <luis@igalia.com> --- lib/fuse_lowlevel.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index 36baaa65b..840142b27 100644 --- a/lib/fuse_lowlevel.c +++ b/lib/fuse_lowlevel.c @@ -1999,8 +1999,6 @@ static bool want_flags_valid(uint64_t capable, uint64_t want) /** * Get the wanted capability flags, converting from old format if necessary - * Also applies the first 32 bits of capable_ext to capable - * */ static inline int convert_to_conn_want_ext(struct fuse_conn_info *conn, uint64_t want_ext_default) From 32d84aaacd1a6020940598be0c792fc7e0369b67 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Feb 2025 22:14:55 +0000 Subject: [PATCH 028/310] build(deps): bump github/codeql-action from 3.28.6 to 3.28.8 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.28.6 to 3.28.8. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/17a820bf2e43b47be2c72b39cc905417bc1ab6d0...dd746615b3b9d728a6a37ca2045b68ca76d4841a) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index ef3126c52..89fbe2494 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -53,7 +53,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@17a820bf2e43b47be2c72b39cc905417bc1ab6d0 # v3.28.6 + uses: github/codeql-action/init@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -78,7 +78,7 @@ jobs: meson compile -C build - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@17a820bf2e43b47be2c72b39cc905417bc1ab6d0 # v3.28.6 + uses: github/codeql-action/analyze@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8 with: category: "/language:${{matrix.language}}" From 458f433b89f3a599ed90bced16cb1e3e8403a774 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Sat, 8 Feb 2025 14:08:32 +0100 Subject: [PATCH 029/310] Rename fuse_main_real_317 to fuse_main_real_versioned As suggested by Bill in Issue #1092, rename to _versioned so that applications using dlopen/dlvsym better understand the meaning of this function. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> --- include/fuse.h | 27 +++++++++++++++------------ lib/helper.c | 14 ++++++++------ 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/include/fuse.h b/include/fuse.h index 969ed9785..70d0d13ab 100644 --- a/include/fuse.h +++ b/include/fuse.h @@ -896,12 +896,14 @@ static inline int fuse_main_real(int argc, char *argv[], __func__); /* not declared globally, to restrict usage of this function */ - int fuse_main_real_317(int argc, char *argv[], - const struct fuse_operations *op, size_t op_size, - struct libfuse_version *version, - void *user_data); - - return fuse_main_real_317(argc, argv, op, op_size, &version, user_data); + int fuse_main_real_versioned(int argc, char *argv[], + const struct fuse_operations *op, + size_t op_size, + struct libfuse_version *version, + void *user_data); + + return fuse_main_real_versioned(argc, argv, op, op_size, &version, + user_data); } /** @@ -970,12 +972,13 @@ fuse_main(int argc, char *argv[], const struct fuse_operations *op, }; /* not declared globally, to restrict usage of this function */ - int fuse_main_real_317(int argc, char *argv[], - const struct fuse_operations *op, size_t op_size, - struct libfuse_version *version, - void *user_data); - return fuse_main_real_317(argc, argv, op, sizeof(*(op)), &version, - user_data); + int fuse_main_real_versioned(int argc, char *argv[], + const struct fuse_operations *op, + size_t op_size, + struct libfuse_version *version, + void *user_data); + return fuse_main_real_versioned(argc, argv, op, sizeof(*(op)), &version, + user_data); } /* ----------------------------------------------------------- * diff --git a/lib/helper.c b/lib/helper.c index 660f029cd..cab5ada70 100644 --- a/lib/helper.c +++ b/lib/helper.c @@ -305,11 +305,12 @@ int fuse_daemonize(int foreground) } /* Not symboled, as not part of the official API */ -int fuse_main_real_317(int argc, char *argv[], const struct fuse_operations *op, - size_t op_size, struct libfuse_version *version, - void *user_data); -int fuse_main_real_317(int argc, char *argv[], const struct fuse_operations *op, - size_t op_size, struct libfuse_version *version, void *user_data) +int fuse_main_real_versioned(int argc, char *argv[], + const struct fuse_operations *op, size_t op_size, + struct libfuse_version *version, void *user_data); +int fuse_main_real_versioned(int argc, char *argv[], + const struct fuse_operations *op, size_t op_size, + struct libfuse_version *version, void *user_data) { struct fuse_args args = FUSE_ARGS_INIT(argc, argv); struct fuse *fuse; @@ -408,7 +409,8 @@ int fuse_main_real_30(int argc, char *argv[], const struct fuse_operations *op, size_t op_size, void *user_data) { struct libfuse_version version = { 0 }; - return fuse_main_real_317(argc, argv, op, op_size, &version, user_data); + return fuse_main_real_versioned(argc, argv, op, op_size, &version, + user_data); } void fuse_apply_conn_info_opts(struct fuse_conn_info_opts *opts, From b0f32a8b318d1a01f88eb37f09fd4bb8ac175bb4 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Sat, 8 Feb 2025 14:14:43 +0100 Subject: [PATCH 030/310] Make fuse_main a macro again and wrap that to fuse_main_fn As suggested by Bill in Issue #1092 make fuse_main a macro again, just in case some applications expect it to be a macro. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> --- include/fuse.h | 8 +++++--- lib/fuse_versionscript | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/include/fuse.h b/include/fuse.h index 70d0d13ab..ae8d80ee4 100644 --- a/include/fuse.h +++ b/include/fuse.h @@ -960,9 +960,9 @@ static inline int fuse_main_real(int argc, char *argv[], * * Example usage, see hello.c */ -static inline int -fuse_main(int argc, char *argv[], const struct fuse_operations *op, - void *user_data) +static inline int fuse_main_fn(int argc, char *argv[], + const struct fuse_operations *op, + void *user_data) { struct libfuse_version version = { .major = FUSE_MAJOR_VERSION, @@ -980,6 +980,8 @@ fuse_main(int argc, char *argv[], const struct fuse_operations *op, return fuse_main_real_versioned(argc, argv, op, sizeof(*(op)), &version, user_data); } +#define fuse_main(argc, argv, op, user_data) \ + fuse_main_fn(argc, argv, op, user_data) /* ----------------------------------------------------------- * * More detailed API * diff --git a/lib/fuse_versionscript b/lib/fuse_versionscript index 78df3f029..b31453b28 100644 --- a/lib/fuse_versionscript +++ b/lib/fuse_versionscript @@ -189,7 +189,7 @@ FUSE_3.12 { FUSE_3.17 { global: - fuse_main_real_317; + fuse_main_real_versioned; #if !defined(LIBFUSE_BUILT_WITH_VERSIONED_SYMBOLS) _fuse_session_new_317; _fuse_new_317; From 53eefce4e735dd1d8f77ceadfc8beb9679390e08 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Sat, 8 Feb 2025 14:35:46 +0100 Subject: [PATCH 031/310] Rename to fuse_session_new_versioned Similar previous renames to fuse_main_real_versioned, but here for the low level fuse_session_new. Also remove symbol versioned as part of "fuse_session_new" as that function is not part of the official API/ABI and to allow easier access with dlopen/dlsym. Also switch back to a macro fuse_session_new, just in case some code has some expectations on that. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> --- include/fuse_lowlevel.h | 25 +++++++------------------ lib/compat.c | 24 ++++++++++++------------ lib/fuse.c | 6 +++--- lib/fuse_lowlevel.c | 28 ++++++++++++---------------- lib/fuse_versionscript | 2 +- 5 files changed, 35 insertions(+), 50 deletions(-) diff --git a/include/fuse_lowlevel.h b/include/fuse_lowlevel.h index f8213cae1..d0617ba35 100644 --- a/include/fuse_lowlevel.h +++ b/include/fuse_lowlevel.h @@ -2047,18 +2047,6 @@ int fuse_parse_cmdline_312(struct fuse_args *args, #endif #endif -/* Do not call this directly, but only through fuse_session_new() */ -#if (!defined(LIBFUSE_BUILT_WITH_VERSIONED_SYMBOLS)) -struct fuse_session * -_fuse_session_new_317(struct fuse_args *args, - const struct fuse_lowlevel_ops *op, - size_t op_size, - struct libfuse_version *version, - void *userdata); -#define _fuse_session_new(args, op, op_size, version, userdata) \ - _fuse_session_new_317(args, op, op_size, version, userdata) -#endif - /** * Create a low level session. * @@ -2088,10 +2076,8 @@ _fuse_session_new_317(struct fuse_args *args, * @return the fuse session on success, NULL on failure **/ static inline struct fuse_session * -fuse_session_new(struct fuse_args *args, - const struct fuse_lowlevel_ops *op, - size_t op_size, - void *userdata) +fuse_session_new_fn(struct fuse_args *args, const struct fuse_lowlevel_ops *op, + size_t op_size, void *userdata) { struct libfuse_version version = { .major = FUSE_MAJOR_VERSION, @@ -2101,13 +2087,16 @@ fuse_session_new(struct fuse_args *args, }; /* not declared globally, to restrict usage of this function */ - struct fuse_session *_fuse_session_new( + struct fuse_session *fuse_session_new_versioned( struct fuse_args *args, const struct fuse_lowlevel_ops *op, size_t op_size, struct libfuse_version *version, void *userdata); - return _fuse_session_new(args, op, op_size, &version, userdata); + return fuse_session_new_versioned(args, op, op_size, &version, + userdata); } +#define fuse_session_new(args, op, op_size, userdata) \ + fuse_session_new_fn(args, op, op_size, userdata) /* * This should mostly not be called directly, but instead the diff --git a/lib/compat.c b/lib/compat.c index 685229920..b98ca4b04 100644 --- a/lib/compat.c +++ b/lib/compat.c @@ -60,6 +60,18 @@ int fuse_session_custom_io(struct fuse_session *se, return fuse_session_custom_io_30(se, io, fd); } +#endif /* LIBFUSE_BUILT_WITH_VERSIONED_SYMBOLS */ + +int fuse_main_real_30(int argc, char *argv[], const struct fuse_operations *op, + size_t op_size, void *user_data); +int fuse_main_real(int argc, char *argv[], const struct fuse_operations *op, + size_t op_size, void *user_data); +int fuse_main_real(int argc, char *argv[], const struct fuse_operations *op, + size_t op_size, void *user_data) +{ + return fuse_main_real_30(argc, argv, op, op_size, user_data); +} + struct fuse_session *fuse_session_new_30(struct fuse_args *args, const struct fuse_lowlevel_ops *op, size_t op_size, void *userdata); @@ -72,15 +84,3 @@ struct fuse_session *fuse_session_new(struct fuse_args *args, { return fuse_session_new_30(args, op, op_size, userdata); } - -#endif /* LIBFUSE_BUILT_WITH_VERSIONED_SYMBOLS */ - -int fuse_main_real_30(int argc, char *argv[], const struct fuse_operations *op, - size_t op_size, void *user_data); -int fuse_main_real(int argc, char *argv[], const struct fuse_operations *op, - size_t op_size, void *user_data); -int fuse_main_real(int argc, char *argv[], const struct fuse_operations *op, - size_t op_size, void *user_data) -{ - return fuse_main_real_30(argc, argv, op, op_size, user_data); -} diff --git a/lib/fuse.c b/lib/fuse.c index 5d57d2428..0b04f47d3 100644 --- a/lib/fuse.c +++ b/lib/fuse.c @@ -5014,12 +5014,12 @@ struct fuse *_fuse_new_317(struct fuse_args *args, #endif /* not declared globally, to restrict usage of this function */ - struct fuse_session *_fuse_session_new( + struct fuse_session *fuse_session_new_versioned( struct fuse_args *args, const struct fuse_lowlevel_ops *op, size_t op_size, struct libfuse_version *version, void *userdata); - - f->se = _fuse_session_new(args, &llop, sizeof(llop), version, f); + f->se = fuse_session_new_versioned(args, &llop, sizeof(llop), version, + f); if (f->se == NULL) goto out_free_fs; diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index 840142b27..e3e79d528 100644 --- a/lib/fuse_lowlevel.c +++ b/lib/fuse_lowlevel.c @@ -3244,17 +3244,14 @@ int fuse_session_receive_buf_internal(struct fuse_session *se, return _fuse_session_receive_buf(se, buf, ch, true); } -FUSE_SYMVER("_fuse_session_new_317", "_fuse_session_new@@FUSE_3.17") -struct fuse_session *_fuse_session_new_317(struct fuse_args *args, - const struct fuse_lowlevel_ops *op, - size_t op_size, - struct libfuse_version *version, - void *userdata); -struct fuse_session *_fuse_session_new_317(struct fuse_args *args, - const struct fuse_lowlevel_ops *op, - size_t op_size, - struct libfuse_version *version, - void *userdata) +struct fuse_session * +fuse_session_new_versioned(struct fuse_args *args, + const struct fuse_lowlevel_ops *op, size_t op_size, + struct libfuse_version *version, void *userdata); +struct fuse_session * +fuse_session_new_versioned(struct fuse_args *args, + const struct fuse_lowlevel_ops *op, size_t op_size, + struct libfuse_version *version, void *userdata) { int err; struct fuse_session *se; @@ -3355,10 +3352,8 @@ struct fuse_session *_fuse_session_new_317(struct fuse_args *args, } struct fuse_session *fuse_session_new_30(struct fuse_args *args, - const struct fuse_lowlevel_ops *op, - size_t op_size, - void *userdata); -FUSE_SYMVER("fuse_session_new_30", "fuse_session_new@FUSE_3.0") + const struct fuse_lowlevel_ops *op, + size_t op_size, void *userdata); struct fuse_session *fuse_session_new_30(struct fuse_args *args, const struct fuse_lowlevel_ops *op, size_t op_size, @@ -3367,7 +3362,8 @@ struct fuse_session *fuse_session_new_30(struct fuse_args *args, /* unknown version */ struct libfuse_version version = { 0 }; - return _fuse_session_new_317(args, op, op_size, &version, userdata); + return fuse_session_new_versioned(args, op, op_size, &version, + userdata); } FUSE_SYMVER("fuse_session_custom_io_317", "fuse_session_custom_io@@FUSE_3.17") diff --git a/lib/fuse_versionscript b/lib/fuse_versionscript index b31453b28..2d314c051 100644 --- a/lib/fuse_versionscript +++ b/lib/fuse_versionscript @@ -190,8 +190,8 @@ FUSE_3.12 { FUSE_3.17 { global: fuse_main_real_versioned; + fuse_session_new_versioned; #if !defined(LIBFUSE_BUILT_WITH_VERSIONED_SYMBOLS) - _fuse_session_new_317; _fuse_new_317; #endif fuse_passthrough_open; From 38da521b762debd93910cea029a08328b6a198bb Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Mon, 10 Feb 2025 01:52:08 +0100 Subject: [PATCH 032/310] fuse_new: Fix non symboled call to _fuse_new_317 Acidentally the wrong non-existing function was ccalled. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> --- include/fuse.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/fuse.h b/include/fuse.h index ae8d80ee4..ab0c04a80 100644 --- a/include/fuse.h +++ b/include/fuse.h @@ -1094,7 +1094,7 @@ fuse_new(struct fuse_args *args, .padding = 0 }; - return _fuse_new(args, op, op_size, &version, user_data); + return _fuse_new_317(args, op, op_size, &version, user_data); } #endif /* LIBFUSE_BUILT_WITH_VERSIONED_SYMBOLS */ #endif From 65dfe9c2907157732a1346dd679f126bce86f84f Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Mon, 10 Feb 2025 01:50:01 +0100 Subject: [PATCH 033/310] fuse_new version fixes: Change to fuse_new_versioned Another additon for https://github.com/libfuse/libfuse/issues/1092 Use _fuse_new_versioned() instead of _fuse_new_317 and actually also remove symbol versioning for it - we don't need it. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> --- include/fuse.h | 43 ++++++++++-------------------------------- lib/fuse.c | 9 ++++----- lib/fuse_versionscript | 5 ++--- lib/helper.c | 4 ++-- 4 files changed, 18 insertions(+), 43 deletions(-) diff --git a/include/fuse.h b/include/fuse.h index ab0c04a80..c0857fdbe 100644 --- a/include/fuse.h +++ b/include/fuse.h @@ -1034,12 +1034,12 @@ struct fuse *_fuse_new_30(struct fuse_args *args, struct libfuse_version *version, void *user_data); static inline struct fuse * -fuse_new(struct fuse_args *args, +fuse_new_fn(struct fuse_args *args, const struct fuse_operations *op, size_t op_size, void *user_data) { /* not declared globally, to restrict usage of this function */ - struct fuse *_fuse_new(struct fuse_args *args, + struct fuse *_fuse_new_30(struct fuse_args *args, const struct fuse_operations *op, size_t op_size, struct libfuse_version *version, void *user_data); @@ -1053,10 +1053,9 @@ fuse_new(struct fuse_args *args, return _fuse_new_30(args, op, op_size, &version, user_data); } -#else -#if (defined(LIBFUSE_BUILT_WITH_VERSIONED_SYMBOLS)) +#else /* FUSE_USE_VERSION */ static inline struct fuse * -fuse_new(struct fuse_args *args, +fuse_new_fn(struct fuse_args *args, const struct fuse_operations *op, size_t op_size, void *user_data) { @@ -1068,36 +1067,14 @@ fuse_new(struct fuse_args *args, }; /* not declared globally, to restrict usage of this function */ - struct fuse *_fuse_new(struct fuse_args *args, - const struct fuse_operations *op, size_t op_size, - struct libfuse_version *version, - void *user_data); - - return _fuse_new(args, op, op_size, &version, user_data); + struct fuse *_fuse_new_31(struct fuse_args *args, + const struct fuse_operations *op, + size_t op_size, struct libfuse_version *version, + void *user_data); + return _fuse_new_31(args, op, op_size, &version, user_data); } -#else /* LIBFUSE_BUILT_WITH_VERSIONED_SYMBOLS */ -struct fuse *_fuse_new_317(struct fuse_args *args, - const struct fuse_operations *op, size_t op_size, - struct libfuse_version *version, - void *private_data); -#define _fuse_new(args, op, size, version, data) \ - _fuse_new_317(args, op, size, version, data) -static inline struct fuse * -fuse_new(struct fuse_args *args, - const struct fuse_operations *op, size_t op_size, - void *user_data) -{ - struct libfuse_version version = { - .major = FUSE_MAJOR_VERSION, - .minor = FUSE_MINOR_VERSION, - .hotfix = FUSE_HOTFIX_VERSION, - .padding = 0 - }; - - return _fuse_new_317(args, op, op_size, &version, user_data); -} -#endif /* LIBFUSE_BUILT_WITH_VERSIONED_SYMBOLS */ #endif +#define fuse_new(args, op, size, data) fuse_new_fn(args, op, size, data) /** * Mount a FUSE file system. diff --git a/lib/fuse.c b/lib/fuse.c index 0b04f47d3..6c69a6897 100644 --- a/lib/fuse.c +++ b/lib/fuse.c @@ -4923,12 +4923,11 @@ void fuse_stop_cleanup_thread(struct fuse *f) * Not supposed to be called directly, but supposed to be called * through the fuse_new macro */ -struct fuse *_fuse_new_317(struct fuse_args *args, +struct fuse *_fuse_new_31(struct fuse_args *args, const struct fuse_operations *op, size_t op_size, struct libfuse_version *version, void *user_data); -FUSE_SYMVER("_fuse_new_317", "_fuse_new@@FUSE_3.17") -struct fuse *_fuse_new_317(struct fuse_args *args, +struct fuse *_fuse_new_31(struct fuse_args *args, const struct fuse_operations *op, size_t op_size, struct libfuse_version *version, void *user_data) @@ -5103,7 +5102,7 @@ struct fuse *_fuse_new_30(struct fuse_args *args, fuse_lib_help(args); return NULL; } else - return _fuse_new_317(args, op, op_size, version, user_data); + return _fuse_new_31(args, op, op_size, version, user_data); } /* ABI compat version */ @@ -5117,7 +5116,7 @@ struct fuse *fuse_new_31(struct fuse_args *args, /* unknown version */ struct libfuse_version version = { 0 }; - return _fuse_new_317(args, op, op_size, &version, user_data); + return _fuse_new_31(args, op, op_size, &version, user_data); } /* diff --git a/lib/fuse_versionscript b/lib/fuse_versionscript index 2d314c051..6c5fc83eb 100644 --- a/lib/fuse_versionscript +++ b/lib/fuse_versionscript @@ -191,9 +191,8 @@ FUSE_3.17 { global: fuse_main_real_versioned; fuse_session_new_versioned; -#if !defined(LIBFUSE_BUILT_WITH_VERSIONED_SYMBOLS) - _fuse_new_317; -#endif + _fuse_new_30; + _fuse_new_31; fuse_passthrough_open; fuse_passthrough_close; fuse_session_custom_io_30; diff --git a/lib/helper.c b/lib/helper.c index cab5ada70..a1cf98c3e 100644 --- a/lib/helper.c +++ b/lib/helper.c @@ -346,11 +346,11 @@ int fuse_main_real_versioned(int argc, char *argv[], goto out1; } - struct fuse *_fuse_new(struct fuse_args *args, + struct fuse *_fuse_new_31(struct fuse_args *args, const struct fuse_operations *op, size_t op_size, struct libfuse_version *version, void *user_data); - fuse = _fuse_new(&args, op, op_size, version, user_data); + fuse = _fuse_new_31(&args, op, op_size, version, user_data); if (fuse == NULL) { res = 3; goto out1; From dca6e514c81e3ad0ec806f79b774b3707b313f63 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Mon, 10 Feb 2025 02:45:42 +0100 Subject: [PATCH 034/310] Build fixes for -Og MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cleaning... 18 files. [35/69] Compiling C object example/passthrough_ll.p/passthrough_ll.c.o ../example/passthrough_ll.c: In function ‘lo_opendir’: ../example/passthrough_ll.c:666:20: warning: ‘fd’ may be used uninitialized [-Wmaybe-uninitialized] 666 | if (fd != -1) | ^ ../example/passthrough_ll.c:637:13: note: ‘fd’ was declared here 637 | int fd; | ^~ [38/69] Compiling C object test/test_syscalls.p/test_syscalls.c.o ../test/test_syscalls.c: In function ‘test_seekdir’: ../test/test_syscalls.c:804:16: warning: ‘de’ may be used uninitialized [-Wmaybe-uninitialized] 804 | while (de) | ^~ ../test/test_syscalls.c:776:24: note: ‘de’ was declared here 776 | struct dirent *de; | ^~ These are actually valid. Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- example/passthrough_ll.c | 2 +- test/test_syscalls.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/example/passthrough_ll.c b/example/passthrough_ll.c index e1a36efa6..fa8abb897 100644 --- a/example/passthrough_ll.c +++ b/example/passthrough_ll.c @@ -634,7 +634,7 @@ static void lo_opendir(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info *fi int error = ENOMEM; struct lo_data *lo = lo_data(req); struct lo_dirp *d; - int fd; + int fd = -1; d = calloc(1, sizeof(struct lo_dirp)); if (d == NULL) diff --git a/test/test_syscalls.c b/test/test_syscalls.c index 7a2389df1..26b2ff2be 100644 --- a/test/test_syscalls.c +++ b/test/test_syscalls.c @@ -773,7 +773,7 @@ static int test_seekdir(void) int i; int res; DIR *dp; - struct dirent *de; + struct dirent *de = NULL; start_test("seekdir"); res = create_dir(testdir, testdir_files); From 805969f5284505e48ee87175476cf9df241ded42 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Mon, 10 Feb 2025 02:36:39 +0100 Subject: [PATCH 035/310] Change version to 3.17.1-rc0 Somehow really hard to set -rcX with meson. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> --- lib/meson.build | 10 +++++++--- meson.build | 25 ++++++++++++++++--------- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/lib/meson.build b/lib/meson.build index e201a5a98..6a52d06a9 100644 --- a/lib/meson.build +++ b/lib/meson.build @@ -33,9 +33,13 @@ else endif fusermount_path = join_paths(get_option('prefix'), get_option('bindir')) -libfuse = library('fuse3', libfuse_sources, version: meson.project_version(), - soversion: '4', include_directories: include_dirs, - dependencies: deps, install: true, +libfuse = library('fuse3', + libfuse_sources, + version: base_version, + soversion: '4', + include_directories: include_dirs, + dependencies: deps, + install: true, link_depends: 'fuse_versionscript', c_args: [ '-DFUSE_USE_VERSION=317', '-DFUSERMOUNT_DIR="@0@"'.format(fusermount_path) ], diff --git a/meson.build b/meson.build index f05b94fb0..e191fc62c 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,6 @@ -project('libfuse3', ['c'], version: '3.17.0', - meson_version: '>= 0.51', +project('libfuse3', ['c'], + version: '3.17.1-rc0', # Version with RC suffix + meson_version: '>= 0.51.0', default_options: [ 'buildtype=debugoptimized', 'c_std=gnu11', @@ -11,10 +12,16 @@ project('libfuse3', ['c'], version: '3.17.0', # from integers, i.e. concatenating strings instead # of splitting a string, but 'project' needs to be # the first meson.build keyword... -version_list = meson.project_version().split('.') + +# split version into base and rc +version_parts = meson.project_version().split('-') +base_version = version_parts[0] + +version_list = base_version.split('.') FUSE_MAJOR_VERSION = version_list[0] FUSE_MINOR_VERSION = version_list[1] FUSE_HOTFIX_VERSION = version_list[2] +FUSE_RC_VERSION = version_parts.length() > 1 ? version_parts[1] : '' platform = host_machine.system() if platform == 'darwin' @@ -28,12 +35,6 @@ endif cc = meson.get_compiler('c') -# -# Feature detection, only available at libfuse compilation time, -# but not for application linking to libfuse. -# -private_cfg = configuration_data() - # # Feature detection, the resulting config file is installed # with the package. @@ -46,6 +47,7 @@ public_cfg = configuration_data() public_cfg.set('FUSE_MAJOR_VERSION', FUSE_MAJOR_VERSION) public_cfg.set('FUSE_MINOR_VERSION', FUSE_MINOR_VERSION) public_cfg.set('FUSE_HOTFIX_VERSION', FUSE_HOTFIX_VERSION) +public_cfg.set('FUSE_RC_VERSION', FUSE_RC_VERSION) # Default includes when checking for presence of functions and # struct members @@ -60,6 +62,11 @@ include_default = ''' ''' args_default = [ '-D_GNU_SOURCE' ] +# +# Feature detection, only available at libfuse compilation time, +# but not for application linking to libfuse. +# +private_cfg = configuration_data() private_cfg.set_quoted('PACKAGE_VERSION', meson.project_version()) # Test for presence of some functions From 34c505316083fed363752676ae4b9056abd251c7 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Mon, 10 Feb 2025 15:55:20 +0100 Subject: [PATCH 036/310] Prepare 3.17.1 release Update ChangeLog.rst and AUTHORS Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- AUTHORS | 4 ++++ ChangeLog.rst | 20 ++++++++++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/AUTHORS b/AUTHORS index 9fd3b49ac..837a62c84 100644 --- a/AUTHORS +++ b/AUTHORS @@ -259,3 +259,7 @@ George Hilliard <thirtythreeforty@gmail.com> Tyler Hall <tylerwhall@gmail.com> yangyun <yangyun50@huawei.com> Abhishek <abhi_45645@yahoo.com> + +# New authors since 666b2c3fa5159e3c72a0d08117507475117c9319 +Luis Henriques <luis@igalia.com> +Zegang <zegang.luo@qq.com> diff --git a/ChangeLog.rst b/ChangeLog.rst index 3bf401d99..658f898aa 100644 --- a/ChangeLog.rst +++ b/ChangeLog.rst @@ -1,5 +1,21 @@ -libfuse 3.17 (2024-01-01) -========================= +libfuse 3.17.1-rc0 (2024-02.10) +=============================== + +* Fix libfuse build with FUSE_USE_VERSION 30 +* Fix build of memfs_ll without manual meson reconfigure +* Fix junk readdirplus results when filesystem not filling stat info +* Fix conn.want_ext truncation to 32bit +* Fix some build warnings with -Og +* Fix fuse_main_real symbols +* Several changes related to functions/symbols that added in + the libfuse version in 3.17 +* Add thread names to libfuse threads +* With auto-umounts the FUSE_COMMFD2 (parent process fd is + exported to be able to silence leak checkers + + +libfuse 3.17 (2024-01-01, not officially releaesed) +================================================== * 3.11 and 3.14.2 introduced ABI incompatibilities, the ABI is restored to 3.10, .so version was increased since there were releases with From 62c79d580ed9d89e6071c93986ea92c5d43aaeb9 Mon Sep 17 00:00:00 2001 From: Joanne Koong <joannelkoong@gmail.com> Date: Wed, 12 Feb 2025 11:00:11 -0800 Subject: [PATCH 037/310] passthrough_hp: enable splice move by default This commit enables splice moves by default, unless the caller opts into nosplice. Signed-off-by: Joanne Koong <joannelkoong@gmail.com> --- example/passthrough_hp.cc | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/example/passthrough_hp.cc b/example/passthrough_hp.cc index 41904e501..1c8db6c4d 100644 --- a/example/passthrough_hp.cc +++ b/example/passthrough_hp.cc @@ -165,7 +165,7 @@ static Fs fs{}; #define FUSE_BUF_COPY_FLAGS \ (fs.nosplice ? \ FUSE_BUF_NO_SPLICE : \ - static_cast<fuse_buf_copy_flags>(0)) + static_cast<fuse_buf_copy_flags>(FUSE_BUF_SPLICE_MOVE)) static Inode& get_inode(fuse_ino_t ino) { @@ -202,13 +202,15 @@ static void sfs_init(void *userdata, fuse_conn_info *conn) { if (fs.nosplice) { // FUSE_CAP_SPLICE_READ is enabled in libfuse3 by default, // see do_init() in in fuse_lowlevel.c - // Just unset both, in case FUSE_CAP_SPLICE_WRITE would also get enabled - // by default. + // 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 */ From 374892840a5767309905dcce1fcde5f80173eedf Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Thu, 13 Feb 2025 14:55:52 +0100 Subject: [PATCH 038/310] Add signify key for 3.18 Signed-off-by: Bernd Schubert <bernd@bsbernd.com> --- signify/fuse-3.18.pub | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 signify/fuse-3.18.pub diff --git a/signify/fuse-3.18.pub b/signify/fuse-3.18.pub new file mode 100644 index 000000000..eeee6bf74 --- /dev/null +++ b/signify/fuse-3.18.pub @@ -0,0 +1,2 @@ +untrusted comment: signify public key +RWS6gMnNrKp/zRSYWv13J+KwXE26vCUsbC/hVZmjQ8PA3xjixGLjodz3 From 5889ae6733298ab2f79c25b03b53f7a49255156e Mon Sep 17 00:00:00 2001 From: "Laszlo Boszormenyi (GCS)" <gcs@debian.org> Date: Sun, 16 Feb 2025 09:07:36 +0100 Subject: [PATCH 039/310] Fix build of example/memfs_ll.cc on 32 bit architectures The code uses std::min() which expects its arguments to be size_t. Two times it uses an offset declared as off_t. While both size_t and off_t are 32-bit integers, the latter is signed. On 64 bit architectures the conversation of off_t -> size_t performed automatically. On 32 bit architectures it needs a type cast. Signed-off-by: Laszlo Boszormenyi (GCS) <gcs@debian.org> --- example/memfs_ll.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/example/memfs_ll.cc b/example/memfs_ll.cc index c7b966359..9898f2528 100644 --- a/example/memfs_ll.cc +++ b/example/memfs_ll.cc @@ -150,7 +150,7 @@ class Inode { void read_content(char *buf, size_t size, off_t offset) const { - size_t bytes_to_read = std::min(size, content.size() - offset); + size_t bytes_to_read = std::min(size, content.size() - (size_t)offset); std::copy(content.begin() + offset, content.begin() + offset + bytes_to_read, buf); } @@ -613,7 +613,7 @@ static void memfs_read(fuse_req_t req, fuse_ino_t ino, size_t size, } std::vector<char> content( - std::min(size, inode->content_size() - offset)); + std::min(size, inode->content_size() - (size_t)offset)); inode->read_content(content.data(), content.size(), offset); inode->unlock(); From 51cb3e2940317ce0cb409dee4f3d9e9bdde69da7 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Sun, 16 Feb 2025 22:40:41 +0100 Subject: [PATCH 040/310] fusermount: Exclude UFSD from whitelist for 32-bit builds The UFSD super magic is larger than 32-bit - I don't know if truncating to 32-bit would work - we just exclude it for now. Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- util/fusermount.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/util/fusermount.c b/util/fusermount.c index b87d8bb70..683c5496f 100644 --- a/util/fusermount.c +++ b/util/fusermount.c @@ -1141,7 +1141,9 @@ static int check_perm(const char **mntp, struct stat *stbuf, int *mountpoint_fd) 0x73717368 /* SQUASHFS_MAGIC */, 0x01021994 /* TMPFS_MAGIC */, 0x24051905 /* UBIFS_SUPER_MAGIC */, +#if __SIZEOF_LONG__ > 4 0x736675005346544e /* UFSD */, +#endif 0x58465342 /* XFS_SB_MAGIC */, 0x2FC12FC1 /* ZFS_SUPER_MAGIC */, 0x858458f6 /* RAMFS_MAGIC */, From 9971ec56d848519a1a5d4dc27ac709d90ff03e4b Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Sun, 16 Feb 2025 22:56:46 +0100 Subject: [PATCH 041/310] example/hello_ll_uds: Switch to %zu and avoid 32bit build warning Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- example/hello_ll_uds.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/example/hello_ll_uds.c b/example/hello_ll_uds.c index a566155dc..e9cd17315 100644 --- a/example/hello_ll_uds.c +++ b/example/hello_ll_uds.c @@ -185,8 +185,8 @@ static int create_socket(const char *socket_path) { if (strnlen(socket_path, sizeof(addr.sun_path)) >= sizeof(addr.sun_path)) { - printf("Socket path may not be longer than %lu characters\n", - sizeof(addr.sun_path) - 1); + printf("Socket path may not be longer than %zu characters\n", + sizeof(addr.sun_path) - 1); return -1; } From 55f696228e5728cd89008e0af1ca5178bd4b9fb6 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Mon, 17 Feb 2025 19:23:44 +0100 Subject: [PATCH 042/310] tests: Skip the ioctl test for x86 cross compilation Probably some weird corner case in cross compilation, for now we ignore this. Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- test/test_examples.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/test_examples.py b/test/test_examples.py index e0fb8c467..54a2f88f9 100755 --- a/test/test_examples.py +++ b/test/test_examples.py @@ -284,6 +284,11 @@ 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') @@ -427,6 +432,14 @@ def test_dev_auto_unmount(short_tmpdir, output_checker, intended_user): @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' From 7e02c20c650c378729ab4999933f556c4bfb25c6 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Sun, 16 Feb 2025 22:15:31 +0100 Subject: [PATCH 043/310] ci-build test: Add a 32-bit compilation test That was missing so far. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> --- .github/workflows/pr-ci.yml | 7 ++++++- test/ci-build.sh | 17 +++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr-ci.yml b/.github/workflows/pr-ci.yml index bec616621..ccaaf7231 100644 --- a/.github/workflows/pr-ci.yml +++ b/.github/workflows/pr-ci.yml @@ -26,7 +26,12 @@ jobs: steps: - name: Install dependencies (Ubuntu) if: runner.os == 'Linux' - run: sudo apt-get update && sudo apt-get install -y clang doxygen gcc gcc-10 gcc-9 valgrind + 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 with: diff --git a/test/ci-build.sh b/test/ci-build.sh index 4f7da4c8f..3f0ff8546 100755 --- a/test/ci-build.sh +++ b/test/ci-build.sh @@ -118,6 +118,23 @@ sanitized_build() 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++ From d7d7f12d115b8853af0665c41e07d9f17d271a6f Mon Sep 17 00:00:00 2001 From: Maksim Harbachou <maksim.harbachou@resilio.com> Date: Fri, 14 Feb 2025 14:20:19 +0100 Subject: [PATCH 044/310] Add comment for fuse_passthrough_open() See https://github.com/libfuse/libfuse/issues/1125 Signed-off-by: Maksim Harbachou <maksim.harbachou@resilio.com> --- include/fuse_lowlevel.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/include/fuse_lowlevel.h b/include/fuse_lowlevel.h index d0617ba35..b03b37aae 100644 --- a/include/fuse_lowlevel.h +++ b/include/fuse_lowlevel.h @@ -1427,6 +1427,8 @@ int fuse_reply_readlink(fuse_req_t req, const char *link); /** * Setup passthrough backing file for open reply * + * Currently there should be only one backing id per node / backing file. + * * Possible requests: * open, opendir, create * From 42045175964ca3f46dd8f49409ac0cc97e51289e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Feb 2025 19:28:30 +0000 Subject: [PATCH 045/310] build(deps): bump github/codeql-action from 3.28.8 to 3.28.9 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.28.8 to 3.28.9. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/dd746615b3b9d728a6a37ca2045b68ca76d4841a...9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 89fbe2494..cefd1fcbf 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -53,7 +53,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8 + uses: github/codeql-action/init@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -78,7 +78,7 @@ jobs: meson compile -C build - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8 + uses: github/codeql-action/analyze@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9 with: category: "/language:${{matrix.language}}" From 84da6e08fb533bea595373e20b3b8a4edb4987cb Mon Sep 17 00:00:00 2001 From: Vassili Tchersky <vt+git@vbcy.org> Date: Mon, 17 Feb 2025 08:47:57 +0100 Subject: [PATCH 046/310] tests: Disable tests with TMP_FILE on FreeBSD Not supported yet on this platform. See https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=283179 Signed-off-by: Vassili Tchersky <vt+git@vbcy.org> --- test/test_syscalls.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/test_syscalls.c b/test/test_syscalls.c index 26b2ff2be..97906a3e6 100644 --- a/test/test_syscalls.c +++ b/test/test_syscalls.c @@ -1957,6 +1957,7 @@ static int do_test_create_ro_dir(int flags, const char *flags_str) return 0; } +#ifndef __FreeBSD__ /* this tests open with O_TMPFILE note that this will only work with the fuse low level api you will get ENOTSUP with the high level api */ @@ -2054,6 +2055,7 @@ static int test_create_and_link_tmpfile(void) success(); return 0; } +#endif int main(int argc, char *argv[]) { @@ -2183,8 +2185,10 @@ 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(); +#ifndef __FreeBSD__ err += test_create_tmpfile(); err += test_create_and_link_tmpfile(); +#endif unlink(testfile2); unlink(testsock); From 42824c4afc7da568b3a1914c5e5954049d71f253 Mon Sep 17 00:00:00 2001 From: Vassili Tchersky <vt+git@vbcy.org> Date: Mon, 17 Feb 2025 08:48:42 +0100 Subject: [PATCH 047/310] tests: Re-enable mknod and mkfifo tests on FreeBSD Signed-off-by: Vassili Tchersky <vt+git@vbcy.org> --- test/test_syscalls.c | 6 ------ 1 file changed, 6 deletions(-) diff --git a/test/test_syscalls.c b/test/test_syscalls.c index 97906a3e6..4bbe97340 100644 --- a/test/test_syscalls.c +++ b/test/test_syscalls.c @@ -1065,7 +1065,6 @@ static int test_create_unlink(void) return 0; } -#ifndef __FreeBSD__ static int test_mknod(void) { int err = 0; @@ -1098,7 +1097,6 @@ static int test_mknod(void) success(); return 0; } -#endif #define test_open(exist, flags, mode) do_test_open(exist, flags, #flags, mode) @@ -1792,7 +1790,6 @@ static int test_rename_dir_loop(void) #undef PATH } -#ifndef __FreeBSD__ static int test_mkfifo(void) { int res; @@ -1824,7 +1821,6 @@ static int test_mkfifo(void) success(); return 0; } -#endif static int test_mkdir(void) { @@ -2120,10 +2116,8 @@ int main(int argc, char *argv[]) err += test_symlink(); err += test_link(); err += test_link2(); -#ifndef __FreeBSD__ err += test_mknod(); err += test_mkfifo(); -#endif err += test_mkdir(); err += test_rename_file(); err += test_rename_dir(); From 3232d6e05fed808878ef8ae86af348a94f0da585 Mon Sep 17 00:00:00 2001 From: Vassili Tchersky <vt+git@vbcy.org> Date: Mon, 17 Feb 2025 08:50:19 +0100 Subject: [PATCH 048/310] mount_bsd: Fix usage of libfuse_strtol The check on fd < 0 was recently removed. However, this check is important as the content of FUSE_DEV_FD is passed as-is to mount_fusefs, and a string beginning by '-' is treated as an option. Additionally, add a proper include and type. Signed-off-by: Vassili Tchersky <vt+git@vbcy.org> --- lib/mount_bsd.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/mount_bsd.c b/lib/mount_bsd.c index 1863c49c6..85332d34c 100644 --- a/lib/mount_bsd.c +++ b/lib/mount_bsd.c @@ -12,6 +12,7 @@ #include "fuse_i.h" #include "fuse_misc.h" #include "fuse_opt.h" +#include "util.h" #include <sys/param.h> #include "fuse_mount_compat.h" @@ -151,7 +152,7 @@ static int init_backgrounded(void) static int fuse_mount_core(const char *mountpoint, const char *opts) { const char *mountprog = FUSERMOUNT_PROG; - int fd; + long fd; char *fdnam, *dev; pid_t pid, cpid; int status; @@ -161,7 +162,7 @@ static int fuse_mount_core(const char *mountpoint, const char *opts) if (fdnam) { err = libfuse_strtol(fdnam, &fd); - if (err) { + if (err || fd < 0) { fuse_log(FUSE_LOG_ERR, "invalid value given in FUSE_DEV_FD\n"); return -1; } @@ -216,7 +217,7 @@ static int fuse_mount_core(const char *mountpoint, const char *opts) if (! fdnam) { - ret = asprintf(&fdnam, "%d", fd); + ret = asprintf(&fdnam, "%ld", fd); if(ret == -1) { perror("fuse: failed to assemble mount arguments"); From 124ceb7382d5d13436d9bf47a8df349880a40c52 Mon Sep 17 00:00:00 2001 From: Vassili Tchersky <vt+git@vbcy.org> Date: Mon, 17 Feb 2025 08:53:03 +0100 Subject: [PATCH 049/310] FreeBSD: Remove useless options These options never had any effect. See https://svnweb.freebsd.org/base?view=revision&revision=347544 Signed-off-by: Vassili Tchersky <vt+git@vbcy.org> --- lib/mount_bsd.c | 37 ++++++------------------------------- 1 file changed, 6 insertions(+), 31 deletions(-) diff --git a/lib/mount_bsd.c b/lib/mount_bsd.c index 85332d34c..789f61b7a 100644 --- a/lib/mount_bsd.c +++ b/lib/mount_bsd.c @@ -19,7 +19,6 @@ #include <sys/stat.h> #include <sys/wait.h> -#include <sys/sysctl.h> #include <sys/user.h> #include <stdio.h> #include <stdlib.h> @@ -86,7 +85,6 @@ static const struct fuse_opt fuse_mount_opts[] = { FUSE_DUAL_OPT_KEY("private", KEY_KERN), FUSE_DUAL_OPT_KEY("neglect_shares", KEY_KERN), FUSE_DUAL_OPT_KEY("push_symlinks_in", KEY_KERN), - FUSE_OPT_KEY("nosync_unmount", KEY_KERN), #if __FreeBSD_version >= 1200519 FUSE_DUAL_OPT_KEY("intr", KEY_KERN), #endif @@ -134,21 +132,6 @@ void fuse_kern_unmount(const char *mountpoint, int fd) unmount(mountpoint, MNT_FORCE); } -/* Check if kernel is doing init in background */ -static int init_backgrounded(void) -{ - unsigned ibg; - size_t len; - - len = sizeof(ibg); - - if (sysctlbyname("vfs.fuse.init_backgrounded", &ibg, &len, NULL, 0)) - return 0; - - return ibg; -} - - static int fuse_mount_core(const char *mountpoint, const char *opts) { const char *mountprog = FUSERMOUNT_PROG; @@ -194,20 +177,12 @@ static int fuse_mount_core(const char *mountpoint, const char *opts) } if (pid == 0) { - if (! init_backgrounded()) { - /* - * If init is not backgrounded, we have to - * call the mount util backgrounded, to avoid - * deadlock. - */ - - pid = fork(); - - if (pid == -1) { - perror("fuse: fork() failed"); - close(fd); - exit(1); - } + pid = fork(); + + if (pid == -1) { + perror("fuse: fork() failed"); + close(fd); + _exit(EXIT_FAILURE); } if (pid == 0) { From f665917e93509183eb798a1c9862f6cb851b7530 Mon Sep 17 00:00:00 2001 From: Vassili Tchersky <vt+git@vbcy.org> Date: Mon, 17 Feb 2025 08:54:10 +0100 Subject: [PATCH 050/310] mount_bsd: Proper exit calls Use _exit() instead of exit() inside children Signed-off-by: Vassili Tchersky <vt+git@vbcy.org> --- lib/mount_bsd.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/mount_bsd.c b/lib/mount_bsd.c index 789f61b7a..ba17240ec 100644 --- a/lib/mount_bsd.c +++ b/lib/mount_bsd.c @@ -189,7 +189,7 @@ static int fuse_mount_core(const char *mountpoint, const char *opts) const char *argv[32]; int a = 0; int ret = -1; - + if (! fdnam) { ret = asprintf(&fdnam, "%ld", fd); @@ -197,7 +197,7 @@ static int fuse_mount_core(const char *mountpoint, const char *opts) { perror("fuse: failed to assemble mount arguments"); close(fd); - exit(1); + _exit(EXIT_FAILURE); } } @@ -212,10 +212,10 @@ static int fuse_mount_core(const char *mountpoint, const char *opts) execvp(mountprog, (char **) argv); perror("fuse: failed to exec mount program"); free(fdnam); - exit(1); + _exit(EXIT_FAILURE); } - exit(0); + _exit(EXIT_SUCCESS); } if (waitpid(cpid, &status, 0) == -1 || WEXITSTATUS(status) != 0) { From 06fef4f8c3873e43027b8221a745a5fdf63d867c Mon Sep 17 00:00:00 2001 From: Vassili Tchersky <vt+git@vbcy.org> Date: Mon, 17 Feb 2025 08:54:45 +0100 Subject: [PATCH 051/310] mount_bsd: Show errors when syscalls failed Log on unmount() and close() failure Signed-off-by: Vassili Tchersky <vt+git@vbcy.org> --- lib/mount_bsd.c | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/mount_bsd.c b/lib/mount_bsd.c index ba17240ec..0e841df5f 100644 --- a/lib/mount_bsd.c +++ b/lib/mount_bsd.c @@ -128,8 +128,11 @@ static int fuse_mount_opt_proc(void *data, const char *arg, int key, void fuse_kern_unmount(const char *mountpoint, int fd) { - close(fd); - unmount(mountpoint, MNT_FORCE); + if (close(fd) < 0) + fuse_log(FUSE_LOG_ERR, "closing FD %d failed: %s", fd, strerror(errno)); + if (unmount(mountpoint, MNT_FORCE) < 0) + fuse_log(FUSE_LOG_ERR, "unmounting %s failed: %s", + mountpoint, strerror(errno)); } static int fuse_mount_core(const char *mountpoint, const char *opts) @@ -220,7 +223,8 @@ static int fuse_mount_core(const char *mountpoint, const char *opts) if (waitpid(cpid, &status, 0) == -1 || WEXITSTATUS(status) != 0) { perror("fuse: failed to mount file system"); - close(fd); + if (close(fd) < 0) + perror("fuse: closing FD"); return -1; } From 619538e5d5cd19ee3e3919dea223464285d3cb8a Mon Sep 17 00:00:00 2001 From: Vassili Tchersky <vt+git@vbcy.org> Date: Tue, 18 Feb 2025 13:24:40 +0100 Subject: [PATCH 052/310] mount_bsd: Remove unused headers Removed: - <sys/user.h>: used to be for kinfo_proc, long gone from the code - <paths.h>: used to be for _PATH_DEV and _PATH_DEVNULL, long gone - <limits.h>: used to be for _POSIX2_LINE_MAX, long gone - <sys/stat.h>: used to be for fstat/stat, long gone Signed-off-by: Vassili Tchersky <vt+git@vbcy.org> --- lib/mount_bsd.c | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/mount_bsd.c b/lib/mount_bsd.c index 0e841df5f..bd95a76d3 100644 --- a/lib/mount_bsd.c +++ b/lib/mount_bsd.c @@ -17,9 +17,7 @@ #include <sys/param.h> #include "fuse_mount_compat.h" -#include <sys/stat.h> #include <sys/wait.h> -#include <sys/user.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> @@ -27,8 +25,6 @@ #include <fcntl.h> #include <errno.h> #include <string.h> -#include <paths.h> -#include <limits.h> #define FUSERMOUNT_PROG "mount_fusefs" #define FUSE_DEV_TRUNK "/dev/fuse" From f8bdca32b4dfbed4b21a69dcc751cdcbf8b2d58e Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Mon, 17 Feb 2025 23:39:04 +0100 Subject: [PATCH 053/310] Avoid nested function declarations in helper functions libfuse-3.17 introduced several functions that should only be called via inlined helper functions, never directly. To enforce this, these functions were declared within the inlined functions. However, this triggers the compiler warning "-Werror=nested-externs". While this warning is valid, the nested declarations were intentional to prevent direct usage of these functions. Rather than suppressing the warning with pragmas, move these function declarations outside the helper functions while maintaining the intended access restrictions through other means. Closes: https://github.com/libfuse/libfuse/issues/1134 Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- include/fuse.h | 57 +++++++++++++++-------------------------- include/fuse_lowlevel.h | 12 ++++----- lib/fuse.c | 13 ++-------- lib/fuse_lowlevel.c | 4 --- lib/helper.c | 4 --- 5 files changed, 28 insertions(+), 62 deletions(-) diff --git a/include/fuse.h b/include/fuse.h index c0857fdbe..c94b62851 100644 --- a/include/fuse.h +++ b/include/fuse.h @@ -882,6 +882,9 @@ struct fuse_context { * * Do not call this directly, use fuse_main() */ +int fuse_main_real_versioned(int argc, char *argv[], + const struct fuse_operations *op, size_t op_size, + struct libfuse_version *version, void *user_data); static inline int fuse_main_real(int argc, char *argv[], const struct fuse_operations *op, size_t op_size, void *user_data) @@ -895,13 +898,6 @@ static inline int fuse_main_real(int argc, char *argv[], "%s is a libfuse internal function, please use fuse_main()\n", __func__); - /* not declared globally, to restrict usage of this function */ - int fuse_main_real_versioned(int argc, char *argv[], - const struct fuse_operations *op, - size_t op_size, - struct libfuse_version *version, - void *user_data); - return fuse_main_real_versioned(argc, argv, op, op_size, &version, user_data); } @@ -960,6 +956,9 @@ static inline int fuse_main_real(int argc, char *argv[], * * Example usage, see hello.c */ +int fuse_main_real_versioned(int argc, char *argv[], + const struct fuse_operations *op, size_t op_size, + struct libfuse_version *version, void *user_data); static inline int fuse_main_fn(int argc, char *argv[], const struct fuse_operations *op, void *user_data) @@ -971,12 +970,6 @@ static inline int fuse_main_fn(int argc, char *argv[], .padding = 0 }; - /* not declared globally, to restrict usage of this function */ - int fuse_main_real_versioned(int argc, char *argv[], - const struct fuse_operations *op, - size_t op_size, - struct libfuse_version *version, - void *user_data); return fuse_main_real_versioned(argc, argv, op, sizeof(*(op)), &version, user_data); } @@ -1000,6 +993,14 @@ static inline int fuse_main_fn(int argc, char *argv[], */ void fuse_lib_help(struct fuse_args *args); +/* Do not call this directly, use fuse_new() instead */ +struct fuse *_fuse_new_30(struct fuse_args *args, + const struct fuse_operations *op, size_t op_size, + struct libfuse_version *version, void *user_data); +struct fuse *_fuse_new_31(struct fuse_args *args, + const struct fuse_operations *op, size_t op_size, + struct libfuse_version *version, void *user_data); + /** * Create a new FUSE filesystem. * @@ -1028,22 +1029,10 @@ void fuse_lib_help(struct fuse_args *args); * @return the created FUSE handle */ #if FUSE_USE_VERSION == 30 -struct fuse *_fuse_new_30(struct fuse_args *args, - const struct fuse_operations *op, - size_t op_size, - struct libfuse_version *version, - void *user_data); -static inline struct fuse * -fuse_new_fn(struct fuse_args *args, - const struct fuse_operations *op, size_t op_size, - void *user_data) +static inline struct fuse *fuse_new_fn(struct fuse_args *args, + const struct fuse_operations *op, + size_t op_size, void *user_data) { - /* not declared globally, to restrict usage of this function */ - struct fuse *_fuse_new_30(struct fuse_args *args, - const struct fuse_operations *op, size_t op_size, - struct libfuse_version *version, - void *user_data); - struct libfuse_version version = { .major = FUSE_MAJOR_VERSION, .minor = FUSE_MINOR_VERSION, @@ -1054,10 +1043,9 @@ fuse_new_fn(struct fuse_args *args, return _fuse_new_30(args, op, op_size, &version, user_data); } #else /* FUSE_USE_VERSION */ -static inline struct fuse * -fuse_new_fn(struct fuse_args *args, - const struct fuse_operations *op, size_t op_size, - void *user_data) +static inline struct fuse *fuse_new_fn(struct fuse_args *args, + const struct fuse_operations *op, + size_t op_size, void *user_data) { struct libfuse_version version = { .major = FUSE_MAJOR_VERSION, @@ -1066,11 +1054,6 @@ fuse_new_fn(struct fuse_args *args, .padding = 0 }; - /* not declared globally, to restrict usage of this function */ - struct fuse *_fuse_new_31(struct fuse_args *args, - const struct fuse_operations *op, - size_t op_size, struct libfuse_version *version, - void *user_data); return _fuse_new_31(args, op, op_size, &version, user_data); } #endif diff --git a/include/fuse_lowlevel.h b/include/fuse_lowlevel.h index b03b37aae..93bcba296 100644 --- a/include/fuse_lowlevel.h +++ b/include/fuse_lowlevel.h @@ -2049,6 +2049,12 @@ int fuse_parse_cmdline_312(struct fuse_args *args, #endif #endif +/* Do not call this directly, use fuse_session_new() instead */ +struct fuse_session * +fuse_session_new_versioned(struct fuse_args *args, + const struct fuse_lowlevel_ops *op, size_t op_size, + struct libfuse_version *version, void *userdata); + /** * Create a low level session. * @@ -2088,12 +2094,6 @@ fuse_session_new_fn(struct fuse_args *args, const struct fuse_lowlevel_ops *op, .padding = 0 }; - /* not declared globally, to restrict usage of this function */ - struct fuse_session *fuse_session_new_versioned( - struct fuse_args *args, const struct fuse_lowlevel_ops *op, - size_t op_size, struct libfuse_version *version, - void *userdata); - return fuse_session_new_versioned(args, op, op_size, &version, userdata); } diff --git a/lib/fuse.c b/lib/fuse.c index 6c69a6897..933542992 100644 --- a/lib/fuse.c +++ b/lib/fuse.c @@ -4924,13 +4924,8 @@ void fuse_stop_cleanup_thread(struct fuse *f) * through the fuse_new macro */ struct fuse *_fuse_new_31(struct fuse_args *args, - const struct fuse_operations *op, - size_t op_size, struct libfuse_version *version, - void *user_data); -struct fuse *_fuse_new_31(struct fuse_args *args, - const struct fuse_operations *op, - size_t op_size, struct libfuse_version *version, - void *user_data) + const struct fuse_operations *op, size_t op_size, + struct libfuse_version *version, void *user_data) { struct fuse *f; struct node *root; @@ -5075,10 +5070,6 @@ struct fuse *_fuse_new_31(struct fuse_args *args, } /* Emulates 3.0-style fuse_new(), which processes --help */ -struct fuse *_fuse_new_30(struct fuse_args *args, const struct fuse_operations *op, - size_t op_size, - struct libfuse_version *version, - void *user_data); FUSE_SYMVER("_fuse_new_30", "_fuse_new@FUSE_3.0") struct fuse *_fuse_new_30(struct fuse_args *args, const struct fuse_operations *op, diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index e3e79d528..d650944a3 100644 --- a/lib/fuse_lowlevel.c +++ b/lib/fuse_lowlevel.c @@ -3244,10 +3244,6 @@ int fuse_session_receive_buf_internal(struct fuse_session *se, return _fuse_session_receive_buf(se, buf, ch, true); } -struct fuse_session * -fuse_session_new_versioned(struct fuse_args *args, - const struct fuse_lowlevel_ops *op, size_t op_size, - struct libfuse_version *version, void *userdata); struct fuse_session * fuse_session_new_versioned(struct fuse_args *args, const struct fuse_lowlevel_ops *op, size_t op_size, diff --git a/lib/helper.c b/lib/helper.c index a1cf98c3e..a7b2fe002 100644 --- a/lib/helper.c +++ b/lib/helper.c @@ -304,10 +304,6 @@ int fuse_daemonize(int foreground) return 0; } -/* Not symboled, as not part of the official API */ -int fuse_main_real_versioned(int argc, char *argv[], - const struct fuse_operations *op, size_t op_size, - struct libfuse_version *version, void *user_data); int fuse_main_real_versioned(int argc, char *argv[], const struct fuse_operations *op, size_t op_size, struct libfuse_version *version, void *user_data) From 38cfa386d5397e54817b684a60228a8ee1a3f48d Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Tue, 18 Feb 2025 20:23:09 +0100 Subject: [PATCH 054/310] Fix a typo in test/ci-build.sh (ct vs cat) Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- test/ci-build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/ci-build.sh b/test/ci-build.sh index 3f0ff8546..40bb79e8d 100755 --- a/test/ci-build.sh +++ b/test/ci-build.sh @@ -81,7 +81,7 @@ sanitized_build() meson setup -Dprefix=${PREFIX_DIR} -D werror=true\ "${SOURCE_DIR}" \ - || (ct meson-logs/meson-log.txt; false) + || (cat meson-logs/meson-log.txt; false) meson configure $SAN # b_lundef=false is required to work around clang From 21c87768e400d46c3f7c38edba2f338b7476b0c2 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Tue, 18 Feb 2025 21:45:05 +0100 Subject: [PATCH 055/310] github ci tests: Update the stable branch name to include fuse- The branch is actually called fuse-3.17.x Also disable checkpatch for branches except master, as it is to do basic checks, while stable branches do not need that check, assuming cherry-pick happens from master. Issue with it is that persistently complains about dependabot changes. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> --- .github/workflows/abicheck.yml | 4 ++-- .github/workflows/checkpatch.yml | 8 +++++++- .github/workflows/codeql.yml | 4 ++-- .github/workflows/codespell.yml | 4 ++-- .github/workflows/pr-ci.yml | 4 ++-- 5 files changed, 15 insertions(+), 9 deletions(-) diff --git a/.github/workflows/abicheck.yml b/.github/workflows/abicheck.yml index 526fb4296..10a8e6d98 100644 --- a/.github/workflows/abicheck.yml +++ b/.github/workflows/abicheck.yml @@ -5,11 +5,11 @@ on: push: branches: - master - - '[0-9]+.[0-9]+' # This will match branches like 3.17, 3.18, 4.0, etc. + - 'fuse-[0-9]+.[0-9]+*' # This will match branches like 3.17, 3.18, 4.0, etc. pull_request: branches: - master - - '[0-9]+.[0-9]+' + - 'fuse-[0-9]+.[0-9]+*' permissions: contents: read diff --git a/.github/workflows/checkpatch.yml b/.github/workflows/checkpatch.yml index 1adada4c5..622542c54 100644 --- a/.github/workflows/checkpatch.yml +++ b/.github/workflows/checkpatch.yml @@ -1,8 +1,14 @@ name: Checkpatch on: + push: + branches: + - master pull_request: - types: [opened, synchronize, reopened] + branches: + - master +permissions: + contents: read jobs: checkpatch: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index cefd1fcbf..474f2c8d2 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -15,11 +15,11 @@ on: push: branches: - master - - '[0-9]+.[0-9]+' # This will match branches like 3.17, 3.18, 4.0, etc. + - 'fuse-[0-9]+.[0-9]+*' # This will match branches like 3.17, 3.18, 4.0, etc. pull_request: branches: - master - - '[0-9]+.[0-9]+' + - 'fuse-[0-9]+.[0-9]+*' jobs: analyze: diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml index 3b771c1ea..184fbcfe1 100644 --- a/.github/workflows/codespell.yml +++ b/.github/workflows/codespell.yml @@ -5,11 +5,11 @@ on: push: branches: - master - - '[0-9]+.[0-9]+' # This will match branches like 3.17, 3.18, 4.0, etc. + - 'fuse-[0-9]+.[0-9]+*' # This will match branches like 3.17, 3.18, 4.0, etc. pull_request: branches: - master - - '[0-9]+.[0-9]+' + - 'fuse-[0-9]+.[0-9]+*' permissions: contents: read diff --git a/.github/workflows/pr-ci.yml b/.github/workflows/pr-ci.yml index ccaaf7231..4bb7f66de 100644 --- a/.github/workflows/pr-ci.yml +++ b/.github/workflows/pr-ci.yml @@ -4,11 +4,11 @@ on: push: branches: - master - - '[0-9]+.[0-9]+' # This will match branches like 3.17, 3.18, 4.0, etc. + - 'fuse-[0-9]+.[0-9]+*' # This will match branches like 3.17, 3.18, 4.0, etc. pull_request: branches: - master - - '[0-9]+.[0-9]+' + - 'fuse-[0-9]+.[0-9]+*' permissions: contents: read From c389e09f3564f52ac0cf10719915347e57053b00 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Tue, 18 Feb 2025 23:06:20 +0100 Subject: [PATCH 056/310] github checkpatch test: Fix for non pull requests It should only run for pull requests. Indentation for the branch to run on was also not right. Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- .github/workflows/checkpatch.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/checkpatch.yml b/.github/workflows/checkpatch.yml index 622542c54..f6acb1bcb 100644 --- a/.github/workflows/checkpatch.yml +++ b/.github/workflows/checkpatch.yml @@ -1,14 +1,10 @@ name: Checkpatch on: - push: - branches: - - master pull_request: + types: [opened, synchronize, reopened] branches: - master -permissions: - contents: read jobs: checkpatch: @@ -23,6 +19,10 @@ 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" From 26b0e0ee2522d88a093204433aad72da0d44cb26 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Tue, 18 Feb 2025 23:50:12 +0100 Subject: [PATCH 057/310] Update meson.build for master branch to 3.18 Signed-off-by: Bernd Schubert <bernd@bsbernd.com> --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index e191fc62c..0487639ca 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('libfuse3', ['c'], - version: '3.17.1-rc0', # Version with RC suffix + version: '3.18.0-rc0', # Version with RC suffix meson_version: '>= 0.51.0', default_options: [ 'buildtype=debugoptimized', From 8ca2fe632de965c13d26e64dc1ff8517240a4078 Mon Sep 17 00:00:00 2001 From: Vassili Tchersky <vt+git@vbc.su> Date: Wed, 19 Feb 2025 03:43:11 +0100 Subject: [PATCH 058/310] mount: fix closing stdout/err and error logs Don't spawn a setuid children with FD 1&2 closed. Check status and not errno after posix_spawn(p). Add comments to fix later error checking, as posix_spawn(p) returns non-zero status only if clone/vfork/rfork fails. If only setup (open, dup2) or execve fails, the forked process exit with 127 but posix_spawn returns zero. Signed-off-by: Vassili Tchersky <vt+git@vbc.su> --- lib/mount.c | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/lib/mount.c b/lib/mount.c index 7dd727ce2..6ed4444b3 100644 --- a/lib/mount.c +++ b/lib/mount.c @@ -153,7 +153,7 @@ static int fusermount_posix_spawn(posix_spawn_file_actions_t *action, if (out_pid) *out_pid = pid; else - waitpid(pid, NULL, 0); + waitpid(pid, NULL, 0); /* FIXME: check exit code and return error if any */ return 0; } @@ -341,8 +341,8 @@ void fuse_kern_unmount(const char *mountpoint, int fd) "--", mountpoint, NULL }; int status = fusermount_posix_spawn(NULL, argv, NULL); if(status != 0) { - fuse_log(FUSE_LOG_ERR, "Spawaning %s to unumount failed", - FUSERMOUNT_PROG); + fuse_log(FUSE_LOG_ERR, "Spawning %s to unmount failed: %s", + FUSERMOUNT_PROG, strerror(-status)); return; } } @@ -390,8 +390,8 @@ static int setup_auto_unmount(const char *mountpoint, int quiet) posix_spawn_file_actions_init(&action); if (quiet) { - posix_spawn_file_actions_addclose(&action, 1); - posix_spawn_file_actions_addclose(&action, 2); + posix_spawn_file_actions_addopen(&action, STDOUT_FILENO, "/dev/null", O_WRONLY, 0); + posix_spawn_file_actions_addopen(&action, STDERR_FILENO, "/dev/null", O_WRONLY, 0); } posix_spawn_file_actions_addclose(&action, fds[1]); @@ -406,7 +406,8 @@ static int setup_auto_unmount(const char *mountpoint, int quiet) if(status != 0) { close(fds[0]); close(fds[1]); - fuse_log(FUSE_LOG_ERR, "fuse: Setting up auto-unmount failed"); + fuse_log(FUSE_LOG_ERR, "fuse: Setting up auto-unmount failed (spawn): %s", + strerror(-status)); return -1; } // passed to child now, so can close here. @@ -462,8 +463,8 @@ static int fuse_mount_fusermount(const char *mountpoint, struct mount_opts *mo, posix_spawn_file_actions_init(&action); if (quiet) { - posix_spawn_file_actions_addclose(&action, 1); - posix_spawn_file_actions_addclose(&action, 2); + posix_spawn_file_actions_addopen(&action, STDOUT_FILENO, "/dev/null", O_WRONLY, 0); + posix_spawn_file_actions_addopen(&action, STDERR_FILENO, "/dev/null", O_WRONLY, 0); } posix_spawn_file_actions_addclose(&action, fds[1]); @@ -474,8 +475,8 @@ static int fuse_mount_fusermount(const char *mountpoint, struct mount_opts *mo, if(status != 0) { close(fds[0]); close(fds[1]); - fuse_log(FUSE_LOG_ERR, "posix_spawnp() for %s failed", - FUSERMOUNT_PROG, strerror(errno)); + fuse_log(FUSE_LOG_ERR, "posix_spawn(p)() for %s failed: %s", + FUSERMOUNT_PROG, strerror(-status)); return -1; } From c516c643de27cc5fb7d30495f5c9047651a64447 Mon Sep 17 00:00:00 2001 From: Vassili Tchersky <vt+git@vbc.su> Date: Thu, 20 Feb 2025 13:55:03 +0100 Subject: [PATCH 059/310] fuse_log: simplify default log function fuse_log_level is guaranteed to be the same as libc as syslog is a network protocol and levels are numerical constants enforced in RFCs. Syslog is originally BSD-only and was imported by glibc and standardised in SUS and POSIX. Use vsyslog rather than formatting a new intermediate string. Signed-off-by: Vassili Tchersky <vt+git@vbc.su> --- lib/fuse_log.c | 47 +++++------------------------------------------ 1 file changed, 5 insertions(+), 42 deletions(-) diff --git a/lib/fuse_log.c b/lib/fuse_log.c index c1d16c148..5039b0454 100644 --- a/lib/fuse_log.c +++ b/lib/fuse_log.c @@ -10,58 +10,21 @@ #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; From 2a1bd0b0c8b774d70bd2154a50db97fc8875accd Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Tue, 11 Mar 2025 14:09:29 +0100 Subject: [PATCH 060/310] checkpatch: More ignores: ENOSYS,FROM_SIGN_OFF_MISMATCH,QUOTED_COMMIT_ID ENOSYS is right for fuse, automatic github action updates persistently run into sign-off-mismatch and commit-id quoting persistently conflicts with long line warnings. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> --- .github/workflows/checkpatch.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/checkpatch.yml b/.github/workflows/checkpatch.yml index f6acb1bcb..5bf92621e 100644 --- a/.github/workflows/checkpatch.yml +++ b/.github/workflows/checkpatch.yml @@ -30,7 +30,7 @@ jobs: git rev-list --reverse $base_commit..HEAD | while read commit; do 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 ! ./checkpatch.pl --no-tree --ignore MAINTAINERS,SPDX_LICENSE_TAG,COMMIT_MESSAGE,FILE_PATH_CHANGES,EMAIL_SUBJECT,AVOID_EXTERNS,GIT_COMMIT_ID,ENOSYS_SYSCALL,ENOSYS,FROM_SIGN_OFF_MISMATCH,QUOTED_COMMIT_ID -g $commit; then echo "checkpatch.pl found issues in commit $commit - $subject" exit 1 fi From f466ce3c91e33427f1387cde47a19006d4144553 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Tue, 11 Mar 2025 14:29:25 +0100 Subject: [PATCH 061/310] checkpatch: 100 chars per line Accept up to 100 chars per line to silence more github action updates. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> --- .github/workflows/checkpatch.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/checkpatch.yml b/.github/workflows/checkpatch.yml index 5bf92621e..45133eb87 100644 --- a/.github/workflows/checkpatch.yml +++ b/.github/workflows/checkpatch.yml @@ -30,7 +30,7 @@ jobs: git rev-list --reverse $base_commit..HEAD | while read commit; do 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,ENOSYS,FROM_SIGN_OFF_MISMATCH,QUOTED_COMMIT_ID -g $commit; then + if ! ./checkpatch.pl --max-line-length=100 --no-tree --ignore MAINTAINERS,SPDX_LICENSE_TAG,COMMIT_MESSAGE,FILE_PATH_CHANGES,EMAIL_SUBJECT,AVOID_EXTERNS,GIT_COMMIT_ID,ENOSYS_SYSCALL,ENOSYS,FROM_SIGN_OFF_MISMATCH,QUOTED_COMMIT_ID -g $commit; then echo "checkpatch.pl found issues in commit $commit - $subject" exit 1 fi From 622dca0b37c6dda09e406f39fe12f71a55682d78 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Mar 2025 22:14:36 +0000 Subject: [PATCH 062/310] build(deps): bump github/codeql-action from 3.28.9 to 3.28.11 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.28.9 to 3.28.11. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0...6bb031afdd8eb862ea3fc1848194185e076637e5) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 474f2c8d2..c2cb051a7 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -53,7 +53,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9 + uses: github/codeql-action/init@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3.28.11 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -78,7 +78,7 @@ jobs: meson compile -C build - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9 + uses: github/codeql-action/analyze@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3.28.11 with: category: "/language:${{matrix.language}}" From 431d69a789932d0cc612d541d1e8e3c084df950f Mon Sep 17 00:00:00 2001 From: Luis Henriques <luis@igalia.com> Date: Tue, 4 Mar 2025 10:21:32 +0000 Subject: [PATCH 063/310] doc: Add new README about notification operations The main goal of this new file is to warn about the deadlocking risks when using the out-of-band notify operations while replying to other requests. Signed-off-by: Luis Henriques <luis@igalia.com> --- doc/README.notifications | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 doc/README.notifications 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. From 3a291c355f3a4966ff3ebb6841b31da0d3010f52 Mon Sep 17 00:00:00 2001 From: jnr0006 <jacob.nick.riley@gmail.com> Date: Wed, 12 Mar 2025 16:21:44 -0500 Subject: [PATCH 064/310] Add PanFS to whitelist Added PanFSto whitelist. This should allow us to mount gocryptfs onto the parallel filesystem. Signed-off-by: Jacob Riley <jnr0006@uah.edu> --- util/fusermount.c | 1 + 1 file changed, 1 insertion(+) diff --git a/util/fusermount.c b/util/fusermount.c index 683c5496f..acbff61fb 100644 --- a/util/fusermount.c +++ b/util/fusermount.c @@ -1136,6 +1136,7 @@ static int check_perm(const char **mntp, struct stat *stbuf, int *mountpoint_fd) 0x7366746E /* NTFS3_SUPER_MAGIC */, 0x5346414f /* OPENAFS_SUPER_MAGIC */, 0x794C7630 /* OVERLAYFS_SUPER_MAGIC */, + 0xAAD7AAEA /* PANFS_SUPER_MAGIC */, 0x52654973 /* REISERFS_SUPER_MAGIC */, 0xFE534D42 /* SMB2_SUPER_MAGIC */, 0x73717368 /* SQUASHFS_MAGIC */, From 3ae5ca7443348aabad9bc71b9d5b0999f8292379 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Thu, 13 Mar 2025 23:52:01 +0100 Subject: [PATCH 065/310] fuse_common.h: Convert back FUSE_CAP_ from enum to defines Some applications use that for detection of features between distributions/libfuse version. Closes: https://github.com/libfuse/libfuse/issues/1163 Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- include/fuse_common.h | 637 +++++++++++++++++++++--------------------- 1 file changed, 315 insertions(+), 322 deletions(-) diff --git a/include/fuse_common.h b/include/fuse_common.h index aba1c15c2..77efc5de5 100644 --- a/include/fuse_common.h +++ b/include/fuse_common.h @@ -174,355 +174,348 @@ struct fuse_loop_config_v1 { * Capability bits for 'fuse_conn_info.capable' and 'fuse_conn_info.want' * **************************************************************************/ -enum fuse_capability { - /** - * Indicates that the filesystem supports asynchronous read requests. - * - * If this capability is not requested/available, the kernel will - * ensure that there is at most one pending read request per - * file-handle at any time, and will attempt to order read requests by - * increasing offset. - * - * This feature is enabled by default when supported by the kernel. - */ - FUSE_CAP_ASYNC_READ = (1 << 0), - - /** - * Indicates that the filesystem supports "remote" locking. - * - * This feature is enabled by default when supported by the kernel, - * and if getlk() and setlk() handlers are implemented. - */ - FUSE_CAP_POSIX_LOCKS = (1 << 1), +/** + * Indicates that the filesystem supports asynchronous read requests. + * + * If this capability is not requested/available, the kernel will + * ensure that there is at most one pending read request per + * file-handle at any time, and will attempt to order read requests by + * increasing offset. + * + * This feature is enabled by default when supported by the kernel. + */ +#define FUSE_CAP_ASYNC_READ (1 << 0) - /** - * Indicates that the filesystem supports the O_TRUNC open flag. If - * disabled, and an application specifies O_TRUNC, fuse first calls - * truncate() and then open() with O_TRUNC filtered out. - * - * This feature is enabled by default when supported by the kernel. - */ - FUSE_CAP_ATOMIC_O_TRUNC = (1 << 3), +/** + * Indicates that the filesystem supports "remote" locking. + * + * 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) - /** - * Indicates that the filesystem supports lookups of "." and "..". - * - * When this flag is set, the filesystem must be prepared to receive requests - * for invalid inodes (i.e., for which a FORGET request was received or - * which have been used in a previous instance of the filesystem daemon) and - * must not reuse node-ids (even when setting generation numbers). - * - * This feature is disabled by default. - */ - FUSE_CAP_EXPORT_SUPPORT = (1 << 4), +/** + * Indicates that the filesystem supports the O_TRUNC open flag. If + * disabled, and an application specifies O_TRUNC, fuse first calls + * truncate() and then open() with O_TRUNC filtered out. + * + * This feature is enabled by default when supported by the kernel. + */ +#define FUSE_CAP_ATOMIC_O_TRUNC (1 << 3) - /** - * Indicates that the kernel should not apply the umask to the - * file mode on create operations. - * - * This feature is disabled by default. - */ - FUSE_CAP_DONT_MASK = (1 << 6), +/** + * Indicates that the filesystem supports lookups of "." and "..". + * + * When this flag is set, the filesystem must be prepared to receive requests + * for invalid inodes (i.e., for which a FORGET request was received or + * which have been used in a previous instance of the filesystem daemon) and + * must not reuse node-ids (even when setting generation numbers). + * + * This feature is disabled by default. + */ +#define FUSE_CAP_EXPORT_SUPPORT (1 << 4) - /** - * Indicates that libfuse should try to use splice() when writing to - * the fuse device. This may improve performance. - * - * This feature is disabled by default. - */ - FUSE_CAP_SPLICE_WRITE = (1 << 7), +/** + * Indicates that the kernel should not apply the umask to the + * file mode on create operations. + * + * This feature is disabled by default. + */ +#define FUSE_CAP_DONT_MASK (1 << 6) - /** - * Indicates that libfuse should try to move pages instead of copying when - * writing to / reading from the fuse device. This may improve performance. - * - * This feature is disabled by default. - */ - FUSE_CAP_SPLICE_MOVE = (1 << 8), +/** + * Indicates that libfuse should try to use splice() when writing to + * the fuse device. This may improve performance. + * + * This feature is disabled by default. + */ +#define FUSE_CAP_SPLICE_WRITE (1 << 7) - /** - * Indicates that libfuse should try to use splice() when reading from - * the fuse device. This may improve performance. - * - * This feature is enabled by default when supported by the kernel and - * if the filesystem implements a write_buf() handler. - */ - FUSE_CAP_SPLICE_READ = (1 << 9), +/** + * Indicates that libfuse should try to move pages instead of copying when + * writing to / reading from the fuse device. This may improve performance. + * + * This feature is disabled by default. + */ +#define FUSE_CAP_SPLICE_MOVE (1 << 8) - /** - * If set, the calls to flock(2) will be emulated using POSIX locks and must - * then be handled by the filesystem's setlock() handler. - * - * If not set, flock(2) calls will be handled by the FUSE kernel module - * internally (so any access that does not go through the kernel cannot be taken - * into account). - * - * This feature is enabled by default when supported by the kernel and - * if the filesystem implements a flock() handler. - */ - FUSE_CAP_FLOCK_LOCKS = (1 << 10), +/** + * Indicates that libfuse should try to use splice() when reading from + * the fuse device. This may improve performance. + * + * 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) - /** - * Indicates that the filesystem supports ioctl's on directories. - * - * This feature is enabled by default when supported by the kernel. - */ - FUSE_CAP_IOCTL_DIR = (1 << 11), +/** + * If set, the calls to flock(2) will be emulated using POSIX locks and must + * then be handled by the filesystem's setlock() handler. + * + * If not set, flock(2) calls will be handled by the FUSE kernel module + * internally (so any access that does not go through the kernel cannot be taken + * into account). + * + * 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) - /** - * Traditionally, while a file is open the FUSE kernel module only - * asks the filesystem for an update of the file's attributes when a - * client attempts to read beyond EOF. This is unsuitable for - * e.g. network filesystems, where the file contents may change - * without the kernel knowing about it. - * - * If this flag is set, FUSE will check the validity of the attributes - * on every read. If the attributes are no longer valid (i.e., if the - * *attr_timeout* passed to fuse_reply_attr() or set in `struct - * fuse_entry_param` has passed), it will first issue a `getattr` - * request. If the new mtime differs from the previous value, any - * cached file *contents* will be invalidated as well. - * - * This flag should always be set when available. If all file changes - * go through the kernel, *attr_timeout* should be set to a very large - * number to avoid unnecessary getattr() calls. - * - * This feature is enabled by default when supported by the kernel. - */ - FUSE_CAP_AUTO_INVAL_DATA = (1 << 12), +/** + * 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) - /** - * Indicates that the filesystem supports readdirplus. - * - * This feature is enabled by default when supported by the kernel and if the - * filesystem implements a readdirplus() handler. - */ - FUSE_CAP_READDIRPLUS = (1 << 13), +/** + * Traditionally, while a file is open the FUSE kernel module only + * asks the filesystem for an update of the file's attributes when a + * client attempts to read beyond EOF. This is unsuitable for + * e.g. network filesystems, where the file contents may change + * without the kernel knowing about it. + * + * If this flag is set, FUSE will check the validity of the attributes + * on every read. If the attributes are no longer valid (i.e., if the + * *attr_timeout* passed to fuse_reply_attr() or set in `struct + * fuse_entry_param` has passed), it will first issue a `getattr` + * request. If the new mtime differs from the previous value, any + * cached file *contents* will be invalidated as well. + * + * This flag should always be set when available. If all file changes + * go through the kernel, *attr_timeout* should be set to a very large + * number to avoid unnecessary getattr() calls. + * + * This feature is enabled by default when supported by the kernel. + */ +#define FUSE_CAP_AUTO_INVAL_DATA (1 << 12) - /** - * Indicates that the filesystem supports adaptive readdirplus. - * - * If FUSE_CAP_READDIRPLUS is not set, this flag has no effect. - * - * If FUSE_CAP_READDIRPLUS is set and this flag is not set, the kernel - * will always issue readdirplus() requests to retrieve directory - * contents. - * - * If FUSE_CAP_READDIRPLUS is set and this flag is set, the kernel - * will issue both readdir() and readdirplus() requests, depending on - * how much information is expected to be required. - * - * As of Linux 4.20, the algorithm is as follows: when userspace - * starts to read directory entries, issue a READDIRPLUS request to - * the filesystem. If any entry attributes have been looked up by the - * time userspace requests the next batch of entries continue with - * READDIRPLUS, otherwise switch to plain READDIR. This will reasult - * in eg plain "ls" triggering READDIRPLUS first then READDIR after - * that because it doesn't do lookups. "ls -l" should result in all - * READDIRPLUS, except if dentries are already cached. - * - * This feature is enabled by default when supported by the kernel and - * if the filesystem implements both a readdirplus() and a readdir() - * handler. - */ - FUSE_CAP_READDIRPLUS_AUTO = (1 << 14), +/** + * Indicates that the filesystem supports readdirplus. + * + * 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) - /** - * Indicates that the filesystem supports asynchronous direct I/O submission. - * - * If this capability is not requested/available, the kernel will ensure that - * there is at most one pending read and one pending write request per direct - * I/O file-handle at any time. - * - * This feature is enabled by default when supported by the kernel. - */ - FUSE_CAP_ASYNC_DIO = (1 << 15), +/** + * Indicates that the filesystem supports adaptive readdirplus. + * + * If FUSE_CAP_READDIRPLUS is not set, this flag has no effect. + * + * If FUSE_CAP_READDIRPLUS is set and this flag is not set, the kernel + * will always issue readdirplus() requests to retrieve directory + * contents. + * + * If FUSE_CAP_READDIRPLUS is set and this flag is set, the kernel + * will issue both readdir() and readdirplus() requests, depending on + * how much information is expected to be required. + * + * As of Linux 4.20, the algorithm is as follows: when userspace + * starts to read directory entries, issue a READDIRPLUS request to + * the filesystem. If any entry attributes have been looked up by the + * time userspace requests the next batch of entries continue with + * READDIRPLUS, otherwise switch to plain READDIR. This will reasult + * in eg plain "ls" triggering READDIRPLUS first then READDIR after + * that because it doesn't do lookups. "ls -l" should result in all + * READDIRPLUS, except if dentries are already cached. + * + * This feature is enabled by default when supported by the kernel and + * if the filesystem implements both a readdirplus() and a readdir() + * handler. + */ +#define FUSE_CAP_READDIRPLUS_AUTO (1 << 14) - /** - * Indicates that writeback caching should be enabled. This means that - * individual write request may be buffered and merged in the kernel - * before they are send to the filesystem. - * - * This feature is disabled by default. - */ - FUSE_CAP_WRITEBACK_CACHE = (1 << 16), +/** + * Indicates that the filesystem supports asynchronous direct I/O submission. + * + * If this capability is not requested/available, the kernel will ensure that + * there is at most one pending read and one pending write request per direct + * I/O file-handle at any time. + * + * This feature is enabled by default when supported by the kernel. + */ +#define FUSE_CAP_ASYNC_DIO (1 << 15) - /** - * Indicates support for zero-message opens. If this flag is set in - * the `capable` field of the `fuse_conn_info` structure, then the - * filesystem may return `ENOSYS` from the open() handler to indicate - * success. Further attempts to open files will be handled in the - * kernel. (If this flag is not set, returning ENOSYS will be treated - * as an error and signaled to the caller). - * - * Setting this flag in the `want` field enables this behavior automatically - * within libfuse for low level API users. If non-low level users wish to have - * this behavior you must return `ENOSYS` from the open() handler on supporting - * kernels. - */ - FUSE_CAP_NO_OPEN_SUPPORT = (1 << 17), +/** + * Indicates that writeback caching should be enabled. This means that + * individual write request may be buffered and merged in the kernel + * before they are send to the filesystem. + * + * This feature is disabled by default. + */ +#define FUSE_CAP_WRITEBACK_CACHE (1 << 16) - /** - * Indicates support for parallel directory operations. If this flag - * is unset, the FUSE kernel module will ensure that lookup() and - * readdir() requests are never issued concurrently for the same - * directory. - */ - FUSE_CAP_PARALLEL_DIROPS = (1 << 18), +/** + * Indicates support for zero-message opens. If this flag is set in + * the `capable` field of the `fuse_conn_info` structure, then the + * filesystem may return `ENOSYS` from the open() handler to indicate + * success. Further attempts to open files will be handled in the + * kernel. (If this flag is not set, returning ENOSYS will be treated + * as an error and signaled to the caller). + * + * Setting this flag in the `want` field enables this behavior automatically + * within libfuse for low level API users. If non-low level users wish to have + * this behavior you must return `ENOSYS` from the open() handler on supporting + * kernels. + */ +#define FUSE_CAP_NO_OPEN_SUPPORT (1 << 17) - /** - * Indicates support for POSIX ACLs. - * - * If this feature is enabled, the kernel will cache and have - * responsibility for enforcing ACLs. ACL will be stored as xattrs and - * passed to userspace, which is responsible for updating the ACLs in - * the filesystem, keeping the file mode in sync with the ACL, and - * ensuring inheritance of default ACLs when new filesystem nodes are - * created. Note that this requires that the file system is able to - * parse and interpret the xattr representation of ACLs. - * - * Enabling this feature implicitly turns on the - * ``default_permissions`` mount option (even if it was not passed to - * mount(2)). - * - * This feature is disabled by default. - */ - FUSE_CAP_POSIX_ACL = (1 << 19), +/** + * Indicates support for parallel directory operations. If this flag + * is unset, the FUSE kernel module will ensure that lookup() and + * readdir() requests are never issued concurrently for the same + * directory. + */ +#define FUSE_CAP_PARALLEL_DIROPS (1 << 18) - /** - * Indicates that the filesystem is responsible for unsetting - * setuid and setgid bits when a file is written, truncated, or - * its owner is changed. - * - * This feature is disabled by default. - */ - FUSE_CAP_HANDLE_KILLPRIV = (1 << 20), +/** + * Indicates support for POSIX ACLs. + * + * If this feature is enabled, the kernel will cache and have + * responsibility for enforcing ACLs. ACL will be stored as xattrs and + * passed to userspace, which is responsible for updating the ACLs in + * the filesystem, keeping the file mode in sync with the ACL, and + * ensuring inheritance of default ACLs when new filesystem nodes are + * created. Note that this requires that the file system is able to + * parse and interpret the xattr representation of ACLs. + * + * Enabling this feature implicitly turns on the + * ``default_permissions`` mount option (even if it was not passed to + * mount(2)). + * + * This feature is disabled by default. + */ +#define FUSE_CAP_POSIX_ACL (1 << 19) - /** - * Indicates that the filesystem is responsible for unsetting - * setuid and setgid bit and additionally cap (stored as xattr) when a - * file is written, truncated, or its owner is changed. - * Upon write/truncate suid/sgid is only killed if caller - * does not have CAP_FSETID. Additionally upon - * write/truncate sgid is killed only if file has group - * execute permission. (Same as Linux VFS behavior). - * KILLPRIV_V2 requires handling of - * - FUSE_OPEN_KILL_SUIDGID (set in struct fuse_create_in::open_flags) - * - FATTR_KILL_SUIDGID (set in struct fuse_setattr_in::valid) - * - FUSE_WRITE_KILL_SUIDGID (set in struct fuse_write_in::write_flags) - * - * This feature is disabled by default. - */ - FUSE_CAP_HANDLE_KILLPRIV_V2 = (1 << 21), +/** + * Indicates that the filesystem is responsible for unsetting + * setuid and setgid bits when a file is written, truncated, or + * its owner is changed. + * + * This feature is disabled by default. + */ +#define FUSE_CAP_HANDLE_KILLPRIV (1 << 20) - /** - * Indicates that the kernel supports caching symlinks in its page cache. - * - * When this feature is enabled, symlink targets are saved in the page cache. - * You can invalidate a cached link by calling: - * `fuse_lowlevel_notify_inval_inode(se, ino, 0, 0);` - * - * This feature is disabled by default. - * 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. - */ - FUSE_CAP_CACHE_SYMLINKS = (1 << 23), +/** + * Indicates that the filesystem is responsible for unsetting + * setuid and setgid bit and additionally cap (stored as xattr) when a + * file is written, truncated, or its owner is changed. + * Upon write/truncate suid/sgid is only killed if caller + * does not have CAP_FSETID. Additionally upon + * write/truncate sgid is killed only if file has group + * execute permission. (Same as Linux VFS behavior). + * KILLPRIV_V2 requires handling of + * - FUSE_OPEN_KILL_SUIDGID (set in struct fuse_create_in::open_flags) + * - FATTR_KILL_SUIDGID (set in struct fuse_setattr_in::valid) + * - FUSE_WRITE_KILL_SUIDGID (set in struct fuse_write_in::write_flags) + * + * This feature is disabled by default. + */ +#define FUSE_CAP_HANDLE_KILLPRIV_V2 (1 << 21) - /** - * Indicates support for zero-message opendirs. If this flag is set in - * the `capable` field of the `fuse_conn_info` structure, then the filesystem - * may return `ENOSYS` from the opendir() handler to indicate success. Further - * opendir and releasedir messages will be handled in the kernel. (If this - * flag is not set, returning ENOSYS will be treated as an error and signalled - * to the caller.) - * - * Setting this flag in the `want` field enables this behavior automatically - * within libfuse for low level API users. If non-low level users with to have - * this behavior you must return `ENOSYS` from the opendir() handler on - * supporting kernels. - */ - FUSE_CAP_NO_OPENDIR_SUPPORT = (1 << 24), +/** + * Indicates that the kernel supports caching symlinks in its page cache. + * + * When this feature is enabled, symlink targets are saved in the page cache. + * You can invalidate a cached link by calling: + * `fuse_lowlevel_notify_inval_inode(se, ino, 0, 0);` + * + * This feature is disabled by default. + * 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) - /** - * Indicates support for invalidating cached pages only on explicit request. - * - * If this flag is set in the `capable` field of the `fuse_conn_info` structure, - * then the FUSE kernel module supports invalidating cached pages only on - * explicit request by the filesystem through fuse_lowlevel_notify_inval_inode() - * or fuse_invalidate_path(). - * - * By setting this flag in the `want` field of the `fuse_conn_info` structure, - * the filesystem is responsible for invalidating cached pages through explicit - * requests to the kernel. - * - * Note that setting this flag does not prevent the cached pages from being - * flushed by OS itself and/or through user actions. - * - * Note that if both FUSE_CAP_EXPLICIT_INVAL_DATA and FUSE_CAP_AUTO_INVAL_DATA - * are set in the `capable` field of the `fuse_conn_info` structure then - * FUSE_CAP_AUTO_INVAL_DATA takes precedence. - * - * This feature is disabled by default. - */ - FUSE_CAP_EXPLICIT_INVAL_DATA = (1 << 25), +/** + * Indicates support for zero-message opendirs. If this flag is set in + * the `capable` field of the `fuse_conn_info` structure, then the filesystem + * may return `ENOSYS` from the opendir() handler to indicate success. Further + * opendir and releasedir messages will be handled in the kernel. (If this + * flag is not set, returning ENOSYS will be treated as an error and signalled + * to the caller.) + * + * Setting this flag in the `want` field enables this behavior automatically + * within libfuse for low level API users. If non-low level users with to have + * this behavior you must return `ENOSYS` from the opendir() handler on + * supporting kernels. + */ +#define FUSE_CAP_NO_OPENDIR_SUPPORT (1 << 24) - /** - * Indicates support that dentries can be expired. - * - * Expiring dentries, instead of invalidating them, makes a difference for - * overmounted dentries, where plain invalidation would detach all submounts - * before dropping the dentry from the cache. If only expiry is set on the - * dentry, then any overmounts are left alone and until ->d_revalidate() - * is called. - * - * Note: ->d_revalidate() is not called for the case of following a submount, - * so invalidation will only be triggered for the non-overmounted case. - * The dentry could also be mounted in a different mount instance, in which case - * any submounts will still be detached. - */ - FUSE_CAP_EXPIRE_ONLY = (1 << 26), +/** + * Indicates support for invalidating cached pages only on explicit request. + * + * If this flag is set in the `capable` field of the `fuse_conn_info` structure, + * then the FUSE kernel module supports invalidating cached pages only on + * explicit request by the filesystem through fuse_lowlevel_notify_inval_inode() + * or fuse_invalidate_path(). + * + * By setting this flag in the `want` field of the `fuse_conn_info` structure, + * the filesystem is responsible for invalidating cached pages through explicit + * requests to the kernel. + * + * Note that setting this flag does not prevent the cached pages from being + * flushed by OS itself and/or through user actions. + * + * Note that if both FUSE_CAP_EXPLICIT_INVAL_DATA and FUSE_CAP_AUTO_INVAL_DATA + * are set in the `capable` field of the `fuse_conn_info` structure then + * FUSE_CAP_AUTO_INVAL_DATA takes precedence. + * + * This feature is disabled by default. + */ +#define FUSE_CAP_EXPLICIT_INVAL_DATA (1 << 25) - /** - * 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. - */ - FUSE_CAP_SETXATTR_EXT = (1 << 27), +/** + * Indicates support that dentries can be expired. + * + * Expiring dentries, instead of invalidating them, makes a difference for + * overmounted dentries, where plain invalidation would detach all submounts + * before dropping the dentry from the cache. If only expiry is set on the + * dentry, then any overmounts are left alone and until ->d_revalidate() + * is called. + * + * Note: ->d_revalidate() is not called for the case of following a submount, + * so invalidation will only be triggered for the non-overmounted case. + * 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) - /** - * Files opened with FUSE_DIRECT_IO do not support MAP_SHARED mmap. This restriction - * is relaxed through FUSE_CAP_DIRECT_IO_RELAX (kernel flag: FUSE_DIRECT_IO_RELAX). - * MAP_SHARED is disabled by default for FUSE_DIRECT_IO, as this flag can be used to - * ensure coherency between mount points (or network clients) and with kernel page - * cache as enforced by mmap that cannot be guaranteed anymore. - */ - FUSE_CAP_DIRECT_IO_ALLOW_MMAP = (1 << 28), +/** + * 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) - /** - * Indicates support for passthrough mode access for read/write operations. - * - * If this flag is set in the `capable` field of the `fuse_conn_info` - * structure, then the FUSE kernel module supports redirecting read/write - * operations to the backing file instead of letting them to be handled - * by the FUSE daemon. - * - * This feature is disabled by default. - */ - FUSE_CAP_PASSTHROUGH = (1 << 29), +/** + * Files opened with FUSE_DIRECT_IO do not support MAP_SHARED mmap. This restriction + * is relaxed through FUSE_CAP_DIRECT_IO_RELAX (kernel flag: FUSE_DIRECT_IO_RELAX). + * MAP_SHARED is disabled by default for FUSE_DIRECT_IO, as this flag can be used to + * 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) - /** - * Indicates that the file system cannot handle NFS export - * - * If this flag is set NFS export and name_to_handle_at - * is not going to work at all and will fail with EOPNOTSUPP. - */ - FUSE_CAP_NO_EXPORT_SUPPORT = (1 << 30), +/** + * Indicates support for passthrough mode access for read/write operations. + * + * If this flag is set in the `capable` field of the `fuse_conn_info` + * structure, then the FUSE kernel module supports redirecting read/write + * operations to the backing file instead of letting them to be handled + * by the FUSE daemon. + * + * This feature is disabled by default. + */ +#define FUSE_CAP_PASSTHROUGH (1 << 29) - /** - * Current maximum capability value. - */ - FUSE_CAP_CURRENT_MAX -}; +/** + * Indicates that the file system cannot handle NFS export + * + * 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) /** * Ioctl flags From 065272a23392011ea1728c4f2d20e63cf6579e7d Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Fri, 7 Mar 2025 22:43:50 +0100 Subject: [PATCH 066/310] fuse_session_receive_buf: Fix the pipe buf size This fixes dynamic buffer allocation in commit 0e0f43b79b9b ("Reallocate fuse_session buffer...") I noticed that when I increased the default fuse buf size as possible in recent kernels. Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- lib/fuse_i.h | 4 ++-- lib/fuse_lowlevel.c | 12 +++++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/fuse_i.h b/lib/fuse_i.h index ea04c34f4..69ca15917 100644 --- a/lib/fuse_i.h +++ b/lib/fuse_i.h @@ -72,14 +72,14 @@ struct fuse_session { int broken_splice_nonblock; uint64_t notify_ctr; struct fuse_notify_req notify_list; - size_t bufsize; + _Atomic size_t bufsize; int error; /* 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; - bool buf_reallocable; + _Atomic bool buf_reallocable; }; struct fuse_chan { diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index d650944a3..06425723f 100644 --- a/lib/fuse_lowlevel.c +++ b/lib/fuse_lowlevel.c @@ -3042,11 +3042,14 @@ static int _fuse_session_receive_buf(struct fuse_session *se, { int err; ssize_t res; - size_t bufsize = se->bufsize; + size_t bufsize; #ifdef HAVE_SPLICE struct fuse_ll_pipe *llp; struct fuse_buf tmpbuf; +pipe_retry: + bufsize = se->bufsize; + if (se->conn.proto_minor < 14 || !(se->conn.want_ext & FUSE_CAP_SPLICE_READ)) goto fallback; @@ -3091,6 +3094,13 @@ static int _fuse_session_receive_buf(struct fuse_session *se, fuse_session_exit(se); return 0; } + + /* FUSE_INIT might have increased the required bufsize */ + if (err == EINVAL && bufsize < se->bufsize) { + fuse_ll_clear_pipe(se); + goto pipe_retry; + } + if (err != EINTR && err != EAGAIN) perror("fuse: splice from device"); return -err; From 1b86fe4c4de96daa4e766425193595f1c6b88a73 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Tue, 11 Mar 2025 22:21:09 +0100 Subject: [PATCH 067/310] fuse_lowlevel: Simplify se->buf_reallocable se->buf_reallocable is true when reading /dev/fuse is handled from internal functions - we can set the variable in fuse_session_receive_buf_internal(). With that we also don't need to have it an _Atomic variable anymore. In _fuse_session_receive_buf() we can use "bool internal" to check if the buffer can be re-allocated. Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- lib/fuse_i.h | 4 +++- lib/fuse_lowlevel.c | 23 ++++++++++++----------- lib/util.h | 3 +++ 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/lib/fuse_i.h b/lib/fuse_i.h index 69ca15917..bf8410f33 100644 --- a/lib/fuse_i.h +++ b/lib/fuse_i.h @@ -79,7 +79,9 @@ struct fuse_session { * a later version, to 'fix' it at run time. */ struct libfuse_version version; - _Atomic bool buf_reallocable; + + /* true if reading requests from /dev/fuse are handled internally */ + bool buf_reallocable; }; struct fuse_chan { diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index 06425723f..1e2491bbe 100644 --- a/lib/fuse_lowlevel.c +++ b/lib/fuse_lowlevel.c @@ -3136,8 +3136,6 @@ static int _fuse_session_receive_buf(struct fuse_session *se, return -ENOMEM; } buf->mem_size = se->bufsize; - if (internal) - se->buf_reallocable = true; } buf->size = se->bufsize; buf->flags = 0; @@ -3177,13 +3175,9 @@ static int _fuse_session_receive_buf(struct fuse_session *se, return -ENOMEM; } buf->mem_size = se->bufsize; - if (internal) - se->buf_reallocable = true; } restart: - if (se->buf_reallocable) - bufsize = buf->mem_size; if (se->io != NULL) { /* se->io->read is never NULL if se->io is not NULL as specified by fuse_session_custom_io()*/ @@ -3197,9 +3191,10 @@ static int _fuse_session_receive_buf(struct fuse_session *se, if (fuse_session_exited(se)) return 0; if (res == -1) { - if (err == EINVAL && se->buf_reallocable && - se->bufsize > buf->mem_size) { - void *newbuf = buf_alloc(se->bufsize, internal); + if (err == EINVAL && internal && se->bufsize > buf->mem_size) { + /* FUSE_INIT might have increased the required bufsize */ + bufsize = se->bufsize; + void *newbuf = buf_alloc(bufsize, internal); if (!newbuf) { fuse_log( FUSE_LOG_ERR, @@ -3208,8 +3203,7 @@ static int _fuse_session_receive_buf(struct fuse_session *se, } fuse_buf_free(buf); buf->mem = newbuf; - buf->mem_size = se->bufsize; - se->buf_reallocable = true; + buf->mem_size = bufsize; goto restart; } @@ -3251,6 +3245,13 @@ int fuse_session_receive_buf_internal(struct fuse_session *se, struct fuse_buf *buf, struct fuse_chan *ch) { + /* + * if run internally thread buffers are from libfuse - we can + * reallocate them + */ + if (unlikely(!se->got_init) && !se->buf_reallocable) + se->buf_reallocable = true; + return _fuse_session_receive_buf(se, buf, ch, true); } diff --git a/lib/util.h b/lib/util.h index 74ce74845..a5c5463a5 100644 --- a/lib/util.h +++ b/lib/util.h @@ -1,3 +1,6 @@ #define ROUND_UP(val, round_to) (((val) + (round_to - 1)) & ~(round_to - 1)) +#define likely(x) __builtin_expect(!!(x), 1) +#define unlikely(x) __builtin_expect(!!(x), 0) + int libfuse_strtol(const char *str, long *res); From b9fc5a0ff8905a27e48cdac1ae26a8964fe87727 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Sun, 23 Feb 2025 00:17:39 +0100 Subject: [PATCH 068/310] fusermount: prevent stdio FDs from being reused Redirect stdin/stdout/stderr to /dev/null to prevent newly opened file descriptors from reusing these low numbers (0,1,2) and potential issues with that. Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- meson.build | 2 +- util/fusermount.c | 80 +++++++++++++++++++++++++++++++---------------- 2 files changed, 54 insertions(+), 28 deletions(-) diff --git a/meson.build b/meson.build index 0487639ca..cbcd70d02 100644 --- a/meson.build +++ b/meson.build @@ -72,7 +72,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', 'close_range' ] foreach func : test_funcs private_cfg.set('HAVE_' + func.to_upper(), cc.has_function(func, prefix: include_default, args: args_default)) diff --git a/util/fusermount.c b/util/fusermount.c index acbff61fb..dbd947c60 100644 --- a/util/fusermount.c +++ b/util/fusermount.c @@ -7,7 +7,7 @@ */ /* This program does the mounting and unmounting of FUSE filesystems */ -#define _GNU_SOURCE /* for clone and strchrnul */ +#define _GNU_SOURCE /* for clone,strchrnul and close_range */ #include "fuse_config.h" #include "mount_util.h" #include "util.h" @@ -36,6 +36,10 @@ #include <stdbool.h> #include <sys/vfs.h> +#ifdef HAVE_CLOSE_RANGE +#include <linux/close_range.h> +#endif + #define FUSE_COMMFD_ENV "_FUSE_COMMFD" #define FUSE_DEV "/dev/fuse" @@ -1452,44 +1456,63 @@ static void show_version(void) exit(0); } +static void close_range_loop(int min_fd, int max_fd, int cfd) +{ + for (int fd = min_fd; fd <= max_fd; fd++) + if (fd != cfd) + close(fd); +} + /* * Close all inherited fds that are not needed * Ideally these wouldn't come up at all, applications should better * use FD_CLOEXEC / O_CLOEXEC */ -static void close_inherited_fds(int cfd) +static int close_inherited_fds(int cfd) { - int max_fd = sysconf(_SC_OPEN_MAX); - int rc; + int rc = -1; + int nullfd; -#ifdef CLOSE_RANGE_CLOEXEC - /* high range first to be able to log errors through stdout/err*/ - rc = close_range(cfd + 1, ~0U, 0); - if (rc < 0) { - fprintf(stderr, "Failed to close high range of FDs: %s", - strerror(errno)); - goto fallback; - } + /* We can't even report an error */ + if (cfd <= STDERR_FILENO) + return -EINVAL; - rc = close_range(0, cfd - 1, 0); - if (rc < 0) { - fprintf(stderr, "Failed to close low range of FDs: %s", - strerror(errno)); - goto fallback; +#ifdef HAVE_CLOSE_RANGE + if (cfd < STDERR_FILENO + 2) { + close_range_loop(STDERR_FILENO + 1, cfd - 1, cfd); + } else { + rc = close_range(STDERR_FILENO + 1, cfd - 1, 0); + if (rc < 0) + goto fallback; } + + /* Close high range */ + rc = close_range(cfd + 1, ~0U, 0); +#else + goto fallback; /* make use of fallback to avoid compiler warnings */ #endif fallback: - /* - * This also needs to close stdout/stderr, as the application - * using libfuse might have closed these FDs and might be using - * it. Although issue is now that logging errors won't be possible - * after that. - */ - for (int fd = 0; fd <= max_fd; fd++) { - if (fd != cfd) - close(fd); + if (rc < 0) { + int max_fd = sysconf(_SC_OPEN_MAX) - 1; + + close_range_loop(STDERR_FILENO + 1, max_fd, cfd); } + + nullfd = open("/dev/null", O_RDWR); + if (nullfd < 0) { + perror("fusermount: cannot open /dev/null"); + return -errno; + } + + /* Redirect stdin, stdout, stderr to /dev/null */ + dup2(nullfd, STDIN_FILENO); + dup2(nullfd, STDOUT_FILENO); + dup2(nullfd, STDERR_FILENO); + if (nullfd > STDERR_FILENO) + close(nullfd); + + return 0; } int main(int argc, char *argv[]) @@ -1616,6 +1639,7 @@ int main(int argc, char *argv[]) goto err_out; } + { struct stat statbuf; fstat(cfd, &statbuf); @@ -1653,7 +1677,9 @@ int main(int argc, char *argv[]) Btw, we don't want to use daemon() function here because it forks and messes with the file descriptors. */ - close_inherited_fds(cfd); + res = close_inherited_fds(cfd); + if (res < 0) + exit(EXIT_FAILURE); setsid(); res = chdir("/"); From bdddba72c2a0a9e941960656ad476ef6d29684bf Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Thu, 20 Feb 2025 13:47:48 +0100 Subject: [PATCH 069/310] Switch .dir-locals.el to linux kernel style We have a non ideal configuration for different editors. The project uses linux style - emacs config should also correspond to that. Taken from https://www.kernel.org/doc/html/v6.13/process/coding-style.html#you-ve-made-a-mess-of-it with the attempt to update for c++. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> --- .dir-locals.el | 107 ++++++++++++++++++++++++++++--------------------- 1 file changed, 62 insertions(+), 45 deletions(-) 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) + )) +) From 9f7597a576eb6329ece28a4baee007970a2f278c Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Wed, 19 Mar 2025 18:46:33 +0100 Subject: [PATCH 070/310] Fix a comment typo "passed to the filesystem.n" Closes: https://github.com/libfuse/libfuse/issues/1168 Signed-off-by: Bernd Schubert <bernd@bsbernd.com> --- include/fuse_lowlevel.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/fuse_lowlevel.h b/include/fuse_lowlevel.h index 93bcba296..c7b44d963 100644 --- a/include/fuse_lowlevel.h +++ b/include/fuse_lowlevel.h @@ -292,7 +292,7 @@ struct fuse_lowlevel_ops { * If writeback caching is enabled, the kernel may have a * better idea of a file's length than the FUSE file system * (eg if there has been a write that extended the file size, - * but that has not yet been passed to the filesystem.n + * but that has not yet been passed to the filesystem. * * In this case, the st_size value provided by the file system * will be ignored. From 14520a6ef7eaf010d0d9354d3431fed362e205ce Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Sun, 23 Mar 2025 00:19:04 +0100 Subject: [PATCH 071/310] convert LL_ENABLE/LL_DISABLE to do-while-loop This is to avoid checkpatch reports and might potentially avoid future issues if these macros would be part of conditions. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> --- lib/helper.c | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/lib/helper.c b/lib/helper.c index a7b2fe002..1fe3fdeb8 100644 --- a/lib/helper.c +++ b/lib/helper.c @@ -423,10 +423,17 @@ void fuse_apply_conn_info_opts(struct fuse_conn_info_opts *opts, if(opts->set_max_readahead) conn->max_readahead = opts->max_readahead; -#define LL_ENABLE(cond,cap) \ - if (cond) conn->want |= (cap) -#define LL_DISABLE(cond,cap) \ - if (cond) conn->want &= ~(cap) +#define LL_ENABLE(cond, cap) \ + do { \ + if (cond) \ + conn->want |= (cap); \ + } while (0) + +#define LL_DISABLE(cond, cap) \ + do { \ + if (cond) \ + conn->want &= ~(cap); \ + } while (0) LL_ENABLE(opts->splice_read, FUSE_CAP_SPLICE_READ); LL_DISABLE(opts->no_splice_read, FUSE_CAP_SPLICE_READ); From 5d1c49e54ca226f144b618a1623117e2a52e7e17 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Sun, 23 Mar 2025 00:34:52 +0100 Subject: [PATCH 072/310] test/test_write_cache.c: Reformat with clang-format The file is going to be updated - should be more conform Signed-off-by: Bernd Schubert <bernd@bsbernd.com> --- test/test_write_cache.c | 416 ++++++++++++++++++++-------------------- 1 file changed, 212 insertions(+), 204 deletions(-) diff --git a/test/test_write_cache.c b/test/test_write_cache.c index 0c8fa7e8b..2e9c671e2 100644 --- a/test/test_write_cache.c +++ b/test/test_write_cache.c @@ -6,7 +6,6 @@ See the file COPYING. */ - #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)); - conn->want |= FUSE_CAP_WRITEBACK_CACHE; - } + if (options.writeback) { + assert(fuse_get_feature_flag(conn, FUSE_CAP_WRITEBACK_CACHE)); + conn->want |= 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 From f68970cd235a7e14026ca0f6240428bbebe8223b Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Sat, 22 Mar 2025 23:57:55 +0100 Subject: [PATCH 073/310] fuse: Fix want flag conversion 32-bit conn->want flags been left to be ABI compatible to 3.10, even though the so version was changed. The more recent way is to use fuse_set_feature_flag(), which will use conn->want_ext. Given that we now have two flags (want and want_ext), we need to convert and that brought several issues - If the application sets conn->want, that needs to be set into the lower 32 bit of conn->want_ext. As the application might actually unset values, it really has to be a copy and not just 'or' - fixed now. - convert_to_conn_want_ext() actually needs to check for _modified_ conn->want and conn->want_ext - convert_to_conn_want_ext() must consider being called from high and lowlevel interfact, with different want_ext_default and want_default values. It is only a failure, if the application changed both, conn->want and conn->want_ext. This function was failing in issue #1171, because high level fuse_fs_init() was changing values and then lowlevel do_init() was incorrectly failing on that. This also adds a new test (test_want_conversion) and sets values into example/{hello.c,hello_ll.c} Also some more internal users of conn->want are converted to fuse_{set,unset}_feature_flag(). Closes: https://github.com/libfuse/libfuse/issues/1171 Signed-off-by: Bernd Schubert <bernd@bsbernd.com> --- .github/workflows/checkpatch.yml | 2 +- example/hello.c | 5 + example/hello_ll.c | 4 + lib/fuse.c | 33 ++++++- lib/fuse_i.h | 34 +++++++ lib/fuse_lowlevel.c | 30 ++---- lib/helper.c | 4 +- lib/util.h | 27 ++++++ test/meson.build | 3 + test/test_want_conversion.c | 152 +++++++++++++++++++++++++++++++ test/test_write_cache.c | 2 +- 11 files changed, 265 insertions(+), 31 deletions(-) create mode 100644 test/test_want_conversion.c diff --git a/.github/workflows/checkpatch.yml b/.github/workflows/checkpatch.yml index 45133eb87..8eb6acf06 100644 --- a/.github/workflows/checkpatch.yml +++ b/.github/workflows/checkpatch.yml @@ -30,7 +30,7 @@ jobs: git rev-list --reverse $base_commit..HEAD | while read commit; do subject=$(git log -1 --format=%s $commit) echo "Checking commit: $commit - $subject" - if ! ./checkpatch.pl --max-line-length=100 --no-tree --ignore MAINTAINERS,SPDX_LICENSE_TAG,COMMIT_MESSAGE,FILE_PATH_CHANGES,EMAIL_SUBJECT,AVOID_EXTERNS,GIT_COMMIT_ID,ENOSYS_SYSCALL,ENOSYS,FROM_SIGN_OFF_MISMATCH,QUOTED_COMMIT_ID -g $commit; then + if ! ./checkpatch.pl --max-line-length=100 --no-tree --ignore MAINTAINERS,SPDX_LICENSE_TAG,COMMIT_MESSAGE,FILE_PATH_CHANGES,EMAIL_SUBJECT,AVOID_EXTERNS,GIT_COMMIT_ID,ENOSYS_SYSCALL,ENOSYS,FROM_SIGN_OFF_MISMATCH,QUOTED_COMMIT_ID,PREFER_ATTRIBUTE_ALWAYS_UNUSED,PREFER_DEFINED_ATTRIBUTE_MACRO -g $commit; then echo "checkpatch.pl found issues in commit $commit - $subject" exit 1 fi diff --git a/example/hello.c b/example/hello.c index 6df817337..90919f4d9 100644 --- a/example/hello.c +++ b/example/hello.c @@ -57,6 +57,11 @@ static void *hello_init(struct fuse_conn_info *conn, { (void) conn; cfg->kernel_cache = 1; + + /* Test setting flags the old way */ + conn->want = FUSE_CAP_ASYNC_READ; + conn->want &= ~FUSE_CAP_ASYNC_READ; + return NULL; } diff --git a/example/hello_ll.c b/example/hello_ll.c index 0fcb7fe55..12927cc2f 100644 --- a/example/hello_ll.c +++ b/example/hello_ll.c @@ -59,6 +59,10 @@ static void hello_ll_init(void *userdata, struct fuse_conn_info *conn) /* Disable the receiving and processing of FUSE_INTERRUPT requests */ conn->no_interrupt = 1; + + /* Test setting flags the old way */ + conn->want = FUSE_CAP_ASYNC_READ; + conn->want &= ~FUSE_CAP_ASYNC_READ; } static void hello_ll_getattr(fuse_req_t req, fuse_ino_t ino, diff --git a/lib/fuse.c b/lib/fuse.c index 933542992..136f0c2bd 100644 --- a/lib/fuse.c +++ b/lib/fuse.c @@ -10,6 +10,8 @@ */ #define _GNU_SOURCE +#include "fuse.h" +#include <pthread.h> #include "fuse_config.h" #include "fuse_i.h" @@ -17,7 +19,9 @@ #include "fuse_opt.h" #include "fuse_misc.h" #include "fuse_kernel.h" +#include "util.h" +#include <stdint.h> #include <stdio.h> #include <string.h> #include <stdlib.h> @@ -2606,13 +2610,34 @@ void fuse_fs_init(struct fuse_fs *fs, struct fuse_conn_info *conn, { fuse_get_context()->private_data = fs->user_data; if (!fs->op.write_buf) - conn->want &= ~FUSE_CAP_SPLICE_READ; + fuse_unset_feature_flag(conn, FUSE_CAP_SPLICE_READ); if (!fs->op.lock) - conn->want &= ~FUSE_CAP_POSIX_LOCKS; + fuse_unset_feature_flag(conn, FUSE_CAP_POSIX_LOCKS); if (!fs->op.flock) - conn->want &= ~FUSE_CAP_FLOCK_LOCKS; - if (fs->op.init) + fuse_unset_feature_flag(conn, FUSE_CAP_FLOCK_LOCKS); + if (fs->op.init) { + uint64_t want_ext_default = conn->want_ext; + uint32_t want_default = fuse_lower_32_bits(conn->want_ext); + int rc; + + conn->want = want_default; fs->user_data = fs->op.init(conn, cfg); + + rc = convert_to_conn_want_ext(conn, want_ext_default, + want_default); + + if (rc != 0) { + /* + * This is a grave developer error, but + * we cannot return an error here, as the function + * signature does not allow it. + */ + fuse_log( + FUSE_LOG_ERR, + "fuse: Aborting due to invalid conn want flags.\n"); + _exit(EXIT_FAILURE); + } + } } static int fuse_init_intr_signal(int signum, int *installed); diff --git a/lib/fuse_i.h b/lib/fuse_i.h index bf8410f33..23fcaa6d4 100644 --- a/lib/fuse_i.h +++ b/lib/fuse_i.h @@ -8,8 +8,11 @@ #include "fuse.h" #include "fuse_lowlevel.h" +#include "util.h" +#include <stdint.h> #include <stdbool.h> +#include <errno.h> #define MIN(a, b) \ ({ \ @@ -224,3 +227,34 @@ int fuse_loop_cfg_verify(struct fuse_loop_config *config); /* room needed in buffer to accommodate header */ #define FUSE_BUFFER_HEADER_SIZE 0x1000 +/** + * Get the wanted capability flags, converting from old format if necessary + */ +static inline int convert_to_conn_want_ext(struct fuse_conn_info *conn, + uint64_t want_ext_default, + uint32_t want_default) +{ + /* + * Convert want to want_ext if necessary. + * For the high level interface this function might be called + * twice, once from the high level interface and once from the + * low level interface. Both, with different want_ext_default and + * want_default values. In order to suppress a failure for the + * second call, we check if the lower 32 bits of want_ext are + * already set to the value of want. + */ + if (conn->want != want_default && + fuse_lower_32_bits(conn->want_ext) != conn->want) { + if (conn->want_ext != want_ext_default) { + fuse_log(FUSE_LOG_ERR, + "fuse: both 'want' and 'want_ext' are set\n"); + return -EINVAL; + } + + /* high bits from want_ext, low bits from want */ + conn->want_ext = fuse_higher_32_bits(conn->want_ext) | + conn->want; + } + + return 0; +} diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index 1e2491bbe..17bc81692 100644 --- a/lib/fuse_lowlevel.c +++ b/lib/fuse_lowlevel.c @@ -9,7 +9,6 @@ See the file COPYING.LIB */ -#include <stdbool.h> #define _GNU_SOURCE #include "fuse_config.h" @@ -20,6 +19,8 @@ #include "mount_util.h" #include "util.h" +#include <stdint.h> +#include <stdbool.h> #include <stdio.h> #include <stdlib.h> #include <stddef.h> @@ -1997,25 +1998,6 @@ static bool want_flags_valid(uint64_t capable, uint64_t want) return true; } -/** - * Get the wanted capability flags, converting from old format if necessary - */ -static inline int convert_to_conn_want_ext(struct fuse_conn_info *conn, - uint64_t want_ext_default) -{ - /* Convert want to want_ext if necessary */ - if (conn->want != 0) { - if (conn->want_ext != want_ext_default) { - fuse_log(FUSE_LOG_ERR, - "fuse: both 'want' and 'want_ext' are set\n"); - return -EINVAL; - } - conn->want_ext |= conn->want; - } - - return 0; -} - /* Prevent bogus data races (bogus since "init" is called before * multi-threading becomes relevant */ static __attribute__((no_sanitize("thread"))) @@ -2177,11 +2159,12 @@ void do_init(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) se->got_init = 1; if (se->op.init) { uint64_t want_ext_default = se->conn.want_ext; + uint32_t want_default = fuse_lower_32_bits(se->conn.want_ext); int rc; // Apply the first 32 bits of capable_ext to capable - se->conn.capable = - (uint32_t)(se->conn.capable_ext & 0xFFFFFFFF); + se->conn.capable = fuse_lower_32_bits(se->conn.capable_ext); + se->conn.want = want_default; se->op.init(se->userdata, &se->conn); @@ -2190,7 +2173,8 @@ void do_init(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) * se->conn.want_ext * Userspace might still use conn.want - we need to convert it */ - rc = convert_to_conn_want_ext(&se->conn, want_ext_default); + rc = convert_to_conn_want_ext(&se->conn, want_ext_default, + want_default); if (rc != 0) { fuse_reply_err(req, EPROTO); se->error = -EPROTO; diff --git a/lib/helper.c b/lib/helper.c index 1fe3fdeb8..0b0dad262 100644 --- a/lib/helper.c +++ b/lib/helper.c @@ -426,13 +426,13 @@ void fuse_apply_conn_info_opts(struct fuse_conn_info_opts *opts, #define LL_ENABLE(cond, cap) \ do { \ if (cond) \ - conn->want |= (cap); \ + conn->want_ext |= (cap); \ } while (0) #define LL_DISABLE(cond, cap) \ do { \ if (cond) \ - conn->want &= ~(cap); \ + conn->want_ext &= ~(cap); \ } while (0) LL_ENABLE(opts->splice_read, FUSE_CAP_SPLICE_READ); diff --git a/lib/util.h b/lib/util.h index a5c5463a5..508fafb12 100644 --- a/lib/util.h +++ b/lib/util.h @@ -1,6 +1,33 @@ +#ifndef FUSE_UTIL_H_ +#define FUSE_UTIL_H_ + +#include <stdint.h> + #define ROUND_UP(val, round_to) (((val) + (round_to - 1)) & ~(round_to - 1)) #define likely(x) __builtin_expect(!!(x), 1) #define unlikely(x) __builtin_expect(!!(x), 0) int libfuse_strtol(const char *str, long *res); + +/** + * Return the low bits of a number + */ +static inline uint32_t fuse_lower_32_bits(uint64_t nr) +{ + return (uint32_t)(nr & 0xffffffff); +} + +/** + * Return the high bits of a number + */ +static inline uint64_t fuse_higher_32_bits(uint64_t nr) +{ + return nr & ~0xffffffffULL; +} + +#ifndef FUSE_VAR_UNUSED +#define FUSE_VAR_UNUSED(var) (__attribute__((unused)) var) +#endif + +#endif diff --git a/test/meson.build b/test/meson.build index 3d74b9ae0..599703064 100644 --- a/test/meson.build +++ b/test/meson.build @@ -16,6 +16,9 @@ td += executable('readdir_inode', 'readdir_inode.c', td += executable('release_unlink_race', 'release_unlink_race.c', dependencies: [ libfuse_dep ], install: false) +td += executable('test_want_conversion', 'test_want_conversion.c', + dependencies: [ libfuse_dep ], + install: false) test_scripts = [ 'conftest.py', 'pytest.ini', 'test_examples.py', 'util.py', 'test_ctests.py', 'test_custom_io.py' ] diff --git a/test/test_want_conversion.c b/test/test_want_conversion.c new file mode 100644 index 000000000..935b58d7d --- /dev/null +++ b/test/test_want_conversion.c @@ -0,0 +1,152 @@ +#include "util.h" +#include <string.h> +#define FUSE_USE_VERSION FUSE_MAKE_VERSION(3, 17) + +#include "fuse_i.h" +#include <stdio.h> +#include <assert.h> +#include <inttypes.h> + +static void print_conn_info(const char *prefix, struct fuse_conn_info *conn) +{ + printf("%s: want=0x%" PRIx32 " want_ext=0x%" PRIx64 "\n", prefix, + conn->want, conn->want_ext); +} + +static void application_init(struct fuse_conn_info *conn) +{ + /* Simulate application init */ + conn->want |= FUSE_CAP_ASYNC_READ; + conn->want &= ~FUSE_CAP_SPLICE_READ; +} + +static void test_fuse_fs_init(struct fuse_conn_info *conn) +{ + uint64_t want_ext_default = conn->want_ext; + uint32_t want_default = fuse_lower_32_bits(conn->want_ext); + int rc; + + /* High-level init */ + fuse_set_feature_flag(conn, FUSE_CAP_EXPORT_SUPPORT); + + conn->want = want_default; + + application_init(conn); + + rc = convert_to_conn_want_ext(conn, want_ext_default, want_default); + assert(rc == 0); +} + +static void test_do_init(struct fuse_conn_info *conn) +{ + /* Initial setup */ + conn->capable_ext = FUSE_CAP_SPLICE_READ | FUSE_CAP_SPLICE_WRITE | + FUSE_CAP_SPLICE_MOVE | FUSE_CAP_POSIX_LOCKS | + FUSE_CAP_FLOCK_LOCKS | FUSE_CAP_EXPORT_SUPPORT; + conn->capable = fuse_lower_32_bits(conn->capable_ext); + conn->want_ext = conn->capable_ext; + + print_conn_info("Initial state", conn); + + uint64_t want_ext_default = conn->want_ext; + uint32_t want_default = fuse_lower_32_bits(conn->want_ext); + int rc; + + conn->want = want_default; + conn->capable = fuse_lower_32_bits(conn->capable_ext); + + test_fuse_fs_init(conn); + + rc = convert_to_conn_want_ext(conn, want_ext_default, want_default); + assert(rc == 0); + + /* Verify all expected flags are set */ + assert(!(conn->want_ext & FUSE_CAP_SPLICE_READ)); + assert(conn->want_ext & FUSE_CAP_SPLICE_WRITE); + assert(conn->want_ext & FUSE_CAP_SPLICE_MOVE); + assert(conn->want_ext & FUSE_CAP_POSIX_LOCKS); + assert(conn->want_ext & FUSE_CAP_FLOCK_LOCKS); + assert(conn->want_ext & FUSE_CAP_EXPORT_SUPPORT); + assert(conn->want_ext & FUSE_CAP_ASYNC_READ); + /* Verify no other flags are set */ + assert(conn->want_ext == + (FUSE_CAP_SPLICE_WRITE | FUSE_CAP_SPLICE_MOVE | + FUSE_CAP_POSIX_LOCKS | FUSE_CAP_FLOCK_LOCKS | + FUSE_CAP_EXPORT_SUPPORT | FUSE_CAP_ASYNC_READ)); + + print_conn_info("After init", conn); +} + +static void test_want_conversion_basic(void) +{ + struct fuse_conn_info conn = { 0 }; + + printf("\nTesting basic want conversion:\n"); + test_do_init(&conn); + print_conn_info("After init", &conn); +} + +static void test_want_conversion_conflict(void) +{ + struct fuse_conn_info conn = { 0 }; + int rc; + + printf("\nTesting want conversion conflict:\n"); + + /* Test conflicting values */ + /* Initialize like fuse_lowlevel.c does */ + conn.capable_ext = FUSE_CAP_SPLICE_READ | FUSE_CAP_SPLICE_WRITE | + FUSE_CAP_SPLICE_MOVE | FUSE_CAP_POSIX_LOCKS | + FUSE_CAP_FLOCK_LOCKS; + conn.capable = fuse_lower_32_bits(conn.capable_ext); + conn.want_ext = conn.capable_ext; + conn.want = fuse_lower_32_bits(conn.want_ext); + print_conn_info("Test conflict initial", &conn); + + /* Initialize default values like in basic test */ + uint64_t want_ext_default_ll = conn.want_ext; + uint32_t want_default_ll = fuse_lower_32_bits(want_ext_default_ll); + + /* Simulate application init modifying capabilities */ + conn.want_ext |= FUSE_CAP_ATOMIC_O_TRUNC; /* Add new capability */ + conn.want &= ~FUSE_CAP_SPLICE_READ; /* Remove a capability */ + + rc = convert_to_conn_want_ext(&conn, want_ext_default_ll, + want_default_ll); + assert(rc == -EINVAL); + print_conn_info("Test conflict after", &conn); + + printf("Want conversion conflict test passed\n"); +} + +static void test_want_conversion_high_bits(void) +{ + struct fuse_conn_info conn = { 0 }; + int rc; + + printf("\nTesting want conversion high bits preservation:\n"); + + /* Test high bits preservation */ + conn.want_ext = (1ULL << 33) | FUSE_CAP_ASYNC_READ; + conn.want = fuse_lower_32_bits(conn.want_ext); + print_conn_info("Test high bits initial", &conn); + + uint64_t want_ext_default_ll = conn.want_ext; + uint32_t want_default_ll = fuse_lower_32_bits(want_ext_default_ll); + + rc = convert_to_conn_want_ext(&conn, want_ext_default_ll, + want_default_ll); + assert(rc == 0); + assert(conn.want_ext == ((1ULL << 33) | FUSE_CAP_ASYNC_READ)); + print_conn_info("Test high bits after", &conn); + + printf("Want conversion high bits test passed\n"); +} + +int main(void) +{ + test_want_conversion_basic(); + test_want_conversion_conflict(); + test_want_conversion_high_bits(); + return 0; +} diff --git a/test/test_write_cache.c b/test/test_write_cache.c index 2e9c671e2..2b5b4f383 100644 --- a/test/test_write_cache.c +++ b/test/test_write_cache.c @@ -66,7 +66,7 @@ static void tfs_init(void *userdata, struct fuse_conn_info *conn) if (options.writeback) { assert(fuse_get_feature_flag(conn, FUSE_CAP_WRITEBACK_CACHE)); - conn->want |= FUSE_CAP_WRITEBACK_CACHE; + fuse_set_feature_flag(conn, FUSE_CAP_WRITEBACK_CACHE); } } From 1f1a78b18d29fc95b0f134b1134532000f4a726f Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Tue, 25 Mar 2025 09:50:00 +0100 Subject: [PATCH 074/310] checkpatch: Ignore dependabot commits Thes commits have the habit to persistently fail, mostly with long lines. We don't have control over these commits, so let's ignore them. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> --- .github/workflows/checkpatch.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/checkpatch.yml b/.github/workflows/checkpatch.yml index 8eb6acf06..0da6c5410 100644 --- a/.github/workflows/checkpatch.yml +++ b/.github/workflows/checkpatch.yml @@ -28,6 +28,11 @@ jobs: 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 + # 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 --max-line-length=100 --no-tree --ignore MAINTAINERS,SPDX_LICENSE_TAG,COMMIT_MESSAGE,FILE_PATH_CHANGES,EMAIL_SUBJECT,AVOID_EXTERNS,GIT_COMMIT_ID,ENOSYS_SYSCALL,ENOSYS,FROM_SIGN_OFF_MISMATCH,QUOTED_COMMIT_ID,PREFER_ATTRIBUTE_ALWAYS_UNUSED,PREFER_DEFINED_ATTRIBUTE_MACRO -g $commit; then From 86817c3ca0d2574aa87cdd84eb7a326c74e38b59 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Mar 2025 09:27:43 +0000 Subject: [PATCH 075/310] build(deps): bump github/codeql-action from 3.28.11 to 3.28.13 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.28.11 to 3.28.13. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/6bb031afdd8eb862ea3fc1848194185e076637e5...1b549b9259bda1cb5ddde3b41741a82a2d15a841) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index c2cb051a7..12c6c8dee 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -53,7 +53,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3.28.11 + uses: github/codeql-action/init@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3.28.13 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -78,7 +78,7 @@ jobs: meson compile -C build - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3.28.11 + uses: github/codeql-action/analyze@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3.28.13 with: category: "/language:${{matrix.language}}" From 6cdb65047f60057724d0939836c261bb40433e53 Mon Sep 17 00:00:00 2001 From: izxl007 <zeng.zheng@zte.com.cn> Date: Tue, 25 Mar 2025 22:15:38 +0800 Subject: [PATCH 076/310] example/hello_ll.c: Improve the compilation comments When building and installing FUSE3 using Meson on CentOS8, the fuse3.pc file is installed in the /usr/local/lib64/pkgconfig directory. However, pkg-config does not search for fuse3.pc in this directory, leading to GCC compilation failures. This patch improves the compilation comments, helping users successfully execute the GCC command. Signed-off-by: izxl007 <zeng.zheng@zte.com.cn> --- example/hello_ll.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/example/hello_ll.c b/example/hello_ll.c index 12927cc2f..b1ef69275 100644 --- a/example/hello_ll.c +++ b/example/hello_ll.c @@ -14,6 +14,10 @@ * * 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 */ From a25fb9bd49ef56a2223262784f18dd9bbc2601dc Mon Sep 17 00:00:00 2001 From: izxl007 <zeng.zheng@zte.com.cn> Date: Thu, 27 Mar 2025 12:24:40 +0800 Subject: [PATCH 077/310] example: Add a compilation instruction in README.compile When building FUSE3 with Meson on CentOS 8, the fuse3.pc file gets installed in /usr/local/lib64/pkgconfig. Since pkg-config doesn't search this path by default, GCC compilation fails due to missing FUSE3 flags. This patch adds an instruction for setting PKG_CONFIG_PATH variable in README.compile to fix GCC compilation issues. Signed-off-by: izxl007 <zeng.zheng@zte.com.cn> --- example/README.compile | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 example/README.compile 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 From eabe4b62d1ca945eaa65054cee5b0cd0b7583459 Mon Sep 17 00:00:00 2001 From: Joanne Koong <joannelkoong@gmail.com> Date: Mon, 24 Feb 2025 12:56:34 -0800 Subject: [PATCH 078/310] lib: Add usdt.h Copy over usdt.h from [1] as part of the prepatory work for adding tracepoints for requests. [1] https://github.com/libbpf/usdt Signed-off-by: Joanne Koong <joannelkoong@gmail.com> --- lib/usdt.h | 540 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 540 insertions(+) create mode 100644 lib/usdt.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 */ From 11fde2346885683202eace0302ae4dc39869bfec Mon Sep 17 00:00:00 2001 From: Joanne Koong <joannelkoong@gmail.com> Date: Fri, 21 Feb 2025 13:24:39 -0800 Subject: [PATCH 079/310] fuse_lowlevel: add tracepoints for request receiving, processing, and replying Add user statically-defined tracepoints for request lifecycle. Verified by: [machine]$ readelf -n /home/libfuse/build/lib/libfuse3.so | grep -A 1000 '.note.stapsdt' Displaying notes found in: .note.stapsdt Owner Data size Description stapsdt 0x00000035 NT_STAPSDT (SystemTap probe descriptors) Provider: libfuse Name: request_receive Location: 0x0000000000016360, Base: 0x00000000000002fc, Semaphore: 0x0000000000000000 Arguments: -4@%edi stapsdt 0x00000055 NT_STAPSDT (SystemTap probe descriptors) Provider: libfuse Name: request_reply Location: 0x0000000000017c0f, Base: 0x00000000000002fc, Semaphore: 0x0000000000000000 Arguments: 8@8(%r12) 4@(%r12) -4@4(%r12) -4@%r13d stapsdt 0x0000003e NT_STAPSDT (SystemTap probe descriptors) Provider: libfuse Name: request_process Location: 0x000000000001acf7, Base: 0x00000000000002fc, Semaphore: 0x0000000000000000 Arguments: 4@%r8d 4@%edx Signed-off-by: Joanne Koong <joannelkoong@gmail.com> --- lib/fuse_lowlevel.c | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index 17bc81692..a5d9e0c21 100644 --- a/lib/fuse_lowlevel.c +++ b/lib/fuse_lowlevel.c @@ -17,6 +17,7 @@ #include "fuse_opt.h" #include "fuse_misc.h" #include "mount_util.h" +#include "usdt.h" #include "util.h" #include <stdint.h> @@ -60,6 +61,23 @@ static __attribute__((constructor)) void fuse_ll_init_pagesize(void) pagesize = getpagesize(); } +/* 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); +} + static void convert_stat(const struct stat *stbuf, struct fuse_attr *attr) { attr->ino = stbuf->st_ino; @@ -207,6 +225,7 @@ static int fuse_send_msg(struct fuse_session *se, struct fuse_chan *ch, res = writev(ch ? ch->fd : se->fd, iov, count); int err = errno; + trace_request_reply(out->unique, out->len, out->error, err); if (res == -1) { /* ENOENT means the operation was interrupted */ @@ -2809,6 +2828,8 @@ 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", @@ -3067,6 +3088,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; @@ -3171,6 +3193,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; From 9265134c5097d6a640171566510f26633a98614e Mon Sep 17 00:00:00 2001 From: Joanne Koong <joannelkoong@gmail.com> Date: Mon, 24 Feb 2025 13:11:38 -0800 Subject: [PATCH 080/310] example: add usdt.bt Add example bpftrace file for monitoring tracepoints. Signed-off-by: Joanne Koong <joannelkoong@gmail.com> --- example/usdt.bt | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 example/usdt.bt 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); +} From 206f4a4ec05cb7ce2f8f47c8f5b4b2c80ea221a2 Mon Sep 17 00:00:00 2001 From: Giulio Benetti <giulio.benetti@benettiengineering.com> Date: Tue, 1 Apr 2025 00:53:07 +0200 Subject: [PATCH 081/310] Fix build with kernel < 5.9 linux/close_range.h is only available since kernel 5.9 and https://github.com/torvalds/linux/commit/60997c3d45d9a67daf01c56d805ae4fec37e0bd8 resulting in the following build failure: ../util/fusermount.c:40:10: fatal error: linux/close_range.h: No such file or directory So let's check for header presence and emit HAVE_LINUX_CLOSE_RANGE_H accordingly and check for it when including <linux/close_range.h> and calling close_range() instead of checking for close_range() function in meson and check against HAVE_CLOSE_RANGE. Signed-off-by: Giulio Benetti <giulio.benetti@benettiengineering.com> --- meson.build | 6 +++++- util/fusermount.c | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/meson.build b/meson.build index cbcd70d02..96c655c31 100644 --- a/meson.build +++ b/meson.build @@ -72,7 +72,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', 'close_range' ] + 'utimensat', 'copy_file_range', 'fallocate' ] foreach func : test_funcs private_cfg.set('HAVE_' + func.to_upper(), cc.has_function(func, prefix: include_default, args: args_default)) @@ -84,6 +84,10 @@ private_cfg.set('HAVE_ICONV', private_cfg.set('HAVE_BACKTRACE', cc.has_function('backtrace', prefix: '#include <execinfo.h>')) +# Test if headers exist +private_cfg.set('HAVE_LINUX_CLOSE_RANGE_H', + cc.check_header('#include <linux/close_range.h>')) + # Test if structs have specific member private_cfg.set('HAVE_STRUCT_STAT_ST_ATIM', cc.has_member('struct stat', 'st_atim', diff --git a/util/fusermount.c b/util/fusermount.c index dbd947c60..da6d5f2be 100644 --- a/util/fusermount.c +++ b/util/fusermount.c @@ -36,7 +36,7 @@ #include <stdbool.h> #include <sys/vfs.h> -#ifdef HAVE_CLOSE_RANGE +#ifdef HAVE_LINUX_CLOSE_RANGE_H #include <linux/close_range.h> #endif @@ -1477,7 +1477,7 @@ static int close_inherited_fds(int cfd) if (cfd <= STDERR_FILENO) return -EINVAL; -#ifdef HAVE_CLOSE_RANGE +#ifdef HAVE_LINUX_CLOSE_RANGE_H if (cfd < STDERR_FILENO + 2) { close_range_loop(STDERR_FILENO + 1, cfd - 1, cfd); } else { From 197ea99fe4520b1ada8a4f2e3e1dcaf6de145002 Mon Sep 17 00:00:00 2001 From: Giulio Benetti <giulio.benetti@benettiengineering.com> Date: Fri, 4 Apr 2025 22:17:49 +0200 Subject: [PATCH 082/310] Fix static_assert build failure with C++ version < 11 At the moment build fails due to lack of static_assert: https://gitlab.com/jolivain/buildroot/-/jobs/9606292537 this means that the check per date is not enough, so let's use meson to check if static_assert() is present or not and simplify fuse_static_assert() definition by only checking HAVE_STATIC_ASSERT. Signed-off-by: Giulio Benetti <giulio.benetti@benettiengineering.com> --- include/fuse_common.h | 4 +--- meson.build | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/include/fuse_common.h b/include/fuse_common.h index 77efc5de5..582505fa9 100644 --- a/include/fuse_common.h +++ b/include/fuse_common.h @@ -30,9 +30,7 @@ #define FUSE_MAKE_VERSION(maj, min) ((maj) * 100 + (min)) #define FUSE_VERSION FUSE_MAKE_VERSION(FUSE_MAJOR_VERSION, FUSE_MINOR_VERSION) -#if (defined(__cplusplus) && __cplusplus >= 201103L) || \ - (!defined(__cplusplus) && defined(__STDC_VERSION__) && \ - __STDC_VERSION__ >= 201112L) +#ifdef HAVE_STATIC_ASSERT #define fuse_static_assert(condition, message) static_assert(condition, message) #else #define fuse_static_assert(condition, message) diff --git a/meson.build b/meson.build index 96c655c31..bab98da3c 100644 --- a/meson.build +++ b/meson.build @@ -72,7 +72,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', 'static_assert' ] foreach func : test_funcs private_cfg.set('HAVE_' + func.to_upper(), cc.has_function(func, prefix: include_default, args: args_default)) From ead0a13d57dcbc5ac7b5396722a61cc99cd0c9dc Mon Sep 17 00:00:00 2001 From: Joanne Koong <joannelkoong@gmail.com> Date: Wed, 2 Apr 2025 16:24:12 -0700 Subject: [PATCH 083/310] meson: add option for enabling usdt Default is having usdt disabled. Signed-off-by: Joanne Koong <joannelkoong@gmail.com> --- lib/fuse_lowlevel.c | 27 ++++++++++++++++++++++++++- meson.build | 2 ++ meson_options.txt | 2 ++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index a5d9e0c21..ff7a73b2b 100644 --- a/lib/fuse_lowlevel.c +++ b/lib/fuse_lowlevel.c @@ -17,7 +17,6 @@ #include "fuse_opt.h" #include "fuse_misc.h" #include "mount_util.h" -#include "usdt.h" #include "util.h" #include <stdint.h> @@ -34,6 +33,10 @@ #include <sys/file.h> #include <sys/ioctl.h> +#ifdef USDT_ENABLED +#include "usdt.h" +#endif + #ifndef F_LINUX_SPECIFIC_BASE #define F_LINUX_SPECIFIC_BASE 1024 #endif @@ -61,6 +64,7 @@ static __attribute__((constructor)) void fuse_ll_init_pagesize(void) pagesize = getpagesize(); } +#ifdef USDT_ENABLED /* tracepoints */ static void trace_request_receive(int err) { @@ -77,6 +81,27 @@ static void trace_request_reply(uint64_t unique, unsigned int len, { 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) { diff --git a/meson.build b/meson.build index bab98da3c..f28df3db5 100644 --- a/meson.build +++ b/meson.build @@ -98,6 +98,8 @@ private_cfg.set('HAVE_STRUCT_STAT_ST_ATIMESPEC', prefix: include_default, args: args_default)) +private_cfg.set('USDT_ENABLED', get_option('enable-usdt')) + # # Compiler configuration # diff --git a/meson_options.txt b/meson_options.txt index fa4749c72..957c5fc36 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -22,3 +22,5 @@ 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') From 12507408c311601b9e933645b10d8f801f32452c Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Mon, 7 Apr 2025 11:50:19 +0200 Subject: [PATCH 084/310] fuse_lowlevel: Set bufsize if HAVE_SPLICE is not define and avoid race These are several buf size fixes 1) Set bufsize when HAVE_SPLICE is not defined. Addresses https://github.com/libfuse/libfuse/issues/1184 2) Check in the read retry condition for bufsize, i.e. the value passed to read and not for the buf->mem_size. Using buf->mem_size can be startup racy. Additionally we now also set bufsize on allocation to avoid these races. 3) Allocation and value assigned needs to follow the pattern bufsize = se->bufsize; buf_alloc(bufsize, internal) buf->mem_size = bufsize; I.e. bufsize has to be retrieved first, as se->bufsize might change at anytime - the value used for allocation and must not differ from the value actually used. This also tries to set large sizes in passthrough_hp, to catch issues in xfstests - though requires to set /proc/sys/fs/fuse/max_pages_limit Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- example/passthrough_hp.cc | 3 +++ lib/fuse_lowlevel.c | 16 ++++++++++------ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/example/passthrough_hp.cc b/example/passthrough_hp.cc index 1c8db6c4d..8b5214c21 100644 --- a/example/passthrough_hp.cc +++ b/example/passthrough_hp.cc @@ -228,6 +228,9 @@ static void sfs_init(void *userdata, fuse_conn_info *conn) { /* 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; } diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index ff7a73b2b..cf8a7b0d6 100644 --- a/lib/fuse_lowlevel.c +++ b/lib/fuse_lowlevel.c @@ -3159,16 +3159,16 @@ static int _fuse_session_receive_buf(struct fuse_session *se, struct fuse_bufvec dst = { .count = 1 }; if (!buf->mem) { - buf->mem = buf_alloc(se->bufsize, internal); + buf->mem = buf_alloc(bufsize, internal); if (!buf->mem) { fuse_log( FUSE_LOG_ERR, "fuse: failed to allocate read buffer\n"); return -ENOMEM; } - buf->mem_size = se->bufsize; + buf->mem_size = bufsize; } - buf->size = se->bufsize; + buf->size = bufsize; buf->flags = 0; dst.buf[0] = *buf; @@ -3198,14 +3198,18 @@ static int _fuse_session_receive_buf(struct fuse_session *se, fallback: #endif + bufsize = internal ? buf->mem_size : se->bufsize; if (!buf->mem) { - buf->mem = buf_alloc(se->bufsize, internal); + bufsize = se->bufsize; /* might have changed */ + buf->mem = buf_alloc(bufsize, internal); if (!buf->mem) { fuse_log(FUSE_LOG_ERR, "fuse: failed to allocate read buffer\n"); return -ENOMEM; } - buf->mem_size = se->bufsize; + + if (internal) + buf->mem_size = bufsize; } restart: @@ -3223,7 +3227,7 @@ static int _fuse_session_receive_buf(struct fuse_session *se, if (fuse_session_exited(se)) return 0; if (res == -1) { - if (err == EINVAL && internal && se->bufsize > buf->mem_size) { + if (err == EINVAL && internal && se->bufsize > bufsize) { /* FUSE_INIT might have increased the required bufsize */ bufsize = se->bufsize; void *newbuf = buf_alloc(bufsize, internal); From cb586bbfc3a1fc0e00ef8d8f8b73c881ff99f908 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Apr 2025 00:28:39 +0000 Subject: [PATCH 085/310] build(deps): bump github/codeql-action from 3.28.13 to 3.28.15 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.28.13 to 3.28.15. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/1b549b9259bda1cb5ddde3b41741a82a2d15a841...45775bd8235c68ba998cffa5171334d58593da47) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 3.28.15 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 12c6c8dee..9c59174eb 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -53,7 +53,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3.28.13 + uses: github/codeql-action/init@45775bd8235c68ba998cffa5171334d58593da47 # v3.28.15 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -78,7 +78,7 @@ jobs: meson compile -C build - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3.28.13 + uses: github/codeql-action/analyze@45775bd8235c68ba998cffa5171334d58593da47 # v3.28.15 with: category: "/language:${{matrix.language}}" From 0d4a6281c297aeb200dc30664372fc252fff0f99 Mon Sep 17 00:00:00 2001 From: Ben Dooks <ben.dooks@codethink.co.uk> Date: Wed, 9 Apr 2025 07:38:45 +0100 Subject: [PATCH 086/310] lib: remove second fuse_main_real_versioned declaration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Newer gccs now use -Werror=redundant-decls which means that anyone including fuse.h is getting an error of: /usr/include/fuse3/fuse.h:959:5: error: redundant redeclaration of ‘fuse_main_real_versioned’ [-Werror=redundant-decls] 959 | int fuse_main_real_versioned(int argc, char *argv[], | ^~~~~~~~~~~~~~~~~~~~~~~~ /usr/include/fuse3/fuse.h:885:5: note: previous declaration of ‘fuse_main_real_versioned’ with type ‘int(int, char **, const struct fuse_operations *, size_t, struct libfuse_version *, void *)’ {aka ‘int(int, char **, const struct fuse_operations *, long unsigned int, struct libfuse_version *, void *)’} 885 | int fuse_main_real_versioned(int argc, char *argv[], | ^~~~~~~~~~~~~~~~~~~~~~~~ Signed-off-by: Ben Dooks <ben.dooks@codethink.co.uk> --- include/fuse.h | 3 --- 1 file changed, 3 deletions(-) diff --git a/include/fuse.h b/include/fuse.h index c94b62851..4582cc7ac 100644 --- a/include/fuse.h +++ b/include/fuse.h @@ -956,9 +956,6 @@ static inline int fuse_main_real(int argc, char *argv[], * * Example usage, see hello.c */ -int fuse_main_real_versioned(int argc, char *argv[], - const struct fuse_operations *op, size_t op_size, - struct libfuse_version *version, void *user_data); static inline int fuse_main_fn(int argc, char *argv[], const struct fuse_operations *op, void *user_data) From d66ca89e86a72fa5ad48d88ef5570062a79397be Mon Sep 17 00:00:00 2001 From: Meng Lu Wang <mwang@ddn.com> Date: Thu, 10 Apr 2025 16:58:57 +0800 Subject: [PATCH 087/310] mount: Add FUSE_KERN_DEVICE env variable to specify fuse kernel device For kernel development it might be necessary to load a module with renamed symbols and renamed /dev/<devicenode>. Reason is that for example ubuntu kernels have fuse compiled in and it is not possible to replace it at run time. And fuse might also be used for other file systems - a different device node is then needed. Also consolidate device path handling and remove unnecessary string duplication in mount_fuse() in fusermount.c. Signed-off-by: Meng Lu Wang <mwang@ddn.com> --- lib/mount.c | 7 ++++-- util/fusermount.c | 54 ++++++++++++----------------------------------- 2 files changed, 19 insertions(+), 42 deletions(-) diff --git a/lib/mount.c b/lib/mount.c index 6ed4444b3..34e5dd454 100644 --- a/lib/mount.c +++ b/lib/mount.c @@ -49,6 +49,7 @@ #define FUSERMOUNT_PROG "fusermount3" #define FUSE_COMMFD_ENV "_FUSE_COMMFD" #define FUSE_COMMFD2_ENV "_FUSE_COMMFD2" +#define FUSE_KERN_DEVICE_ENV "FUSE_KERN_DEVICE" #ifndef MS_DIRSYNC #define MS_DIRSYNC 128 @@ -506,7 +507,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; @@ -528,7 +529,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)); diff --git a/util/fusermount.c b/util/fusermount.c index da6d5f2be..68fa31e55 100644 --- a/util/fusermount.c +++ b/util/fusermount.c @@ -41,6 +41,7 @@ #endif #define FUSE_COMMFD_ENV "_FUSE_COMMFD" +#define FUSE_KERN_DEVICE_ENV "FUSE_KERN_DEVICE" #define FUSE_DEV "/dev/fuse" @@ -1163,56 +1164,30 @@ static int check_perm(const char **mntp, struct stat *stbuf, int *mountpoint_fd) return -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; -} - -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 +1196,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 +1267,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); From 3863da58b1f7904675ca050434d8219bc410f34a Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Wed, 16 Apr 2025 00:24:42 +0200 Subject: [PATCH 088/310] conn: prevent duplicate flag conversion in high-level interface The high-level interface triggers flag conversion twice: once in the high-level init and once in the low-level init. This caused false "both 'want' and 'want_ext' are set" errors when using fuse_set_feature_flag() or fuse_unset_feature_flag(). The existing check for duplicate conversion only worked when 32-bit flags were set directly. When using the preferred flag manipulation functions, conn->want and the lower 32 bits of conn->want_ext would differ, triggering the error. Fix this by synchronizing conn->want with the lower 32 bits of conn->want_ext after conversion, ensuring consistent state for subsequent calls. Closes: https://github.com/libfuse/libfuse/issues/1171 Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- example/hello.c | 4 +- lib/fuse_i.h | 3 + test/hello.c | 180 ++++++++++++++++++++++++++++++++++++ test/meson.build | 2 +- test/test_examples.py | 11 ++- test/test_want_conversion.c | 30 ++++-- 6 files changed, 215 insertions(+), 15 deletions(-) create mode 100644 test/hello.c diff --git a/example/hello.c b/example/hello.c index 90919f4d9..d9f01b9f0 100644 --- a/example/hello.c +++ b/example/hello.c @@ -59,8 +59,8 @@ static void *hello_init(struct fuse_conn_info *conn, cfg->kernel_cache = 1; /* Test setting flags the old way */ - conn->want = FUSE_CAP_ASYNC_READ; - conn->want &= ~FUSE_CAP_ASYNC_READ; + fuse_set_feature_flag(conn, FUSE_CAP_ASYNC_READ); + fuse_unset_feature_flag(conn, FUSE_CAP_ASYNC_READ); return NULL; } diff --git a/lib/fuse_i.h b/lib/fuse_i.h index 23fcaa6d4..48b8294f3 100644 --- a/lib/fuse_i.h +++ b/lib/fuse_i.h @@ -256,5 +256,8 @@ static inline int convert_to_conn_want_ext(struct fuse_conn_info *conn, conn->want; } + /* ensure there won't be a second conversion */ + conn->want = fuse_lower_32_bits(conn->want_ext); + return 0; } diff --git a/test/hello.c b/test/hello.c new file mode 100644 index 000000000..a07df0e48 --- /dev/null +++ b/test/hello.c @@ -0,0 +1,180 @@ +/* + * FUSE: Filesystem in Userspace + * 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. + */ + +/** @file + * + * minimal example filesystem using high-level API + * + * Compile with: + * + * gcc -Wall hello.c `pkg-config fuse3 --cflags --libs` -o hello + * + * ## Source code ## + * \include hello.c + */ + +#define FUSE_USE_VERSION 31 + +#include <fuse.h> +#include <stdio.h> +#include <string.h> +#include <errno.h> +#include <fcntl.h> +#include <stddef.h> +#include <assert.h> + +/* + * Command line options + * + * We can't set default values for the char* fields here because + * fuse_opt_parse would attempt to free() them when the user specifies + * different values on the command line. + */ +static struct options { + const char *filename; + const char *contents; + int show_help; +} options; + +#define OPTION(t, p) { t, offsetof(struct options, p), 1 } +static const struct fuse_opt option_spec[] = { + OPTION("--name=%s", filename), OPTION("--contents=%s", contents), + OPTION("-h", show_help), OPTION("--help", show_help), FUSE_OPT_END +}; + +static void *hello_init(struct fuse_conn_info *conn, struct fuse_config *cfg) +{ + (void)conn; + cfg->kernel_cache = 1; + + /* Test setting flags the old way */ + conn->want = FUSE_CAP_ASYNC_READ; + conn->want &= ~FUSE_CAP_ASYNC_READ; + + return NULL; +} + +static int hello_getattr(const char *path, struct stat *stbuf, + struct fuse_file_info *fi) +{ + (void)fi; + int res = 0; + + memset(stbuf, 0, sizeof(struct stat)); + if (strcmp(path, "/") == 0) { + stbuf->st_mode = S_IFDIR | 0755; + stbuf->st_nlink = 2; + } else if (strcmp(path + 1, options.filename) == 0) { + stbuf->st_mode = S_IFREG | 0444; + stbuf->st_nlink = 1; + stbuf->st_size = strlen(options.contents); + } else + res = -ENOENT; + + return res; +} + +static int hello_readdir(const char *path, void *buf, fuse_fill_dir_t filler, + off_t offset, struct fuse_file_info *fi, + enum fuse_readdir_flags flags) +{ + (void)offset; + (void)fi; + (void)flags; + + if (strcmp(path, "/") != 0) + return -ENOENT; + + filler(buf, ".", NULL, 0, FUSE_FILL_DIR_DEFAULTS); + filler(buf, "..", NULL, 0, FUSE_FILL_DIR_DEFAULTS); + filler(buf, options.filename, NULL, 0, FUSE_FILL_DIR_DEFAULTS); + + return 0; +} + +static int hello_open(const char *path, struct fuse_file_info *fi) +{ + if (strcmp(path + 1, options.filename) != 0) + return -ENOENT; + + if ((fi->flags & O_ACCMODE) != O_RDONLY) + return -EACCES; + + return 0; +} + +static int hello_read(const char *path, char *buf, size_t size, off_t offset, + struct fuse_file_info *fi) +{ + size_t len; + (void)fi; + if (strcmp(path + 1, options.filename) != 0) + return -ENOENT; + + len = strlen(options.contents); + if (offset < len) { + if (offset + size > len) + size = len - offset; + memcpy(buf, options.contents + offset, size); + } else + size = 0; + + return size; +} + +static const struct fuse_operations hello_oper = { + .init = hello_init, + .getattr = hello_getattr, + .readdir = hello_readdir, + .open = hello_open, + .read = hello_read, +}; + +static void show_help(const char *progname) +{ + printf("usage: %s [options] <mountpoint>\n\n", progname); + printf("File-system specific options:\n" + " --name=<s> Name of the \"hello\" file\n" + " (default: \"hello\")\n" + " --contents=<s> Contents \"hello\" file\n" + " (default \"Hello, World!\\n\")\n" + "\n"); +} + +int main(int argc, char *argv[]) +{ + int ret; + struct fuse_args args = FUSE_ARGS_INIT(argc, argv); + + /* Set defaults -- we have to use strdup so that + * fuse_opt_parse can free the defaults if other + * values are specified + */ + options.filename = strdup("hello"); + options.contents = strdup("Hello World!\n"); + + /* Parse options */ + if (fuse_opt_parse(&args, &options, option_spec, NULL) == -1) + return 1; + + /* When --help is specified, first print our own file-system + * specific help text, then signal fuse_main to show + * additional help (by adding `--help` to the options again) + * without usage: line (by setting argv[0] to the empty + * string) + */ + if (options.show_help) { + show_help(argv[0]); + assert(fuse_opt_add_arg(&args, "--help") == 0); + args.argv[0][0] = '\0'; + } + + ret = fuse_main(args.argc, args.argv, &hello_oper, NULL); + fuse_opt_free_args(&args); + return ret; +} diff --git a/test/meson.build b/test/meson.build index 599703064..56568d8f7 100644 --- a/test/meson.build +++ b/test/meson.build @@ -1,6 +1,6 @@ # Compile helper programs td = [] -foreach prog: [ 'test_write_cache', 'test_setattr' ] +foreach prog: [ 'test_write_cache', 'test_setattr', 'hello' ] td += executable(prog, prog + '.c', include_directories: include_dirs, link_with: [ libfuse ], diff --git a/test/test_examples.py b/test/test_examples.py index 54a2f88f9..9c8b77eec 100755 --- a/test/test_examples.py +++ b/test/test_examples.py @@ -44,8 +44,13 @@ def name_generator(__ctr=[0]): options.append('clone_fd') def invoke_directly(mnt_dir, name, options): - cmdline = base_cmdline + [ pjoin(basename, 'example', name), - '-f', mnt_dir, '-o', ','.join(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') @@ -88,7 +93,7 @@ def readdir_inode(dir): @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')) +@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) diff --git a/test/test_want_conversion.c b/test/test_want_conversion.c index 935b58d7d..bee23cc6e 100644 --- a/test/test_want_conversion.c +++ b/test/test_want_conversion.c @@ -1,11 +1,11 @@ #include "util.h" -#include <string.h> #define FUSE_USE_VERSION FUSE_MAKE_VERSION(3, 17) #include "fuse_i.h" #include <stdio.h> #include <assert.h> #include <inttypes.h> +#include <stdbool.h> static void print_conn_info(const char *prefix, struct fuse_conn_info *conn) { @@ -13,14 +13,21 @@ static void print_conn_info(const char *prefix, struct fuse_conn_info *conn) conn->want, conn->want_ext); } -static void application_init(struct fuse_conn_info *conn) +static void application_init_old_style(struct fuse_conn_info *conn) { - /* Simulate application init */ + /* Simulate application init the old style */ conn->want |= FUSE_CAP_ASYNC_READ; conn->want &= ~FUSE_CAP_SPLICE_READ; } -static void test_fuse_fs_init(struct fuse_conn_info *conn) +static void application_init_new_style(struct fuse_conn_info *conn) +{ + /* Simulate application init the new style */ + fuse_set_feature_flag(conn, FUSE_CAP_ASYNC_READ); + fuse_unset_feature_flag(conn, FUSE_CAP_SPLICE_READ); +} + +static void test_fuse_fs_init(struct fuse_conn_info *conn, bool new_style) { uint64_t want_ext_default = conn->want_ext; uint32_t want_default = fuse_lower_32_bits(conn->want_ext); @@ -31,18 +38,22 @@ static void test_fuse_fs_init(struct fuse_conn_info *conn) conn->want = want_default; - application_init(conn); + if (new_style) + application_init_new_style(conn); + else + application_init_old_style(conn); rc = convert_to_conn_want_ext(conn, want_ext_default, want_default); assert(rc == 0); } -static void test_do_init(struct fuse_conn_info *conn) +static void test_do_init(struct fuse_conn_info *conn, bool new_style) { /* Initial setup */ conn->capable_ext = FUSE_CAP_SPLICE_READ | FUSE_CAP_SPLICE_WRITE | FUSE_CAP_SPLICE_MOVE | FUSE_CAP_POSIX_LOCKS | - FUSE_CAP_FLOCK_LOCKS | FUSE_CAP_EXPORT_SUPPORT; + FUSE_CAP_FLOCK_LOCKS | FUSE_CAP_EXPORT_SUPPORT | + FUSE_CAP_ASYNC_READ; conn->capable = fuse_lower_32_bits(conn->capable_ext); conn->want_ext = conn->capable_ext; @@ -55,7 +66,7 @@ static void test_do_init(struct fuse_conn_info *conn) conn->want = want_default; conn->capable = fuse_lower_32_bits(conn->capable_ext); - test_fuse_fs_init(conn); + test_fuse_fs_init(conn, new_style); rc = convert_to_conn_want_ext(conn, want_ext_default, want_default); assert(rc == 0); @@ -82,7 +93,8 @@ static void test_want_conversion_basic(void) struct fuse_conn_info conn = { 0 }; printf("\nTesting basic want conversion:\n"); - test_do_init(&conn); + test_do_init(&conn, false); + test_do_init(&conn, true); print_conn_info("After init", &conn); } From 7c60178b42a301c3cd068d38d5606c9b044ba41c Mon Sep 17 00:00:00 2001 From: Giulio Benetti <giulio.benetti@benettiengineering.com> Date: Wed, 23 Apr 2025 14:28:29 +0200 Subject: [PATCH 089/310] Check if pthread_setname_np() exists before use it Since pthread_setname_np() is the only pthread function that requires NPTL and it basically only set thread name, let's check if pthread_setname_np() does exist, otherwise let's not call pthread_setname_np() to shrink dependencies. Signed-off-by: Giulio Benetti <giulio.benetti@benettiengineering.com> --- lib/fuse.c | 2 ++ lib/fuse_loop_mt.c | 2 ++ meson.build | 3 ++- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/fuse.c b/lib/fuse.c index 136f0c2bd..49f57112a 100644 --- a/lib/fuse.c +++ b/lib/fuse.c @@ -4917,7 +4917,9 @@ 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 while(1) { sleep_time = fuse_clean_cache(f); diff --git a/lib/fuse_loop_mt.c b/lib/fuse_loop_mt.c index 95316f7fd..d6be99849 100644 --- a/lib/fuse_loop_mt.c +++ b/lib/fuse_loop_mt.c @@ -132,7 +132,9 @@ static void *fuse_do_work(void *data) struct fuse_worker *w = (struct fuse_worker *) data; struct fuse_mt *mt = w->mt; +#ifdef HAVE_PTHREAD_SETNAME_NP pthread_setname_np(pthread_self(), "fuse_worker"); +#endif while (!fuse_session_exited(mt->se)) { int isforget = 0; diff --git a/meson.build b/meson.build index f28df3db5..a3aa73510 100644 --- a/meson.build +++ b/meson.build @@ -72,7 +72,8 @@ 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', 'static_assert' ] + 'utimensat', 'copy_file_range', 'fallocate', 'static_assert', + 'pthread_setname_np' ] foreach func : test_funcs private_cfg.set('HAVE_' + func.to_upper(), cc.has_function(func, prefix: include_default, args: args_default)) From f0dd86451023a6e5b0aeb5625130e404a6cb1db6 Mon Sep 17 00:00:00 2001 From: swj <1186093704@qq.com> Date: Wed, 23 Apr 2025 22:39:28 +0800 Subject: [PATCH 090/310] example: fix memfs_rename deadlock error Signed-off-by: swj <1186093704@qq.com> --- example/memfs_ll.cc | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/example/memfs_ll.cc b/example/memfs_ll.cc index 9898f2528..603885017 100644 --- a/example/memfs_ll.cc +++ b/example/memfs_ll.cc @@ -900,6 +900,7 @@ static void memfs_rename(fuse_req_t req, fuse_ino_t parent, const char *name, Inode *parentInode = nullptr; Inode *newparentInode = nullptr; Dentry *child_dentry = nullptr; + Dentry *child_dentry_copy = nullptr; Dentry *existing_dentry = nullptr; if (flags & (RENAME_EXCHANGE | RENAME_NOREPLACE)) { @@ -909,8 +910,8 @@ static void memfs_rename(fuse_req_t req, fuse_ino_t parent, const char *name, Inodes.lock(); - parentInode = Inodes.find(parent); - newparentInode = Inodes.find(newparent); + parentInode = Inodes.find_locked(parent); + newparentInode = Inodes.find_locked(newparent); if (!parentInode || !parentInode->is_dir() || !newparentInode || !newparentInode->is_dir()) { error = ENOENT; @@ -941,9 +942,9 @@ static void memfs_rename(fuse_req_t req, fuse_ino_t parent, const char *name, existing_dentry->get_inode()->dec_nlink(); } + child_dentry_copy = new Dentry(newname, child_dentry->get_inode()); parentInode->remove_child(name); - child_dentry->name = newname; - newparentInode->add_child(newname, child_dentry); + newparentInode->add_child_locked(newname, child_dentry_copy); out_unlock: parentInode->unlock(); From e025a78d9b296bc78e9e3ac2925d8bc0ec26d702 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Wed, 23 Apr 2025 15:16:42 +0200 Subject: [PATCH 091/310] signal handlers: Store fuse_session unconditionally MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Commit dae1184 ("Add syslog and fatal signal handler feature") added fuse_set_fail_signal_handlers() which can store "se". But as fuse_set_signal_handlers() also stores the object storing it was made conditionally if not set already. As per https://github.com/libfuse/libfuse/issues/1182 this breaks some applications like osspd, that have multiple sessions and rely on the right order and that the last call of fuse_set_signal_handlers() wins. Special thanks to Sébastien Noel to debug this issue. Closes: https://github.com/libfuse/libfuse/issues/1182 Signed-off-by: Bernd Schubert <bernd@bsbernd.com> --- lib/fuse_signals.c | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/fuse_signals.c b/lib/fuse_signals.c index fb1304c0d..6ac72308a 100644 --- a/lib/fuse_signals.c +++ b/lib/fuse_signals.c @@ -150,8 +150,13 @@ int fuse_set_signal_handlers(struct fuse_session *se) if (rc < 0) return rc; - if (fuse_instance == NULL) - fuse_instance = se; + /* + * needs to be set independently if already set, as some applications + * may have multiple sessions and might rely on traditional behavior + * that the last session is used. + */ + fuse_instance = se; + return 0; } @@ -164,8 +169,8 @@ int fuse_set_fail_signal_handlers(struct fuse_session *se) if (rc < 0) return rc; - if (fuse_instance == NULL) - fuse_instance = se; + /* See fuse_set_signal_handlers, why set unconditionally */ + fuse_instance = se; return 0; } From c5dbcdce2d1942abb567d03bf9dafb74f06b5769 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Sun, 30 Mar 2025 22:43:09 +0200 Subject: [PATCH 092/310] Fix multi-threaded fuse session exit Issue with previous code was that fuse_session_exit() didn't wake up the semaphore in fuse_loop_mt.c. Lock, semaphore and all uses of checking for "exited" are now moved to struct fuse_session to have it available for the signal handler. This also removes internal fuse_session_reset() calls, as that makes testing hard. From git history I also don't see why it was added. Closes: https://github.com/libfuse/libfuse/issues/997 Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- lib/fuse.c | 1 - lib/fuse_i.h | 8 +- lib/fuse_loop_mt.c | 60 ++++++------- lib/fuse_lowlevel.c | 18 +++- lib/fuse_signals.c | 13 ++- test/meson.build | 3 + test/test_ctests.py | 11 +++ test/test_signals.c | 202 ++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 277 insertions(+), 39 deletions(-) create mode 100644 test/test_signals.c diff --git a/lib/fuse.c b/lib/fuse.c index 49f57112a..02ceed202 100644 --- a/lib/fuse.c +++ b/lib/fuse.c @@ -4595,7 +4595,6 @@ static int fuse_session_loop_remember(struct fuse *f) } free(fbuf.mem); - fuse_session_reset(se); return res < 0 ? -1 : 0; } diff --git a/lib/fuse_i.h b/lib/fuse_i.h index 48b8294f3..1f5994427 100644 --- a/lib/fuse_i.h +++ b/lib/fuse_i.h @@ -10,6 +10,8 @@ #include "fuse_lowlevel.h" #include "util.h" +#include <pthread.h> +#include <semaphore.h> #include <stdint.h> #include <stdbool.h> #include <errno.h> @@ -55,7 +57,6 @@ struct fuse_notify_req { struct fuse_session { char *mountpoint; - volatile int exited; int fd; struct fuse_custom_io *io; struct mount_opts *mo; @@ -83,6 +84,11 @@ struct fuse_session { */ 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; }; diff --git a/lib/fuse_loop_mt.c b/lib/fuse_loop_mt.c index d6be99849..c13b476f9 100644 --- a/lib/fuse_loop_mt.c +++ b/lib/fuse_loop_mt.c @@ -53,14 +53,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 +129,32 @@ 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 - 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; } @@ -176,11 +174,11 @@ static void *fuse_do_work(void *data) mt->numavail--; if (mt->numavail == 0 && mt->numworker < mt->max_threads) 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; } @@ -354,9 +351,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); @@ -392,22 +389,21 @@ 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); @@ -415,11 +411,9 @@ int err; err = mt.error; } - pthread_mutex_destroy(&mt.lock); - sem_destroy(&mt.finish); + pthread_mutex_destroy(&se->mt_lock); if(se->error != 0) err = se->error; - fuse_session_reset(se); if (created_config) { fuse_loop_cfg_destroy(config); diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index cf8a7b0d6..d66e6aba2 100644 --- a/lib/fuse_lowlevel.c +++ b/lib/fuse_lowlevel.c @@ -19,6 +19,8 @@ #include "mount_util.h" #include "util.h" +#include <pthread.h> +#include <stdatomic.h> #include <stdint.h> #include <stdbool.h> #include <stdio.h> @@ -3006,6 +3008,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) @@ -3358,6 +3362,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) { @@ -3382,6 +3388,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); @@ -3605,18 +3613,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_signals.c b/lib/fuse_signals.c index 6ac72308a..3848aec65 100644 --- a/lib/fuse_signals.c +++ b/lib/fuse_signals.c @@ -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; @@ -53,8 +58,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/test/meson.build b/test/meson.build index 56568d8f7..332921657 100644 --- a/test/meson.build +++ b/test/meson.build @@ -19,6 +19,9 @@ 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) test_scripts = [ 'conftest.py', 'pytest.ini', 'test_examples.py', 'util.py', 'test_ctests.py', 'test_custom_io.py' ] diff --git a/test/test_ctests.py b/test/test_ctests.py index feefc0707..ae5cc8f15 100644 --- a/test/test_ctests.py +++ b/test/test_ctests.py @@ -144,3 +144,14 @@ def test_notify_file_size(tmpdir, notify, output_checker): logger.error(f"Failure in unmount: '{' '.join(cmdline)}'") cleanup(mount_process, mnt_dir) logger.debug("Unmount completed") + +def test_signals(output_checker): + """Test for proper signal handling (issue #1182)""" + logger = logging.getLogger(__name__) + logger.debug("Testing signal handling") + cmdline = [ pjoin(basename, 'test', 'test_signals') ] + logger.debug(f"Command line: {' '.join(cmdline)}") + subprocess.run(cmdline, stdout=output_checker.fd, \ + stderr=output_checker.fd, timeout=10, check=True) + logger.debug("Signal handling test completed successfully") + diff --git a/test/test_signals.c b/test/test_signals.c new file mode 100644 index 000000000..558202698 --- /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 COPYING.LIB + */ + +#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; +} From e940beb7405e672ad98dddc13679bc9d267b78e2 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Thu, 24 Apr 2025 18:43:43 +0200 Subject: [PATCH 093/310] Fix test/test_examples.py::test_passthrough The test had multiple issues - default passthrough_ll timeout was used - the created file was then not listed if timeout was not passed yet - mnt_name was actually point to src_dir, file comparison of stat, etc succeeded, because the same file was compared. Switching to the right dir made stat to always fail, because st_dev is different for source and mount. I.e. the test must not compare all stat values. Not sure how this test ever passed, but in a very slow debug VM with lots of kernel debug options enabled, the default passthrough_ll timeout it systematically failed. Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- test/test_examples.py | 57 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 51 insertions(+), 6 deletions(-) diff --git a/test/test_examples.py b/test/test_examples.py index 9c8b77eec..2e23697fd 100755 --- a/test/test_examples.py +++ b/test/test_examples.py @@ -157,10 +157,20 @@ def test_passthrough(short_tmpdir, name, debug, output_checker, writeback): cmdline = base_cmdline + \ [ pjoin(basename, 'example', 'passthrough'), '--plus', '-f', mnt_dir ] - else: + elif name == 'passthrough_ll': + cmdline = base_cmdline + \ + [ pjoin(basename, 'example', name), + '-f', mnt_dir, '-o', 'timeout=0' ] + else: # passthrough and passthrough_fh cmdline = base_cmdline + \ [ pjoin(basename, 'example', name), '-f', mnt_dir ] + + # Set all timeouts to 0 for everything except passthrough_ll + # (this includes passthrough, passthrough_plus, and passthrough_fh) + if name != 'passthrough_ll': + cmdline.extend(['-o', 'entry_timeout=0,negative_timeout=0,attr_timeout=0,ac_attr_timeout=0']) + if debug: cmdline.append('-d') @@ -169,7 +179,9 @@ def test_passthrough(short_tmpdir, name, debug, output_checker, writeback): pytest.skip('example does not support writeback caching') cmdline.append('-o') cmdline.append('writeback') - + + print(f"\nDebug: Command line: {' '.join(cmdline)}") + mount_process = subprocess.Popen(cmdline, stdout=output_checker.fd, stderr=output_checker.fd) try: @@ -871,25 +883,58 @@ def tst_utimens(mnt_dir, ns_tol=0): def tst_passthrough(src_dir, mnt_dir): name = name_generator() src_name = pjoin(src_dir, name) - mnt_name = pjoin(src_dir, name) + mnt_name = pjoin(mnt_dir, name) + + print(f"\nDebug: Creating file {name}") + print(f"Debug: src_name={src_name}") + print(f"Debug: mnt_name={mnt_name}") + + # First test: write to source directory 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') + + print(f"Debug: File written to src_name") + + start_time = time.time() + while time.time() - start_time < 10: # 10 second timeout + if name in os.listdir(mnt_dir): + break + print(f"Debug: Waiting for file to appear... ({time.time() - start_time:.1f}s)") + time.sleep(0.1) + else: + pytest.fail("File did not appear in mount directory within 10 seconds") + assert name in os.listdir(src_dir) assert name in os.listdir(mnt_dir) - assert os.stat(src_name) == os.stat(mnt_name) + # Compare relevant stat attributes + src_stat = os.stat(src_name) + mnt_stat = os.stat(mnt_name) + assert src_stat.st_mode == mnt_stat.st_mode + assert src_stat.st_ino == mnt_stat.st_ino + assert src_stat.st_size == mnt_stat.st_size + assert src_stat.st_mtime == mnt_stat.st_mtime + + # Second test: write to mount directory name = name_generator() src_name = pjoin(src_dir, name) - mnt_name = pjoin(src_dir, name) + mnt_name = pjoin(mnt_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) + + # Compare relevant stat attributes + src_stat = os.stat(src_name) + mnt_stat = os.stat(mnt_name) + assert src_stat.st_mode == mnt_stat.st_mode + assert src_stat.st_ino == mnt_stat.st_ino + assert src_stat.st_size == mnt_stat.st_size + assert abs(src_stat.st_mtime - mnt_stat.st_mtime) < 0.01 def tst_xattr(path): From 82bcd818fb3e7d5ced9b0c04b7b7a98a892e807e Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Thu, 24 Apr 2025 16:49:08 +0200 Subject: [PATCH 094/310] Fix meson function tests Several meson tests were incorrectly failing Checking for function "static_assert" : NO (cached) Checking for function "pthread_setname_np" : NO (cached) Check usable header "#include <linux/close_range.h>" : NO (cached) These functions get now tested with compilation tests and get found on my system. Checking if "static_assert check" compiles: YES Checking if "pthread_setname_np check" compiles: YES Checking if "close_range check" compiles: YES Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- meson.build | 67 +++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 49 insertions(+), 18 deletions(-) diff --git a/meson.build b/meson.build index a3aa73510..4e5c90c34 100644 --- a/meson.build +++ b/meson.build @@ -59,6 +59,8 @@ include_default = ''' #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> +#include <assert.h> /* For static_assert */ +#include <pthread.h> /* For pthread_setname_np */ ''' args_default = [ '-D_GNU_SOURCE' ] @@ -72,32 +74,61 @@ 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', 'static_assert', - 'pthread_setname_np' ] + 'utimensat', 'copy_file_range', 'fallocate' ] foreach func : test_funcs private_cfg.set('HAVE_' + func.to_upper(), cc.has_function(func, prefix: include_default, args: args_default)) endforeach -private_cfg.set('HAVE_SETXATTR', - cc.has_function('setxattr', prefix: '#include <sys/xattr.h>')) -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>')) -# Test if headers exist -private_cfg.set('HAVE_LINUX_CLOSE_RANGE_H', - cc.check_header('#include <linux/close_range.h>')) +# Special case checks that need custom code +special_funcs = { + 'static_assert': ''' + #include <assert.h> + static_assert(1, "test"); + int main(void) { return 0; } + ''', + 'pthread_setname_np': ''' + #include <pthread.h> + int main(void) { + pthread_t thread = pthread_self(); + pthread_setname_np(thread, "test"); + return 0; + } + ''', + 'close_range': ''' + #include <unistd.h> + #include <fcntl.h> + #include <linux/close_range.h> + int main(void) { + unsigned int flags = CLOSE_RANGE_UNSHARE; + return close_range(3, ~0U, flags); + } + ''' +} + +foreach name, code : special_funcs + private_cfg.set('HAVE_' + name.to_upper(), + cc.compiles(code, args: ['-Werror'] + args_default, + name: name + ' check')) +endforeach + +# Regular function checks +private_cfg.set('HAVE_SETXATTR', + cc.has_function('setxattr', prefix: '#include <sys/xattr.h>')) +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>')) -# Test if structs have specific member +# Struct member checks private_cfg.set('HAVE_STRUCT_STAT_ST_ATIM', - cc.has_member('struct stat', 'st_atim', - prefix: include_default, - args: args_default)) + cc.has_member('struct stat', 'st_atim', + prefix: include_default + '#include <sys/stat.h>', + args: args_default)) private_cfg.set('HAVE_STRUCT_STAT_ST_ATIMESPEC', - cc.has_member('struct stat', 'st_atimespec', - prefix: include_default, - args: args_default)) + cc.has_member('struct stat', 'st_atimespec', + prefix: include_default + '#include <sys/stat.h>', + args: args_default)) private_cfg.set('USDT_ENABLED', get_option('enable-usdt')) From 0eb8513206c2c467fa7510d0c762eb5c868dc66d Mon Sep 17 00:00:00 2001 From: Giulio Benetti <giulio.benetti@benettiengineering.com> Date: Fri, 25 Apr 2025 19:00:14 +0200 Subject: [PATCH 095/310] meson.build: make special_funcs check more reliable Unfortunately while cross-compiling with build tools like Buildroot it happens to have repeated flags or anything that could lead to a warning. This way the check fails because of a warning not related to the special function. So let's use cc.links() and increase minimum meson_version to 0.60 since cc.links() has been added during that version. Signed-off-by: Giulio Benetti <giulio.benetti@benettiengineering.com> --- meson.build | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meson.build b/meson.build index 4e5c90c34..3615a5a26 100644 --- a/meson.build +++ b/meson.build @@ -1,6 +1,6 @@ project('libfuse3', ['c'], version: '3.18.0-rc0', # Version with RC suffix - meson_version: '>= 0.51.0', + meson_version: '>= 0.60.0', default_options: [ 'buildtype=debugoptimized', 'c_std=gnu11', @@ -108,7 +108,7 @@ special_funcs = { foreach name, code : special_funcs private_cfg.set('HAVE_' + name.to_upper(), - cc.compiles(code, args: ['-Werror'] + args_default, + cc.links(code, args: args_default, name: name + ' check')) endforeach From 066ed644762714c099667564f748058201f3f598 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Mon, 24 Mar 2025 17:44:56 +0100 Subject: [PATCH 096/310] Add fuse-io-uring dependencies to github workfow files Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- .github/workflows/abicheck.yml | 2 +- .github/workflows/codeql.yml | 2 +- .github/workflows/pr-ci.yml | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/abicheck.yml b/.github/workflows/abicheck.yml index 10a8e6d98..b79ccc6ed 100644 --- a/.github/workflows/abicheck.yml +++ b/.github/workflows/abicheck.yml @@ -26,7 +26,7 @@ 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 with: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 9c59174eb..de23031a7 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -73,7 +73,7 @@ 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 diff --git a/.github/workflows/pr-ci.yml b/.github/workflows/pr-ci.yml index 4bb7f66de..78b5e02a9 100644 --- a/.github/workflows/pr-ci.yml +++ b/.github/workflows/pr-ci.yml @@ -31,7 +31,8 @@ jobs: 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 + libpcap0.8-dev:i386 libudev-dev:i386 pkg-config:i386 \ + liburing-dev libnuma-dev - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/setup-python@v5 with: From d393ffa85b0926374c8df543a9ffc81b1d0ce232 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Mon, 24 Mar 2025 17:53:01 +0100 Subject: [PATCH 097/310] Synchronize fuse_kernel.h with linux-6.14 Also add FUSE_CAP_OVER_IO_URING Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- include/fuse_common.h | 7 +++- include/fuse_kernel.h | 96 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 101 insertions(+), 2 deletions(-) diff --git a/include/fuse_common.h b/include/fuse_common.h index 582505fa9..f1d4b170a 100644 --- a/include/fuse_common.h +++ b/include/fuse_common.h @@ -7,7 +7,6 @@ /** @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> @@ -515,6 +515,11 @@ struct fuse_loop_config_v1 { */ #define FUSE_CAP_NO_EXPORT_SUPPORT (1 << 30) +/** + * Indicates support for io-uring between fuse-server and fuse-client + */ +#define FUSE_CAP_OVER_IO_URING (1 << 31) + /** * Ioctl flags * diff --git a/include/fuse_kernel.h b/include/fuse_kernel.h index d08b99d60..5e0eb41d9 100644 --- a/include/fuse_kernel.h +++ b/include/fuse_kernel.h @@ -217,6 +217,18 @@ * - 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 */ #ifndef _LINUX_FUSE_H @@ -252,7 +264,7 @@ #define FUSE_KERNEL_VERSION 7 /** Minor version number of this interface */ -#define FUSE_KERNEL_MINOR_VERSION 40 +#define FUSE_KERNEL_MINOR_VERSION 42 /** The node ID of the root inode */ #define FUSE_ROOT_ID 1 @@ -421,6 +433,8 @@ 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 */ #define FUSE_ASYNC_READ (1 << 0) #define FUSE_POSIX_LOCKS (1 << 1) @@ -466,6 +480,8 @@ struct fuse_file_lock { /* 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) /** * CUSE INIT request/reply flags @@ -984,6 +1000,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; @@ -1186,4 +1217,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 */ From 2f4d7ca9c47d0266196578d45c6dad3bdcbee3e6 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Mon, 24 Mar 2025 18:07:27 +0100 Subject: [PATCH 098/310] fuse_lowlevel: Add support for header/payload separation Header/payload separation is part of the fuse-io-uring protocol and might be later on for /dev/fuse legacy communication as well. This is a preparation commit, for now fuse_ll_ops2 is unused. Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- lib/cuse_lowlevel.c | 11 +- lib/fuse_i.h | 2 + lib/fuse_lowlevel.c | 766 ++++++++++++++++++++++++++++++++++---------- 3 files changed, 608 insertions(+), 171 deletions(-) diff --git a/lib/cuse_lowlevel.c b/lib/cuse_lowlevel.c index 5387f8430..a25711bfc 100644 --- a/lib/cuse_lowlevel.c +++ b/lib/cuse_lowlevel.c @@ -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_i.h b/lib/fuse_i.h index 1f5994427..89a5c6f9b 100644 --- a/lib/fuse_i.h +++ b/lib/fuse_i.h @@ -195,6 +195,8 @@ 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 _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); diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index d66e6aba2..e0aa52380 100644 --- a/lib/fuse_lowlevel.c +++ b/lib/fuse_lowlevel.c @@ -1183,9 +1183,12 @@ 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) +static void _do_lookup(fuse_req_t req, const fuse_ino_t nodeid, + const void *op_in, const void *in_payload) { - char *name = (char *) inarg; + (void)op_in; + + char *name = (char *)in_payload; if (req->se->op.lookup) req->se->op.lookup(req, nodeid, name); @@ -1193,9 +1196,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); @@ -1203,21 +1215,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) +{ + _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) { - struct fuse_batch_forget_in *arg = (void *) inarg; - struct fuse_forget_one *param = (void *) PARAM(arg); + (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); @@ -1237,14 +1255,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; @@ -1258,9 +1287,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) { - struct fuse_setattr_in *arg = (struct fuse_setattr_in *) 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) +{ + (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; @@ -1269,32 +1307,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) { - struct fuse_access_in *arg = (struct fuse_access_in *) 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) +{ + (void)in_payload; + const struct fuse_access_in *arg = op_in; if (req->se->op.access) req->se->op.access(req, nodeid, arg->mask); @@ -1302,9 +1342,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) +{ + _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) inarg; + (void)op_in; + (void)in_payload; if (req->se->op.readlink) req->se->op.readlink(req, nodeid); @@ -1312,15 +1360,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); @@ -1328,22 +1381,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) +{ + 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) { - char *name = (char *) inarg; + (void)op_in; + const char *name = in_payload; if (req->se->op.unlink) req->se->op.unlink(req, nodeid, name); @@ -1351,9 +1427,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) +{ + _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) { - char *name = (char *) inarg; + (void)op_in; + const char *name = in_payload; if (req->se->op.rmdir) req->se->op.rmdir(req, nodeid, name); @@ -1361,10 +1445,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) +{ + _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) { - char *name = (char *) inarg; - char *linkname = ((char *) inarg) + strlen((char *) inarg) + 1; + (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); @@ -1372,45 +1463,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) { - struct fuse_rename_in *arg = (struct fuse_rename_in *) inarg; - char *oldname = PARAM(arg); - char *newname = oldname + strlen(oldname) + 1; + _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) +{ + 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; @@ -1426,35 +1535,79 @@ 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); + + /* XXX: fuse_create_in::open_flags */ 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; + /* XXX: fuse_open_in::open_flags */ + if (req->se->op.open) req->se->op.open(req, nodeid, &fi); else if (req->se->conn.want_ext & FUSE_CAP_NO_OPEN_SUPPORT) @@ -1463,9 +1616,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) +{ + _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) { - struct fuse_read_in *arg = (struct fuse_read_in *) inarg; + (void)in_payload; + struct fuse_read_in *arg = (struct fuse_read_in *)op_in; if (req->se->op.read) { struct fuse_file_info fi; @@ -1481,68 +1641,97 @@ 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) +{ + _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 *) inarg; + 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.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.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 */ @@ -1550,9 +1739,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)); @@ -1567,9 +1758,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)); @@ -1590,9 +1788,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) +{ + _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) { - struct fuse_fsync_in *arg = (struct fuse_fsync_in *) inarg; + (void)in_payload; + const struct fuse_fsync_in *arg = op_in; struct fuse_file_info fi; int datasync = arg->fsync_flags & 1; @@ -1605,13 +1811,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) { - struct fuse_open_in *arg = (struct fuse_open_in *) 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) +{ + (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); @@ -1621,9 +1835,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)); @@ -1635,9 +1857,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)); @@ -1649,9 +1879,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) { - struct fuse_release_in *arg = (struct fuse_release_in *) 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) +{ + (void)in_payload; + struct fuse_release_in *arg = (struct fuse_release_in *)op_in; struct fuse_file_info fi; memset(&fi, 0, sizeof(fi)); @@ -1664,9 +1902,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) { - struct fuse_fsync_in *arg = (struct fuse_fsync_in *) 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) +{ + (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; @@ -1678,11 +1924,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); @@ -1694,55 +1947,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; -static void do_getxattr(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) + _do_setxattr(req, nodeid, arg, payload); +} + +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) { - struct fuse_getxattr_in *arg = (struct fuse_getxattr_in *) 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) +{ + (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)); @@ -1756,9 +2047,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; @@ -1772,11 +2065,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; @@ -1814,14 +2111,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) { - do_setlk_common(req, nodeid, inarg, 0); + (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(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) @@ -1864,9 +2174,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; @@ -1885,6 +2197,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) @@ -1911,21 +2227,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 && @@ -1949,15 +2272,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)); @@ -1983,23 +2315,39 @@ 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) +{ + _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) { - struct fuse_fallocate_in *arg = (struct fuse_fallocate_in *) inarg; + (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 _do_copy_file_range(fuse_req_t req, const fuse_ino_t nodeid_in, + const void *op_in, const void *in_payload) { - struct fuse_copy_file_range_in *arg = (struct fuse_copy_file_range_in *) inarg; + (void)in_payload; + const struct fuse_copy_file_range_in *arg = op_in; struct fuse_file_info fi_in, fi_out; memset(&fi_in, 0, sizeof(fi_in)); @@ -2008,19 +2356,25 @@ 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 *inarg) +{ + _do_copy_file_range(req, nodeid_in, inarg, NULL); +} + +static void _do_lseek(fuse_req_t req, const fuse_ino_t nodeid, + const void *op_in, const void *in_payload) { - struct fuse_lseek_in *arg = (struct fuse_lseek_in *) inarg; + (void)in_payload; + const struct fuse_lseek_in *arg = op_in; struct fuse_file_info fi; memset(&fi, 0, sizeof(fi)); @@ -2032,6 +2386,11 @@ 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); +} + static bool want_flags_valid(uint64_t capable, uint64_t want) { uint64_t unknown_flags = want & (~capable); @@ -2046,10 +2405,12 @@ static bool want_flags_valid(uint64_t capable, uint64_t want) /* 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; @@ -2366,12 +2727,20 @@ void do_init(fuse_req_t req, fuse_ino_t nodeid, const void *inarg) send_reply_ok(req, &outarg, outargsize); } -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; (void) nodeid; - (void) inarg; + (void)op_in; + (void)in_payload; se->got_destroy = 1; se->got_init = 0; @@ -2381,6 +2750,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; @@ -2768,7 +3142,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" }, @@ -2784,6 +3158,60 @@ static struct { [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_LSEEK] = { _do_lseek, "LSEEK" }, + [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. From c5a032b3410d7225ac0355355faa63565a209943 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Tue, 15 Apr 2025 22:03:09 +0200 Subject: [PATCH 099/310] Add container_of and ROUND_UP macros Needed by follow up commits. container_of is actually just moved/consolidated to util.h. Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- lib/fuse.c | 4 ---- lib/fuse_lowlevel.c | 4 ---- lib/util.h | 6 ++++++ 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/lib/fuse.c b/lib/fuse.c index 02ceed202..bbf573a2b 100644 --- a/lib/fuse.c +++ b/lib/fuse.c @@ -92,10 +92,6 @@ struct node_table { size_t split; }; -#define container_of(ptr, type, member) ({ \ - const typeof( ((type *)0)->member ) *__mptr = (ptr); \ - (type *)( (char *)__mptr - offsetof(type,member) );}) - #define list_entry(ptr, type, member) \ container_of(ptr, type, member) diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index e0aa52380..4a4c71eae 100644 --- a/lib/fuse_lowlevel.c +++ b/lib/fuse_lowlevel.c @@ -50,10 +50,6 @@ #define PARAM(inarg) (((char *)(inarg)) + sizeof(*(inarg))) #define OFFSET_MAX 0x7fffffffffffffffLL -#define container_of(ptr, type, member) ({ \ - const typeof( ((type *)0)->member ) *__mptr = (ptr); \ - (type *)( (char *)__mptr - offsetof(type,member) );}) - struct fuse_pollhandle { uint64_t kh; struct fuse_session *se; diff --git a/lib/util.h b/lib/util.h index 508fafb12..ed03ad40e 100644 --- a/lib/util.h +++ b/lib/util.h @@ -30,4 +30,10 @@ static inline uint64_t fuse_higher_32_bits(uint64_t nr) #define FUSE_VAR_UNUSED(var) (__attribute__((unused)) var) #endif +#define container_of(ptr, type, member) \ + ({ \ + unsigned long __mptr = (unsigned long)(ptr); \ + ((type *)(__mptr - offsetof(type, member))); \ + }) + #endif From ef533e2e3fedbd647726416fa7f36ca543ee13ea Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Thu, 24 Apr 2025 13:53:59 +0200 Subject: [PATCH 100/310] Add a fuse_set_thread_name() helper Avoid splattering the code with ifdef Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- lib/fuse.c | 4 +--- lib/fuse_loop_mt.c | 4 +--- lib/util.c | 19 +++++++++++++++++++ lib/util.h | 3 ++- 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/lib/fuse.c b/lib/fuse.c index bbf573a2b..9b9b98327 100644 --- a/lib/fuse.c +++ b/lib/fuse.c @@ -4912,9 +4912,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(pthread_self(), "fuse_prune_nodes"); while(1) { sleep_time = fuse_clean_cache(f); diff --git a/lib/fuse_loop_mt.c b/lib/fuse_loop_mt.c index c13b476f9..46e2d6eba 100644 --- a/lib/fuse_loop_mt.c +++ b/lib/fuse_loop_mt.c @@ -131,9 +131,7 @@ static void *fuse_do_work(void *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(pthread_self(), "fuse_worker"); while (!fuse_session_exited(se)) { int isforget = 0; diff --git a/lib/util.c b/lib/util.c index a529d383c..00f1e1adc 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> @@ -25,3 +34,13 @@ int libfuse_strtol(const char *str, long *res) *res = val; return 0; } + +void fuse_set_thread_name(unsigned long tid, const char *name) +{ +#ifdef HAVE_PTHREAD_SETNAME_NP + pthread_setname_np(tid, name); +#else + (void)tid; + (void)name; +#endif +} diff --git a/lib/util.h b/lib/util.h index ed03ad40e..d1939cfef 100644 --- a/lib/util.h +++ b/lib/util.h @@ -9,6 +9,7 @@ #define unlikely(x) __builtin_expect(!!(x), 0) int libfuse_strtol(const char *str, long *res); +void fuse_set_thread_name(unsigned long tid, const char *name); /** * Return the low bits of a number @@ -36,4 +37,4 @@ static inline uint64_t fuse_higher_32_bits(uint64_t nr) ((type *)(__mptr - offsetof(type, member))); \ }) -#endif +#endif /* FUSE_UTIL_H_ */ From dde540e413eba6d22a4515659dd72262b8a01af4 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Mon, 24 Mar 2025 18:46:29 +0100 Subject: [PATCH 101/310] fuse: Add ring creation Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- lib/fuse_i.h | 18 +- lib/fuse_loop.c | 6 +- lib/fuse_uring.c | 548 +++++++++++++++++++++++++++++++++++++++++++++ lib/fuse_uring_i.h | 18 ++ lib/meson.build | 8 + meson.build | 26 +++ meson_options.txt | 3 + test/ci-build.sh | 6 + 8 files changed, 630 insertions(+), 3 deletions(-) create mode 100644 lib/fuse_uring.c create mode 100644 lib/fuse_uring_i.h diff --git a/lib/fuse_i.h b/lib/fuse_i.h index 89a5c6f9b..b643e9041 100644 --- a/lib/fuse_i.h +++ b/lib/fuse_i.h @@ -6,6 +6,9 @@ See the file COPYING.LIB */ +#ifndef LIB_FUSE_I_H_ +#define LIB_FUSE_I_H_ + #include "fuse.h" #include "fuse_lowlevel.h" #include "util.h" @@ -24,6 +27,7 @@ }) struct mount_opts; +struct fuse_ring_pool; struct fuse_req { struct fuse_session *se; @@ -34,6 +38,7 @@ struct fuse_req { struct fuse_chan *ch; int interrupted; unsigned int ioctl_64bit : 1; + unsigned int is_uring : 1; union { struct { uint64_t unique; @@ -55,6 +60,11 @@ struct fuse_notify_req { struct fuse_notify_req *prev; }; +struct fuse_session_uring { + unsigned int q_depth; + struct fuse_ring_pool *pool; +}; + struct fuse_session { char *mountpoint; int fd; @@ -79,7 +89,8 @@ 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; @@ -91,6 +102,9 @@ struct fuse_session { /* true if reading requests from /dev/fuse are handled internally */ bool buf_reallocable; + + /* io_uring */ + struct fuse_session_uring uring; }; struct fuse_chan { @@ -269,3 +283,5 @@ static inline int convert_to_conn_want_ext(struct fuse_conn_info *conn, return 0; } + +#endif /* LIB_FUSE_I_H_*/ diff --git a/lib/fuse_loop.c b/lib/fuse_loop.c index 410f43fbb..a27111ca8 100644 --- a/lib/fuse_loop.c +++ b/lib/fuse_loop.c @@ -11,7 +11,7 @@ #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_uring.c b/lib/fuse_uring.c new file mode 100644 index 000000000..ce567169a --- /dev/null +++ b/lib/fuse_uring.c @@ -0,0 +1,548 @@ +/* + * FUSE: Filesystem in Userspace + * Copyright (C) 2025 Bernd Schubert <bschubert@ddn.com> + * + * Implementation of (most of) the low-level FUSE API. The session loop + * functions are implemented in separate files. + * + * This program can be distributed under the terms of the GNU LGPLv2. + * See the file COPYING.LIB + */ + +#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 <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; + + /* 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; + + /* 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; + + /* 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 unsigned int 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_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; + + /* 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) { + int value = 1; + + write(queue->eventfd, &value, sizeof(value)); + 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); + } + } + + free(fuse_ring->queues); + free(fuse_ring); +} + +static int fuse_uring_prepare_fetch_sqes(struct fuse_ring_queue *queue) +{ + struct fuse_ring_pool *ring_pool = queue->ring_pool; + unsigned int sq_ready; + struct io_uring_sqe *sqe; + + for (size_t idx = 0; idx < ring_pool->queue_depth; idx++) { + struct fuse_ring_ent *ent = &queue->ent[idx]; + + 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; + } + + fuse_uring_sqe_prepare(sqe, ent, FUSE_IO_URING_CMD_REGISTER); + + /* 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); + } + + sq_ready = io_uring_sq_ready(&queue->ring); + if (sq_ready != ring_pool->queue_depth) { + fuse_log(FUSE_LOG_ERR, + "SQE ready mismatch, expected %d got %d\n", + ring_pool->queue_depth, sq_ready); + return -EINVAL; + } + + // Add the 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); + + io_uring_submit(&queue->ring); + + 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; + } + + return fuse_ring; + +err: + if (fuse_ring) + fuse_session_destruct_uring(fuse_ring); + + return NULL; +} + +/* stub function */ +static int fuse_uring_queue_handle_cqes(struct fuse_ring_queue *queue) +{ + (void)queue; + + return 0; +} + +/** + * 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); + goto err; + } + + queue->req_header_sz = ROUND_UP(sizeof(struct fuse_ring_ent), + 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); + ring_ent->req_payload_sz = ring->max_req_payload_sz; + + ring_ent->op_payload = + numa_alloc_local(ring_ent->req_payload_sz); + + req->se = se; + pthread_mutex_init(&req->lock, NULL); + req->is_uring = true; + req->ref_cnt = 1; + } + + res = fuse_uring_prepare_fetch_sqes(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 queue->ring.ring_fd; + +err: + close(queue->eventfd); + return res; +} + +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(pthread_self(), thread_name); + + fuse_uring_set_thread_core(queue->qid); + + err = fuse_uring_init_queue(queue); + if (err < 0) { + fuse_log(FUSE_LOG_ERR, "qid=%d queue setup failed\n", + queue->qid); + goto err; + } + + /* 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); + + err = fuse_uring_queue_handle_cqes(queue); + if (err < 0) { + /* + * fuse-over-io-uring is not supported, operation can + * continue over /dev/fuse + */ + if (err == -EOPNOTSUPP) + goto ret; + goto err; + } + } + + return NULL; + +err: + fuse_session_exit(se); +ret: + 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(void) +{ + _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(); + + fuse_ring = fuse_create_ring(se); + if (fuse_ring == NULL) { + err = -EADDRNOTAVAIL; + goto out; + } + + se->uring.pool = fuse_ring; + + err = fuse_uring_start_ring_threads(fuse_ring); +out: + 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; +} diff --git a/lib/fuse_uring_i.h b/lib/fuse_uring_i.h new file mode 100644 index 000000000..fefb8a0a8 --- /dev/null +++ b/lib/fuse_uring_i.h @@ -0,0 +1,18 @@ +/* + * 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 COPYING.LIB + */ + +#ifndef FUSE_URING_I_H_ +#define FUSE_URING_I_H_ + +#include "fuse_lowlevel.h" + +struct fuse_in_header; + +int fuse_uring_start(struct fuse_session *se); +int fuse_uring_stop(struct fuse_session *se); + +#endif // FUSE_URING_I_H_ diff --git a/lib/meson.build b/lib/meson.build index 6a52d06a9..fcd95741c 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 ] diff --git a/meson.build b/meson.build index 3615a5a26..b6631098d 100644 --- a/meson.build +++ b/meson.build @@ -132,6 +132,32 @@ private_cfg.set('HAVE_STRUCT_STAT_ST_ATIMESPEC', 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 = get_option('enable-io-uring') ? \ + dependency('liburing', required: false) :\ + dependency('', required: false) +libnuma = dependency('numa', required: false) + +if 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 + # # Compiler configuration # diff --git a/meson_options.txt b/meson_options.txt index 957c5fc36..c1f8fe694 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -24,3 +24,6 @@ option('disable-libc-symbol-version', type : 'boolean', value : false, 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') diff --git a/test/ci-build.sh b/test/ci-build.sh index 40bb79e8d..86230bdfd 100755 --- a/test/ci-build.sh +++ b/test/ci-build.sh @@ -146,6 +146,12 @@ export CC=clang export CXX=clang++ sanitized_build "-Ddisable-libc-symbol-version=true" +# Sanitized build without fuse-io-uring +export CC=clang +export CXX=clang++ +sanitized_build "-Denable-io-uring=false" + +# Build without any sanitizer non_sanitized_build # Documentation. From da355f79362f5724f75147dfd51d58d6ee3e552d Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Mon, 24 Mar 2025 23:18:10 +0100 Subject: [PATCH 102/310] Add support for ring creation in fuse_lowlevel.c Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- lib/fuse_i.h | 1 + lib/fuse_loop_mt.c | 5 +++++ lib/fuse_lowlevel.c | 31 +++++++++++++++++++++++++++++-- lib/fuse_uring_i.h | 27 +++++++++++++++++++++++++++ lib/util.h | 2 +- 5 files changed, 63 insertions(+), 3 deletions(-) diff --git a/lib/fuse_i.h b/lib/fuse_i.h index b643e9041..23e58ef13 100644 --- a/lib/fuse_i.h +++ b/lib/fuse_i.h @@ -61,6 +61,7 @@ struct fuse_notify_req { }; struct fuse_session_uring { + bool enable; unsigned int q_depth; struct fuse_ring_pool *pool; }; diff --git a/lib/fuse_loop_mt.c b/lib/fuse_loop_mt.c index 46e2d6eba..046256c20 100644 --- a/lib/fuse_loop_mt.c +++ b/lib/fuse_loop_mt.c @@ -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> @@ -407,12 +408,16 @@ int err; fuse_join_worker(&mt, mt.main.next); err = mt.error; + + if (se->uring.pool) + fuse_uring_stop(se); } pthread_mutex_destroy(&se->mt_lock); if(se->error != 0) err = se->error; + if (created_config) { fuse_loop_cfg_destroy(config); config = NULL; diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index 4a4c71eae..433594fba 100644 --- a/lib/fuse_lowlevel.c +++ b/lib/fuse_lowlevel.c @@ -18,6 +18,7 @@ #include "fuse_misc.h" #include "mount_util.h" #include "util.h" +#include "fuse_uring_i.h" #include <pthread.h> #include <stdatomic.h> @@ -34,6 +35,7 @@ #include <assert.h> #include <sys/file.h> #include <sys/ioctl.h> +#include <stdalign.h> #ifdef USDT_ENABLED #include "usdt.h" @@ -46,7 +48,6 @@ #define F_SETPIPE_SZ (F_LINUX_SPECIFIC_BASE + 7) #endif - #define PARAM(inarg) (((char *)(inarg)) + sizeof(*(inarg))) #define OFFSET_MAX 0x7fffffffffffffffLL @@ -169,6 +170,10 @@ static void list_add_req(struct fuse_req *req, struct fuse_req *next) static void destroy_req(fuse_req_t req) { + if (req->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); @@ -179,7 +184,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 alreasdy, though. But then would add + * a lock across ring queues. + */ + if (se->conn.no_interrupt || req->is_uring) { ctr = --req->ref_cnt; fuse_chan_put(req->ch); req->ch = NULL; @@ -209,6 +218,7 @@ static struct fuse_req *fuse_ll_alloc_req(struct fuse_session *se) req->ref_cnt = 1; list_init_req(req); pthread_mutex_init(&req->lock, NULL); + req->is_uring = false; } return req; @@ -2415,6 +2425,8 @@ _do_init(fuse_req_t req, const fuse_ino_t nodeid, const void *op_in, 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) { @@ -2673,6 +2685,10 @@ _do_init(fuse_req_t req, const fuse_ino_t nodeid, const void *op_in, } if (se->conn.want_ext & FUSE_CAP_NO_EXPORT_SUPPORT) outargflags |= FUSE_NO_EXPORT_SUPPORT; + if (se->uring.enable && se->conn.want_ext & FUSE_CAP_OVER_IO_URING) { + outargflags |= FUSE_OVER_IO_URING; + enable_io_uring = true; + } if (inargflags & FUSE_INIT_EXT) { outargflags |= FUSE_INIT_EXT; @@ -2721,6 +2737,17 @@ _do_init(fuse_req_t req, const fuse_ino_t nodeid, const void *op_in, outargsize = FUSE_COMPAT_22_INIT_OUT_SIZE; send_reply_ok(req, &outarg, outargsize); + + /* XXX: Split the start, and send SQEs only after send_reply_ok() */ + if (enable_io_uring) { + int ring_rc = fuse_uring_start(se); + + if (ring_rc != 0) { + fuse_log(FUSE_LOG_ERR, "fuse: failed to start io-uring: %s\n", + strerror(ring_rc)); + fuse_session_exit(se); + } + } } static __attribute__((no_sanitize("thread"))) void diff --git a/lib/fuse_uring_i.h b/lib/fuse_uring_i.h index fefb8a0a8..e9f298908 100644 --- a/lib/fuse_uring_i.h +++ b/lib/fuse_uring_i.h @@ -8,11 +8,38 @@ #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 + +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); int fuse_uring_stop(struct fuse_session *se); +#else // HAVE_URING + +static inline int fuse_uring_start(struct fuse_session *se FUSE_VAR_UNUSED) +{ + return -ENOTSUP; +} + +static inline int fuse_uring_stop(struct fuse_session *se FUSE_VAR_UNUSED) +{ + return -ENOTSUP; +} + +#endif // HAVE_URING + #endif // FUSE_URING_I_H_ diff --git a/lib/util.h b/lib/util.h index d1939cfef..96b59d3af 100644 --- a/lib/util.h +++ b/lib/util.h @@ -28,7 +28,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) \ From 04ecefb9869791934fd496139d82cec1587d7751 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Mon, 24 Mar 2025 23:40:16 +0100 Subject: [PATCH 103/310] fuse_lowlevel: Split fuse_send_msg The actual function to write to /dev/fuse is offloaded into _fuse_send_msg() Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- lib/fuse_lowlevel.c | 51 ++++++++++++++++++++++++++++----------------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index 433594fba..b4458c4f6 100644 --- a/lib/fuse_lowlevel.c +++ b/lib/fuse_lowlevel.c @@ -224,11 +224,41 @@ static struct fuse_req *fuse_ll_alloc_req(struct fuse_session *se) return req; } +/* + * Send data to fuse-kernel using an fd of the fuse device. + */ +static int _fuse_send_msg(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; +} + /* Send data. If *ch* is NULL, send via session master fd */ static int fuse_send_msg(struct fuse_session *se, struct fuse_chan *ch, struct iovec *iov, int count) { struct fuse_out_header *out = iov[0].iov_base; + int err; assert(se != NULL); out->len = iov_length(iov, count); @@ -248,26 +278,9 @@ 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); - else - res = writev(ch ? ch->fd : se->fd, iov, count); - - int err = errno; + err = _fuse_send_msg(se, ch, iov, count); trace_request_reply(out->unique, out->len, out->error, err); - - if (res == -1) { - /* ENOENT means the operation was interrupted */ - if (!fuse_session_exited(se) && err != ENOENT) - perror("fuse: writing device"); - return -err; - } - - return 0; + return err; } From 9367748bef846d8430f7e9ee0ea31fe806c5b9f2 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Tue, 25 Mar 2025 18:55:30 +0100 Subject: [PATCH 104/310] fuse_ll_ops: Make fuse_ino_t const It will never be modified, should be const. Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- lib/fuse_lowlevel.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index b4458c4f6..fdbb60cda 100644 --- a/lib/fuse_lowlevel.c +++ b/lib/fuse_lowlevel.c @@ -3142,7 +3142,7 @@ int fuse_req_interrupted(fuse_req_t req) } 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" }, From 913c3779886b0c9c4aa610eabf7c21eb8064f382 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Wed, 26 Mar 2025 00:57:08 +0100 Subject: [PATCH 105/310] Add a basic io-uring cqe handler Not function yet, just preparation. Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- lib/fuse_uring.c | 95 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 92 insertions(+), 3 deletions(-) diff --git a/lib/fuse_uring.c b/lib/fuse_uring.c index ce567169a..47efa9517 100644 --- a/lib/fuse_uring.c +++ b/lib/fuse_uring.c @@ -336,12 +336,101 @@ static struct fuse_ring_pool *fuse_create_ring(struct fuse_session *se) return NULL; } -/* stub function */ +/* stub function*/ +static 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 op_payload_sz) +{ + (void)se; + (void)req; + (void)in; + (void)op_in; + (void)op_payload; + (void)op_payload_sz; +} + +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(); + } + + req->is_uring = true; + req->ref_cnt++; + req->ch = NULL; /* not needed for uring */ + + 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) { - (void)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; + int ret = 0; + + io_uring_for_each_cqe(&queue->ring, head, cqe) { + int err = 0; + + num_completed++; + + err = cqe->res; + if (err != 0) { + if (err > 0 && ((uintptr_t)io_uring_cqe_get_data(cqe) == + (unsigned int)queue->eventfd)) { + /* teardown from eventfd */ + return -ENOTCONN; + } + + // XXX: Needs rate limited logs, otherwise log spam + //fuse_log(FUSE_LOG_ERR, "cqe res: %d\n", cqe->res); + + /* -ENOTCONN is ok on umount */ + if (err != -EINTR && err != -EOPNOTSUPP && + err != -EAGAIN && err != -ENOTCONN) { + se->error = cqe->res; + + /* return first error */ + if (ret == 0) + ret = err; + } + + } else { + fuse_uring_handle_cqe(queue, cqe); + } + } - return 0; + if (num_completed) + io_uring_cq_advance(&queue->ring, num_completed); + + return ret == 0 ? 0 : num_completed; } /** From d4abf661f9948075e1f043a825c37bdaa660f911 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Wed, 26 Mar 2025 01:07:05 +0100 Subject: [PATCH 106/310] Add fuse_session_process_uring_cqe Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- lib/fuse_lowlevel.c | 133 ++++++++++++++++++++++++++++++++++++-------- lib/fuse_uring.c | 15 ----- 2 files changed, 111 insertions(+), 37 deletions(-) diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index fdbb60cda..d6b254e5b 100644 --- a/lib/fuse_lowlevel.c +++ b/lib/fuse_lowlevel.c @@ -3255,6 +3255,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) @@ -3323,7 +3375,7 @@ void fuse_session_process_buf_internal(struct fuse_session *se, 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); @@ -3344,31 +3396,15 @@ void fuse_session_process_buf_internal(struct fuse_session *se, 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; @@ -3432,6 +3468,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_ops[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_in, &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 } diff --git a/lib/fuse_uring.c b/lib/fuse_uring.c index 47efa9517..65ee9870f 100644 --- a/lib/fuse_uring.c +++ b/lib/fuse_uring.c @@ -336,21 +336,6 @@ static struct fuse_ring_pool *fuse_create_ring(struct fuse_session *se) return NULL; } -/* stub function*/ -static 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 op_payload_sz) -{ - (void)se; - (void)req; - (void)in; - (void)op_in; - (void)op_payload; - (void)op_payload_sz; -} - static void fuse_uring_handle_cqe(struct fuse_ring_queue *queue, struct io_uring_cqe *cqe) { From 988ef1172255dfc3abfc99097ceddfa9e1da66cd Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Wed, 26 Mar 2025 01:25:23 +0100 Subject: [PATCH 107/310] Add fuse-io-uring reply support Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- lib/fuse_lowlevel.c | 70 +++++++++++++-------- lib/fuse_uring.c | 150 ++++++++++++++++++++++++++++++++++++++++++++ lib/fuse_uring_i.h | 29 +++++++++ 3 files changed, 222 insertions(+), 27 deletions(-) diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index d6b254e5b..03aeca7c7 100644 --- a/lib/fuse_lowlevel.c +++ b/lib/fuse_lowlevel.c @@ -227,21 +227,22 @@ static struct fuse_req *fuse_ll_alloc_req(struct fuse_session *se) /* * Send data to fuse-kernel using an fd of the fuse device. */ -static int _fuse_send_msg(struct fuse_session *se, struct fuse_chan *ch, - struct iovec *iov, int count) +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) { + 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 { + else res = writev(ch ? ch->fd : se->fd, iov, count); - } + if (res == -1) { /* ENOENT means the operation was interrupted */ err = errno; @@ -253,15 +254,17 @@ static int _fuse_send_msg(struct fuse_session *se, struct fuse_chan *ch, return 0; } -/* Send data. If *ch* is NULL, send via session master fd */ 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->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", @@ -278,12 +281,15 @@ static int fuse_send_msg(struct fuse_session *se, struct fuse_chan *ch, } } - err = _fuse_send_msg(se, ch, iov, count); + if (is_uring) + err = fuse_send_msg_uring(req, iov, count); + else + err = fuse_write_msg_dev(se, ch, iov, count); + 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) { @@ -305,7 +311,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, @@ -321,6 +327,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->is_uring) + return send_reply_uring(req, error, arg, argsize); + struct iovec iov[2]; int count = 1; if (argsize) { @@ -599,7 +608,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; @@ -614,7 +623,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); @@ -632,7 +641,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; @@ -764,8 +773,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); @@ -917,7 +927,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; } @@ -964,17 +974,17 @@ 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) { - 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); } #endif @@ -985,13 +995,16 @@ int fuse_reply_data(fuse_req_t req, struct fuse_bufvec *bufv, struct fuse_out_header out; int res; + if (req->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; @@ -1743,8 +1756,8 @@ static void do_write_buf(fuse_req_t req, const fuse_ino_t nodeid, sizeof(struct fuse_write_in); } if (bufv.buf[0].size < arg->size) { - fuse_log(FUSE_LOG_ERR, "fuse: %s: buffer size too small\n", - __func__); + fuse_log(FUSE_LOG_ERR, + "fuse: %s: buffer size too small\n", __func__); fuse_reply_err(req, EIO); goto out; } @@ -2576,6 +2589,7 @@ _do_init(fuse_req_t req, const fuse_ino_t nodeid, const void *op_in, 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 @@ -2840,6 +2854,7 @@ 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; @@ -2849,7 +2864,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) @@ -2991,6 +3006,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; @@ -3011,7 +3027,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; @@ -3392,7 +3408,7 @@ 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; } diff --git a/lib/fuse_uring.c b/lib/fuse_uring.c index 65ee9870f..55d68fa27 100644 --- a/lib/fuse_uring.c +++ b/lib/fuse_uring.c @@ -141,6 +141,156 @@ fuse_uring_sqe_prepare(struct io_uring_sqe *sqe, struct fuse_ring_ent *req, 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) +{ + 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 = 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; + } + + fuse_uring_sqe_prepare(sqe, ring_ent, + FUSE_IO_URING_CMD_COMMIT_AND_FETCH); + + 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: %llu, result=%d\n", + out->unique, ent_in_out->payload_sz); + } + + /* XXX: This needs to be a ring config option */ + io_uring_submit(&queue->ring); + + 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) { + 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) { diff --git a/lib/fuse_uring_i.h b/lib/fuse_uring_i.h index e9f298908..40cda5ba9 100644 --- a/lib/fuse_uring_i.h +++ b/lib/fuse_uring_i.h @@ -27,6 +27,12 @@ struct fuse_in_header; int fuse_uring_start(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 @@ -40,6 +46,29 @@ 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_ From 3824fabf8aaa6210876ac0519f451f4885c7ebbc Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Tue, 15 Apr 2025 17:03:09 +0200 Subject: [PATCH 108/310] fuse_common.h: Prefix flags with 1UL Ensure it is correctly stored in an unsigned 32 bit int. Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- include/fuse_common.h | 58 +++++++++++++++++++++---------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/include/fuse_common.h b/include/fuse_common.h index f1d4b170a..249e0c94f 100644 --- a/include/fuse_common.h +++ b/include/fuse_common.h @@ -182,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. @@ -190,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 @@ -199,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 "..". @@ -211,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 @@ -219,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 @@ -227,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 @@ -235,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 @@ -244,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 @@ -257,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 @@ -286,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. @@ -294,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. @@ -322,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. @@ -333,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 @@ -342,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 @@ -357,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 @@ -365,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. @@ -384,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 @@ -393,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 @@ -410,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. @@ -423,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 @@ -438,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. @@ -461,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. @@ -477,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 @@ -493,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. @@ -505,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 @@ -513,12 +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 (1 << 31) +#define FUSE_CAP_OVER_IO_URING (1UL << 31) /** * Ioctl flags From 71b6fb723aacd9c216c68db3cc1f4d87291cf408 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Tue, 15 Apr 2025 18:08:54 +0200 Subject: [PATCH 109/310] fuse: Add io-uring options Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- lib/fuse_lowlevel.c | 10 +++++++++- lib/fuse_uring_i.h | 4 ++++ lib/helper.c | 6 +++++- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index 03aeca7c7..ad3641423 100644 --- a/lib/fuse_lowlevel.c +++ b/lib/fuse_lowlevel.c @@ -3545,6 +3545,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 }; @@ -3562,7 +3564,11 @@ 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 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) @@ -3892,6 +3898,8 @@ fuse_session_new_versioned(struct fuse_args *args, se->conn.max_write = FUSE_DEFAULT_MAX_PAGES_LIMIT * getpagesize(); se->bufsize = se->conn.max_write + FUSE_BUFFER_HEADER_SIZE; se->conn.max_readahead = UINT_MAX; + se->uring.enable = SESSION_DEF_URING_ENABLE; + se->uring.q_depth = SESSION_DEF_URING_Q_DEPTH; /* Parse options */ if(fuse_opt_parse(args, se, fuse_ll_opts, NULL) == -1) diff --git a/lib/fuse_uring_i.h b/lib/fuse_uring_i.h index 40cda5ba9..a65241798 100644 --- a/lib/fuse_uring_i.h +++ b/lib/fuse_uring_i.h @@ -16,6 +16,10 @@ #include "util.h" #endif +/* 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, diff --git a/lib/helper.c b/lib/helper.c index 0b0dad262..5811c53d3 100644 --- a/lib/helper.c +++ b/lib/helper.c @@ -139,7 +139,11 @@ void fuse_cmdline_help(void) " -o max_idle_threads the maximum number of idle worker threads\n" " allowed (default: -1)\n" " -o max_threads the maximum number of worker threads\n" - " allowed (default: 10)\n"); + " allowed (default: 10)\n" + /* fuse_ll_opts in fuse_lowlevel.c, FIXME, call into that file */ + " -o io_uring enable io-uring\n" + " -o io_uring_q_depth=<n> io-uring queue depth\n" +); } static int fuse_helper_opt_proc(void *data, const char *arg, int key, From 538b51feedbcdcd78beefd7517ff19c77c4e4526 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Tue, 15 Apr 2025 20:13:47 +0200 Subject: [PATCH 110/310] Add fuse_req_is_uring() to check if a req comes through io-uring This might be useful to optimize code paths. For example, with io-uring the request buffer is valid until the request is replied to, while without io-uring the request buffer is only valid in current thread context. Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- include/fuse_lowlevel.h | 5 +++++ lib/fuse_lowlevel.c | 5 +++++ lib/fuse_versionscript | 5 +++++ 3 files changed, 15 insertions(+) diff --git a/include/fuse_lowlevel.h b/include/fuse_lowlevel.h index c7b44d963..138a78436 100644 --- a/include/fuse_lowlevel.h +++ b/include/fuse_lowlevel.h @@ -2315,6 +2315,11 @@ 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); + #ifdef __cplusplus } #endif diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index ad3641423..c7cbebd26 100644 --- a/lib/fuse_lowlevel.c +++ b/lib/fuse_lowlevel.c @@ -3157,6 +3157,11 @@ int fuse_req_interrupted(fuse_req_t req) return interrupted; } +bool fuse_req_is_uring(fuse_req_t req) +{ + return req->is_uring; +} + static struct { void (*func)(fuse_req_t req, const fuse_ino_t node, const void *arg); const char *name; diff --git a/lib/fuse_versionscript b/lib/fuse_versionscript index 6c5fc83eb..22c59e1af 100644 --- a/lib/fuse_versionscript +++ b/lib/fuse_versionscript @@ -202,6 +202,11 @@ FUSE_3.17 { fuse_log_close_syslog; } FUSE_3.12; +FUSE_3.18 { + global: + fuse_req_is_uring; +} FUSE_3.17; + # Local Variables: # indent-tabs-mode: t # End: From 91c9803cb4dc5251abbb9af312c917dddfdd689e Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Tue, 15 Apr 2025 20:42:19 +0200 Subject: [PATCH 111/310] passthrough_hp: Add io-uring options Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- example/passthrough_hp.cc | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/example/passthrough_hp.cc b/example/passthrough_hp.cc index 8b5214c21..b88c2c5dc 100644 --- a/example/passthrough_hp.cc +++ b/example/passthrough_hp.cc @@ -77,6 +77,9 @@ using namespace std; #define SFS_DEFAULT_THREADS "-1" // take libfuse value as default #define SFS_DEFAULT_CLONE_FD "0" +#define SFS_DEFAULT_URING "1" +#define SFS_DEFAULT_URING_Q_DEPTH "0" +#define SFS_DEFAULT_URING_ARGLEN "0" /* We are re-using pointers to our `struct sfs_inode` and `struct sfs_dirp` elements as inodes and file handles. This means that we @@ -155,6 +158,11 @@ struct Fs { bool nocache; size_t num_threads; bool clone_fd; + struct { + bool enable; + int queue_depth; + } uring; + std::string fuse_mount_options; bool direct_io; bool passthrough; @@ -1400,10 +1408,12 @@ static cxxopts::ParseResult parse_options(int argc, char **argv) { ("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)) + 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"); - + ("direct-io", "enable fuse kernel internal direct-io") + ("uring", "use uring communication") + ("uring-q-depth", "io-uring queue depth", + cxxopts::value<int>()->default_value(SFS_DEFAULT_URING_Q_DEPTH)); // 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); @@ -1435,6 +1445,10 @@ static cxxopts::ParseResult parse_options(int argc, char **argv) { fs.num_threads = options["num-threads"].as<int>(); fs.clone_fd = options.count("clone-fd"); fs.direct_io = options.count("direct-io"); + + fs.uring.enable = options.count("uring"); + fs.uring.queue_depth = options["uring-q-depth"].as<int>(); + char* resolved_path = realpath(argv[1], NULL); if (resolved_path == NULL) warn("WARNING: realpath() failed with"); @@ -1518,7 +1532,14 @@ int main(int argc, char *argv[]) { 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"); + errx(3, "ERROR: Out of memory adding arguments"); + + if (fs.uring.enable) { + if (fuse_opt_add_arg(&args, "-oio_uring") || + fuse_opt_add_arg(&args, ("-oio_uring_q_depth=" + + std::to_string(fs.uring.queue_depth)).c_str())) + errx(3, "ERROR: Out of memory adding io-uring arguments"); + } ret = -1; fuse_lowlevel_ops sfs_oper {}; From 3421cab7773268e0afb4b300775b4d60f2b7acf5 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Thu, 24 Apr 2025 19:38:21 +0200 Subject: [PATCH 112/310] env variables to override default io-uring enable and q-depth We want to especially test with and without io-uring being enabled. Ideally without modifying all tests - that is what the env variable can be used for. Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- lib/fuse_lowlevel.c | 13 +++++++++++-- test/ci-build.sh | 7 +++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index c7cbebd26..8c9cb9074 100644 --- a/lib/fuse_lowlevel.c +++ b/lib/fuse_lowlevel.c @@ -3903,8 +3903,17 @@ fuse_session_new_versioned(struct fuse_args *args, se->conn.max_write = FUSE_DEFAULT_MAX_PAGES_LIMIT * getpagesize(); se->bufsize = se->conn.max_write + FUSE_BUFFER_HEADER_SIZE; se->conn.max_readahead = UINT_MAX; - se->uring.enable = SESSION_DEF_URING_ENABLE; - se->uring.q_depth = SESSION_DEF_URING_Q_DEPTH; + + /* + * 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) diff --git a/test/ci-build.sh b/test/ci-build.sh index 86230bdfd..2bced379c 100755 --- a/test/ci-build.sh +++ b/test/ci-build.sh @@ -118,6 +118,13 @@ sanitized_build() sudo rm -fr ${PREFIX_DIR} ) +# Sanitized with io-uring +export CC=clang +export CXX=clang++ +export FUSE_URING_ENABLE=1 +sanitized_build +unset FUSE_URING_ENABLE + # 32-bit sanitized build export CC=clang export CXX=clang++ From e70f91cb6387a4e14e7a4a4ebe9dc1100cbc1126 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Tue, 15 Apr 2025 20:57:05 +0200 Subject: [PATCH 113/310] Set FUSE_CAP_OVER_IO_URING flag - allow io-uring mode So far it was disabled as the series was not complete yet. Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- lib/fuse_lowlevel.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index 8c9cb9074..8c5d060dc 100644 --- a/lib/fuse_lowlevel.c +++ b/lib/fuse_lowlevel.c @@ -2546,6 +2546,9 @@ _do_init(fuse_req_t req, const fuse_ino_t nodeid, const void *op_in, 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; } From e122b3faf365df72ca397b8ed88e7dcff8789cca Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Mon, 28 Apr 2025 21:47:27 +0200 Subject: [PATCH 114/310] Add a README for fuse-over-io-uring Also update the comment description of fuse_uring.c Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- doc/README.fuse-io-uring | 38 ++++++++++++++++++++++++++++++++++++++ lib/fuse_uring.c | 3 +-- 2 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 doc/README.fuse-io-uring diff --git a/doc/README.fuse-io-uring b/doc/README.fuse-io-uring new file mode 100644 index 000000000..7207c6e74 --- /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-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 '-oio_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: -oio_uring +3. Optionally adjust queue depth: -oio_uring_q_depth=<depth> + +Example: +./my_fuse_fs /source /mountpoint -oio_uring -oio_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/lib/fuse_uring.c b/lib/fuse_uring.c index 55d68fa27..62c5a4d78 100644 --- a/lib/fuse_uring.c +++ b/lib/fuse_uring.c @@ -2,8 +2,7 @@ * FUSE: Filesystem in Userspace * Copyright (C) 2025 Bernd Schubert <bschubert@ddn.com> * - * Implementation of (most of) the low-level FUSE API. The session loop - * functions are implemented in separate files. + * Implementation of (most of) FUSE-over-io-uring. * * This program can be distributed under the terms of the GNU LGPLv2. * See the file COPYING.LIB From 3ea426d8eec1d1306ba8c125b0fd59e2360eb524 Mon Sep 17 00:00:00 2001 From: Joanne Koong <joannelkoong@gmail.com> Date: Tue, 18 Feb 2025 10:42:41 -0800 Subject: [PATCH 115/310] lib/fuse.c: refactor handler logic for readability Refactor handler logic to make it more readable. No functional changes. Signed-off-by: Joanne Koong <joannelkoong@gmail.com> --- lib/fuse.c | 849 +++++++++++++++++++++++++---------------------------- 1 file changed, 399 insertions(+), 450 deletions(-) diff --git a/lib/fuse.c b/lib/fuse.c index 9b9b98327..7f9c982fb 100644 --- a/lib/fuse.c +++ b/lib/fuse.c @@ -1592,146 +1592,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 +1737,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 +1906,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 +1962,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 +1978,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 %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); } 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 +2226,52 @@ 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) { - fuse_get_context()->private_data = fs->user_data; - if (fs->op.poll) { - int res; + 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); + fuse_get_context()->private_data = fs->user_data; + if (!fs->op.poll) + return -ENOSYS; + 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); - res = fs->op.poll(path, fi, ph, reventsp); + res = fs->op.poll(path, fi, ph, reventsp); - if (fs->debug && !res) - fuse_log(FUSE_LOG_DEBUG, " poll[%llu] revents: 0x%x\n", - (unsigned long long) fi->fh, *reventsp); + if (fs->debug && !res) + fuse_log(FUSE_LOG_DEBUG, " poll[%llu] revents: 0x%x\n", + (unsigned long long) fi->fh, *reventsp); - return res; - } else - return -ENOSYS; + 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,37 +2281,35 @@ 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->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); - } else { + 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); } static int is_open(struct fuse *f, fuse_ino_t dir, const char *name) @@ -2796,17 +2745,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, From aa2e891b0d9f2fa82ae1a2d3b61f09cb0d5db480 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Apr 2025 23:54:56 +0000 Subject: [PATCH 116/310] build(deps): bump github/codeql-action from 3.28.15 to 3.28.16 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.28.15 to 3.28.16. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/45775bd8235c68ba998cffa5171334d58593da47...28deaeda66b76a05916b6923827895f2b14ab387) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 3.28.16 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index de23031a7..130ae449b 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -53,7 +53,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@45775bd8235c68ba998cffa5171334d58593da47 # v3.28.15 + uses: github/codeql-action/init@28deaeda66b76a05916b6923827895f2b14ab387 # v3.28.16 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -78,7 +78,7 @@ jobs: meson compile -C build - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@45775bd8235c68ba998cffa5171334d58593da47 # v3.28.15 + uses: github/codeql-action/analyze@28deaeda66b76a05916b6923827895f2b14ab387 # v3.28.16 with: category: "/language:${{matrix.language}}" From 83e7dce82b11dc077517b066eb639ee00ce1eb77 Mon Sep 17 00:00:00 2001 From: izxl007 <zeng.zheng@zte.com.cn> Date: Tue, 29 Apr 2025 23:54:45 +0800 Subject: [PATCH 117/310] fuse_lowlevel.c: Fix a small spelling mistake Signed-off-by: izxl007 <zeng.zheng@zte.com.cn> --- lib/fuse_lowlevel.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index 8c5d060dc..e10b937f0 100644 --- a/lib/fuse_lowlevel.c +++ b/lib/fuse_lowlevel.c @@ -185,7 +185,7 @@ void fuse_free_req(fuse_req_t req) struct fuse_session *se = req->se; /* XXX: for now no support for interrupts with io-uring - * It actually might work alreasdy, though. But then would add + * It actually might work already, though. But then would add * a lock across ring queues. */ if (se->conn.no_interrupt || req->is_uring) { From cd503168287e82e1698fb2fdf6397f1dbb751e75 Mon Sep 17 00:00:00 2001 From: Joanne Koong <joannelkoong@gmail.com> Date: Thu, 23 Jan 2025 14:32:17 -0800 Subject: [PATCH 118/310] Update include/fuse_kernel.h to version 7.43 Sync include/fuse_kernel.h with the most up to date fuse uapi headers in the kernel. Signed-off-by: Joanne Koong <joannelkoong@gmail.com> --- include/fuse_kernel.h | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/include/fuse_kernel.h b/include/fuse_kernel.h index 5e0eb41d9..42db04c0c 100644 --- a/include/fuse_kernel.h +++ b/include/fuse_kernel.h @@ -229,6 +229,9 @@ * - FUSE_URING_IN_OUT_HEADER_SZ * - FUSE_URING_OP_IN_OUT_SZ * - enum fuse_uring_cmd + * + * 7.43 + * - add FUSE_REQUEST_TIMEOUT */ #ifndef _LINUX_FUSE_H @@ -435,6 +438,8 @@ struct fuse_file_lock { * 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) @@ -477,11 +482,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 @@ -909,7 +914,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 From 84a27de6a644163eda97b7052e064774ec73f925 Mon Sep 17 00:00:00 2001 From: Joanne Koong <joannelkoong@gmail.com> Date: Wed, 17 Jul 2024 13:42:14 -0700 Subject: [PATCH 119/310] Support request timeouts This adds the libfuse changes needed to support request timeouts. A timeout may be set by the server in its init call. If a request is not completed by the timeout, the connection will be aborted by the kernel. Signed-off-by: Joanne Koong <joannelkoong@gmail.com> --- include/fuse_common.h | 8 +++++++- lib/fuse_lowlevel.c | 5 +++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/include/fuse_common.h b/include/fuse_common.h index 249e0c94f..0b0ea4cfc 100644 --- a/include/fuse_common.h +++ b/include/fuse_common.h @@ -708,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]; }; fuse_static_assert(sizeof(struct fuse_conn_info) == 128, "Size of struct fuse_conn_info must be 128 bytes"); diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index e10b937f0..619edfc3e 100644 --- a/lib/fuse_lowlevel.c +++ b/lib/fuse_lowlevel.c @@ -2720,6 +2720,11 @@ _do_init(fuse_req_t req, const fuse_ino_t nodeid, const void *op_in, enable_io_uring = true; } + if ((inargflags & FUSE_REQUEST_TIMEOUT) && se->conn.request_timeout) { + outargflags |= FUSE_REQUEST_TIMEOUT; + outarg.request_timeout = se->conn.request_timeout; + } + if (inargflags & FUSE_INIT_EXT) { outargflags |= FUSE_INIT_EXT; outarg.flags2 = outargflags >> 32; From 7812b66cb7325c22efe699bff2475b519931cea4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 May 2025 23:09:32 +0000 Subject: [PATCH 120/310] build(deps): bump github/codeql-action from 3.28.16 to 3.28.17 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.28.16 to 3.28.17. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/28deaeda66b76a05916b6923827895f2b14ab387...60168efe1c415ce0f5521ea06d5c2062adbeed1b) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 3.28.17 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 130ae449b..85accb651 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -53,7 +53,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@28deaeda66b76a05916b6923827895f2b14ab387 # v3.28.16 + uses: github/codeql-action/init@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -78,7 +78,7 @@ jobs: meson compile -C build - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@28deaeda66b76a05916b6923827895f2b14ab387 # v3.28.16 + uses: github/codeql-action/analyze@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17 with: category: "/language:${{matrix.language}}" From 65b388f0d6b414253a5804a9bbd36040a2158045 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 May 2025 22:57:16 +0000 Subject: [PATCH 121/310] build(deps): bump github/codeql-action from 3.28.17 to 3.28.18 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.28.17 to 3.28.18. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/60168efe1c415ce0f5521ea06d5c2062adbeed1b...ff0a06e83cb2de871e5a09832bc6a81e7276941f) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 3.28.18 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 85accb651..9f2a97c6f 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -53,7 +53,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17 + uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -78,7 +78,7 @@ jobs: meson compile -C build - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17 + uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 with: category: "/language:${{matrix.language}}" From eadd6a5454373c7c404463c81be652275da5e07b Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Sat, 17 May 2025 23:52:47 +0200 Subject: [PATCH 122/310] Make conn->want/want_ext conversion non fatal there are too many issues with conn->want and conn->want_ext conversion, for now just log a warning, but setting both flags is now not fatal anymore. Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- lib/fuse.c | 16 +--------------- lib/fuse_i.h | 5 +---- lib/fuse_lowlevel.c | 9 +-------- 3 files changed, 3 insertions(+), 27 deletions(-) diff --git a/lib/fuse.c b/lib/fuse.c index 7f9c982fb..ccf4aeb61 100644 --- a/lib/fuse.c +++ b/lib/fuse.c @@ -2563,25 +2563,11 @@ void fuse_fs_init(struct fuse_fs *fs, struct fuse_conn_info *conn, if (fs->op.init) { uint64_t want_ext_default = conn->want_ext; uint32_t want_default = fuse_lower_32_bits(conn->want_ext); - int rc; conn->want = want_default; fs->user_data = fs->op.init(conn, cfg); - rc = convert_to_conn_want_ext(conn, want_ext_default, - want_default); - - if (rc != 0) { - /* - * This is a grave developer error, but - * we cannot return an error here, as the function - * signature does not allow it. - */ - fuse_log( - FUSE_LOG_ERR, - "fuse: Aborting due to invalid conn want flags.\n"); - _exit(EXIT_FAILURE); - } + convert_to_conn_want_ext(conn, want_ext_default, want_default); } } diff --git a/lib/fuse_i.h b/lib/fuse_i.h index 23e58ef13..14d99ad9f 100644 --- a/lib/fuse_i.h +++ b/lib/fuse_i.h @@ -268,11 +268,8 @@ static inline int convert_to_conn_want_ext(struct fuse_conn_info *conn, */ if (conn->want != want_default && fuse_lower_32_bits(conn->want_ext) != conn->want) { - if (conn->want_ext != want_ext_default) { - fuse_log(FUSE_LOG_ERR, - "fuse: both 'want' and 'want_ext' are set\n"); + if (conn->want_ext != want_ext_default) return -EINVAL; - } /* high bits from want_ext, low bits from want */ conn->want_ext = fuse_higher_32_bits(conn->want_ext) | diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index 619edfc3e..9fb793bd3 100644 --- a/lib/fuse_lowlevel.c +++ b/lib/fuse_lowlevel.c @@ -2605,7 +2605,6 @@ _do_init(fuse_req_t req, const fuse_ino_t nodeid, const void *op_in, if (se->op.init) { uint64_t want_ext_default = se->conn.want_ext; uint32_t want_default = fuse_lower_32_bits(se->conn.want_ext); - int rc; // Apply the first 32 bits of capable_ext to capable se->conn.capable = fuse_lower_32_bits(se->conn.capable_ext); @@ -2618,14 +2617,8 @@ _do_init(fuse_req_t req, const fuse_ino_t nodeid, const void *op_in, * se->conn.want_ext * Userspace might still use conn.want - we need to convert it */ - rc = convert_to_conn_want_ext(&se->conn, want_ext_default, + convert_to_conn_want_ext(&se->conn, want_ext_default, want_default); - if (rc != 0) { - fuse_reply_err(req, EPROTO); - se->error = -EPROTO; - fuse_session_exit(se); - return; - } } if (!want_flags_valid(se->conn.capable_ext, se->conn.want_ext)) { From baadab0492a495fda98216b351976d2e5d6d0866 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Sun, 18 May 2025 00:24:07 +0200 Subject: [PATCH 123/310] conn->want conversion: Fix fuse_apply_conn_info_opts() fuse_apply_conn_info_opts() was applying to 'want_ext', which would cause conflicts with 'want' if the application applied its own flags to 'conn->want'. Solution is: - to move fuse_{set,unset,get}_feature_flag and convert_to_conn_want_ext() to fuse_lowlevel.c and to define them as part of the public API, although convert_to_conn_want_ext() should not be used - it is currently needed to be a public function due as it needs to be defined for the tests. Related to https://github.com/libfuse/libfuse/issues/1171 and https://github.com/libfuse/libfuse/pull/1172. Closes: https://github.com/libfuse/libfuse/issues/1171 Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- include/fuse_common.h | 50 +++++++++++-------- lib/fuse.c | 9 +--- lib/fuse_i.h | 38 +++------------ lib/fuse_lowlevel.c | 78 ++++++++++++++++++++++++++--- lib/fuse_versionscript | 6 +++ lib/helper.c | 4 +- lib/util.c | 8 +++ lib/util.h | 3 ++ test/test_want_conversion.c | 97 ++++++++++++++++++++++--------------- 9 files changed, 187 insertions(+), 106 deletions(-) diff --git a/include/fuse_common.h b/include/fuse_common.h index 0b0ea4cfc..054c61874 100644 --- a/include/fuse_common.h +++ b/include/fuse_common.h @@ -1107,28 +1107,40 @@ void fuse_loop_cfg_convert(struct fuse_loop_config *config, struct fuse_loop_config_v1 *v1_conf); #endif +/** + * Set a feature flag in the want_ext field of fuse_conn_info. + * + * @param conn connection information + * @param flag feature flag to be set + * @return true if the flag was set, false if the flag is not supported + */ +bool fuse_set_feature_flag(struct fuse_conn_info *conn, uint64_t flag); -static inline bool fuse_set_feature_flag(struct fuse_conn_info *conn, - uint64_t flag) -{ - if (conn->capable_ext & flag) { - conn->want_ext |= flag; - return true; - } - return false; -} +/** + * Unset a feature flag in the want_ext field of fuse_conn_info. + * + * @param conn connection information + * @param flag feature flag to be unset + */ +void fuse_unset_feature_flag(struct fuse_conn_info *conn, uint64_t flag); + +/** + * Get the value of a feature flag in the want_ext field of fuse_conn_info. + * + * @param conn connection information + * @param flag feature flag to be checked + * @return true if the flag is set, false otherwise + */ +bool fuse_get_feature_flag(struct fuse_conn_info *conn, uint64_t flag); + +/* + * DO NOT USE: Not part of public API, for internal test use only. + * The function signature or any use of it is not guaranteeed to + * remain stable. And neither are results of what this function does. + */ +int fuse_convert_to_conn_want_ext(struct fuse_conn_info *conn); -static inline void fuse_unset_feature_flag(struct fuse_conn_info *conn, - uint64_t flag) -{ - conn->want_ext &= ~flag; -} -static inline bool fuse_get_feature_flag(struct fuse_conn_info *conn, - uint64_t flag) -{ - return conn->capable_ext & flag ? true : false; -} /* ----------------------------------------------------------- * * Compatibility stuff * diff --git a/lib/fuse.c b/lib/fuse.c index ccf4aeb61..b116661df 100644 --- a/lib/fuse.c +++ b/lib/fuse.c @@ -2560,15 +2560,8 @@ void fuse_fs_init(struct fuse_fs *fs, struct fuse_conn_info *conn, fuse_unset_feature_flag(conn, FUSE_CAP_POSIX_LOCKS); if (!fs->op.flock) fuse_unset_feature_flag(conn, FUSE_CAP_FLOCK_LOCKS); - if (fs->op.init) { - uint64_t want_ext_default = conn->want_ext; - uint32_t want_default = fuse_lower_32_bits(conn->want_ext); - - conn->want = want_default; + if (fs->op.init) fs->user_data = fs->op.init(conn, cfg); - - convert_to_conn_want_ext(conn, want_ext_default, want_default); - } } static int fuse_init_intr_signal(int signum, int *installed); diff --git a/lib/fuse_i.h b/lib/fuse_i.h index 14d99ad9f..80ec80315 100644 --- a/lib/fuse_i.h +++ b/lib/fuse_i.h @@ -106,6 +106,13 @@ struct fuse_session { /* 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 + */ + uint32_t conn_want; + uint64_t conn_want_ext; }; struct fuse_chan { @@ -250,36 +257,5 @@ int fuse_loop_cfg_verify(struct fuse_loop_config *config); /* room needed in buffer to accommodate header */ #define FUSE_BUFFER_HEADER_SIZE 0x1000 -/** - * Get the wanted capability flags, converting from old format if necessary - */ -static inline int convert_to_conn_want_ext(struct fuse_conn_info *conn, - uint64_t want_ext_default, - uint32_t want_default) -{ - /* - * Convert want to want_ext if necessary. - * For the high level interface this function might be called - * twice, once from the high level interface and once from the - * low level interface. Both, with different want_ext_default and - * want_default values. In order to suppress a failure for the - * second call, we check if the lower 32 bits of want_ext are - * already set to the value of want. - */ - if (conn->want != want_default && - fuse_lower_32_bits(conn->want_ext) != conn->want) { - if (conn->want_ext != want_ext_default) - return -EINVAL; - - /* high bits from want_ext, low bits from want */ - conn->want_ext = fuse_higher_32_bits(conn->want_ext) | - conn->want; - } - - /* ensure there won't be a second conversion */ - conn->want = fuse_lower_32_bits(conn->want_ext); - - return 0; -} #endif /* LIB_FUSE_I_H_*/ diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index 9fb793bd3..c2f3ccb38 100644 --- a/lib/fuse_lowlevel.c +++ b/lib/fuse_lowlevel.c @@ -2435,6 +2435,77 @@ static bool want_flags_valid(uint64_t capable, uint64_t want) return true; } +/** + * Get the wanted capability flags, converting from old format if necessary + */ +int fuse_convert_to_conn_want_ext(struct fuse_conn_info *conn) +{ + struct fuse_session *se = container_of(conn, struct fuse_session, conn); + + /* + * Convert want to want_ext if necessary. + * For the high level interface this function might be called + * twice, once from the high level interface and once from the + * low level interface. Both, with different want_ext_default and + * want_default values. In order to suppress a failure for the + * second call, we check if the lower 32 bits of want_ext are + * already set to the value of want. + */ + if (conn->want != se->conn_want && + 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); + return -EINVAL; + } + + /* high bits from want_ext, low bits from want */ + conn->want_ext = fuse_higher_32_bits(conn->want_ext) | + conn->want; + } + + /* ensure there won't be a second conversion */ + conn->want = fuse_lower_32_bits(conn->want_ext); + + return 0; +} + +bool fuse_set_feature_flag(struct fuse_conn_info *conn, + uint64_t flag) +{ + struct fuse_session *se = container_of(conn, struct fuse_session, conn); + + if (conn->capable_ext & flag) { + conn->want_ext |= flag; + se->conn_want_ext |= flag; + conn->want |= flag; + se->conn_want |= flag; + return true; + } + return false; +} + +void fuse_unset_feature_flag(struct fuse_conn_info *conn, + uint64_t flag) +{ + struct fuse_session *se = container_of(conn, struct fuse_session, conn); + + conn->want_ext &= ~flag; + se->conn_want_ext &= ~flag; + conn->want &= ~flag; + se->conn_want &= ~flag; +} + +bool fuse_get_feature_flag(struct fuse_conn_info *conn, + uint64_t flag) +{ + 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 @@ -2603,12 +2674,8 @@ _do_init(fuse_req_t req, const fuse_ino_t nodeid, const void *op_in, se->got_init = 1; if (se->op.init) { - uint64_t want_ext_default = se->conn.want_ext; - uint32_t want_default = fuse_lower_32_bits(se->conn.want_ext); - // Apply the first 32 bits of capable_ext to capable se->conn.capable = fuse_lower_32_bits(se->conn.capable_ext); - se->conn.want = want_default; se->op.init(se->userdata, &se->conn); @@ -2617,8 +2684,7 @@ _do_init(fuse_req_t req, const fuse_ino_t nodeid, const void *op_in, * se->conn.want_ext * Userspace might still use conn.want - we need to convert it */ - convert_to_conn_want_ext(&se->conn, want_ext_default, - want_default); + fuse_convert_to_conn_want_ext(&se->conn); } if (!want_flags_valid(se->conn.capable_ext, se->conn.want_ext)) { diff --git a/lib/fuse_versionscript b/lib/fuse_versionscript index 22c59e1af..ab57d7c08 100644 --- a/lib/fuse_versionscript +++ b/lib/fuse_versionscript @@ -205,6 +205,12 @@ FUSE_3.17 { FUSE_3.18 { global: fuse_req_is_uring; + fuse_set_feature_flag; + fuse_unset_feature_flag; + fuse_get_feature_flag; + + # Not part of public API, for internal test use only + fuse_convert_to_conn_want_ext; } FUSE_3.17; # Local Variables: diff --git a/lib/helper.c b/lib/helper.c index 5811c53d3..ced70a224 100644 --- a/lib/helper.c +++ b/lib/helper.c @@ -430,13 +430,13 @@ void fuse_apply_conn_info_opts(struct fuse_conn_info_opts *opts, #define LL_ENABLE(cond, cap) \ do { \ if (cond) \ - conn->want_ext |= (cap); \ + fuse_set_feature_flag(conn, cap); \ } while (0) #define LL_DISABLE(cond, cap) \ do { \ if (cond) \ - conn->want_ext &= ~(cap); \ + fuse_unset_feature_flag(conn, cap); \ } while (0) LL_ENABLE(opts->splice_read, FUSE_CAP_SPLICE_READ); diff --git a/lib/util.c b/lib/util.c index 00f1e1adc..7583d47f7 100644 --- a/lib/util.c +++ b/lib/util.c @@ -10,7 +10,14 @@ #include <stdlib.h> #include <errno.h> +#ifndef FUSE_USE_VERSION +#define FUSE_USE_VERSION (FUSE_MAKE_VERSION(3, 18)) +#endif + #include "util.h" +#include "fuse_log.h" +#include "fuse_lowlevel.h" +#include <stdio.h> int libfuse_strtol(const char *str, long *res) { @@ -44,3 +51,4 @@ void fuse_set_thread_name(unsigned long tid, const char *name) (void)name; #endif } + diff --git a/lib/util.h b/lib/util.h index 96b59d3af..d91a5e91f 100644 --- a/lib/util.h +++ b/lib/util.h @@ -2,12 +2,15 @@ #define FUSE_UTIL_H_ #include <stdint.h> +#include <stdbool.h> #define ROUND_UP(val, round_to) (((val) + (round_to - 1)) & ~(round_to - 1)) #define likely(x) __builtin_expect(!!(x), 1) #define unlikely(x) __builtin_expect(!!(x), 0) +struct fuse_conn_info; + int libfuse_strtol(const char *str, long *res); void fuse_set_thread_name(unsigned long tid, const char *name); diff --git a/test/test_want_conversion.c b/test/test_want_conversion.c index bee23cc6e..db731edbf 100644 --- a/test/test_want_conversion.c +++ b/test/test_want_conversion.c @@ -1,16 +1,22 @@ -#include "util.h" #define FUSE_USE_VERSION FUSE_MAKE_VERSION(3, 17) +#include "util.h" #include "fuse_i.h" +#include "fuse_lowlevel.h" #include <stdio.h> #include <assert.h> #include <inttypes.h> #include <stdbool.h> +#include <err.h> static void print_conn_info(const char *prefix, struct fuse_conn_info *conn) { - printf("%s: want=0x%" PRIx32 " want_ext=0x%" PRIx64 "\n", prefix, - conn->want, conn->want_ext); + struct fuse_session *se = container_of(conn, struct fuse_session, conn); + + printf("%s: want=0x%" PRIx32 " want_ext=0x%" PRIx64 + " want_default=0x%" PRIx32 " want_ext_default=0x%" PRIx64 "\n", + prefix, conn->want, conn->want_ext, se->conn_want, + se->conn_want_ext); } static void application_init_old_style(struct fuse_conn_info *conn) @@ -18,33 +24,31 @@ static void application_init_old_style(struct fuse_conn_info *conn) /* Simulate application init the old style */ conn->want |= FUSE_CAP_ASYNC_READ; conn->want &= ~FUSE_CAP_SPLICE_READ; + + /* + * Also use new style API, as that might happen through + * fuse_apply_conn_info_opts() + */ + fuse_set_feature_flag(conn, FUSE_CAP_IOCTL_DIR); } static void application_init_new_style(struct fuse_conn_info *conn) { /* Simulate application init the new style */ fuse_set_feature_flag(conn, FUSE_CAP_ASYNC_READ); + fuse_set_feature_flag(conn, FUSE_CAP_IOCTL_DIR); fuse_unset_feature_flag(conn, FUSE_CAP_SPLICE_READ); } static void test_fuse_fs_init(struct fuse_conn_info *conn, bool new_style) { - uint64_t want_ext_default = conn->want_ext; - uint32_t want_default = fuse_lower_32_bits(conn->want_ext); - int rc; - /* High-level init */ fuse_set_feature_flag(conn, FUSE_CAP_EXPORT_SUPPORT); - conn->want = want_default; - if (new_style) application_init_new_style(conn); else application_init_old_style(conn); - - rc = convert_to_conn_want_ext(conn, want_ext_default, want_default); - assert(rc == 0); } static void test_do_init(struct fuse_conn_info *conn, bool new_style) @@ -53,49 +57,71 @@ static void test_do_init(struct fuse_conn_info *conn, bool new_style) conn->capable_ext = FUSE_CAP_SPLICE_READ | FUSE_CAP_SPLICE_WRITE | FUSE_CAP_SPLICE_MOVE | FUSE_CAP_POSIX_LOCKS | FUSE_CAP_FLOCK_LOCKS | FUSE_CAP_EXPORT_SUPPORT | - FUSE_CAP_ASYNC_READ; + FUSE_CAP_ASYNC_READ | FUSE_CAP_IOCTL_DIR; conn->capable = fuse_lower_32_bits(conn->capable_ext); - conn->want_ext = conn->capable_ext; + + fuse_set_feature_flag(conn, FUSE_CAP_SPLICE_READ | + FUSE_CAP_SPLICE_WRITE | + FUSE_CAP_SPLICE_MOVE); print_conn_info("Initial state", conn); - uint64_t want_ext_default = conn->want_ext; - uint32_t want_default = fuse_lower_32_bits(conn->want_ext); int rc; - conn->want = want_default; - conn->capable = fuse_lower_32_bits(conn->capable_ext); - test_fuse_fs_init(conn, new_style); + print_conn_info("After init", conn); - rc = convert_to_conn_want_ext(conn, want_ext_default, want_default); + rc = fuse_convert_to_conn_want_ext(conn); assert(rc == 0); /* Verify all expected flags are set */ assert(!(conn->want_ext & FUSE_CAP_SPLICE_READ)); assert(conn->want_ext & FUSE_CAP_SPLICE_WRITE); assert(conn->want_ext & FUSE_CAP_SPLICE_MOVE); - assert(conn->want_ext & FUSE_CAP_POSIX_LOCKS); - assert(conn->want_ext & FUSE_CAP_FLOCK_LOCKS); assert(conn->want_ext & FUSE_CAP_EXPORT_SUPPORT); assert(conn->want_ext & FUSE_CAP_ASYNC_READ); + assert(conn->want_ext & FUSE_CAP_IOCTL_DIR); + /* Verify no other flags are set */ assert(conn->want_ext == (FUSE_CAP_SPLICE_WRITE | FUSE_CAP_SPLICE_MOVE | - FUSE_CAP_POSIX_LOCKS | FUSE_CAP_FLOCK_LOCKS | - FUSE_CAP_EXPORT_SUPPORT | FUSE_CAP_ASYNC_READ)); + FUSE_CAP_EXPORT_SUPPORT | FUSE_CAP_ASYNC_READ | + FUSE_CAP_IOCTL_DIR)); print_conn_info("After init", conn); } static void test_want_conversion_basic(void) { - struct fuse_conn_info conn = { 0 }; + const struct fuse_lowlevel_ops ops = { 0 }; + struct fuse_args args = FUSE_ARGS_INIT(0, NULL); + struct fuse_session *se; + struct fuse_conn_info *conn; + + /* Add the program name to arg[0] */ + if (fuse_opt_add_arg(&args, "test_signals")) { + fprintf(stderr, "Failed to add argument\n"); + errx(1, "Failed to add argument"); + } + + + se = fuse_session_new(&args, &ops, sizeof(ops), NULL); + assert(se); + conn = &se->conn; + printf("\nTesting basic want conversion, old style:\n"); + test_do_init(conn, false); + fuse_session_destroy(se); + + se = fuse_session_new(&args, &ops, sizeof(ops), NULL); + assert(se); + conn = &se->conn; + printf("\nTesting basic want conversion, new style:\n"); + test_do_init(conn, true); + print_conn_info("After init", conn); + fuse_session_destroy(se); + + fuse_opt_free_args(&args); - printf("\nTesting basic want conversion:\n"); - test_do_init(&conn, false); - test_do_init(&conn, true); - print_conn_info("After init", &conn); } static void test_want_conversion_conflict(void) @@ -115,16 +141,11 @@ static void test_want_conversion_conflict(void) conn.want = fuse_lower_32_bits(conn.want_ext); print_conn_info("Test conflict initial", &conn); - /* Initialize default values like in basic test */ - uint64_t want_ext_default_ll = conn.want_ext; - uint32_t want_default_ll = fuse_lower_32_bits(want_ext_default_ll); - /* Simulate application init modifying capabilities */ conn.want_ext |= FUSE_CAP_ATOMIC_O_TRUNC; /* Add new capability */ conn.want &= ~FUSE_CAP_SPLICE_READ; /* Remove a capability */ - rc = convert_to_conn_want_ext(&conn, want_ext_default_ll, - want_default_ll); + rc = fuse_convert_to_conn_want_ext(&conn); assert(rc == -EINVAL); print_conn_info("Test conflict after", &conn); @@ -143,11 +164,7 @@ static void test_want_conversion_high_bits(void) conn.want = fuse_lower_32_bits(conn.want_ext); print_conn_info("Test high bits initial", &conn); - uint64_t want_ext_default_ll = conn.want_ext; - uint32_t want_default_ll = fuse_lower_32_bits(want_ext_default_ll); - - rc = convert_to_conn_want_ext(&conn, want_ext_default_ll, - want_default_ll); + rc = fuse_convert_to_conn_want_ext(&conn); assert(rc == 0); assert(conn.want_ext == ((1ULL << 33) | FUSE_CAP_ASYNC_READ)); print_conn_info("Test high bits after", &conn); From cea9b33acc13e7683e3646fca04953d14e3211b7 Mon Sep 17 00:00:00 2001 From: Luis Henriques <luis@igalia.com> Date: Thu, 22 May 2025 15:55:27 +0100 Subject: [PATCH 124/310] Fix build in musl libc Function fuse_set_thread_name() assumes that pthread_t is an unsigned long and fails to compile in musl libc with the following: ../lib/util.c: In function 'fuse_set_thread_name': ../lib/util.c:48:28: error: passing argument 1 of 'pthread_setname_np' makes \ pointer from integer without a cast [-Wint-conversion] Fix fuse_set_thread_name() by dropping the 'tid' parameter, as it is always set to pthread_self(). Signed-off-by: Luis Henriques <luis@igalia.com> --- lib/fuse.c | 2 +- lib/fuse_loop_mt.c | 2 +- lib/fuse_uring.c | 2 +- lib/util.c | 5 ++--- lib/util.h | 2 +- 5 files changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/fuse.c b/lib/fuse.c index b116661df..1f01351c5 100644 --- a/lib/fuse.c +++ b/lib/fuse.c @@ -4840,7 +4840,7 @@ static void *fuse_prune_nodes(void *fuse) struct fuse *f = fuse; int sleep_time; - fuse_set_thread_name(pthread_self(), "fuse_prune_nodes"); + fuse_set_thread_name("fuse_prune_nodes"); while(1) { sleep_time = fuse_clean_cache(f); diff --git a/lib/fuse_loop_mt.c b/lib/fuse_loop_mt.c index 046256c20..e75580cf1 100644 --- a/lib/fuse_loop_mt.c +++ b/lib/fuse_loop_mt.c @@ -132,7 +132,7 @@ static void *fuse_do_work(void *data) struct fuse_mt *mt = w->mt; struct fuse_session *se = mt->se; - fuse_set_thread_name(pthread_self(), "fuse_worker"); + fuse_set_thread_name("fuse_worker"); while (!fuse_session_exited(se)) { int isforget = 0; diff --git a/lib/fuse_uring.c b/lib/fuse_uring.c index 62c5a4d78..fb5cd8f3f 100644 --- a/lib/fuse_uring.c +++ b/lib/fuse_uring.c @@ -679,7 +679,7 @@ static void *fuse_uring_thread(void *arg) snprintf(thread_name, 16, "fuse-ring-%d", queue->qid); thread_name[15] = '\0'; - fuse_set_thread_name(pthread_self(), thread_name); + fuse_set_thread_name(thread_name); fuse_uring_set_thread_core(queue->qid); diff --git a/lib/util.c b/lib/util.c index 7583d47f7..2914f1cfc 100644 --- a/lib/util.c +++ b/lib/util.c @@ -42,12 +42,11 @@ int libfuse_strtol(const char *str, long *res) return 0; } -void fuse_set_thread_name(unsigned long tid, const char *name) +void fuse_set_thread_name(const char *name) { #ifdef HAVE_PTHREAD_SETNAME_NP - pthread_setname_np(tid, name); + pthread_setname_np(pthread_self(), name); #else - (void)tid; (void)name; #endif } diff --git a/lib/util.h b/lib/util.h index d91a5e91f..aae40e21d 100644 --- a/lib/util.h +++ b/lib/util.h @@ -12,7 +12,7 @@ struct fuse_conn_info; int libfuse_strtol(const char *str, long *res); -void fuse_set_thread_name(unsigned long tid, const char *name); +void fuse_set_thread_name(const char *name); /** * Return the low bits of a number From fc71597c8dba7ca8da474cbae3509552e30cbd46 Mon Sep 17 00:00:00 2001 From: izxl007 <zeng.zheng@zte.com.cn> Date: Wed, 28 May 2025 21:54:15 +0800 Subject: [PATCH 125/310] fuse_lowlevel.c: Remove duplicate descriptions of auto_unmount Signed-off-by: izxl007 <zeng.zheng@zte.com.cn> --- lib/fuse_lowlevel.c | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index c2f3ccb38..556878630 100644 --- a/lib/fuse_lowlevel.c +++ b/lib/fuse_lowlevel.c @@ -3637,7 +3637,6 @@ void fuse_lowlevel_help(void) " -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" ); From b773020464641d3e9cec5ad5fa35e7153e54e118 Mon Sep 17 00:00:00 2001 From: izxl007 <zeng.zheng@zte.com.cn> Date: Tue, 3 Jun 2025 22:04:52 +0800 Subject: [PATCH 126/310] Update how to use the io_uring and io_uring_q_depth mount options MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This patch updates how to use the io_uring and io_uring_q_depth mount options. There are two reasons for this: 1. It is recommended for end-users to use a format like “-o io_uring" (with a space) in README.fuse-io-uring. This is because, in the passthrough_hp example, cxxopts does not support the "-oio_uring" format (without a space), and using it would result in a syntax error. 2. Remove redundant "--uring" and "--uring-q-depth=" from passthrough_hp example. Signed-off-by: izxl007 <zeng.zheng@zte.com.cn> --- doc/README.fuse-io-uring | 8 ++++---- example/passthrough_hp.cc | 23 ++--------------------- 2 files changed, 6 insertions(+), 25 deletions(-) diff --git a/doc/README.fuse-io-uring b/doc/README.fuse-io-uring index 7207c6e74..51348903d 100644 --- a/doc/README.fuse-io-uring +++ b/doc/README.fuse-io-uring @@ -6,7 +6,7 @@ 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 '-oio_uring'. +achieved with the libfuse option '-o io_uring'. Default queue-depth is 8 and can be changed with the parameter '-oio_uring_q_depth'. @@ -22,11 +22,11 @@ Benefits: 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: -oio_uring -3. Optionally adjust queue depth: -oio_uring_q_depth=<depth> +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 -oio_uring -oio_uring_q_depth=16 +./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 diff --git a/example/passthrough_hp.cc b/example/passthrough_hp.cc index b88c2c5dc..f1fc92795 100644 --- a/example/passthrough_hp.cc +++ b/example/passthrough_hp.cc @@ -77,9 +77,6 @@ using namespace std; #define SFS_DEFAULT_THREADS "-1" // take libfuse value as default #define SFS_DEFAULT_CLONE_FD "0" -#define SFS_DEFAULT_URING "1" -#define SFS_DEFAULT_URING_Q_DEPTH "0" -#define SFS_DEFAULT_URING_ARGLEN "0" /* We are re-using pointers to our `struct sfs_inode` and `struct sfs_dirp` elements as inodes and file handles. This means that we @@ -158,10 +155,6 @@ struct Fs { bool nocache; size_t num_threads; bool clone_fd; - struct { - bool enable; - int queue_depth; - } uring; std::string fuse_mount_options; bool direct_io; @@ -1410,10 +1403,8 @@ static cxxopts::ParseResult parse_options(int argc, char **argv) { ("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") - ("uring", "use uring communication") - ("uring-q-depth", "io-uring queue depth", - cxxopts::value<int>()->default_value(SFS_DEFAULT_URING_Q_DEPTH)); + ("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); @@ -1446,9 +1437,6 @@ static cxxopts::ParseResult parse_options(int argc, char **argv) { fs.clone_fd = options.count("clone-fd"); fs.direct_io = options.count("direct-io"); - fs.uring.enable = options.count("uring"); - fs.uring.queue_depth = options["uring-q-depth"].as<int>(); - char* resolved_path = realpath(argv[1], NULL); if (resolved_path == NULL) warn("WARNING: realpath() failed with"); @@ -1534,13 +1522,6 @@ int main(int argc, char *argv[]) { (fs.debug_fuse && fuse_opt_add_arg(&args, "-odebug"))) errx(3, "ERROR: Out of memory adding arguments"); - if (fs.uring.enable) { - if (fuse_opt_add_arg(&args, "-oio_uring") || - fuse_opt_add_arg(&args, ("-oio_uring_q_depth=" + - std::to_string(fs.uring.queue_depth)).c_str())) - errx(3, "ERROR: Out of memory adding io-uring arguments"); - } - ret = -1; fuse_lowlevel_ops sfs_oper {}; assign_operations(sfs_oper); From 22f6062f07fbffa9eeb6d491e6e5e3b19395fd46 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Jun 2025 22:51:35 +0000 Subject: [PATCH 127/310] build(deps): bump github/codeql-action from 3.28.18 to 3.28.19 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.28.18 to 3.28.19. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/ff0a06e83cb2de871e5a09832bc6a81e7276941f...fca7ace96b7d713c7035871441bd52efbe39e27e) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 3.28.19 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 9f2a97c6f..714c20ad8 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -53,7 +53,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 + uses: github/codeql-action/init@fca7ace96b7d713c7035871441bd52efbe39e27e # v3.28.19 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -78,7 +78,7 @@ jobs: meson compile -C build - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 + uses: github/codeql-action/analyze@fca7ace96b7d713c7035871441bd52efbe39e27e # v3.28.19 with: category: "/language:${{matrix.language}}" From 05cbdf637483157a8455f19ec37f62c22ad24e08 Mon Sep 17 00:00:00 2001 From: Gleb Popov <6yearold@gmail.com> Date: Thu, 5 Jun 2025 17:03:25 +0300 Subject: [PATCH 128/310] mount_bsd.c: Fix race between actual mounting and returning to the caller Signed-off-by: Gleb Popov <6yearold@gmail.com> --- lib/mount_bsd.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/mount_bsd.c b/lib/mount_bsd.c index bd95a76d3..28a9ae569 100644 --- a/lib/mount_bsd.c +++ b/lib/mount_bsd.c @@ -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,6 +214,7 @@ static int fuse_mount_core(const char *mountpoint, const char *opts) _exit(EXIT_FAILURE); } + waitpid(pid, &status, 0); _exit(EXIT_SUCCESS); } From 3d8aba629504d8a17bcd93fc70dc760791e7786a Mon Sep 17 00:00:00 2001 From: Georgi Valkov <gvalkov@gmail.com> Date: Thu, 12 Jun 2025 07:58:26 +0300 Subject: [PATCH 129/310] gitignore: add .DS_Store .DS_Store files are created all over the place on macOS Signed-off-by: Georgi Valkov <gvalkov@gmail.com> --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 97045f64b..30b16a236 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ \#*# *.orig *~ +.DS_Store Makefile.in Makefile *.m4 From 6787608e5655b3d833a541a9f0d5a3159ae51f0e Mon Sep 17 00:00:00 2001 From: Georgi Valkov <gvalkov@gmail.com> Date: Thu, 12 Jun 2025 07:36:14 +0300 Subject: [PATCH 130/310] fuse_signals.c: fix build warning when HAVE_BACKTRACE is undefined BT_STACK_SZ and backtrace_buffer are not used when HAVE_BACKTRACE is undefined. Wrap them in #ifdef to avoid a build warning: ../lib/fuse_signals.c:31:14: warning: 'backtrace_buffer' defined but not used [-Wunused-variable] 31 | static void *backtrace_buffer[BT_STACK_SZ]; | ^~~~~~~~~~~~~~~~ Signed-off-by: Georgi Valkov <gvalkov@gmail.com> --- lib/fuse_signals.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/fuse_signals.c b/lib/fuse_signals.c index 3848aec65..53129f0ca 100644 --- a/lib/fuse_signals.c +++ b/lib/fuse_signals.c @@ -32,8 +32,10 @@ static int ignore_sigs[] = { SIGPIPE}; static int fail_sigs[] = { SIGILL, SIGTRAP, SIGABRT, SIGBUS, SIGFPE, SIGSEGV }; static struct fuse_session *fuse_instance; +#ifdef HAVE_BACKTRACE #define BT_STACK_SZ (1024 * 1024) static void *backtrace_buffer[BT_STACK_SZ]; +#endif static void dump_stack(void) { From 324f1c7f4aa3f5ad0f88a56794fa3db9e55da430 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Wed, 4 Jun 2025 17:36:02 +0200 Subject: [PATCH 131/310] tests: Add an exception for output "error" The sigterm test triggers debug output like dev unique: 6, opcode: STATFS (17), nodeid: 1, insize: 40, pid: 1808 unique: 6, success, outsize: 96 dev unique: 8, opcode: GETATTR (3), nodeid: 1, insize: 56, pid: 1808 exit_handler called with sig 15 fuse: session exited, terminating workers unique: 8, error: -2 (No such file or directory), outsize: 16 Which then triggers test failures, because the test was acting on the word 'error'. Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- test/conftest.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/conftest.py b/test/conftest.py index f52818928..291c9199b 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -77,8 +77,11 @@ def _check(self): 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)) + # Skip FUSE error messages in the format "unique: X, error: -Y (...), outsize: Z" + # These are no errors, but just fuse debug messages with the return code + if re.search(r'unique: \d+, error: -\d+ \(.*\), outsize: \d+', hit.group(0)): + continue + raise AssertionError(f'Suspicious output to stderr (matched "{hit.group(0)}")') @pytest.fixture() def output_checker(request): From e64a6164ff71911e4c3bd492ceb0532705c1e733 Mon Sep 17 00:00:00 2001 From: izxl007 <zeng.zheng@zte.com.cn> Date: Mon, 9 Jun 2025 23:19:14 +0800 Subject: [PATCH 132/310] help: Remove duplicate io_uring options from fuse_cmdline_help The io_uring options are currently shown in both fuse_cmdline_help() and fuse_lowlevel_help(), which creates unnecessary duplication. Since io_uring is a low-level I/O feature, it makes more sense to only show these options in fuse_lowlevel_help(). This change: - Removes io_uring options from fuse_cmdline_help() - Keeps them in fuse_lowlevel_help() where they belong - Removes the FIXME comment that is no longer needed This is purely a documentation improvement and does not affect any functionality. Users will still see all available options when using --help, just organized in a more logical way. Signed-off-by: izxl007 <zeng.zheng@zte.com.cn> --- lib/helper.c | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/helper.c b/lib/helper.c index ced70a224..aceff9fd5 100644 --- a/lib/helper.c +++ b/lib/helper.c @@ -139,11 +139,7 @@ void fuse_cmdline_help(void) " -o max_idle_threads the maximum number of idle worker threads\n" " allowed (default: -1)\n" " -o max_threads the maximum number of worker threads\n" - " allowed (default: 10)\n" - /* fuse_ll_opts in fuse_lowlevel.c, FIXME, call into that file */ - " -o io_uring enable io-uring\n" - " -o io_uring_q_depth=<n> io-uring queue depth\n" -); + " allowed (default: 10)\n"); } static int fuse_helper_opt_proc(void *data, const char *arg, int key, From 5724a99068d0a3ebc32c123532942bc8efe11cf1 Mon Sep 17 00:00:00 2001 From: Georgi Valkov <gvalkov@gmail.com> Date: Fri, 13 Jun 2025 10:05:29 +0300 Subject: [PATCH 133/310] gitignore: add *.patch When working with patches, git shows them as uncommitted changes. Ignore *.patch to keep the list of changes tidy. Signed-off-by: Georgi Valkov <gvalkov@gmail.com> --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 30b16a236..0412bc73f 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ *.gz \#*# *.orig +*.patch *~ .DS_Store Makefile.in From 77e7df4516a0f0484cd6b013ecfd238534818db3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Jun 2025 23:55:51 +0000 Subject: [PATCH 134/310] build(deps): bump github/codeql-action from 3.28.19 to 3.29.0 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.28.19 to 3.29.0. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/fca7ace96b7d713c7035871441bd52efbe39e27e...ce28f5bb42b7a9f2c824e633a3f6ee835bab6858) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 3.29.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 714c20ad8..3c37133b0 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -53,7 +53,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@fca7ace96b7d713c7035871441bd52efbe39e27e # v3.28.19 + uses: github/codeql-action/init@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -78,7 +78,7 @@ jobs: meson compile -C build - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@fca7ace96b7d713c7035871441bd52efbe39e27e # v3.28.19 + uses: github/codeql-action/analyze@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0 with: category: "/language:${{matrix.language}}" From 00dd846588e3d6a235e3f430f4063b43686adbc3 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Tue, 17 Jun 2025 17:04:02 +0200 Subject: [PATCH 135/310] Reformat passthrough_hp with clang-format Not ideal for git history, but the different formatting within the project is really disturbing. Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- example/passthrough_hp.cc | 2560 +++++++++++++++++++------------------ 1 file changed, 1300 insertions(+), 1260 deletions(-) diff --git a/example/passthrough_hp.cc b/example/passthrough_hp.cc index f1fc92795..dd3a45ae9 100644 --- a/example/passthrough_hp.cc +++ b/example/passthrough_hp.cc @@ -82,15 +82,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,1476 +102,1517 @@ 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 }; + 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); + } }; 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 *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; }; 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 libfuse3 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 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 { + 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 { #ifdef HAVE_UTIMENSAT - char procname[64]; - sprintf(procname, "/proc/self/fd/%i", ifd); - res = utimensat(AT_FDCWD, procname, tv, 0); + char procname[64]; + sprintf(procname, "/proc/self/fd/%i", ifd); + res = 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 + 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 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); + { + 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; } - -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); + 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_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); + 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 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); + 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 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; + + 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); + 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; 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); + 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; 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; + 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; } - 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); } 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); +} 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); + 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); } - -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); + 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_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; -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); + 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; + if (mode) { + fuse_reply_err(req, EOPNOTSUPP); + return; + } + + auto err = posix_fallocate(fi->fh, 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; +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; + sfs_oper.fallocate = sfs_fallocate; #endif - sfs_oper.flock = sfs_flock; + 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); + 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 adding arguments"); + 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; } - From 410890482c8b134525bb9f006089848399eea62a Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Mon, 16 Jun 2025 12:17:26 +0200 Subject: [PATCH 136/310] example/passthrough_hp: Show fuse lowlevel help options Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- example/passthrough_hp.cc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/example/passthrough_hp.cc b/example/passthrough_hp.cc index dd3a45ae9..4f34e2be7 100644 --- a/example/passthrough_hp.cc +++ b/example/passthrough_hp.cc @@ -1455,6 +1455,8 @@ static cxxopts::ParseResult parse_options(int argc, char **argv) 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) { From f4c5d20772145c02f8e78176ae9d6cb6f60c6cfa Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Mon, 16 Jun 2025 15:28:26 +0200 Subject: [PATCH 137/310] Fix io-uring teardown We need to write an uint64_t to eventfd. Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- lib/fuse_uring.c | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/fuse_uring.c b/lib/fuse_uring.c index fb5cd8f3f..33f8f5652 100644 --- a/lib/fuse_uring.c +++ b/lib/fuse_uring.c @@ -345,9 +345,15 @@ static void fuse_session_destruct_uring(struct fuse_ring_pool *fuse_ring) fuse_uring_get_queue(fuse_ring, qid); if (queue->tid != 0) { - int value = 1; - - write(queue->eventfd, &value, sizeof(value)); + 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; } @@ -416,7 +422,7 @@ static int fuse_uring_prepare_fetch_sqes(struct fuse_ring_queue *queue) return -EINVAL; } - // Add the poll SQE for the eventfd to wake up on teardown + /* 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"); From 3793b1748ad151c8043dee1db198fffa3dbb5a67 Mon Sep 17 00:00:00 2001 From: Georgi Valkov <gvalkov@gmail.com> Date: Sun, 15 Jun 2025 15:34:57 +0300 Subject: [PATCH 138/310] mount_util.c: check if utab exists before update Do not attempt to update /run/mount/utab if it doesn't exist. Note: if this path ever changes, utab updates will break. Fixes the following error when mounting iPhone using ifuse: ifuse /mnt --container com.httpstorm.httpstorm mount: mounting ifuse on /mnt failed: Invalid argument On OpenWRT by default mount-utils is not installed and utab does not exist. /bin/mount is a symlink to /bin/busybox and does not support updating of utab. If mount-utils is installed: /run/mount/ exists, but does not contain utab. The mount-utils instance is under /usr/bin/mount, so a hard-coded call to /bin/mount will still fail. Using /usr/bin/mount succeeds but does not create utab. [1] https://github.com/libfuse/libfuse/pull/1247 Signed-off-by: Georgi Valkov <gvalkov@gmail.com> --- lib/mount_util.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/mount_util.c b/lib/mount_util.c index 089ca4548..f19dfb4c8 100644 --- a/lib/mount_util.c +++ b/lib/mount_util.c @@ -75,6 +75,10 @@ static int mtab_needs_update(const char *mnt) if (err == EROFS) return 0; + + res = access("/run/mount/utab", F_OK); + if (res == -1) + return 0; } return 1; From 5ceb0b05cb48e6cfa2c81160df9f1212623e86eb Mon Sep 17 00:00:00 2001 From: Luis Henriques <luis@igalia.com> Date: Thu, 22 May 2025 15:15:15 +0100 Subject: [PATCH 139/310] fuse: add support to FUSE_NOTIFY_INC_EPOCH This patch adds support for the FUSE INC_EPOCH notify. This new operation simply increments the FUSE connection epoch value, allowing to invalidate all the dentries next time they are revalidated. Signed-off-by: Luis Henriques <luis@igalia.com> --- include/fuse_kernel.h | 6 +++++- include/fuse_lowlevel.h | 14 ++++++++++++++ lib/fuse_lowlevel.c | 13 +++++++++++++ lib/fuse_versionscript | 1 + 4 files changed, 33 insertions(+), 1 deletion(-) diff --git a/include/fuse_kernel.h b/include/fuse_kernel.h index 42db04c0c..122d6586e 100644 --- a/include/fuse_kernel.h +++ b/include/fuse_kernel.h @@ -232,6 +232,9 @@ * * 7.43 * - add FUSE_REQUEST_TIMEOUT + * + * 7.44 + * - add FUSE_NOTIFY_INC_EPOCH */ #ifndef _LINUX_FUSE_H @@ -267,7 +270,7 @@ #define FUSE_KERNEL_VERSION 7 /** Minor version number of this interface */ -#define FUSE_KERNEL_MINOR_VERSION 42 +#define FUSE_KERNEL_MINOR_VERSION 44 /** The node ID of the root inode */ #define FUSE_ROOT_ID 1 @@ -671,6 +674,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, }; diff --git a/include/fuse_lowlevel.h b/include/fuse_lowlevel.h index 138a78436..82f4a8de4 100644 --- a/include/fuse_lowlevel.h +++ b/include/fuse_lowlevel.h @@ -1744,6 +1744,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 * diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index 556878630..13c05c623 100644 --- a/lib/fuse_lowlevel.c +++ b/lib/fuse_lowlevel.c @@ -2973,6 +2973,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 * diff --git a/lib/fuse_versionscript b/lib/fuse_versionscript index ab57d7c08..2d8884d7e 100644 --- a/lib/fuse_versionscript +++ b/lib/fuse_versionscript @@ -208,6 +208,7 @@ FUSE_3.18 { fuse_set_feature_flag; fuse_unset_feature_flag; fuse_get_feature_flag; + fuse_lowlevel_notify_increment_epoch; # Not part of public API, for internal test use only fuse_convert_to_conn_want_ext; From 1caf8162d5b08bd32cc311c001c277067bd572c0 Mon Sep 17 00:00:00 2001 From: Luis Henriques <luis@igalia.com> Date: Wed, 18 Jun 2025 10:42:32 +0100 Subject: [PATCH 140/310] tests: example: add new test for increment epoch This patch modifies the notify_inval_entry.c example so that it includes an extra option to use fuse_lowlevel_notify_increment_epoch(). The test test_notify_inval_entry() was also modified to test this extra option. Signed-off-by: Luis Henriques <luis@igalia.com> --- example/notify_inval_entry.c | 47 ++++++++++++++++++++++++++++-------- test/test_examples.py | 7 +++++- 2 files changed, 43 insertions(+), 11 deletions(-) diff --git a/example/notify_inval_entry.c b/example/notify_inval_entry.c index 1060d505e..728a9dccf 100644 --- a/example/notify_inval_entry.c +++ b/example/notify_inval_entry.c @@ -67,6 +67,12 @@ * 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 fuse3 --cflags --libs` -o notify_inval_entry @@ -103,12 +109,14 @@ struct options { 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, + .inc_epoch = 0, }; #define OPTION(t, p) \ @@ -118,6 +126,7 @@ static const struct fuse_opt option_spec[] = { OPTION("--update-interval=%d", update_interval), OPTION("--timeout=%f", timeout), OPTION("--only-expire", only_expire), + OPTION("--inc-epoch", inc_epoch), FUSE_OPT_END }; @@ -263,7 +272,7 @@ static void update_fs(void) { 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); @@ -271,24 +280,27 @@ static void* update_fs_loop(void *data) { 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)); + 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 if (options.inc_epoch) { // increment epoch + ret = fuse_lowlevel_notify_increment_epoch(se); + + if (ret == -ENOSYS) { + printf("fuse_lowlevel_notify_increment_epoch not supported by kernel\n"); + break; + } + assert(ret == 0); } else { // invalidate entry assert(fuse_lowlevel_notify_inval_entry (se, FUSE_ROOT_ID, old_name, strlen(old_name)) == 0); @@ -297,6 +309,15 @@ static void* update_fs_loop(void *data) { 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; } @@ -307,7 +328,8 @@ static void show_help(const char *progname) " --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" + " --only-expire Expire entries instead of invalidating them\n" + " --inc-epoch Increment epoch, invalidating all dentries\n" "\n"); } @@ -336,6 +358,11 @@ int main(int argc, char *argv[]) { 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(); diff --git a/test/test_examples.py b/test/test_examples.py index 2e23697fd..2658081fc 100755 --- a/test/test_examples.py +++ b/test/test_examples.py @@ -376,7 +376,8 @@ def test_fn(name): @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("only_expire", ("invalidate_entries", + "expire_entries", "inc_epoch")) @pytest.mark.parametrize("notify", (True, False)) def test_notify_inval_entry(tmpdir, only_expire, notify, output_checker): mnt_dir = str(tmpdir) @@ -390,6 +391,10 @@ def test_notify_inval_entry(tmpdir, only_expire, notify, output_checker): cmdline.append('--only-expire') if "FUSE_CAP_EXPIRE_ONLY" not in fuse_caps: pytest.skip('only-expire not supported by running kernel') + elif only_expire == "inc_epoch": + cmdline.append('--inc-epoch') + if fuse_proto < (7,44): + pytest.skip('inc-epoch not supported by running kernel') mount_process = subprocess.Popen(cmdline, stdout=output_checker.fd, stderr=output_checker.fd) try: From 43e36ffed84f73528b67ca1835760bf5895c9d60 Mon Sep 17 00:00:00 2001 From: izxl007 <zeng.zheng@zte.com.cn> Date: Thu, 19 Jun 2025 01:04:33 +0800 Subject: [PATCH 141/310] example/ioctl_client: Delete the redundant comment Signed-off-by: izxl007 <zeng.zheng@zte.com.cn> --- example/ioctl_client.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/example/ioctl_client.c b/example/ioctl_client.c index 019d03031..2d7ac0e1a 100644 --- a/example/ioctl_client.c +++ b/example/ioctl_client.c @@ -3,8 +3,6 @@ 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. */ From abd0a98485756caad5e7297669f8788c167b6b2b Mon Sep 17 00:00:00 2001 From: izxl007 <zeng.zheng@zte.com.cn> Date: Sun, 22 Jun 2025 20:00:39 +0800 Subject: [PATCH 142/310] Clarify offset field is signed despite uint64_t type Add comment noting that the uint64_t offset in fuse_lseek_in is derived from kernel loff_t and should be treated as signed for negative offsets. Signed-off-by: izxl007 <zeng.zheng@zte.com.cn> --- lib/fuse_lowlevel.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index 13c05c623..8ddc288d3 100644 --- a/lib/fuse_lowlevel.c +++ b/lib/fuse_lowlevel.c @@ -2402,6 +2402,10 @@ static void do_copy_file_range(fuse_req_t req, const fuse_ino_t nodeid_in, _do_copy_file_range(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) { From 1016d1665ab0d8914171943fadfe85ed2f8e12ba Mon Sep 17 00:00:00 2001 From: Gleb Popov <6yearold@gmail.com> Date: Tue, 24 Jun 2025 14:46:32 +0300 Subject: [PATCH 143/310] Catch up with fuse_send_data_iov() signature changes in the fallback case Signed-off-by: Gleb Popov <6yearold@gmail.com> --- lib/fuse_lowlevel.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index 8ddc288d3..722c11d19 100644 --- a/lib/fuse_lowlevel.c +++ b/lib/fuse_lowlevel.c @@ -979,12 +979,13 @@ static int fuse_send_data_iov(struct fuse_session *se, struct fuse_chan *ch, #else static int fuse_send_data_iov(struct fuse_session *se, struct fuse_chan *ch, struct iovec *iov, int iov_count, - struct fuse_bufvec *req_data, unsigned int flags) + struct fuse_bufvec *req_data, unsigned int flags, + fuse_req_t req) { size_t len = fuse_buf_size(req_data); (void) flags; - return fuse_send_data_iov_fallback(se, ch, iov, iov_count, req_data, len); + return fuse_send_data_iov_fallback(se, ch, iov, iov_count, req_data, len, req); } #endif From 596732dd06f7737e4a4c050773f88c8d36fd9b61 Mon Sep 17 00:00:00 2001 From: Gleb Popov <6yearold@gmail.com> Date: Tue, 24 Jun 2025 09:12:54 +0300 Subject: [PATCH 144/310] meson: Check for some Linux-specific headers Signed-off-by: Gleb Popov <6yearold@gmail.com> --- meson.build | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/meson.build b/meson.build index b6631098d..910a07565 100644 --- a/meson.build +++ b/meson.build @@ -112,6 +112,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>')) From e9a140b8172c312981b2bb834011ef782a386504 Mon Sep 17 00:00:00 2001 From: Gleb Popov <6yearold@gmail.com> Date: Tue, 24 Jun 2025 09:17:42 +0300 Subject: [PATCH 145/310] examples: Guard inclusions of Linux headers with appropriate ifdefs Signed-off-by: Gleb Popov <6yearold@gmail.com> --- example/memfs_ll.cc | 4 +++- example/passthrough_hp.cc | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/example/memfs_ll.cc b/example/memfs_ll.cc index 603885017..68f8e9629 100644 --- a/example/memfs_ll.cc +++ b/example/memfs_ll.cc @@ -6,7 +6,6 @@ See the file COPYING. */ -#include <linux/limits.h> #define FUSE_USE_VERSION 317 #include <algorithm> @@ -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 diff --git a/example/passthrough_hp.cc b/example/passthrough_hp.cc index 4f34e2be7..65377b95e 100644 --- a/example/passthrough_hp.cc +++ b/example/passthrough_hp.cc @@ -59,7 +59,9 @@ #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> From 9168e3be310b9805c269bf85580e46bf30cd63b5 Mon Sep 17 00:00:00 2001 From: Gleb Popov <6yearold@gmail.com> Date: Tue, 24 Jun 2025 09:20:09 +0300 Subject: [PATCH 146/310] memfs_ll: Fix compiling on non-Linux OSes Signed-off-by: Gleb Popov <6yearold@gmail.com> --- example/memfs_ll.cc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/example/memfs_ll.cc b/example/memfs_ll.cc index 68f8e9629..ad0b1871c 100644 --- a/example/memfs_ll.cc +++ b/example/memfs_ll.cc @@ -905,10 +905,12 @@ static void memfs_rename(fuse_req_t req, fuse_ino_t parent, const char *name, Dentry *child_dentry_copy = nullptr; Dentry *existing_dentry = nullptr; +#if defined(RENAME_EXCHANGE) && defined(RENAME_NOREPLACE) if (flags & (RENAME_EXCHANGE | RENAME_NOREPLACE)) { fuse_reply_err(req, EINVAL); return; } +#endif Inodes.lock(); From f8fe398ee14864e2c3c7c524ca851d7cc08bed28 Mon Sep 17 00:00:00 2001 From: Gleb Popov <6yearold@gmail.com> Date: Tue, 24 Jun 2025 14:36:40 +0300 Subject: [PATCH 147/310] passthrough_hp: Abstract out the fd -> path transition Signed-off-by: Gleb Popov <6yearold@gmail.com> --- example/passthrough_hp.cc | 43 ++++++++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/example/passthrough_hp.cc b/example/passthrough_hp.cc index 65377b95e..040bc7548 100644 --- a/example/passthrough_hp.cc +++ b/example/passthrough_hp.cc @@ -66,6 +66,10 @@ #include <unistd.h> #include <pthread.h> #include <limits.h> +#ifdef __FreeBSD__ +#include <fcntl.h> +#include <sys/user.h> +#endif // C++ includes #include <cstddef> @@ -248,6 +252,21 @@ static void sfs_getattr(fuse_req_t req, fuse_ino_t ino, fuse_file_info *fi) 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) { @@ -259,9 +278,9 @@ static void do_setattr(fuse_req_t req, fuse_ino_t ino, struct stat *attr, 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); + res = with_fd_path(ifd, [attr](const char* procname) { + return chmod(procname, attr->st_mode); + }); } if (res == -1) goto out_err; @@ -283,9 +302,9 @@ static void do_setattr(fuse_req_t req, fuse_ino_t ino, struct stat *attr, 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); + res = with_fd_path(ifd, [attr](const char* procname) { + return truncate(procname, attr->st_size); + }); } if (res == -1) goto out_err; @@ -312,9 +331,9 @@ static void do_setattr(fuse_req_t req, fuse_ino_t ino, struct stat *attr, 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; @@ -1067,9 +1086,9 @@ static void sfs_open(fuse_req_t req, fuse_ino_t ino, fuse_file_info *fi) /* 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); + 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) From 8e649e8f5dbb1d619619643296b75db8ee63aa70 Mon Sep 17 00:00:00 2001 From: Gleb Popov <6yearold@gmail.com> Date: Tue, 24 Jun 2025 14:37:07 +0300 Subject: [PATCH 148/310] passthrough_hp: Fix compilation on systems without O_TMPFILE support Signed-off-by: Gleb Popov <6yearold@gmail.com> --- example/passthrough_hp.cc | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/example/passthrough_hp.cc b/example/passthrough_hp.cc index 040bc7548..e5edc1b86 100644 --- a/example/passthrough_hp.cc +++ b/example/passthrough_hp.cc @@ -1012,6 +1012,7 @@ static Inode *create_new_inode(int fd, fuse_entry_param *e) return p_inode; } +#ifdef O_TMPFILE static void sfs_tmpfile(fuse_req_t req, fuse_ino_t parent, mode_t mode, fuse_file_info *fi) { @@ -1049,7 +1050,7 @@ static void sfs_tmpfile(fuse_req_t req, fuse_ino_t parent, mode_t mode, 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) { @@ -1378,7 +1379,9 @@ static void assign_operations(fuse_lowlevel_ops &sfs_oper) 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.open = sfs_open; sfs_oper.release = sfs_release; sfs_oper.flush = sfs_flush; From f0ec646e87d726b6c10c951e8bf5b7f5e16cc38f Mon Sep 17 00:00:00 2001 From: Gleb Popov <6yearold@gmail.com> Date: Tue, 24 Jun 2025 14:37:34 +0300 Subject: [PATCH 149/310] meson: Enable building C++ examples on FreeBSD Signed-off-by: Gleb Popov <6yearold@gmail.com> --- example/meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/meson.build b/example/meson.build index b2e896cc4..701afa768 100644 --- a/example/meson.build +++ b/example/meson.build @@ -31,7 +31,7 @@ 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) From 57227e4fb6c1d3ba67d0df60d03ba6f485562978 Mon Sep 17 00:00:00 2001 From: Vassili Tchersky <vt+git@vbc.su> Date: Thu, 20 Feb 2025 20:44:47 +0100 Subject: [PATCH 150/310] FreeBSD: test runner Draft version of a yet simple runner that just build libfuse under FreeBSD and show the compile log. --- .github/workflows/bsd.yaml | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 .github/workflows/bsd.yaml diff --git a/.github/workflows/bsd.yaml b/.github/workflows/bsd.yaml new file mode 100644 index 000000000..e07d543dd --- /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@v4 + - 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 From 1bb2f2a08e6808a5d48ac57a6ebb422620d587c4 Mon Sep 17 00:00:00 2001 From: Gleb Popov <6yearold@gmail.com> Date: Wed, 25 Jun 2025 14:37:01 +0300 Subject: [PATCH 151/310] memfs_ll: Fix unused variable warning Signed-off-by: Gleb Popov <6yearold@gmail.com> --- example/memfs_ll.cc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/example/memfs_ll.cc b/example/memfs_ll.cc index ad0b1871c..a1bd0543f 100644 --- a/example/memfs_ll.cc +++ b/example/memfs_ll.cc @@ -910,6 +910,8 @@ static void memfs_rename(fuse_req_t req, fuse_ino_t parent, const char *name, fuse_reply_err(req, EINVAL); return; } +#else + (void)flags; #endif Inodes.lock(); From 4de3db413cb419829203d614fda1cea8fe7e0c57 Mon Sep 17 00:00:00 2001 From: Gleb Popov <6yearold@gmail.com> Date: Wed, 25 Jun 2025 14:39:02 +0300 Subject: [PATCH 152/310] passthrough_hp: Fix unused function warning Signed-off-by: Gleb Popov <6yearold@gmail.com> --- example/passthrough_hp.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/passthrough_hp.cc b/example/passthrough_hp.cc index e5edc1b86..ecd862521 100644 --- a/example/passthrough_hp.cc +++ b/example/passthrough_hp.cc @@ -967,6 +967,7 @@ static void sfs_create(fuse_req_t req, fuse_ino_t parent, const char *name, 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)); @@ -1012,7 +1013,6 @@ static Inode *create_new_inode(int fd, fuse_entry_param *e) return p_inode; } -#ifdef O_TMPFILE static void sfs_tmpfile(fuse_req_t req, fuse_ino_t parent, mode_t mode, fuse_file_info *fi) { From fc1c8da0cf8a18d222cb1feed0057ba44ea4d18f Mon Sep 17 00:00:00 2001 From: izxl007 <zeng.zheng@zte.com.cn> Date: Thu, 19 Jun 2025 22:37:48 +0800 Subject: [PATCH 153/310] license: s/COPYING/GPL2.txt, s/COPYING.LIB/LGPL2.txt Signed-off-by: izxl007 <zeng.zheng@zte.com.cn> --- example/cuse.c | 2 +- example/cuse_client.c | 2 +- example/hello.c | 2 +- example/hello_ll.c | 2 +- example/hello_ll_uds.c | 2 +- example/invalidate_path.c | 2 +- example/ioctl.c | 2 +- example/ioctl.h | 2 +- example/ioctl_client.c | 2 +- example/memfs_ll.cc | 2 +- example/notify_inval_entry.c | 2 +- example/notify_inval_inode.c | 2 +- example/notify_store_retrieve.c | 2 +- example/null.c | 2 +- example/passthrough.c | 2 +- example/passthrough_fh.c | 2 +- example/passthrough_hp.cc | 2 +- example/passthrough_ll.c | 2 +- example/poll.c | 2 +- example/poll_client.c | 2 +- example/printcap.c | 2 +- include/cuse_lowlevel.h | 2 +- include/fuse.h | 2 +- include/fuse_common.h | 2 +- include/fuse_log.h | 2 +- include/fuse_lowlevel.h | 2 +- include/fuse_opt.h | 2 +- lib/buffer.c | 2 +- lib/compat.c | 2 +- lib/cuse_lowlevel.c | 2 +- lib/fuse.c | 2 +- lib/fuse_i.h | 2 +- lib/fuse_log.c | 2 +- lib/fuse_loop.c | 2 +- lib/fuse_loop_mt.c | 2 +- lib/fuse_lowlevel.c | 2 +- lib/fuse_misc.h | 2 +- lib/fuse_opt.c | 2 +- lib/fuse_signals.c | 2 +- lib/fuse_uring.c | 2 +- lib/fuse_uring_i.h | 2 +- lib/helper.c | 2 +- lib/modules/iconv.c | 2 +- lib/modules/subdir.c | 2 +- lib/mount.c | 2 +- lib/mount_bsd.c | 2 +- lib/mount_util.c | 2 +- lib/mount_util.h | 2 +- test/hello.c | 2 +- test/release_unlink_race.c | 2 +- test/test_setattr.c | 2 +- test/test_signals.c | 2 +- test/test_write_cache.c | 2 +- util/fusermount.c | 2 +- util/mount.fuse.c | 2 +- 55 files changed, 55 insertions(+), 55 deletions(-) diff --git a/example/cuse.c b/example/cuse.c index 6b333029f..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. */ diff --git a/example/cuse_client.c b/example/cuse_client.c index 903ffc63f..de8244473 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 diff --git a/example/hello.c b/example/hello.c index d9f01b9f0..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 diff --git a/example/hello_ll.c b/example/hello_ll.c index b1ef69275..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 diff --git a/example/hello_ll_uds.c b/example/hello_ll_uds.c index e9cd17315..b0a6e9cd4 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 diff --git a/example/invalidate_path.c b/example/invalidate_path.c index 0e8d77f86..fad43ea77 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 diff --git a/example/ioctl.c b/example/ioctl.c index 9fe5c5bc5..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 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 2d7ac0e1a..99e27505e 100644 --- a/example/ioctl_client.c +++ b/example/ioctl_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 diff --git a/example/memfs_ll.cc b/example/memfs_ll.cc index a1bd0543f..b4f0b63cb 100644 --- a/example/memfs_ll.cc +++ b/example/memfs_ll.cc @@ -3,7 +3,7 @@ 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. */ #define FUSE_USE_VERSION 317 diff --git a/example/notify_inval_entry.c b/example/notify_inval_entry.c index 728a9dccf..5ab3c32d1 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 diff --git a/example/notify_inval_inode.c b/example/notify_inval_inode.c index 99631cc3d..1dcecee14 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 diff --git a/example/notify_store_retrieve.c b/example/notify_store_retrieve.c index 42e6eb05e..23fdecc28 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 diff --git a/example/null.c b/example/null.c index 2dd5e8bdd..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 diff --git a/example/passthrough.c b/example/passthrough.c index cc93f53ee..a5ac4b356 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 diff --git a/example/passthrough_fh.c b/example/passthrough_fh.c index 3602c969f..7bbdb700d 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 diff --git a/example/passthrough_hp.cc b/example/passthrough_hp.cc index ecd862521..cc6b7e8d3 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 diff --git a/example/passthrough_ll.c b/example/passthrough_ll.c index fa8abb897..59f43c53c 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 diff --git a/example/poll.c b/example/poll.c index ffcb4f1fa..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 diff --git a/example/poll_client.c b/example/poll_client.c index 83c58239c..96f14190f 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 diff --git a/example/printcap.c b/example/printcap.c index 82a759884..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 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..b99004334 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_ diff --git a/include/fuse_common.h b/include/fuse_common.h index 054c61874..b82f2c41d 100644 --- a/include/fuse_common.h +++ b/include/fuse_common.h @@ -2,7 +2,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. */ /** @file */ diff --git a/include/fuse_log.h b/include/fuse_log.h index c8559572a..b901db252 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_ diff --git a/include/fuse_lowlevel.h b/include/fuse_lowlevel.h index 82f4a8de4..75e084d09 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_ 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 a25711bfc..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" diff --git a/lib/fuse.c b/lib/fuse.c index 1f01351c5..68b61ce69 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 diff --git a/lib/fuse_i.h b/lib/fuse_i.h index 80ec80315..2221cf2af 100644 --- a/lib/fuse_i.h +++ b/lib/fuse_i.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 LIB_FUSE_I_H_ diff --git a/lib/fuse_log.c b/lib/fuse_log.c index 5039b0454..66dcf8a06 100644 --- a/lib/fuse_log.c +++ b/lib/fuse_log.c @@ -5,7 +5,7 @@ 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" diff --git a/lib/fuse_loop.c b/lib/fuse_loop.c index a27111ca8..1ff075cbf 100644 --- a/lib/fuse_loop.c +++ b/lib/fuse_loop.c @@ -5,7 +5,7 @@ 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" diff --git a/lib/fuse_loop_mt.c b/lib/fuse_loop_mt.c index e75580cf1..8ee7d49ae 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 diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index 722c11d19..91f42440f 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 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 53129f0ca..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" diff --git a/lib/fuse_uring.c b/lib/fuse_uring.c index 33f8f5652..a54973eed 100644 --- a/lib/fuse_uring.c +++ b/lib/fuse_uring.c @@ -5,7 +5,7 @@ * Implementation of (most of) FUSE-over-io-uring. * * 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/fuse_uring_i.h b/lib/fuse_uring_i.h index a65241798..fc236917b 100644 --- a/lib/fuse_uring_i.h +++ b/lib/fuse_uring_i.h @@ -2,7 +2,7 @@ * 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 COPYING.LIB + * See the file LGPL2.txt */ #ifndef FUSE_URING_I_H_ diff --git a/lib/helper.c b/lib/helper.c index aceff9fd5..5c13b93a4 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" diff --git a/lib/modules/iconv.c b/lib/modules/iconv.c index a0bf72be5..599b8df4d 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> diff --git a/lib/modules/subdir.c b/lib/modules/subdir.c index e92eb6284..dd2d49d80 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> diff --git a/lib/mount.c b/lib/mount.c index 34e5dd454..2eb967399 100644 --- a/lib/mount.c +++ b/lib/mount.c @@ -5,7 +5,7 @@ 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. */ /* For environ */ diff --git a/lib/mount_bsd.c b/lib/mount_bsd.c index 28a9ae569..5dba5937d 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" diff --git a/lib/mount_util.c b/lib/mount_util.c index f19dfb4c8..8c0cdf72d 100644 --- a/lib/mount_util.c +++ b/lib/mount_util.c @@ -5,7 +5,7 @@ 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" 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/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/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/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 index 558202698..3ce7fbd32 100644 --- a/test/test_signals.c +++ b/test/test_signals.c @@ -5,7 +5,7 @@ * Test for signal handling in libfuse. * * This program can be distributed under the terms of the GNU LGPLv2. - * See the file COPYING.LIB + * See the file GPL2.txt */ #define FUSE_USE_VERSION FUSE_MAKE_VERSION(3, 17) diff --git a/test/test_write_cache.c b/test/test_write_cache.c index 2b5b4f383..00db5a65b 100644 --- a/test/test_write_cache.c +++ b/test/test_write_cache.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. */ #define FUSE_USE_VERSION 30 diff --git a/util/fusermount.c b/util/fusermount.c index 68fa31e55..48f7fe7fa 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 */ diff --git a/util/mount.fuse.c b/util/mount.fuse.c index b98fb2a65..f1a90fe8a 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" From 4166f2eb97da4e25a516abee3d6fe13b9ed77bc6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Jul 2025 01:36:06 +0000 Subject: [PATCH 154/310] build(deps): bump github/codeql-action from 3.29.0 to 3.29.2 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.29.0 to 3.29.2. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/ce28f5bb42b7a9f2c824e633a3f6ee835bab6858...181d5eefc20863364f96762470ba6f862bdef56b) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 3.29.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 3c37133b0..8c954d476 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -53,7 +53,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0 + uses: github/codeql-action/init@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -78,7 +78,7 @@ jobs: meson compile -C build - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0 + uses: github/codeql-action/analyze@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2 with: category: "/language:${{matrix.language}}" From db5a1b6ae9d39614154590e5b8c62702a5230ca4 Mon Sep 17 00:00:00 2001 From: CismonX <admin@cismon.net> Date: Wed, 9 Jul 2025 23:01:05 +0800 Subject: [PATCH 155/310] example/passthrough: refactor fallocate Move fallocate implementation to passthrough_helpers.h, so that it could be reused by multiple passthrough examples. Signed-off-by: CismonX <admin@cismon.net> --- example/passthrough.c | 9 +-------- example/passthrough_fh.c | 11 +++-------- example/passthrough_helpers.h | 17 +++++++++++++++++ example/passthrough_hp.cc | 13 ++++--------- example/passthrough_ll.c | 16 ++-------------- 5 files changed, 27 insertions(+), 39 deletions(-) diff --git a/example/passthrough.c b/example/passthrough.c index a5ac4b356..7e5c9aaed 100644 --- a/example/passthrough.c +++ b/example/passthrough.c @@ -399,7 +399,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 +407,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 +415,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 */ @@ -554,9 +549,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, diff --git a/example/passthrough_fh.c b/example/passthrough_fh.c index 7bbdb700d..67645542c 100644 --- a/example/passthrough_fh.c +++ b/example/passthrough_fh.c @@ -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 */ @@ -650,9 +647,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, diff --git a/example/passthrough_helpers.h b/example/passthrough_helpers.h index 6b77c3360..aca796a57 100644 --- a/example/passthrough_helpers.h +++ b/example/passthrough_helpers.h @@ -23,6 +23,23 @@ * SUCH DAMAGE */ +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 + + return -EOPNOTSUPP; +#endif // HAVE_FALLOCATE +} + /* * Creates files on the underlying file system in response to a FUSE_MKNOD * operation diff --git a/example/passthrough_hp.cc b/example/passthrough_hp.cc index cc6b7e8d3..a0bf0d5c8 100644 --- a/example/passthrough_hp.cc +++ b/example/passthrough_hp.cc @@ -79,6 +79,8 @@ #include <mutex> #include <syslog.h> +#include "passthrough_helpers.h" + using namespace std; #define SFS_DEFAULT_THREADS "-1" // take libfuse value as default @@ -1216,20 +1218,15 @@ static void sfs_statfs(fuse_req_t req, fuse_ino_t ino) 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); + 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) @@ -1389,9 +1386,7 @@ static void assign_operations(fuse_lowlevel_ops &sfs_oper) 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; -#endif sfs_oper.flock = sfs_flock; #ifdef HAVE_SETXATTR sfs_oper.setxattr = sfs_setxattr; diff --git a/example/passthrough_ll.c b/example/passthrough_ll.c index 59f43c53c..9e027b43b 100644 --- a/example/passthrough_ll.c +++ b/example/passthrough_ll.c @@ -1006,22 +1006,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); } From 21a27e371fbb44e16eec6ef5e18768b476db2e27 Mon Sep 17 00:00:00 2001 From: CismonX <admin@cismon.net> Date: Wed, 9 Jul 2025 23:30:17 +0800 Subject: [PATCH 156/310] example/passthrough: tidy up passthrough_helpers.h Add header guards, include system headers as needed, and declare helper functions as inline. This ensures that the helper header could be properly included. Signed-off-by: CismonX <admin@cismon.net> --- example/passthrough.c | 4 ---- example/passthrough_helpers.h | 18 +++++++++++++++++- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/example/passthrough.c b/example/passthrough.c index 7e5c9aaed..f0e686dc9 100644 --- a/example/passthrough.c +++ b/example/passthrough.c @@ -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> diff --git a/example/passthrough_helpers.h b/example/passthrough_helpers.h index aca796a57..52958c666 100644 --- a/example/passthrough_helpers.h +++ b/example/passthrough_helpers.h @@ -23,6 +23,20 @@ * 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 @@ -44,7 +58,7 @@ static inline int do_fallocate(int fd, int mode, off_t offset, off_t length) * 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; @@ -91,3 +105,5 @@ static int mknod_wrapper(int dirfd, const char *path, const char *link, return res; } + +#endif // FUSE_PASSTHROUGH_HELPERS_H_ From fc9888e0b281dc0d0fcebcfe0c8c392263b1b308 Mon Sep 17 00:00:00 2001 From: CismonX <admin@cismon.net> Date: Wed, 9 Jul 2025 23:10:03 +0800 Subject: [PATCH 157/310] example/passthrough: support fspacectl() FreeBSD 14 introduced a new system call, fspacectl(). Currently, it supports one operation mode, SPACECTL_DEALLOC, which is functionally equivalent to Linux fallocate() with FALLOC_FL_KEEP_SIZE|FALLOC_FL_PUNCH_HOLE flags. fspacectl() calls with SPACECTL_DEALLOC is supported on FUSE filesystems via FUSE_FALLOCATE with the aforementioned flags. Signed-off-by: CismonX <admin@cismon.net> --- example/passthrough_helpers.h | 13 +++++++++++++ meson.build | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/example/passthrough_helpers.h b/example/passthrough_helpers.h index 52958c666..326a5c7d6 100644 --- a/example/passthrough_helpers.h +++ b/example/passthrough_helpers.h @@ -50,6 +50,19 @@ static inline int do_fallocate(int fd, int mode, off_t offset, off_t length) 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 } diff --git a/meson.build b/meson.build index 910a07565..16b804e74 100644 --- a/meson.build +++ b/meson.build @@ -74,7 +74,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)) From 3be844147764b96496bcae6d92fa4b0e43ebff42 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Tue, 15 Jul 2025 18:46:44 +0200 Subject: [PATCH 158/310] Switch to c++20 This only effects example/{passthrough_hp.cc,memfs_ll.cc} and is mainly to avoid these warnings ../example/memfs_ll.cc:1100:1: warning: missing field 'statx' initializer [-Wmissing-designated-field-initializers] Signed-off-by: Bernd Schubert <bernd@bsbernd.com> --- example/meson.build | 2 +- meson.build | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/example/meson.build b/example/meson.build index 701afa768..21e8317a1 100644 --- a/example/meson.build +++ b/example/meson.build @@ -37,7 +37,7 @@ if platform != 'dragonfly' and add_languages('cpp', required : false) 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/meson.build b/meson.build index 16b804e74..7091d5f68 100644 --- a/meson.build +++ b/meson.build @@ -4,7 +4,7 @@ project('libfuse3', ['c'], default_options: [ 'buildtype=debugoptimized', 'c_std=gnu11', - 'cpp_std=c++17', + 'cpp_std=c++20', 'warning_level=2', ]) From b507cbc2b1aaec1931642497edcb6723a0d24dc4 Mon Sep 17 00:00:00 2001 From: Joanne Koong <joannelkoong@gmail.com> Date: Fri, 23 Aug 2024 13:11:09 -0700 Subject: [PATCH 159/310] Add statx support This commit adds libfuse support for FUSE_STATX requests on linux distributions. Currently, statx is only supported on linux. To make the interface a ergonomic as possible (eg using native 'struct statx' vs 'struct fuse_statx'), this implementation gates the 'struct statx' changes by #ifdef linux. Signed-off-by: Joanne Koong <joannelkoong@gmail.com> --- ChangeLog.rst | 2 + example/memfs_ll.cc | 3 ++ example/passthrough.c | 21 ++++++++ example/passthrough_fh.c | 21 ++++++++ example/passthrough_ll.c | 25 +++++++++ include/fuse.h | 18 +++++++ include/fuse_lowlevel.h | 35 ++++++++++++- lib/fuse.c | 108 +++++++++++++++++++++++++++++++++++++++ lib/fuse_lowlevel.c | 66 +++++++++++++++++++++++- lib/fuse_versionscript | 3 ++ lib/modules/iconv.c | 19 +++++++ lib/modules/subdir.c | 19 +++++++ meson.build | 4 ++ test/test_syscalls.c | 49 ++++++++++++++++++ 14 files changed, 391 insertions(+), 2 deletions(-) diff --git a/ChangeLog.rst b/ChangeLog.rst index 658f898aa..505d9dba8 100644 --- a/ChangeLog.rst +++ b/ChangeLog.rst @@ -1,3 +1,5 @@ +libfuse 3.18 + libfuse 3.17.1-rc0 (2024-02.10) =============================== diff --git a/example/memfs_ll.cc b/example/memfs_ll.cc index b4f0b63cb..0da7c2544 100644 --- a/example/memfs_ll.cc +++ b/example/memfs_ll.cc @@ -1094,6 +1094,9 @@ static const struct fuse_lowlevel_ops memfs_oper = { .copy_file_range = nullptr, .lseek = nullptr, .tmpfile = nullptr, +#ifdef HAVE_STATX + .statx = nullptr, +#endif }; int main(int argc, char *argv[]) diff --git a/example/passthrough.c b/example/passthrough.c index f0e686dc9..fdaa19e33 100644 --- a/example/passthrough.c +++ b/example/passthrough.c @@ -519,6 +519,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, @@ -556,6 +574,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_fh.c b/example/passthrough_fh.c index 67645542c..0d4fb5bd4 100644 --- a/example/passthrough_fh.c +++ b/example/passthrough_fh.c @@ -616,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, @@ -662,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_ll.c b/example/passthrough_ll.c index 9e027b43b..5ca6efa23 100644 --- a/example/passthrough_ll.c +++ b/example/passthrough_ll.c @@ -1215,6 +1215,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, @@ -1255,6 +1277,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/include/fuse.h b/include/fuse.h index b99004334..06feacb07 100644 --- a/include/fuse.h +++ b/include/fuse.h @@ -33,6 +33,9 @@ extern "C" { * Basic FUSE API * * ----------------------------------------------------------- */ +/* Forward declaration */ +struct statx; + /** Handle for a FUSE filesystem */ struct fuse; @@ -850,6 +853,19 @@ 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 *); + +#ifdef HAVE_STATX + /** + * 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); +#endif }; /** Extra context that may be needed by some filesystems @@ -1344,6 +1360,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_lowlevel.h b/include/fuse_lowlevel.h index 75e084d09..844ee7102 100644 --- a/include/fuse_lowlevel.h +++ b/include/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 * @@ -1303,7 +1306,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 +1327,23 @@ struct fuse_lowlevel_ops { void (*tmpfile) (fuse_req_t req, fuse_ino_t parent, mode_t mode, struct fuse_file_info *fi); +#ifdef HAVE_STATX + /** + * 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); +#endif }; /** @@ -1705,6 +1724,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 * * ----------------------------------------------------------- */ diff --git a/lib/fuse.c b/lib/fuse.c index 68b61ce69..333fdc7dc 100644 --- a/lib/fuse.c +++ b/lib/fuse.c @@ -1507,6 +1507,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); @@ -2312,6 +2335,39 @@ off_t fuse_fs_lseek(struct fuse_fs *fs, const char *path, off_t off, int 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, "statx[%s] %s %d %d\n", + file_info_string(fi, buf, sizeof(buf)), path, + flags, mask); + } + 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) { struct node *node; @@ -4361,6 +4417,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_nsec; + ST_MTIM_NSEC(&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) { /* @@ -4459,6 +4564,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) diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index 91f42440f..1ae3473be 100644 --- a/lib/fuse_lowlevel.c +++ b/lib/fuse_lowlevel.c @@ -1216,6 +1216,33 @@ int fuse_reply_lseek(fuse_req_t req, off_t off) return send_reply_ok(req, &arg, sizeof(arg)); } +#ifdef HAVE_STATX +int fuse_reply_statx(fuse_req_t req, int flags, struct statx *statx, + double attr_timeout) +{ + 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) { @@ -2428,6 +2455,42 @@ 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)req; + (void)nodeid; + (void)op_in; +} +#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); @@ -2510,7 +2573,6 @@ 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 @@ -3297,6 +3359,7 @@ static struct { [FUSE_RENAME2] = { do_rename2, "RENAME2" }, [FUSE_COPY_FILE_RANGE] = { do_copy_file_range, "COPY_FILE_RANGE" }, [FUSE_LSEEK] = { do_lseek, "LSEEK" }, + [FUSE_STATX] = { do_statx, "STATX" }, [CUSE_INIT] = { cuse_lowlevel_init, "CUSE_INIT" }, }; @@ -3351,6 +3414,7 @@ static struct { [FUSE_RENAME2] = { _do_rename2, "RENAME2" }, [FUSE_COPY_FILE_RANGE] = { _do_copy_file_range, "COPY_FILE_RANGE" }, [FUSE_LSEEK] = { _do_lseek, "LSEEK" }, + [FUSE_STATX] = { _do_statx, "STATX" }, [CUSE_INIT] = { _cuse_lowlevel_init, "CUSE_INIT" }, }; diff --git a/lib/fuse_versionscript b/lib/fuse_versionscript index 2d8884d7e..0e581f171 100644 --- a/lib/fuse_versionscript +++ b/lib/fuse_versionscript @@ -212,6 +212,9 @@ FUSE_3.18 { # Not part of public API, for internal test use only fuse_convert_to_conn_want_ext; + + fuse_reply_statx; + fuse_fs_statx; } FUSE_3.17; # Local Variables: diff --git a/lib/modules/iconv.c b/lib/modules/iconv.c index 599b8df4d..417c9043d 100644 --- a/lib/modules/iconv.c +++ b/lib/modules/iconv.c @@ -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[] = { diff --git a/lib/modules/subdir.c b/lib/modules/subdir.c index dd2d49d80..67c469799 100644 --- a/lib/modules/subdir.c +++ b/lib/modules/subdir.c @@ -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/meson.build b/meson.build index 7091d5f68..64ff334f7 100644 --- a/meson.build +++ b/meson.build @@ -126,6 +126,10 @@ 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>')) +public_cfg.set('HAVE_STATX', + cc.has_function('statx', prefix : '#define _GNU_SOURCE\n#include <sys/stat.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', diff --git a/test/test_syscalls.c b/test/test_syscalls.c index 4bbe97340..61ee953b9 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> @@ -921,6 +922,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; @@ -2179,6 +2227,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(); From 194023c5999651386a3ddbf91fdd710d661d083b Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Tue, 15 Jul 2025 23:54:27 +0200 Subject: [PATCH 160/310] fusermount: Fix the close_range ifdef This fixes commit 82bcd818 That commit had removed HAVE_LINUX_CLOSE_RANGE in meson generation, but didn't remove the usage in fusermount.c - fusermount was then not using the close_range syscall. Closes: https://github.com/libfuse/libfuse/issues/1284 Signed-off-by: Bernd Schubert <bernd@bsbernd.com> --- util/fusermount.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/util/fusermount.c b/util/fusermount.c index 48f7fe7fa..1b94e8e5e 100644 --- a/util/fusermount.c +++ b/util/fusermount.c @@ -36,7 +36,7 @@ #include <stdbool.h> #include <sys/vfs.h> -#ifdef HAVE_LINUX_CLOSE_RANGE_H +#ifdef HAVE_CLOSE_RANGE #include <linux/close_range.h> #endif @@ -1451,7 +1451,7 @@ static int close_inherited_fds(int cfd) if (cfd <= STDERR_FILENO) return -EINVAL; -#ifdef HAVE_LINUX_CLOSE_RANGE_H +#ifdef HAVE_CLOSE_RANGE if (cfd < STDERR_FILENO + 2) { close_range_loop(STDERR_FILENO + 1, cfd - 1, cfd); } else { From 5f6d3be57f73c8b79e9b616b0f30464475116084 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Wed, 16 Jul 2025 00:09:31 +0200 Subject: [PATCH 161/310] fusermount: close_range seems to be available on bsd According to https://man.freebsd.org/cgi/man.cgi?close_range(2) we just need to remove the linux include. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> --- meson.build | 2 ++ util/fusermount.c | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/meson.build b/meson.build index 64ff334f7..05ca1e74a 100644 --- a/meson.build +++ b/meson.build @@ -98,7 +98,9 @@ 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); diff --git a/util/fusermount.c b/util/fusermount.c index 1b94e8e5e..9cc08b4bf 100644 --- a/util/fusermount.c +++ b/util/fusermount.c @@ -36,7 +36,7 @@ #include <stdbool.h> #include <sys/vfs.h> -#ifdef HAVE_CLOSE_RANGE +#if defined HAVE_CLOSE_RANGE && defined linux #include <linux/close_range.h> #endif From d8253770ac2cf4b8769e8cf41eb3c629f30ee80f Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Tue, 15 Jul 2025 20:09:17 +0200 Subject: [PATCH 162/310] Avoid double unmount on FUSE_DESTROY This is a long standing issue, a system could have unmounted /path/to/mnt and then fuse-client/kernel would send FUSE_DESTROY, which would then again try a umount. Given that FUSE_DESTROY is async, that umount might arrive any time later and might possibly unmount a wrong mount point. A warning as in issue #1286 is just minor to that. Code wise this uses atomics to free the char *, as FUSE_DESTROY might race with a signal and a double free might be possible without proctection. A lock might run into the same issue, if the signal would arrive at the wrong time a double lock would be possible. Additionally, fuse_session_mount() is updated, to first duplicatate the pointer and to then do the kernel mount - reverting the kernel mount in case of strdup() failure is much harder. Closes: https://github.com/libfuse/libfuse/issues/1286 Signed-off-by: Bernd Schubert <bernd@bsbernd.com> --- lib/fuse_i.h | 3 ++- lib/fuse_lowlevel.c | 33 ++++++++++++++++++++++----------- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/lib/fuse_i.h b/lib/fuse_i.h index 2221cf2af..0d0e637ae 100644 --- a/lib/fuse_i.h +++ b/lib/fuse_i.h @@ -18,6 +18,7 @@ #include <stdint.h> #include <stdbool.h> #include <errno.h> +#include <stdatomic.h> #define MIN(a, b) \ ({ \ @@ -67,7 +68,7 @@ struct fuse_session_uring { }; struct fuse_session { - char *mountpoint; + _Atomic(char *)mountpoint; int fd; struct fuse_custom_io *io; struct mount_opts *mo; diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index 1ae3473be..9731b44ea 100644 --- a/lib/fuse_lowlevel.c +++ b/lib/fuse_lowlevel.c @@ -2921,11 +2921,15 @@ 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)op_in; (void)in_payload; + mountpoint = atomic_exchange(&se->mountpoint, NULL); + free(mountpoint); + se->got_destroy = 1; se->got_init = 0; if (se->op.destroy) @@ -4206,15 +4210,23 @@ int fuse_session_custom_io_30(struct fuse_session *se, offsetof(struct fuse_custom_io, clone_fd), fd); } -int fuse_session_mount(struct fuse_session *se, const char *mountpoint) +int fuse_session_mount(struct fuse_session *se, const char *_mountpoint) { int fd; + char *mountpoint; - if (mountpoint == NULL) { + if (_mountpoint == NULL) { fuse_log(FUSE_LOG_ERR, "Invalid null-ptr mountpoint!\n"); return -1; } + mountpoint = strdup(_mountpoint); + if (mountpoint == NULL) { + fuse_log(FUSE_LOG_ERR, "Failed to allocate memory for mountpoint. Error: %s\n", + strerror(errno)); + return -1; + } + /* * Make sure file descriptors 0, 1 and 2 are open, otherwise chaos * would ensue. @@ -4237,7 +4249,7 @@ int fuse_session_mount(struct fuse_session *se, const char *mountpoint) fuse_log(FUSE_LOG_ERR, "fuse: Invalid file descriptor /dev/fd/%u\n", fd); - return -1; + goto error_out; } se->fd = fd; return 0; @@ -4246,18 +4258,16 @@ int fuse_session_mount(struct fuse_session *se, const char *mountpoint) /* Open channel */ fd = fuse_kern_mount(mountpoint, se->mo); if (fd == -1) - return -1; + goto error_out; se->fd = fd; /* Save mountpoint */ - se->mountpoint = strdup(mountpoint); - if (se->mountpoint == NULL) - goto error_out; + se->mountpoint = mountpoint; return 0; error_out: - fuse_kern_unmount(mountpoint, fd); + free(mountpoint); return -1; } @@ -4269,10 +4279,11 @@ int fuse_session_fd(struct fuse_session *se) void fuse_session_unmount(struct fuse_session *se) { if (se->mountpoint != NULL) { - fuse_kern_unmount(se->mountpoint, se->fd); + char *mountpoint = atomic_exchange(&se->mountpoint, NULL); + + fuse_kern_unmount(mountpoint, se->fd); se->fd = -1; - free(se->mountpoint); - se->mountpoint = NULL; + free(mountpoint); } } From b96c738b10e9a308725c62b9392ba7d553eec254 Mon Sep 17 00:00:00 2001 From: Long Li <leo.lilong@huawei.com> Date: Wed, 16 Jul 2025 09:54:07 +0800 Subject: [PATCH 163/310] memfs_ll: fix deadlock in truncate operation Remove redundant mutex lock acquisition in the truncate() method to prevent deadlock. The issue occurs when memfs_setattr() already holds the mutex lock and then calls truncate(), which attempts to acquire the same lock again. Signed-off-by: Long Li <leo.lilong@huawei.com> --- example/memfs_ll.cc | 1 - 1 file changed, 1 deletion(-) diff --git a/example/memfs_ll.cc b/example/memfs_ll.cc index 0da7c2544..17d7d034b 100644 --- a/example/memfs_ll.cc +++ b/example/memfs_ll.cc @@ -199,7 +199,6 @@ class Inode { void truncate(off_t size) { - std::lock_guard<std::mutex> lock(mutex); std::lock_guard<std::mutex> attr_lock(attr_mutex); if (size < content.size()) { content.resize(size); From 0d15126f720e5515d9b2d2b661cff80c9a05cc5c Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Sat, 19 Jul 2025 15:33:34 +0200 Subject: [PATCH 164/310] example/memfs_ll: Avoid nullptr field initializations by pragma These nullptr initializations don't make sense - methods that are not explicitly set are not implemented. I thought that C++20 would eventually avoid the nullptr, but looks like compilers still give warnings - avoid them with a pragma. Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- example/memfs_ll.cc | 32 +++----------------------------- 1 file changed, 3 insertions(+), 29 deletions(-) diff --git a/example/memfs_ll.cc b/example/memfs_ll.cc index 17d7d034b..edda34b4e 100644 --- a/example/memfs_ll.cc +++ b/example/memfs_ll.cc @@ -1047,56 +1047,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, -#ifdef HAVE_STATX - .statx = nullptr, -#endif }; +#pragma GCC diagnostic pop int main(int argc, char *argv[]) { From 6d02f5d763b1f1da54c3108707fb2ce6677fb1d0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Jul 2025 23:45:34 +0000 Subject: [PATCH 165/310] build(deps): bump github/codeql-action from 3.29.2 to 3.29.3 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.29.2 to 3.29.3. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/181d5eefc20863364f96762470ba6f862bdef56b...d6bbdef45e766d081b84a2def353b0055f728d3e) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 3.29.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 8c954d476..c7307acfa 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -53,7 +53,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2 + uses: github/codeql-action/init@d6bbdef45e766d081b84a2def353b0055f728d3e # v3.29.3 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -78,7 +78,7 @@ jobs: meson compile -C build - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2 + uses: github/codeql-action/analyze@d6bbdef45e766d081b84a2def353b0055f728d3e # v3.29.3 with: category: "/language:${{matrix.language}}" From e1e7b117ede7d78abde3ca13545a2057b4160b50 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Sat, 19 Jul 2025 15:42:46 +0200 Subject: [PATCH 166/310] fuse-over-io-uring: Remove handling of -EOPNOTSUPP -EOPNOTSUPP was in early RFC kernel patches, but merged version does not have this handler anymore. Closes: https://github.com/libfuse/libfuse/issues/1283 Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- lib/fuse_uring.c | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/lib/fuse_uring.c b/lib/fuse_uring.c index a54973eed..8dea7d06d 100644 --- a/lib/fuse_uring.c +++ b/lib/fuse_uring.c @@ -553,8 +553,8 @@ static int fuse_uring_queue_handle_cqes(struct fuse_ring_queue *queue) //fuse_log(FUSE_LOG_ERR, "cqe res: %d\n", cqe->res); /* -ENOTCONN is ok on umount */ - if (err != -EINTR && err != -EOPNOTSUPP && - err != -EAGAIN && err != -ENOTCONN) { + if (err != -EINTR && err != -EAGAIN && + err != -ENOTCONN) { se->error = cqe->res; /* return first error */ @@ -701,22 +701,14 @@ static void *fuse_uring_thread(void *arg) io_uring_submit_and_wait(&queue->ring, 1); err = fuse_uring_queue_handle_cqes(queue); - if (err < 0) { - /* - * fuse-over-io-uring is not supported, operation can - * continue over /dev/fuse - */ - if (err == -EOPNOTSUPP) - goto ret; + if (err < 0) goto err; - } } return NULL; err: fuse_session_exit(se); -ret: return NULL; } From 062465063500e042723a24704a6e350907cb8f8c Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Sun, 20 Jul 2025 14:31:46 +0200 Subject: [PATCH 167/310] fuse-io-uring: Add sanity check for the number of queue entries fuse-io-uring would create a blocking mount with 0 queue entries, we need a sanity check for it. Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- lib/fuse_uring.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/fuse_uring.c b/lib/fuse_uring.c index 8dea7d06d..10d3286e9 100644 --- a/lib/fuse_uring.c +++ b/lib/fuse_uring.c @@ -727,8 +727,13 @@ static int fuse_uring_start_ring_threads(struct fuse_ring_pool *ring) return rc; } -static int fuse_uring_sanity_check(void) +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"); @@ -741,7 +746,7 @@ int fuse_uring_start(struct fuse_session *se) int err = 0; struct fuse_ring_pool *fuse_ring; - fuse_uring_sanity_check(); + fuse_uring_sanity_check(se); fuse_ring = fuse_create_ring(se); if (fuse_ring == NULL) { From 2f092ef1084fe72c6cc26a8cde61ee94329c2f34 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Mon, 21 Jul 2025 13:11:22 +0200 Subject: [PATCH 168/310] Simplify meson condition for liburing Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- meson.build | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/meson.build b/meson.build index 05ca1e74a..f98ef8a6d 100644 --- a/meson.build +++ b/meson.build @@ -158,12 +158,10 @@ int main(void) { return ret; }''' -liburing = get_option('enable-io-uring') ? \ - dependency('liburing', required: false) :\ - dependency('', required: false) +liburing = dependency('liburing', required: false) libnuma = dependency('numa', required: false) -if liburing.found() and libnuma.found() +if get_option('enable-io-uring') and liburing.found() and libnuma.found() if cc.links(code, name: 'liburing linking and SQE128 support', dependencies: [liburing]) From e915a28ec44ba0f5345eed9985e862ebe13104cb Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Sun, 20 Jul 2025 15:51:55 +0200 Subject: [PATCH 169/310] Split fuse-io-uring startup Start the ring threads before sending fuse_reply_ok() so that io-uring startup issues can be non-fatal. Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- lib/fuse_lowlevel.c | 14 +++++++---- lib/fuse_uring.c | 60 ++++++++++++++++++++++++++++++++++++++++++--- lib/fuse_uring_i.h | 6 +++++ 3 files changed, 71 insertions(+), 9 deletions(-) diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index 9731b44ea..6afcecd3b 100644 --- a/lib/fuse_lowlevel.c +++ b/lib/fuse_lowlevel.c @@ -2897,18 +2897,22 @@ _do_init(fuse_req_t req, const fuse_ino_t nodeid, const void *op_in, else if (arg->minor < 23) outargsize = FUSE_COMPAT_22_INIT_OUT_SIZE; - send_reply_ok(req, &outarg, outargsize); - - /* XXX: Split the start, and send SQEs only after send_reply_ok() */ + /* 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_ERR, "fuse: failed to start io-uring: %s\n", + fuse_log(FUSE_LOG_INFO, + "fuse: failed to start io-uring: %s\n", strerror(ring_rc)); - fuse_session_exit(se); + outargflags &= ~FUSE_OVER_IO_URING; + enable_io_uring = false; } } + + send_reply_ok(req, &outarg, outargsize); + if (enable_io_uring) + fuse_uring_wake_ring_threads(se); } static __attribute__((no_sanitize("thread"))) void diff --git a/lib/fuse_uring.c b/lib/fuse_uring.c index 10d3286e9..985c81700 100644 --- a/lib/fuse_uring.c +++ b/lib/fuse_uring.c @@ -78,6 +78,15 @@ struct fuse_ring_pool { /* 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; }; @@ -432,7 +441,7 @@ static int fuse_uring_prepare_fetch_sqes(struct fuse_ring_queue *queue) io_uring_prep_poll_add(sqe, queue->eventfd, POLLIN); io_uring_sqe_set_data(sqe, (void *)(uintptr_t)queue->eventfd); - io_uring_submit(&queue->ring); + /* Only preparation until here, no submission yet */ return 0; } @@ -690,12 +699,24 @@ static void *fuse_uring_thread(void *arg) fuse_uring_set_thread_core(queue->qid); err = fuse_uring_init_queue(queue); + + if (err < 0) + ring_pool->failed_threads++; + 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; + 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); @@ -709,6 +730,7 @@ static void *fuse_uring_thread(void *arg) err: fuse_session_exit(se); +err_non_fatal: return NULL; } @@ -751,13 +773,34 @@ int fuse_uring_start(struct fuse_session *se) fuse_ring = fuse_create_ring(se); if (fuse_ring == NULL) { err = -EADDRNOTAVAIL; - goto out; + 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); -out: + if (err) + goto err; + + while (fuse_ring->started_threads < fuse_ring->nr_queues) { + /* Wait for all threads to start */ + if (fuse_ring->failed_threads != 0) { + err = -EADDRNOTAVAIL; + goto err; + } + } + + if (fuse_ring->failed_threads != 0) { + err = -EADDRNOTAVAIL; + goto err; + } + +err: return err; } @@ -772,3 +815,12 @@ int fuse_uring_stop(struct fuse_session *se) 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 index fc236917b..14418ef16 100644 --- a/lib/fuse_uring_i.h +++ b/lib/fuse_uring_i.h @@ -30,6 +30,7 @@ void fuse_session_process_uring_cqe(struct fuse_session *se, 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); @@ -45,6 +46,11 @@ 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; From 898def7594b3b63a44119f5d77c797644108e74e Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Mon, 21 Jul 2025 13:24:55 +0200 Subject: [PATCH 170/310] fuse_uring_i.h: Include errno.h Needed for ENOTSUP Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- lib/fuse_uring_i.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/fuse_uring_i.h b/lib/fuse_uring_i.h index 14418ef16..c1da73ad7 100644 --- a/lib/fuse_uring_i.h +++ b/lib/fuse_uring_i.h @@ -16,6 +16,8 @@ #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) From 7e2dc4abbe1333eeefe80f6efbb81df24fe297a0 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Mon, 21 Jul 2025 18:56:53 +0200 Subject: [PATCH 171/310] high level: Remove the nullpath_ok log message This seems to be a left over - the file system has to set this variable in its ->init method, i.e. logging it in startup is not giving any helpful output. Closes: https://github.com/libfuse/libfuse/issues/1272 Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- lib/fuse.c | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/fuse.c b/lib/fuse.c index 333fdc7dc..e997531f4 100644 --- a/lib/fuse.c +++ b/lib/fuse.c @@ -5073,10 +5073,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; From db74ec7441dcdd444ce2599ae196025bc5383bc3 Mon Sep 17 00:00:00 2001 From: Gleb Popov <6yearold@gmail.com> Date: Thu, 24 Jul 2025 10:43:40 +0300 Subject: [PATCH 172/310] mount_bsd.c: Actually report mount failures back to caller Signed-off-by: Gleb Popov <6yearold@gmail.com> --- lib/mount_bsd.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/mount_bsd.c b/lib/mount_bsd.c index 5dba5937d..c12ab322e 100644 --- a/lib/mount_bsd.c +++ b/lib/mount_bsd.c @@ -215,7 +215,9 @@ static int fuse_mount_core(const char *mountpoint, const char *opts) } waitpid(pid, &status, 0); - _exit(EXIT_SUCCESS); + if (!WIFEXITED(status)) + _exit(EXIT_FAILURE); + _exit(WEXITSTATUS(status)); } if (waitpid(cpid, &status, 0) == -1 || WEXITSTATUS(status) != 0) { From 95b9d3b3e09cf9488ac331ea2b30c73c29f16e9d Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Sat, 2 Aug 2025 13:35:29 +0200 Subject: [PATCH 173/310] Update issue templates Signed-off-by: Bernd Schubert <bernd@bsbernd.com> --- .github/ISSUE_TEMPLATE/bug_report.md | 27 ++++++++++++++++++++++++++ .github/ISSUE_TEMPLATE/issue-report.md | 16 --------------- 2 files changed, 27 insertions(+), 16 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md delete mode 100644 .github/ISSUE_TEMPLATE/issue-report.md 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. From ba8a3a420e0421561eb6ff6caf17588e748ab164 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 Aug 2025 04:08:01 +0000 Subject: [PATCH 174/310] build(deps): bump github/codeql-action from 3.29.3 to 3.29.5 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.29.3 to 3.29.5. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/d6bbdef45e766d081b84a2def353b0055f728d3e...51f77329afa6477de8c49fc9c7046c15b9a4e79d) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 3.29.5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index c7307acfa..c31114487 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -53,7 +53,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@d6bbdef45e766d081b84a2def353b0055f728d3e # v3.29.3 + uses: github/codeql-action/init@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -78,7 +78,7 @@ jobs: meson compile -C build - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@d6bbdef45e766d081b84a2def353b0055f728d3e # v3.29.3 + uses: github/codeql-action/analyze@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5 with: category: "/language:${{matrix.language}}" From 33ec2372f36ae96c4828f00720b58a097c578165 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Aug 2025 07:04:06 +0000 Subject: [PATCH 175/310] build(deps): bump actions/checkout from 4 to 5 Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5. - [Release notes](https://github.com/actions/checkout/releases) - [Commits](https://github.com/actions/checkout/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> --- .github/workflows/abicheck.yml | 4 ++-- .github/workflows/bsd.yaml | 2 +- .github/workflows/checkpatch.yml | 2 +- .github/workflows/codeql.yml | 2 +- .github/workflows/codespell.yml | 2 +- .github/workflows/iwyi-check.yml | 2 +- .github/workflows/pr-ci.yml | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/abicheck.yml b/.github/workflows/abicheck.yml index b79ccc6ed..8ab13da8a 100644 --- a/.github/workflows/abicheck.yml +++ b/.github/workflows/abicheck.yml @@ -28,11 +28,11 @@ jobs: sudo apt-get update sudo apt-get -y install abigail-tools clang gcc liburing-dev libnuma-dev - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: path: current - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: path: previous ref: ${{ github.event.pull_request.base.ref }} diff --git a/.github/workflows/bsd.yaml b/.github/workflows/bsd.yaml index e07d543dd..13b30299b 100644 --- a/.github/workflows/bsd.yaml +++ b/.github/workflows/bsd.yaml @@ -18,7 +18,7 @@ jobs: name: Build under FreeBSD steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Build uses: vmactions/freebsd-vm@v1 with: diff --git a/.github/workflows/checkpatch.yml b/.github/workflows/checkpatch.yml index 0da6c5410..86b16aae1 100644 --- a/.github/workflows/checkpatch.yml +++ b/.github/workflows/checkpatch.yml @@ -10,7 +10,7 @@ jobs: checkpatch: runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 0 - name: Install dependencies diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index c31114487..d9ae46883 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -49,7 +49,7 @@ jobs: build-mode: manual steps: - name: Checkout repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml index 184fbcfe1..4c2288b7a 100644 --- a/.github/workflows/codespell.yml +++ b/.github/workflows/codespell.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Codespell uses: codespell-project/actions-codespell@406322ec52dd7b488e48c1c4b82e2a8b3a1bf630 # v2.1 with: diff --git a/.github/workflows/iwyi-check.yml b/.github/workflows/iwyi-check.yml index 3751f35de..0718c3155 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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 0 diff --git a/.github/workflows/pr-ci.yml b/.github/workflows/pr-ci.yml index 78b5e02a9..b0134cf6b 100644 --- a/.github/workflows/pr-ci.yml +++ b/.github/workflows/pr-ci.yml @@ -33,7 +33,7 @@ jobs: gcc-multilib g++-multilib libc6-dev-i386 \ libpcap0.8-dev:i386 libudev-dev:i386 pkg-config:i386 \ liburing-dev libnuma-dev - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/setup-python@v5 with: python-version: '3.12' From 2454ff17512dd0ff8d44ec4a0c35f5fe3aa09d2f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Aug 2025 06:07:12 +0000 Subject: [PATCH 176/310] build(deps): bump github/codeql-action from 3.29.7 to 3.29.8 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.29.7 to 3.29.8. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/51f77329afa6477de8c49fc9c7046c15b9a4e79d...76621b61decf072c1cee8dd1ce2d2a82d33c17ed) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 3.29.8 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index d9ae46883..62dac1451 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -53,7 +53,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5 + uses: github/codeql-action/init@76621b61decf072c1cee8dd1ce2d2a82d33c17ed # v3.29.5 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -78,7 +78,7 @@ jobs: meson compile -C build - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5 + uses: github/codeql-action/analyze@76621b61decf072c1cee8dd1ce2d2a82d33c17ed # v3.29.5 with: category: "/language:${{matrix.language}}" From 42b9b3bbb71d38be6a5670bbd0948cf406c1573c Mon Sep 17 00:00:00 2001 From: CismonX <admin@cismon.net> Date: Tue, 12 Aug 2025 08:55:42 +0800 Subject: [PATCH 177/310] fuse_loop_mt.c: fix close-on-exec flag on clone fd Closes: https://github.com/libfuse/libfuse/issues/1310 Signed-off-by: CismonX <admin@cismon.net> --- lib/fuse_loop_mt.c | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/fuse_loop_mt.c b/lib/fuse_loop_mt.c index 8ee7d49ae..77712b6a2 100644 --- a/lib/fuse_loop_mt.c +++ b/lib/fuse_loop_mt.c @@ -271,9 +271,15 @@ static int fuse_clone_chan_fd_default(struct fuse_session *se) strerror(errno)); return -1; } -#ifndef O_CLOEXEC - fcntl(clonefd, F_SETFD, FD_CLOEXEC); -#endif + if (!O_CLOEXEC) { + res = fcntl(clonefd, F_SETFD, FD_CLOEXEC); + if (res == -1) { + fuse_log(FUSE_LOG_ERR, "fuse: failed to set CLOEXEC: %s\n", + strerror(errno)); + close(clonefd); + return -1; + } + } masterfd = se->fd; res = ioctl(clonefd, FUSE_DEV_IOC_CLONE, &masterfd); From a63fc71a55038f49671eb56d0ade99a48ad8d7ab Mon Sep 17 00:00:00 2001 From: CismonX <admin@cismon.net> Date: Tue, 19 Aug 2025 08:05:45 +0800 Subject: [PATCH 178/310] tests: move struct size assertions into a test These checks are meant for libfuse maintainers only, and should not be exposed to users. Signed-off-by: CismonX <admin@cismon.net> --- include/fuse_common.h | 10 ---------- meson.build | 6 ------ test/meson.build | 3 +++ test/test_abi.c | 18 ++++++++++++++++++ test/test_ctests.py | 4 ++++ 5 files changed, 25 insertions(+), 16 deletions(-) create mode 100644 test/test_abi.c diff --git a/include/fuse_common.h b/include/fuse_common.h index b82f2c41d..041188ec7 100644 --- a/include/fuse_common.h +++ b/include/fuse_common.h @@ -30,12 +30,6 @@ #define FUSE_MAKE_VERSION(maj, min) ((maj) * 100 + (min)) #define FUSE_VERSION FUSE_MAKE_VERSION(FUSE_MAJOR_VERSION, FUSE_MINOR_VERSION) -#ifdef HAVE_STATIC_ASSERT -#define fuse_static_assert(condition, message) static_assert(condition, message) -#else -#define fuse_static_assert(condition, message) -#endif - #ifdef __cplusplus extern "C" { #endif @@ -129,8 +123,6 @@ struct fuse_file_info { uint64_t reserved[2]; }; -fuse_static_assert(sizeof(struct fuse_file_info) == 64, - "fuse_file_info size mismatch"); /** * Configuration parameters passed to fuse_session_loop_mt() and @@ -719,8 +711,6 @@ struct fuse_conn_info { */ uint16_t reserved[31]; }; -fuse_static_assert(sizeof(struct fuse_conn_info) == 128, - "Size of struct fuse_conn_info must be 128 bytes"); struct fuse_session; struct fuse_pollhandle; diff --git a/meson.build b/meson.build index f98ef8a6d..07c729581 100644 --- a/meson.build +++ b/meson.build @@ -59,7 +59,6 @@ include_default = ''' #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> -#include <assert.h> /* For static_assert */ #include <pthread.h> /* For pthread_setname_np */ ''' args_default = [ '-D_GNU_SOURCE' ] @@ -82,11 +81,6 @@ endforeach # Special case checks that need custom code special_funcs = { - 'static_assert': ''' - #include <assert.h> - static_assert(1, "test"); - int main(void) { return 0; } - ''', 'pthread_setname_np': ''' #include <pthread.h> int main(void) { diff --git a/test/meson.build b/test/meson.build index 332921657..9f6f409cc 100644 --- a/test/meson.build +++ b/test/meson.build @@ -22,6 +22,9 @@ td += executable('test_want_conversion', 'test_want_conversion.c', 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' ] diff --git a/test/test_abi.c b/test/test_abi.c new file mode 100644 index 000000000..99daa0988 --- /dev/null +++ b/test/test_abi.c @@ -0,0 +1,18 @@ +#define FUSE_USE_VERSION 30 + +#include "fuse.h" + +#include <stdio.h> +#include <stdlib.h> + +int main(void) +{ + if (sizeof(struct fuse_file_info) != 64) { + fprintf(stderr, "struct fuse_file_info size mismatch\n"); + exit(1); + } + if (sizeof(struct fuse_conn_info) != 128) { + fprintf(stderr, "struct fuse_conn_info size mismatch\n"); + exit(1); + } +} diff --git a/test/test_ctests.py b/test/test_ctests.py index ae5cc8f15..b3863e058 100644 --- a/test/test_ctests.py +++ b/test/test_ctests.py @@ -20,6 +20,10 @@ 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)) From 8b8ce5a8242e78db4f1d22437d8e1f015d71765f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Aug 2025 07:59:16 +0000 Subject: [PATCH 179/310] build(deps): bump github/codeql-action from 3.29.8 to 3.29.10 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.29.8 to 3.29.10. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/76621b61decf072c1cee8dd1ce2d2a82d33c17ed...96f518a34f7a870018057716cc4d7a5c014bd61c) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 3.29.10 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 62dac1451..bcc631d0f 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -53,7 +53,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@76621b61decf072c1cee8dd1ce2d2a82d33c17ed # v3.29.5 + uses: github/codeql-action/init@96f518a34f7a870018057716cc4d7a5c014bd61c # v3.29.5 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -78,7 +78,7 @@ jobs: meson compile -C build - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@76621b61decf072c1cee8dd1ce2d2a82d33c17ed # v3.29.5 + uses: github/codeql-action/analyze@96f518a34f7a870018057716cc4d7a5c014bd61c # v3.29.5 with: category: "/language:${{matrix.language}}" From bd8d8cdda9a5f520b7decfe98fafe540992ecb87 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Mon, 18 Aug 2025 22:37:11 +0200 Subject: [PATCH 180/310] io-uring startup: Fix spinning and variable read Reported-by: lixianming <lixianming.19951001@bytedance.com> Closes: https://github.com/libfuse/libfuse/pull/1317 Signed-off-by: Bernd Schubert <bernd@bsbernd.com> --- lib/fuse_uring.c | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/lib/fuse_uring.c b/lib/fuse_uring.c index 985c81700..a835ea1da 100644 --- a/lib/fuse_uring.c +++ b/lib/fuse_uring.c @@ -384,6 +384,8 @@ static void fuse_session_destruct_uring(struct fuse_ring_pool *fuse_ring) } free(fuse_ring->queues); + pthread_cond_destroy(&fuse_ring->thread_start_cond); + pthread_mutex_destroy(&fuse_ring->thread_start_mutex); free(fuse_ring); } @@ -491,6 +493,10 @@ static struct fuse_ring_pool *fuse_create_ring(struct fuse_session *se) queue->ring_pool = fuse_ring; } + 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: @@ -787,18 +793,17 @@ int fuse_uring_start(struct fuse_session *se) if (err) goto err; - while (fuse_ring->started_threads < fuse_ring->nr_queues) { - /* Wait for all threads to start */ - if (fuse_ring->failed_threads != 0) { - err = -EADDRNOTAVAIL; - 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) { + if (fuse_ring->failed_threads != 0) err = -EADDRNOTAVAIL; - goto err; - } + pthread_mutex_unlock(&fuse_ring->thread_start_mutex); err: return err; From 210d92f66b128b229c5803d6f430f346d690feb0 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Mon, 18 Aug 2025 23:32:22 +0200 Subject: [PATCH 181/310] fuse-io-uring: Fix double count of ring_pool->failed_threads This is already done a few lines below. And actually no reason to hold the lock at all given the variables are atomics now. Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- lib/fuse_uring.c | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/fuse_uring.c b/lib/fuse_uring.c index a835ea1da..53ef8dbb4 100644 --- a/lib/fuse_uring.c +++ b/lib/fuse_uring.c @@ -705,9 +705,6 @@ static void *fuse_uring_thread(void *arg) fuse_uring_set_thread_core(queue->qid); err = fuse_uring_init_queue(queue); - - if (err < 0) - ring_pool->failed_threads++; pthread_mutex_lock(&ring_pool->thread_start_mutex); if (err < 0) ring_pool->failed_threads++; From 253ea916fd5122e1c182cb3ffbff1ac2eeea3991 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Mon, 18 Aug 2025 23:22:02 +0200 Subject: [PATCH 182/310] fuse-io-uring: Release io-uring resources on io-uring startup failure Operation might continue without io-uring, so just free these resources. Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- lib/fuse_uring.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/fuse_uring.c b/lib/fuse_uring.c index 53ef8dbb4..104be14f0 100644 --- a/lib/fuse_uring.c +++ b/lib/fuse_uring.c @@ -803,6 +803,11 @@ int fuse_uring_start(struct fuse_session *se) pthread_mutex_unlock(&fuse_ring->thread_start_mutex); err: + if (err) { + /* Note all threads need to have been started */ + fuse_session_destruct_uring(fuse_ring); + se->uring.pool = fuse_ring; + } return err; } From d52ae4f9d44d7e08375bfacac0bc7cc02ad1b54b Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Wed, 28 May 2025 15:13:47 +0200 Subject: [PATCH 183/310] Allow applications to retrieve the req payload (io-uring only) With io-uring the req owns the payload buffer, the application can directly access it and copy data into it. fuse_buf_copy_one() already has a check for dstmem == srcmem and skips data copies. fuse_reply_data fuse_reply_data_uring fuse_buf_copy fuse_buf_copy_one Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- example/passthrough_hp.cc | 36 ++++++++++++++++++++++++++++++++---- include/fuse_lowlevel.h | 18 ++++++++++++++++++ lib/fuse_lowlevel.c | 12 ++++++++++++ lib/fuse_uring.c | 24 ++++++++++++++++++++++++ lib/fuse_versionscript | 1 + 5 files changed, 87 insertions(+), 4 deletions(-) diff --git a/example/passthrough_hp.cc b/example/passthrough_hp.cc index a0bf0d5c8..2bdfc0add 100644 --- a/example/passthrough_hp.cc +++ b/example/passthrough_hp.cc @@ -1158,10 +1158,38 @@ static void sfs_fsync(fuse_req_t req, fuse_ino_t ino, int datasync, 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; + 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); } diff --git a/include/fuse_lowlevel.h b/include/fuse_lowlevel.h index 844ee7102..dc4ec07e6 100644 --- a/include/fuse_lowlevel.h +++ b/include/fuse_lowlevel.h @@ -2367,6 +2367,24 @@ int fuse_session_receive_buf(struct fuse_session *se, struct fuse_buf *buf); */ 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/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index 6afcecd3b..e4544dfc3 100644 --- a/lib/fuse_lowlevel.c +++ b/lib/fuse_lowlevel.c @@ -3317,6 +3317,18 @@ bool fuse_req_is_uring(fuse_req_t req) return req->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 req, const fuse_ino_t node, const void *arg); const char *name; diff --git a/lib/fuse_uring.c b/lib/fuse_uring.c index 104be14f0..ee68fab57 100644 --- a/lib/fuse_uring.c +++ b/lib/fuse_uring.c @@ -190,6 +190,30 @@ static int fuse_uring_commit_sqe(struct fuse_ring_pool *ring_pool, 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->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; diff --git a/lib/fuse_versionscript b/lib/fuse_versionscript index 0e581f171..2feafcf83 100644 --- a/lib/fuse_versionscript +++ b/lib/fuse_versionscript @@ -205,6 +205,7 @@ FUSE_3.17 { FUSE_3.18 { global: fuse_req_is_uring; + fuse_req_get_payload; fuse_set_feature_flag; fuse_unset_feature_flag; fuse_get_feature_flag; From 7e6cdc754ae82706435e8bdd8e98d0f4f6cf9881 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Wed, 28 May 2025 15:57:27 +0200 Subject: [PATCH 184/310] send_reply_uring: Avoid memcpy if src and dest are identical The application might have just written directly into the payload to no need to copy it and in fact, using memcpy would be undefined behavior. Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- lib/fuse_uring.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/fuse_uring.c b/lib/fuse_uring.c index ee68fab57..4c6f0a483 100644 --- a/lib/fuse_uring.c +++ b/lib/fuse_uring.c @@ -233,7 +233,8 @@ int send_reply_uring(fuse_req_t req, int error, const void *arg, size_t argsize) argsize, max_payload_sz); error = -EINVAL; } else if (argsize) { - memcpy(ring_ent->op_payload, arg, argsize); + if (arg != ring_ent->op_payload) + memcpy(ring_ent->op_payload, arg, argsize); } ent_in_out->payload_sz = argsize; From c7c5dbe7cfa5c956ecb36ed69e13eae62a8f4966 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Fri, 22 Aug 2025 18:12:07 +0200 Subject: [PATCH 185/310] Fix passthrough_hp.cc indentation Commit f8fe398ee14864e2c3c7c524ca851d7cc08bed28 introduced an invalid passthrough_hp.cc indentation and that was not detected by checkpatch, as that only handles C files. Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- example/passthrough_hp.cc | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/example/passthrough_hp.cc b/example/passthrough_hp.cc index 2bdfc0add..1a67704dc 100644 --- a/example/passthrough_hp.cc +++ b/example/passthrough_hp.cc @@ -254,19 +254,19 @@ static void sfs_getattr(fuse_req_t req, fuse_ino_t ino, fuse_file_info *fi) fuse_reply_attr(req, &attr, fs.timeout); } -static int with_fd_path(int fd, const std::function<int(const char*)>& f) +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); + 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); + 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, @@ -280,7 +280,7 @@ static void do_setattr(fuse_req_t req, fuse_ino_t ino, struct stat *attr, if (fi) { res = fchmod(fi->fh, attr->st_mode); } else { - res = with_fd_path(ifd, [attr](const char* procname) { + res = with_fd_path(ifd, [attr](const char *procname) { return chmod(procname, attr->st_mode); }); } @@ -304,7 +304,7 @@ static void do_setattr(fuse_req_t req, fuse_ino_t ino, struct stat *attr, if (fi) { res = ftruncate(fi->fh, attr->st_size); } else { - res = with_fd_path(ifd, [attr](const char* procname) { + res = with_fd_path(ifd, [attr](const char *procname) { return truncate(procname, attr->st_size); }); } @@ -333,7 +333,7 @@ static void do_setattr(fuse_req_t req, fuse_ino_t ino, struct stat *attr, res = futimens(fi->fh, tv); else { #ifdef HAVE_UTIMENSAT - res = with_fd_path(ifd, [&tv](const char* procname) { + res = with_fd_path(ifd, [&tv](const char *procname) { return utimensat(AT_FDCWD, procname, tv, 0); }); #else @@ -1089,7 +1089,7 @@ static void sfs_open(fuse_req_t req, fuse_ino_t ino, fuse_file_info *fi) /* Unfortunately we cannot use inode.fd, because this was opened with O_PATH (so it doesn't allow read/write access). */ - auto fd = with_fd_path(inode.fd, [fi](const char* buf) { + auto fd = with_fd_path(inode.fd, [fi](const char *buf) { return open(buf, fi->flags & ~O_NOFOLLOW); }); if (fd == -1) { From 63579dffce8ab079a1a1eab047e63dc1211185d2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Aug 2025 16:58:03 +0000 Subject: [PATCH 186/310] build(deps): bump github/codeql-action from 3.29.10 to 3.29.11 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.29.10 to 3.29.11. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/96f518a34f7a870018057716cc4d7a5c014bd61c...3c3833e0f8c1c83d449a7478aa59c036a9165498) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 3.29.11 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index bcc631d0f..ba6422916 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -53,7 +53,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@96f518a34f7a870018057716cc4d7a5c014bd61c # v3.29.5 + uses: github/codeql-action/init@3c3833e0f8c1c83d449a7478aa59c036a9165498 # v3.29.5 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -78,7 +78,7 @@ jobs: meson compile -C build - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@96f518a34f7a870018057716cc4d7a5c014bd61c # v3.29.5 + uses: github/codeql-action/analyze@3c3833e0f8c1c83d449a7478aa59c036a9165498 # v3.29.5 with: category: "/language:${{matrix.language}}" From 49ad73a46b6237f0f1584b30ade81a3e6744e695 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Sep 2025 22:05:47 +0000 Subject: [PATCH 187/310] build(deps): bump github/codeql-action from 3.29.11 to 3.30.1 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.29.11 to 3.30.1. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/3c3833e0f8c1c83d449a7478aa59c036a9165498...f1f6e5f6af878fb37288ce1c627459e94dbf7d01) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 3.30.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index ba6422916..f919a086a 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -53,7 +53,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@3c3833e0f8c1c83d449a7478aa59c036a9165498 # v3.29.5 + uses: github/codeql-action/init@f1f6e5f6af878fb37288ce1c627459e94dbf7d01 # v3.29.5 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -78,7 +78,7 @@ jobs: meson compile -C build - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@3c3833e0f8c1c83d449a7478aa59c036a9165498 # v3.29.5 + uses: github/codeql-action/analyze@f1f6e5f6af878fb37288ce1c627459e94dbf7d01 # v3.29.5 with: category: "/language:${{matrix.language}}" From 3ff309ea9efa8fc95274d35a96aa5700f0a0d6c1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Sep 2025 22:05:38 +0000 Subject: [PATCH 188/310] build(deps): bump actions/setup-python from 5 to 6 Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 6. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/setup-python dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> --- .github/workflows/abicheck.yml | 2 +- .github/workflows/pr-ci.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/abicheck.yml b/.github/workflows/abicheck.yml index 8ab13da8a..a00449c3a 100644 --- a/.github/workflows/abicheck.yml +++ b/.github/workflows/abicheck.yml @@ -37,7 +37,7 @@ jobs: 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' diff --git a/.github/workflows/pr-ci.yml b/.github/workflows/pr-ci.yml index b0134cf6b..ebd79c427 100644 --- a/.github/workflows/pr-ci.yml +++ b/.github/workflows/pr-ci.yml @@ -34,7 +34,7 @@ jobs: libpcap0.8-dev:i386 libudev-dev:i386 pkg-config:i386 \ liburing-dev libnuma-dev - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: '3.12' - run: pip install -r requirements.txt From 2317d8602334093ea0c88efee2e6ac326d42adc7 Mon Sep 17 00:00:00 2001 From: Miklos Szeredi <mszeredi@redhat.com> Date: Tue, 5 Aug 2025 20:31:53 +0200 Subject: [PATCH 189/310] libfuse: fix COPY_FILE_RANGE interface The FUSE protocol uses struct fuse_write_out to convey the return value of copy_file_range, which is restricted to uint32_t. But the COPY_FILE_RANGE interface supports a 64-bit size copies. Currently the number of bytes copied is silently truncated to 32-bit, which is unfortunate at best. Implement the COPY_FILE_RANGE_64 interface which is identical to the old one, except the number of bytes copied is returned in a 64-bit value. The library interface remains the same. If the kernel does not support the new interface or the server is running as a 32-bit process, limit the copy size to size to UINT_MAX - 4096. Edit by Bernd: Keep ioctl_64bit and add use new bit is_copy_file_range_64 to keep flags separated from each other - easier code readability IMO. Reported-by: Florian Weimer <fweimer@redhat.com> Closes: https://lore.kernel.org/all/lhuh5ynl8z5.fsf@oldenburg.str.redhat.com/ Signed-off-by: Miklos Szeredi <mszeredi@redhat.com> Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- include/fuse_kernel.h | 12 +++++++- lib/fuse_i.h | 1 + lib/fuse_lowlevel.c | 66 +++++++++++++++++++++++++++++++++++++++---- 3 files changed, 73 insertions(+), 6 deletions(-) diff --git a/include/fuse_kernel.h b/include/fuse_kernel.h index 122d6586e..94621f68a 100644 --- a/include/fuse_kernel.h +++ b/include/fuse_kernel.h @@ -235,6 +235,10 @@ * * 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 @@ -270,7 +274,7 @@ #define FUSE_KERNEL_VERSION 7 /** Minor version number of this interface */ -#define FUSE_KERNEL_MINOR_VERSION 44 +#define FUSE_KERNEL_MINOR_VERSION 45 /** The node ID of the root inode */ #define FUSE_ROOT_ID 1 @@ -657,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, @@ -1148,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 { diff --git a/lib/fuse_i.h b/lib/fuse_i.h index 0d0e637ae..d3d86d417 100644 --- a/lib/fuse_i.h +++ b/lib/fuse_i.h @@ -40,6 +40,7 @@ struct fuse_req { int interrupted; unsigned int ioctl_64bit : 1; unsigned int is_uring : 1; + unsigned int is_copy_file_range_64 : 1; union { struct { uint64_t unique; diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index e4544dfc3..1e4531576 100644 --- a/lib/fuse_lowlevel.c +++ b/lib/fuse_lowlevel.c @@ -589,7 +589,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; @@ -599,6 +599,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->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); @@ -2403,11 +2425,9 @@ static void do_fallocate(fuse_req_t req, const fuse_ino_t nodeid, _do_fallocate(req, nodeid, inarg, NULL); } -static void _do_copy_file_range(fuse_req_t req, const fuse_ino_t nodeid_in, - const void *op_in, const void *in_payload) +static void copy_file_range_common(fuse_req_t req, const fuse_ino_t nodeid_in, + const struct fuse_copy_file_range_in *arg) { - (void)in_payload; - const struct fuse_copy_file_range_in *arg = op_in; struct fuse_file_info fi_in, fi_out; memset(&fi_in, 0, sizeof(fi_in)); @@ -2424,12 +2444,46 @@ static void _do_copy_file_range(fuse_req_t req, const fuse_ino_t nodeid_in, fuse_reply_err(req, ENOSYS); } +static void _do_copy_file_range(fuse_req_t req, const fuse_ino_t nodeid_in, + const void *op_in, const void *in_payload) +{ + 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->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. @@ -3378,6 +3432,7 @@ 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" }, @@ -3433,6 +3488,7 @@ 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" }, From 939fc0dd3ed525c0be780537cfa41ffcbf98648c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Sep 2025 22:05:46 +0000 Subject: [PATCH 190/310] build(deps): bump github/codeql-action from 3.30.1 to 3.30.3 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.30.1 to 3.30.3. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/f1f6e5f6af878fb37288ce1c627459e94dbf7d01...192325c86100d080feab897ff886c34abd4c83a3) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 3.30.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index f919a086a..19a4569a8 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -53,7 +53,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@f1f6e5f6af878fb37288ce1c627459e94dbf7d01 # v3.29.5 + uses: github/codeql-action/init@192325c86100d080feab897ff886c34abd4c83a3 # v3.29.5 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -78,7 +78,7 @@ jobs: meson compile -C build - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@f1f6e5f6af878fb37288ce1c627459e94dbf7d01 # v3.29.5 + uses: github/codeql-action/analyze@192325c86100d080feab897ff886c34abd4c83a3 # v3.29.5 with: category: "/language:${{matrix.language}}" From 6e4557c57c4ce63de4042f77dcbf58627d13a59b Mon Sep 17 00:00:00 2001 From: "Darrick J. Wong" <djwong@kernel.org> Date: Mon, 15 Sep 2025 17:41:47 -0700 Subject: [PATCH 191/310] libfuse: don't put HAVE_STATX in a public header fuse.h and fuse_lowlevel.h are already forward declaring struct statx, there is no need for HAVE_STATX anymore. HAVE_STATX also bears the risk to conflict with an application define. Alternatively it would have been possible to change to HAVE_FUSE_STATX. Get rid of the conditionals in the public header files and also remove HAVE_STATX definition from the public libfuse_config.h. Edit by Bernd: Commit message and removal of HAVE_STATX from public libfuse_config.h. Signed-off-by: Darrick J. Wong <djwong@kernel.org> Signed-off-by: Bernd Schubert <bernd@bsbernd.com> --- example/memfs_ll.cc | 2 +- example/passthrough.c | 2 +- example/passthrough_fh.c | 2 +- example/passthrough_ll.c | 2 +- include/fuse.h | 2 -- include/fuse_lowlevel.h | 2 -- meson.build | 2 -- 7 files changed, 4 insertions(+), 10 deletions(-) diff --git a/example/memfs_ll.cc b/example/memfs_ll.cc index edda34b4e..7055a434a 100644 --- a/example/memfs_ll.cc +++ b/example/memfs_ll.cc @@ -6,7 +6,7 @@ See the file GPL2.txt. */ -#define FUSE_USE_VERSION 317 +#define FUSE_USE_VERSION FUSE_MAKE_VERSION(3, 18) #include <algorithm> #include <stdio.h> diff --git a/example/passthrough.c b/example/passthrough.c index fdaa19e33..1f09c2dc0 100644 --- a/example/passthrough.c +++ b/example/passthrough.c @@ -23,7 +23,7 @@ */ -#define FUSE_USE_VERSION 31 +#define FUSE_USE_VERSION FUSE_MAKE_VERSION(3, 18) #define _GNU_SOURCE diff --git a/example/passthrough_fh.c b/example/passthrough_fh.c index 0d4fb5bd4..6403fbb74 100644 --- a/example/passthrough_fh.c +++ b/example/passthrough_fh.c @@ -23,7 +23,7 @@ * \include passthrough_fh.c */ -#define FUSE_USE_VERSION 31 +#define FUSE_USE_VERSION FUSE_MAKE_VERSION(3, 18) #define _GNU_SOURCE diff --git a/example/passthrough_ll.c b/example/passthrough_ll.c index 5ca6efa23..8a5ac2e92 100644 --- a/example/passthrough_ll.c +++ b/example/passthrough_ll.c @@ -35,7 +35,7 @@ */ #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> diff --git a/include/fuse.h b/include/fuse.h index 06feacb07..209102651 100644 --- a/include/fuse.h +++ b/include/fuse.h @@ -854,7 +854,6 @@ struct fuse_operations { */ off_t (*lseek) (const char *, off_t off, int whence, struct fuse_file_info *); -#ifdef HAVE_STATX /** * Get extended file attributes. * @@ -865,7 +864,6 @@ struct fuse_operations { */ int (*statx)(const char *path, int flags, int mask, struct statx *stxbuf, struct fuse_file_info *fi); -#endif }; /** Extra context that may be needed by some filesystems diff --git a/include/fuse_lowlevel.h b/include/fuse_lowlevel.h index dc4ec07e6..a6cce0143 100644 --- a/include/fuse_lowlevel.h +++ b/include/fuse_lowlevel.h @@ -1327,7 +1327,6 @@ struct fuse_lowlevel_ops { void (*tmpfile) (fuse_req_t req, fuse_ino_t parent, mode_t mode, struct fuse_file_info *fi); -#ifdef HAVE_STATX /** * Get extended file attributes. * @@ -1343,7 +1342,6 @@ struct fuse_lowlevel_ops { */ void (*statx)(fuse_req_t req, fuse_ino_t ino, int flags, int mask, struct fuse_file_info *fi); -#endif }; /** diff --git a/meson.build b/meson.build index 07c729581..1dd687c1a 100644 --- a/meson.build +++ b/meson.build @@ -122,8 +122,6 @@ 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>')) -public_cfg.set('HAVE_STATX', - cc.has_function('statx', prefix : '#define _GNU_SOURCE\n#include <sys/stat.h>')) private_cfg.set('HAVE_STATX', cc.has_function('statx', prefix : '#define _GNU_SOURCE\n#include <sys/stat.h>')) From 52a633a5d4435f1965fc8d44cf9a705e6679f793 Mon Sep 17 00:00:00 2001 From: Alik Aslanyan <inline0@pm.me> Date: Tue, 9 Sep 2025 23:35:40 +0400 Subject: [PATCH 192/310] fuse_lowlevel.c: allow passing ops as NULL During testing of LACT using various sanitizers issue with libfuse.so leaking memory and reading uninitialized memory was uncovered. While this was due to ABI misusage on the side of fuser (Rust wrapper for libfuse.so), we now allow creating a no-op session by passing ops as NULL. fuser and other libfuse users is using fuse_session_new() with NULL struct fuse_lowlevel_ops *op and op_size=0. This resulted in a warning, but otherwise succeeded. The resulting fuse_session is just passed to fuse_session_mount() so that libfuse could do the actual mount. Fuse kernel request handling is not done through libfuse but their own layer. Edit by Bernd: - Update the commit message - moved using null_ops to fuse_session_new_30() as that is actually called by the external fuse_session_new(). - forbidding NULL struct fuse_lowlevel_ops *op and 0 op_size in fuse_session_new_versioned() - various other NULL ptr checks in fuse_session_new() that now result in failure. Signed-off-by: Alik Aslanyan <inline0@pm.me> Signed-off-by: Bernd Schubert <bernd@bsbernd.com> --- include/fuse_lowlevel.h | 2 ++ lib/fuse_lowlevel.c | 26 +++++++++++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/include/fuse_lowlevel.h b/include/fuse_lowlevel.h index a6cce0143..312b33133 100644 --- a/include/fuse_lowlevel.h +++ b/include/fuse_lowlevel.h @@ -2115,6 +2115,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 diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index 1e4531576..cb757cd92 100644 --- a/lib/fuse_lowlevel.c +++ b/lib/fuse_lowlevel.c @@ -4108,12 +4108,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; } @@ -4225,9 +4236,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); } From 3e2cd9e46c87a57de374b82fd198328f7745e942 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Thu, 18 Sep 2025 23:39:11 +0200 Subject: [PATCH 193/310] fuse_log: Add __attribute__((format(printf, ) and fix warnings fuse_log() did not have that attribute and so compilers didn't give warnings for plain printf(). Add the attribute and fix related warnings. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> --- example/passthrough_ll.c | 30 ++++++++++++++++-------------- include/fuse_log.h | 3 ++- lib/fuse.c | 8 ++++---- lib/fuse_lowlevel.c | 23 +++++++++++++++-------- lib/fuse_uring.c | 5 +++-- lib/mount.c | 2 +- 6 files changed, 41 insertions(+), 30 deletions(-) diff --git a/example/passthrough_ll.c b/example/passthrough_ll.c index 8a5ac2e92..dce8355cb 100644 --- a/example/passthrough_ll.c +++ b/example/passthrough_ll.c @@ -315,7 +315,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 +352,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 +712,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 +744,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 +816,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 +981,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) @@ -1187,10 +1187,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, diff --git a/include/fuse_log.h b/include/fuse_log.h index b901db252..7948bab14 100644 --- a/include/fuse_log.h +++ b/include/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/lib/fuse.c b/lib/fuse.c index e997531f4..4cc6f3b1c 100644 --- a/lib/fuse.c +++ b/lib/fuse.c @@ -2122,10 +2122,10 @@ int fuse_fs_utimens(struct fuse_fs *fs, const char *path, 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); + 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); } diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index cb757cd92..273d4dc6a 100644 --- a/lib/fuse_lowlevel.c +++ b/lib/fuse_lowlevel.c @@ -23,6 +23,7 @@ #include <pthread.h> #include <stdatomic.h> #include <stdint.h> +#include <inttypes.h> #include <stdbool.h> #include <stdio.h> #include <stdlib.h> @@ -753,11 +754,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; @@ -2550,8 +2555,8 @@ 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; @@ -2577,10 +2582,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; } diff --git a/lib/fuse_uring.c b/lib/fuse_uring.c index 4c6f0a483..85b5a7fe3 100644 --- a/lib/fuse_uring.c +++ b/lib/fuse_uring.c @@ -18,6 +18,7 @@ #include <liburing.h> #include <sys/sysinfo.h> #include <stdint.h> +#include <inttypes.h> #include <stdbool.h> #include <string.h> #include <unistd.h> @@ -180,7 +181,7 @@ static int fuse_uring_commit_sqe(struct fuse_ring_pool *ring_pool, ring_ent->req_commit_id); if (se->debug) { - fuse_log(FUSE_LOG_DEBUG, " unique: %llu, result=%d\n", + fuse_log(FUSE_LOG_DEBUG, " unique: %" PRIu64 ", result=%d\n", out->unique, ent_in_out->payload_sz); } @@ -453,7 +454,7 @@ static int fuse_uring_prepare_fetch_sqes(struct fuse_ring_queue *queue) sq_ready = io_uring_sq_ready(&queue->ring); if (sq_ready != ring_pool->queue_depth) { fuse_log(FUSE_LOG_ERR, - "SQE ready mismatch, expected %d got %d\n", + "SQE ready mismatch, expected %zu got %u\n", ring_pool->queue_depth, sq_ready); return -EINVAL; } diff --git a/lib/mount.c b/lib/mount.c index 2eb967399..549268006 100644 --- a/lib/mount.c +++ b/lib/mount.c @@ -361,7 +361,7 @@ static int setup_auto_unmount(const char *mountpoint, int quiet) res = socketpair(PF_UNIX, SOCK_STREAM, 0, fds); if(res == -1) { - fuse_log(FUSE_LOG_ERR, "Setting up auto-unmountsocketpair() failed", + fuse_log(FUSE_LOG_ERR, "Setting up auto-unmount socketpair() failed: %s\n", strerror(errno)); return -1; } From 50d1bec4794aae9e77062de5214b91bcc52a890d Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Thu, 18 Sep 2025 23:49:41 +0200 Subject: [PATCH 194/310] io-uring: re-initialize fields in struct fuse_req In io-uring mode these requests are always re-used and not allocated - we need to re-initialize them. In order to set flags to zero a struct holding the flags had to be added. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> --- lib/fuse_i.h | 9 ++++++--- lib/fuse_lowlevel.c | 23 +++++++++++------------ lib/fuse_uring.c | 13 +++++++++---- 3 files changed, 26 insertions(+), 19 deletions(-) diff --git a/lib/fuse_i.h b/lib/fuse_i.h index d3d86d417..d35e1e51d 100644 --- a/lib/fuse_i.h +++ b/lib/fuse_i.h @@ -38,9 +38,11 @@ struct fuse_req { struct fuse_ctx ctx; struct fuse_chan *ch; int interrupted; - unsigned int ioctl_64bit : 1; - unsigned int is_uring : 1; - unsigned int is_copy_file_range_64 : 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; @@ -218,6 +220,7 @@ 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); diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index 273d4dc6a..cacab94bc 100644 --- a/lib/fuse_lowlevel.c +++ b/lib/fuse_lowlevel.c @@ -146,7 +146,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; @@ -171,7 +171,7 @@ static void list_add_req(struct fuse_req *req, struct fuse_req *next) static void destroy_req(fuse_req_t req) { - if (req->is_uring) { + if (req->flags.is_uring) { fuse_log(FUSE_LOG_ERR, "Refusing to destruct uring req\n"); return; } @@ -189,7 +189,7 @@ void fuse_free_req(fuse_req_t req) * It actually might work already, though. But then would add * a lock across ring queues. */ - if (se->conn.no_interrupt || req->is_uring) { + if (se->conn.no_interrupt || req->flags.is_uring) { ctr = --req->ref_cnt; fuse_chan_put(req->ch); req->ch = NULL; @@ -219,7 +219,6 @@ static struct fuse_req *fuse_ll_alloc_req(struct fuse_session *se) req->ref_cnt = 1; list_init_req(req); pthread_mutex_init(&req->lock, NULL); - req->is_uring = false; } return req; @@ -260,7 +259,7 @@ static int fuse_send_msg(struct fuse_session *se, struct fuse_chan *ch, { struct fuse_out_header *out = iov[0].iov_base; int err; - bool is_uring = req && req->is_uring ? true : false; + bool is_uring = req && req->flags.is_uring ? true : false; if (!is_uring) assert(se != NULL); @@ -328,7 +327,7 @@ 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->is_uring) + if (req->flags.is_uring) return send_reply_uring(req, error, arg, argsize); struct iovec iov[2]; @@ -616,7 +615,7 @@ 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->is_copy_file_range_64) + if (req->flags.is_copy_file_range_64) return do_fuse_reply_copy(req, count); else return do_fuse_reply_write(req, count); @@ -1023,7 +1022,7 @@ int fuse_reply_data(fuse_req_t req, struct fuse_bufvec *bufv, struct fuse_out_header out; int res; - if (req->is_uring) + if (req->flags.is_uring) return fuse_reply_data_uring(req, bufv, flags); iov[0].iov_base = &out; @@ -1141,7 +1140,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; } @@ -2349,7 +2348,7 @@ static void _do_ioctl(fuse_req_t req, const fuse_ino_t nodeid, 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) @@ -2475,7 +2474,7 @@ 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->is_copy_file_range_64 = 1; + 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); @@ -3375,7 +3374,7 @@ int fuse_req_interrupted(fuse_req_t req) bool fuse_req_is_uring(fuse_req_t req) { - return req->is_uring; + return req->flags.is_uring; } #ifndef HAVE_URING diff --git a/lib/fuse_uring.c b/lib/fuse_uring.c index 85b5a7fe3..2fea05f97 100644 --- a/lib/fuse_uring.c +++ b/lib/fuse_uring.c @@ -197,7 +197,7 @@ int fuse_req_get_payload(fuse_req_t req, char **payload, size_t *payload_sz, struct fuse_ring_ent *ring_ent; /* Not possible without io-uring interface */ - if (!req->is_uring) + if (!req->flags.is_uring) return -EINVAL; ring_ent = container_of(req, struct fuse_ring_ent, req); @@ -560,9 +560,13 @@ static void fuse_uring_handle_cqe(struct fuse_ring_queue *queue, abort(); } - req->is_uring = true; + 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); @@ -696,8 +700,9 @@ static int fuse_uring_init_queue(struct fuse_ring_queue *queue) req->se = se; pthread_mutex_init(&req->lock, NULL); - req->is_uring = true; - req->ref_cnt = 1; + req->flags.is_uring = 1; + req->ref_cnt = 1; /* extra ref to avoid destruction */ + list_init_req(req); } res = fuse_uring_prepare_fetch_sqes(queue); From 30bf3c86a933887407da901a05d726cebf6c5793 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Sun, 31 Aug 2025 21:56:34 +0200 Subject: [PATCH 195/310] fuse_loop_mt: Don't create new workers before init is received This is to prevent startup races. For example it is hard for threads to know the buffer size the kernel usage. In the past kernel buffer size always was max 1MB, but in the mean time this might be more, depending on system tunings. I thought I had fixed all races, but the report in issue #1296 proves otherwise. Closes: https://github.com/libfuse/libfuse/issues/1296 Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- lib/fuse_loop_mt.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/fuse_loop_mt.c b/lib/fuse_loop_mt.c index 77712b6a2..ec4bb0b2b 100644 --- a/lib/fuse_loop_mt.c +++ b/lib/fuse_loop_mt.c @@ -171,7 +171,8 @@ 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(&se->mt_lock); From 59cf26a2d778b88819db528c67141b788a8b48a1 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Mon, 22 Sep 2025 10:46:46 +0200 Subject: [PATCH 196/310] FUSE_INIT handler: Set got_init only shortly before send_reply_ok A previous commit added single thread behavior as long as se->got_init is not set, especially as buffer allocation should be done after got_init only. Technically it should not make a difference in current libfuse design, as more worker threads are only allocated after the FUSE_INIT handler is completely done. That might at some point change, though. In general it is cleaner to set se->got_init, when the init handler is done. Done as separate commit, as this introduces a slight risk to break something - backport to older version should be done carefully. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> --- lib/fuse_lowlevel.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index cacab94bc..a1a1427d0 100644 --- a/lib/fuse_lowlevel.c +++ b/lib/fuse_lowlevel.c @@ -2799,7 +2799,6 @@ _do_init(fuse_req_t req, const fuse_ino_t nodeid, const void *op_in, 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); @@ -2970,6 +2969,13 @@ _do_init(fuse_req_t req, const fuse_ino_t nodeid, const void *op_in, } } + /* + * 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); From 4337029adf154d94e6fcb69710e4232cd62a3330 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Sep 2025 22:58:24 +0000 Subject: [PATCH 197/310] build(deps): bump github/codeql-action from 3.30.3 to 3.30.5 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.30.3 to 3.30.5. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/192325c86100d080feab897ff886c34abd4c83a3...3599b3baa15b485a2e49ef411a7a4bb2452e7f93) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 3.30.5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 19a4569a8..0d3cdd12a 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -53,7 +53,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@192325c86100d080feab897ff886c34abd4c83a3 # v3.29.5 + uses: github/codeql-action/init@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.29.5 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -78,7 +78,7 @@ jobs: meson compile -C build - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@192325c86100d080feab897ff886c34abd4c83a3 # v3.29.5 + uses: github/codeql-action/analyze@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.29.5 with: category: "/language:${{matrix.language}}" From 382b17dd079f82e7f445de9a9993e0bb6cabaa71 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Tue, 30 Sep 2025 17:41:36 +0200 Subject: [PATCH 198/310] Fix missing \n termination in fusermount_posix_spawn() Also improve the error message a bit. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> --- lib/mount.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/mount.c b/lib/mount.c index 549268006..7a856c101 100644 --- a/lib/mount.c +++ b/lib/mount.c @@ -145,9 +145,8 @@ static int fusermount_posix_spawn(posix_spawn_file_actions_t *action, } if (status != 0) { - fuse_log(FUSE_LOG_ERR, - "On calling fusermount posix_spawn failed: %s\n", - strerror(status)); + fuse_log(FUSE_LOG_ERR, "Failed to call '%s': %s\n", + FUSERMOUNT_PROG, strerror(status)); return -status; } From 75e32d9f250e0541551685ff5c412b8daef28226 Mon Sep 17 00:00:00 2001 From: Zeno Sebastian Endemann <zeno.endemann@mailbox.org> Date: Sun, 5 Oct 2025 15:39:03 +0200 Subject: [PATCH 199/310] Fixup passthrough_ll.c documentation As far as I can tell, all important operations are implemented nowadays. Signed-off-by: Zeno Sebastian Endemann <zeno.endemann@mailbox.org> --- example/passthrough_ll.c | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/example/passthrough_ll.c b/example/passthrough_ll.c index dce8355cb..7dfaee370 100644 --- a/example/passthrough_ll.c +++ b/example/passthrough_ll.c @@ -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 From e12762b71c1cf37a9da535873a9ae17334610cb6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Oct 2025 22:16:13 +0000 Subject: [PATCH 200/310] build(deps): bump github/codeql-action from 3.30.5 to 3.30.6 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.30.5 to 3.30.6. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/3599b3baa15b485a2e49ef411a7a4bb2452e7f93...64d10c13136e1c5bce3e5fbde8d4906eeaafc885) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 3.30.6 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 0d3cdd12a..e8bbc1e78 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -53,7 +53,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.29.5 + uses: github/codeql-action/init@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.29.5 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -78,7 +78,7 @@ jobs: meson compile -C build - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.29.5 + uses: github/codeql-action/analyze@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.29.5 with: category: "/language:${{matrix.language}}" From 564d79e4d3132924d33f0a0848b9d54c84b42a17 Mon Sep 17 00:00:00 2001 From: Zeno Sebastian Endemann <zeno.endemann@mailbox.org> Date: Sun, 12 Oct 2025 10:51:15 +0200 Subject: [PATCH 201/310] fuse_lowlevel.h: Fix doc typo Signed-off-by: Zeno Sebastian Endemann <zeno.endemann@mailbox.org> --- include/fuse_lowlevel.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/fuse_lowlevel.h b/include/fuse_lowlevel.h index 312b33133..c41ad8f13 100644 --- a/include/fuse_lowlevel.h +++ b/include/fuse_lowlevel.h @@ -453,7 +453,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 From 36a1c49fcb890280f96207e590653a9904fa75c6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Oct 2025 22:04:56 +0000 Subject: [PATCH 202/310] build(deps): bump github/codeql-action from 3.30.6 to 4.30.8 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.30.6 to 4.30.8. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/64d10c13136e1c5bce3e5fbde8d4906eeaafc885...f443b600d91635bebf5b0d9ebc620189c0d6fba5) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 4.30.8 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index e8bbc1e78..9e987d091 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -53,7 +53,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.29.5 + uses: github/codeql-action/init@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v3.29.5 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -78,7 +78,7 @@ jobs: meson compile -C build - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.29.5 + uses: github/codeql-action/analyze@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v3.29.5 with: category: "/language:${{matrix.language}}" From 2bb125e8c4c34f2ce290d1d7349c344e9fd4376c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Oct 2025 22:05:39 +0000 Subject: [PATCH 203/310] build(deps): bump github/codeql-action from 4.30.8 to 4.30.9 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.30.8 to 4.30.9. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/f443b600d91635bebf5b0d9ebc620189c0d6fba5...16140ae1a102900babc80a33c44059580f687047) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 4.30.9 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 9e987d091..f93776fee 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -53,7 +53,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v3.29.5 + uses: github/codeql-action/init@16140ae1a102900babc80a33c44059580f687047 # v3.29.5 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -78,7 +78,7 @@ jobs: meson compile -C build - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v3.29.5 + uses: github/codeql-action/analyze@16140ae1a102900babc80a33c44059580f687047 # v3.29.5 with: category: "/language:${{matrix.language}}" From f7da4e965bcbae2627db4f40764998a61c5fcfdb Mon Sep 17 00:00:00 2001 From: Joanne Koong <joannelkoong@gmail.com> Date: Thu, 23 Oct 2025 09:55:28 -0700 Subject: [PATCH 204/310] init: fix io-uring mount hang If libfuse cannot set up the server to use io-uring, then it should not send FUSE_OVER_IO_URING in the init reply. outarg.flags needs to be set after FUSE_OVER_IO_URING is unset in the error case where io-uring setup fails, else the mount will hang. Signed-off-by: Joanne Koong <joannelkoong@gmail.com> --- lib/fuse_lowlevel.c | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index a1a1427d0..d420b257b 100644 --- a/lib/fuse_lowlevel.c +++ b/lib/fuse_lowlevel.c @@ -2910,13 +2910,6 @@ _do_init(fuse_req_t req, const fuse_ino_t nodeid, const void *op_in, outarg.request_timeout = se->conn.request_timeout; } - if (inargflags & FUSE_INIT_EXT) { - outargflags |= FUSE_INIT_EXT; - outarg.flags2 = outargflags >> 32; - } - - outarg.flags = outargflags; - outarg.max_readahead = se->conn.max_readahead; outarg.max_write = se->conn.max_write; if (se->conn.proto_minor >= 13) { @@ -2969,6 +2962,12 @@ _do_init(fuse_req_t req, const fuse_ino_t nodeid, const void *op_in, } } + 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. From bf3dd153fbfcd610d799562490f6555b9d708905 Mon Sep 17 00:00:00 2001 From: Ben Linsay <blinsay@gmail.com> Date: Wed, 22 Oct 2025 12:02:55 -0400 Subject: [PATCH 205/310] fusermount: count mounts with listmount and statmount reading from /proc is not atomic. /etc/mtab specifically is only atomic within a single read syscall, so it's possible to see repeated or stale mounts reading it line by line. this can sometimes cause fusermount to see the same mount hundreds or thousands of times which results in spurious mount failures if fuse_mount_max is not set high enough. listmount and statmount were introduced as apis to work around the limitations of parsing proc. fusermount now tries to use those syscalls when available, and falls back to reading /etc/mtab if that fails. these apis do not have libc wrappers yet. Signed-off-by: Ben Linsay <blinsay@gmail.com> --- meson.build | 19 ++++++++++++ util/fusermount.c | 74 ++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 92 insertions(+), 1 deletion(-) diff --git a/meson.build b/meson.build index 1dd687c1a..e3c7eeba6 100644 --- a/meson.build +++ b/meson.build @@ -99,6 +99,25 @@ special_funcs = { 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; + } + } ''' } diff --git a/util/fusermount.c b/util/fusermount.c index 9cc08b4bf..7f19df7a8 100644 --- a/util/fusermount.c +++ b/util/fusermount.c @@ -40,6 +40,12 @@ #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" @@ -566,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; @@ -586,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) From fd57ba30e35256d1a636bb903aba1d0ad6c01c1b Mon Sep 17 00:00:00 2001 From: Zeno Sebastian Endemann <zeno.endemann@mailbox.org> Date: Wed, 29 Oct 2025 14:55:39 +0100 Subject: [PATCH 206/310] fuse_lowlevel.h: remove incorrect documentation (#1357) It seems fuse_reply_open() cannot actually return -ENOENT under recoverable conditions, so remove the corresponding paragraph claiming otherwise. For a discussion on this see https://github.com/libfuse/libfuse/discussions/1262 Also add an additional note file, which explains fuse_reply errors Signed-off-by: Zeno Endemann <zeno.endemann@mailbox.org> --- doc/README.fuse_reply_errors | 5 +++++ include/fuse_lowlevel.h | 9 ++++----- 2 files changed, 9 insertions(+), 5 deletions(-) create mode 100644 doc/README.fuse_reply_errors 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/include/fuse_lowlevel.h b/include/fuse_lowlevel.h index c41ad8f13..6be0295b7 100644 --- a/include/fuse_lowlevel.h +++ b/include/fuse_lowlevel.h @@ -197,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 From c2168333c63131b3dcb3fc244fd8f3f6d3fdbde9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Oct 2025 22:28:05 +0000 Subject: [PATCH 207/310] build(deps): bump github/codeql-action from 4.30.9 to 4.31.0 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.30.9 to 4.31.0. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/16140ae1a102900babc80a33c44059580f687047...4e94bd11f71e507f7f87df81788dff88d1dacbfb) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 4.31.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index f93776fee..be56ede06 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -53,7 +53,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@16140ae1a102900babc80a33c44059580f687047 # v3.29.5 + uses: github/codeql-action/init@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v3.29.5 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -78,7 +78,7 @@ jobs: meson compile -C build - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@16140ae1a102900babc80a33c44059580f687047 # v3.29.5 + uses: github/codeql-action/analyze@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v3.29.5 with: category: "/language:${{matrix.language}}" From e2886cb91518a885e581c10d80095ed01d44cc6c Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Wed, 5 Nov 2025 15:59:30 +0100 Subject: [PATCH 208/310] Add FUSE_COPY_FILE_RANGE_64 to doc/libfuse-operations.txt Just a doc update to add FUSE_COPY_FILE_RANGE_64. Also corrects FUSE_STATFS, which had an incorrects in_args[0], it actually does not have any inargs. Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- doc/libfuse-operations.txt | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) 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 From 6247606ed8e7fd5dc046061d0e8795fec6d6eb10 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 22:08:18 +0000 Subject: [PATCH 209/310] build(deps): bump github/codeql-action from 4.31.0 to 4.31.2 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.31.0 to 4.31.2. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/4e94bd11f71e507f7f87df81788dff88d1dacbfb...0499de31b99561a6d14a36a5f662c2a54f91beee) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 4.31.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index be56ede06..39f6e5ba4 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -53,7 +53,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v3.29.5 + uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # v3.29.5 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -78,7 +78,7 @@ jobs: meson compile -C build - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v3.29.5 + uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # v3.29.5 with: category: "/language:${{matrix.language}}" From f58d4c5b0d56116d8870753f6b9d1620ee082709 Mon Sep 17 00:00:00 2001 From: Miklos Szeredi <mszeredi@redhat.com> Date: Mon, 10 Nov 2025 14:24:22 +0100 Subject: [PATCH 210/310] tests: fix a race in wait_for_mount() Recheck if target is mounted after the mount process exited, otherwise the test can fail erronously. Signed-off-by: Miklos Szeredi <mszeredi@redhat.com> --- test/util.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/util.py b/test/util.py index a421e7213..125fd50fe 100644 --- a/test/util.py +++ b/test/util.py @@ -50,6 +50,8 @@ def wait_for_mount(mount_process, mnt_dir, if test_fn(mnt_dir): return True if mount_process.poll() is not None: + if test_fn(mnt_dir): + return True pytest.fail('file system process terminated prematurely') time.sleep(0.1) elapsed += 0.1 From c71727d992c7817cf7a8da8c5a6e5876babc8314 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 22:08:54 +0000 Subject: [PATCH 211/310] build(deps): bump codespell-project/actions-codespell from 2.1 to 2.2 Bumps [codespell-project/actions-codespell](https://github.com/codespell-project/actions-codespell) from 2.1 to 2.2. - [Release notes](https://github.com/codespell-project/actions-codespell/releases) - [Commits](https://github.com/codespell-project/actions-codespell/compare/406322ec52dd7b488e48c1c4b82e2a8b3a1bf630...8f01853be192eb0f849a5c7d721450e7a467c579) --- updated-dependencies: - dependency-name: codespell-project/actions-codespell dependency-version: '2.2' dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> --- .github/workflows/codespell.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml index 4c2288b7a..629673606 100644 --- a/.github/workflows/codespell.yml +++ b/.github/workflows/codespell.yml @@ -23,6 +23,6 @@ jobs: - name: Checkout uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Codespell - uses: codespell-project/actions-codespell@406322ec52dd7b488e48c1c4b82e2a8b3a1bf630 # v2.1 + uses: codespell-project/actions-codespell@8f01853be192eb0f849a5c7d721450e7a467c579 # v2.2 with: skip: checkpatch.pl From 6c9a4d698386df1217579615918535afa40380eb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Nov 2025 22:26:33 +0000 Subject: [PATCH 212/310] build(deps): bump github/codeql-action from 4.31.2 to 4.31.3 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.31.2 to 4.31.3. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/0499de31b99561a6d14a36a5f662c2a54f91beee...014f16e7ab1402f30e7c3329d33797e7948572db) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 4.31.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 39f6e5ba4..d6885c8da 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -53,7 +53,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # v3.29.5 + uses: github/codeql-action/init@014f16e7ab1402f30e7c3329d33797e7948572db # v3.29.5 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -78,7 +78,7 @@ jobs: meson compile -C build - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # v3.29.5 + uses: github/codeql-action/analyze@014f16e7ab1402f30e7c3329d33797e7948572db # v3.29.5 with: category: "/language:${{matrix.language}}" From 0fcaec3276d4e380d4084fb22854bf02ae31712e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Nov 2025 22:28:12 +0000 Subject: [PATCH 213/310] build(deps): bump actions/checkout from 5.0.0 to 5.0.1 Bumps [actions/checkout](https://github.com/actions/checkout) from 5.0.0 to 5.0.1. - [Release notes](https://github.com/actions/checkout/releases) - [Commits](https://github.com/actions/checkout/compare/v5...v5.0.1) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 5.0.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> --- .github/workflows/abicheck.yml | 4 ++-- .github/workflows/bsd.yaml | 2 +- .github/workflows/checkpatch.yml | 2 +- .github/workflows/codeql.yml | 2 +- .github/workflows/codespell.yml | 2 +- .github/workflows/iwyi-check.yml | 2 +- .github/workflows/pr-ci.yml | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/abicheck.yml b/.github/workflows/abicheck.yml index a00449c3a..0dc38a833 100644 --- a/.github/workflows/abicheck.yml +++ b/.github/workflows/abicheck.yml @@ -28,11 +28,11 @@ jobs: sudo apt-get update sudo apt-get -y install abigail-tools clang gcc liburing-dev libnuma-dev - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: path: current - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: path: previous ref: ${{ github.event.pull_request.base.ref }} diff --git a/.github/workflows/bsd.yaml b/.github/workflows/bsd.yaml index 13b30299b..02c1c1c01 100644 --- a/.github/workflows/bsd.yaml +++ b/.github/workflows/bsd.yaml @@ -18,7 +18,7 @@ jobs: name: Build under FreeBSD steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v5.0.1 - name: Build uses: vmactions/freebsd-vm@v1 with: diff --git a/.github/workflows/checkpatch.yml b/.github/workflows/checkpatch.yml index 86b16aae1..5d71d4bcc 100644 --- a/.github/workflows/checkpatch.yml +++ b/.github/workflows/checkpatch.yml @@ -10,7 +10,7 @@ jobs: checkpatch: runs-on: ubuntu-latest steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: fetch-depth: 0 - name: Install dependencies diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index d6885c8da..3e8b265d7 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -49,7 +49,7 @@ jobs: build-mode: manual steps: - name: Checkout repository - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml index 629673606..fd73ec5c5 100644 --- a/.github/workflows/codespell.yml +++ b/.github/workflows/codespell.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Codespell uses: codespell-project/actions-codespell@8f01853be192eb0f849a5c7d721450e7a467c579 # v2.2 with: diff --git a/.github/workflows/iwyi-check.yml b/.github/workflows/iwyi-check.yml index 0718c3155..a2ff1c29b 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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: fetch-depth: 0 diff --git a/.github/workflows/pr-ci.yml b/.github/workflows/pr-ci.yml index ebd79c427..ea7ebd8e8 100644 --- a/.github/workflows/pr-ci.yml +++ b/.github/workflows/pr-ci.yml @@ -33,7 +33,7 @@ jobs: gcc-multilib g++-multilib libc6-dev-i386 \ libpcap0.8-dev:i386 libudev-dev:i386 pkg-config:i386 \ liburing-dev libnuma-dev - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - uses: actions/setup-python@v6 with: python-version: '3.12' From 8ad076e45834f94857b88a0b70f577273d986b77 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Mon, 17 Nov 2025 23:23:10 +0100 Subject: [PATCH 214/310] Initialize fuse_entry_param in fuse_lib_lookup() "struct fuse_entry_param e" was not initialized when get_path_name() returned an error, which made recent clang versions to complain. However, the compiler missed that reply_entry() checks for an error code and ignores fuse_entry_param then. Closes: https://github.com/libfuse/libfuse/issues/1360 Signed-off-by: Bernd Schubert <bernd@bsbernd.com> --- lib/fuse.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/fuse.c b/lib/fuse.c index 4cc6f3b1c..c09564b28 100644 --- a/lib/fuse.c +++ b/lib/fuse.c @@ -2659,7 +2659,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; From 1e19235c546ae3ea002b3b1f5b7e877a5687803e Mon Sep 17 00:00:00 2001 From: Long Li <leo.lilong@huawei.com> Date: Sat, 22 Nov 2025 22:57:23 +0800 Subject: [PATCH 215/310] example/passthrough_hp: fix race between forget_one and do_lookup During fsstress stress testing using passthrough_hp as the backend, the backend process crashes. The root cause is that when forget_one() and do_lookup() concurrently process the same inode, do_lookup may return either an invalid inode or a different inode reusing the same memory address. CPU0 CPU1 ----------------------- -------------------- forget_one do_lookup lock fs inode = fs.inodes[id] //inode.fd > 0 unlock fs lock inode inode.nlookup -= n <inode.nlookup equal to 0> lock fs unlock inode fs.inodes.erase unlock fs lock inode inode.nlookup++ unlock inode <lookup a invalid inode> This can lead to abnormalities in the inode nlookup count. Since the value of inode.nlookup determines the inode's lifecycle, and considering the locking order requirements between the inode lock and fs lock, using the inode lock alone does not resolve the issue effectively. The fix is to convert inode.nlookup to an atomic type, which removes the need for write protection via inode lock, while using fs lock to guard the inode's lifetime. Signed-off-by: Long Li <leo.lilong@huawei.com> --- example/passthrough_hp.cc | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/example/passthrough_hp.cc b/example/passthrough_hp.cc index 1a67704dc..1fa49e739 100644 --- a/example/passthrough_hp.cc +++ b/example/passthrough_hp.cc @@ -78,6 +78,7 @@ #include "cxxopts.hpp" #include <mutex> #include <syslog.h> +#include <atomic> #include "passthrough_helpers.h" @@ -130,7 +131,7 @@ struct Inode { int generation{ 0 }; int backing_id{ 0 }; uint64_t nopen{ 0 }; - uint64_t nlookup{ 0 }; + std::atomic<uint64_t> nlookup{ 0 }; std::mutex m; // Delete copy constructor and assignments. We could implement @@ -411,12 +412,10 @@ static int do_lookup(fuse_ino_t parent, const char *name, fuse_entry_param *e) } 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) @@ -424,6 +423,7 @@ static int do_lookup(fuse_ino_t parent, const char *name, fuse_entry_param *e) << "inode " << inode.src_ino << " count " << inode.nlookup << endl; + fs_lock.unlock(); close(newfd); } else { // no existing inode /* This is just here to make Helgrind happy. It violates the @@ -548,7 +548,6 @@ static void sfs_link(fuse_req_t req, fuse_ino_t ino, fuse_ino_t parent, } e.ino = reinterpret_cast<fuse_ino_t>(&inode); { - lock_guard<mutex> g{ inode.m }; inode.nlookup++; if (fs.debug) cerr << "DEBUG:" << __func__ << ":" << __LINE__ << " " @@ -621,7 +620,6 @@ static void sfs_unlink(fuse_req_t req, fuse_ino_t parent, const char *name) 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 " @@ -636,12 +634,10 @@ static void forget_one(fuse_ino_t ino, uint64_t n) << endl; if (!inode.nlookup) { - if (fs.debug) + lock_guard<mutex> g_fs{ fs.mutex }; + if (!inode.nlookup) { cerr << "DEBUG: forget: cleaning up inode " - << inode.src_ino << endl; - { - lock_guard<mutex> g_fs{ fs.mutex }; - l.unlock(); + << inode.src_ino << endl; fs.inodes.erase({ inode.src_ino, inode.src_dev }); } } else if (fs.debug) From 02c0fc628006b0dcd18cf03f70021619ed817f08 Mon Sep 17 00:00:00 2001 From: Long Li <leo.lilong@huawei.com> Date: Tue, 25 Nov 2025 11:03:59 +0800 Subject: [PATCH 216/310] example/passthrough_hp: fix race in forget_one() When multiple threads concurrently call forget_one() on the same inode, a use-after-free memory issue can occur. forget_one() forget_one() ---------------- --------------- <inode.nlookup == 2> inode.nlookup -= 1 inode.nlookup -= 1 <inode.nlookup == 0> if (!inode.nlookup) fs.inodes.erase() if (!inode.nlookup) {} //UAF Fix it by restoring the inode lock protection in forget_one(). Signed-off-by: Long Li <leo.lilong@huawei.com> --- example/passthrough_hp.cc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/example/passthrough_hp.cc b/example/passthrough_hp.cc index 1fa49e739..fcdac4bf2 100644 --- a/example/passthrough_hp.cc +++ b/example/passthrough_hp.cc @@ -620,6 +620,7 @@ static void sfs_unlink(fuse_req_t req, fuse_ino_t parent, const char *name) 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 " @@ -635,6 +636,7 @@ static void forget_one(fuse_ino_t ino, uint64_t n) if (!inode.nlookup) { lock_guard<mutex> g_fs{ fs.mutex }; + l.unlock(); if (!inode.nlookup) { cerr << "DEBUG: forget: cleaning up inode " << inode.src_ino << endl; From ab5033f5d1bd1630870dc725eb3eae6ef3ef4859 Mon Sep 17 00:00:00 2001 From: Long Li <leo.lilong@huawei.com> Date: Tue, 25 Nov 2025 22:01:48 +0800 Subject: [PATCH 217/310] example/passthrough_hp: fix debug output in forget_one The incorrect removeal of the fs.debug check caused the message "DEBUG: forget: cleaning up inode" to be printed even when debug was not enabled. Signed-off-by: Long Li <leo.lilong@huawei.com> --- example/passthrough_hp.cc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/example/passthrough_hp.cc b/example/passthrough_hp.cc index fcdac4bf2..2dc0f9503 100644 --- a/example/passthrough_hp.cc +++ b/example/passthrough_hp.cc @@ -638,8 +638,9 @@ static void forget_one(fuse_ino_t ino, uint64_t n) lock_guard<mutex> g_fs{ fs.mutex }; l.unlock(); if (!inode.nlookup) { - cerr << "DEBUG: forget: cleaning up inode " - << inode.src_ino << endl; + if (fs.debug) + cerr << "DEBUG: forget: cleaning up inode " + << inode.src_ino << endl; fs.inodes.erase({ inode.src_ino, inode.src_dev }); } } else if (fs.debug) From 6268c6690141d093a378e55061212c6739d709af Mon Sep 17 00:00:00 2001 From: Alexander Monakov <amonakov@ispras.ru> Date: Thu, 27 Nov 2025 00:29:48 +0300 Subject: [PATCH 218/310] lib/fuse.c: fix deallocation in fuse_session_loop_remember Starting from commit 752b59ac0876, the buffer must be freed with fuse_buf_free, not plain free. Signed-off-by: Alexander Monakov <amonakov@ispras.ru> --- lib/fuse.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/fuse.c b/lib/fuse.c index c09564b28..c7838e2a1 100644 --- a/lib/fuse.c +++ b/lib/fuse.c @@ -4626,7 +4626,7 @@ static int fuse_session_loop_remember(struct fuse *f) } } - free(fbuf.mem); + fuse_buf_free(&fbuf); return res < 0 ? -1 : 0; } From e9a81fda21fca6af8abcd786792d12937f2ac235 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 22:13:49 +0000 Subject: [PATCH 219/310] build(deps): bump actions/checkout from 5.0.1 to 6.0.0 Bumps [actions/checkout](https://github.com/actions/checkout) from 5.0.1 to 6.0.0. - [Release notes](https://github.com/actions/checkout/releases) - [Commits](https://github.com/actions/checkout/compare/v5.0.1...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 6.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> --- .github/workflows/abicheck.yml | 4 ++-- .github/workflows/bsd.yaml | 2 +- .github/workflows/checkpatch.yml | 2 +- .github/workflows/codeql.yml | 2 +- .github/workflows/codespell.yml | 2 +- .github/workflows/iwyi-check.yml | 2 +- .github/workflows/pr-ci.yml | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/abicheck.yml b/.github/workflows/abicheck.yml index 0dc38a833..08ad24ddb 100644 --- a/.github/workflows/abicheck.yml +++ b/.github/workflows/abicheck.yml @@ -28,11 +28,11 @@ jobs: sudo apt-get update sudo apt-get -y install abigail-tools clang gcc liburing-dev libnuma-dev - - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: path: current - - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: path: previous ref: ${{ github.event.pull_request.base.ref }} diff --git a/.github/workflows/bsd.yaml b/.github/workflows/bsd.yaml index 02c1c1c01..0e50b4b25 100644 --- a/.github/workflows/bsd.yaml +++ b/.github/workflows/bsd.yaml @@ -18,7 +18,7 @@ jobs: name: Build under FreeBSD steps: - name: Checkout - uses: actions/checkout@v5.0.1 + uses: actions/checkout@v6.0.0 - name: Build uses: vmactions/freebsd-vm@v1 with: diff --git a/.github/workflows/checkpatch.yml b/.github/workflows/checkpatch.yml index 5d71d4bcc..947a457e8 100644 --- a/.github/workflows/checkpatch.yml +++ b/.github/workflows/checkpatch.yml @@ -10,7 +10,7 @@ jobs: checkpatch: runs-on: ubuntu-latest steps: - - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: fetch-depth: 0 - name: Install dependencies diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 3e8b265d7..3916b5abe 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -49,7 +49,7 @@ jobs: build-mode: manual steps: - name: Checkout repository - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml index fd73ec5c5..2dd8a4e78 100644 --- a/.github/workflows/codespell.yml +++ b/.github/workflows/codespell.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Codespell uses: codespell-project/actions-codespell@8f01853be192eb0f849a5c7d721450e7a467c579 # v2.2 with: diff --git a/.github/workflows/iwyi-check.yml b/.github/workflows/iwyi-check.yml index a2ff1c29b..a5a0f44b2 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@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: fetch-depth: 0 diff --git a/.github/workflows/pr-ci.yml b/.github/workflows/pr-ci.yml index ea7ebd8e8..0f65e275e 100644 --- a/.github/workflows/pr-ci.yml +++ b/.github/workflows/pr-ci.yml @@ -33,7 +33,7 @@ jobs: gcc-multilib g++-multilib libc6-dev-i386 \ libpcap0.8-dev:i386 libudev-dev:i386 pkg-config:i386 \ liburing-dev libnuma-dev - - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - uses: actions/setup-python@v6 with: python-version: '3.12' From f1a375e1f789ae6637d18c4cb699cc2b609ddd17 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 22:13:40 +0000 Subject: [PATCH 220/310] build(deps): bump github/codeql-action from 4.31.3 to 4.31.5 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.31.3 to 4.31.5. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/014f16e7ab1402f30e7c3329d33797e7948572db...fdbfb4d2750291e159f0156def62b853c2798ca2) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 4.31.5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 3916b5abe..3978b7705 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -53,7 +53,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@014f16e7ab1402f30e7c3329d33797e7948572db # v3.29.5 + uses: github/codeql-action/init@fdbfb4d2750291e159f0156def62b853c2798ca2 # v3.29.5 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -78,7 +78,7 @@ jobs: meson compile -C build - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@014f16e7ab1402f30e7c3329d33797e7948572db # v3.29.5 + uses: github/codeql-action/analyze@fdbfb4d2750291e159f0156def62b853c2798ca2 # v3.29.5 with: category: "/language:${{matrix.language}}" From 4ed0ed7adcf74bc266de79869c5952cc8a1af287 Mon Sep 17 00:00:00 2001 From: Jingbo Xu <jefflexu@linux.alibaba.com> Date: Mon, 11 Aug 2025 19:55:29 +0800 Subject: [PATCH 221/310] fuse-io-uring: fix narrowing integer conversion of commit_id commid_id is 64 bits. fuse_uring_sqe_set_req_data() accepts commid_id as 'unsigned int' type, which is only guaranteed to be no less than 32 bits. Thus the high 32 bits are dropped, and the replied commit_id is truncated to the lower 32 bits as well in the following replied fuse_uring_cmd_req when issuing FUSE_IO_URING_CMD_COMMIT_AND_FETCH subcmd. This can lead to "fuse: qid=XX commit_id YY not found" error, where YY is the low 32 bits of the actual commid_id. Signed-off-by: Jingbo Xu <jefflexu@linux.alibaba.com> --- lib/fuse_uring.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/fuse_uring.c b/lib/fuse_uring.c index 2fea05f97..1311f5a58 100644 --- a/lib/fuse_uring.c +++ b/lib/fuse_uring.c @@ -119,7 +119,7 @@ static void *fuse_uring_get_sqe_cmd(struct io_uring_sqe *sqe) static void fuse_uring_sqe_set_req_data(struct fuse_uring_cmd_req *req, const unsigned int qid, - const unsigned int commit_id) + const uint64_t commit_id) { req->qid = qid; req->commit_id = commit_id; From f96ec67050a6530b71a624140c36676495521f84 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Dec 2025 02:04:50 +0000 Subject: [PATCH 222/310] build(deps): bump github/codeql-action from 4.31.5 to 4.31.6 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.31.5 to 4.31.6. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/fdbfb4d2750291e159f0156def62b853c2798ca2...fe4161a26a8629af62121b670040955b330f9af2) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 4.31.6 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 3978b7705..d84baa685 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -53,7 +53,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@fdbfb4d2750291e159f0156def62b853c2798ca2 # v3.29.5 + uses: github/codeql-action/init@fe4161a26a8629af62121b670040955b330f9af2 # v3.29.5 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -78,7 +78,7 @@ jobs: meson compile -C build - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@fdbfb4d2750291e159f0156def62b853c2798ca2 # v3.29.5 + uses: github/codeql-action/analyze@fe4161a26a8629af62121b670040955b330f9af2 # v3.29.5 with: category: "/language:${{matrix.language}}" From 7bf09951de7afc5f72e30ebc7eaec32720bdaa2c Mon Sep 17 00:00:00 2001 From: Dave Vasilevsky <dave@vasilevsky.ca> Date: Mon, 24 Nov 2025 03:41:14 -0500 Subject: [PATCH 223/310] Don't yield zero d_ino with adaptive readdirplus Under certain conditions, libfuse was yielding a zero d_ino from high-level filesystems. This caused a number of bugs as other software mis-handled these entries. To fix, ensure that direntries stored in a fuse_dh always have either FUSE_UNKNOWN_INO or an intentionally-set st_ino. This bug was triggered if all the following conditions were met: * High-level FS is readdirplus-capable, and does not set use_ino or readdir_ino. * FS does not use offsets in readdir. * FS passes to the dir filler the FUSE_FILL_DIR_PLUS flag, and a non-NULL struct stat with st_ino == 0. * A directory is large enough to need multiple readdir calls. * Adaptive readdirplus causes a readdirplus to be followed by a regular readdir. When this occurred, the fuse_dh was filled with entries with st_ino == 0. On the initial readdirplus we were calling do_lookup() to convert these to synthetic inode numbers, but on the subsequent regular readdirs we were returning the zero inode numbers verbatim. Historically, d_ino == 0 indicated that a direntry should be skipped. Several tools have treated it this way, including Glibc before 2022 (or 2024 for readdir64_r), and current versions of Go. This has caused a number of bugs: * https://github.com/libfuse/libfuse/issues/1338 * https://github.com/golang/go/issues/76428 * https://github.com/restic/restic/pull/5607 * https://gitlab.gnome.org/World/deja-dup/-/issues/623 When libfuse receives st_ino == 0 in readdir, we should therefore treat it as the FS having no opinion about the inode number. We should only truly trust that it wants a zero inode if use_ino or readdir_ino is true. In addition to the fix, this commit adds a mode to passthrough to return st_ino == 0 from readdir, and uses that to test libfuse's behavior in test_examples.py. Signed-off-by: Dave Vasilevsky <dave@vasilevsky.ca> --- example/passthrough.c | 8 +++++- lib/fuse.c | 22 ++++++++-------- test/test_examples.py | 60 +++++++++++++++++++++++++++++++------------ 3 files changed, 62 insertions(+), 28 deletions(-) diff --git a/example/passthrough.c b/example/passthrough.c index 1f09c2dc0..81a265aa4 100644 --- a/example/passthrough.c +++ b/example/passthrough.c @@ -48,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 @@ -140,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; } @@ -590,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/lib/fuse.c b/lib/fuse.c index c7838e2a1..9607bb0d2 100644 --- a/lib/fuse.c +++ b/lib/fuse.c @@ -3568,17 +3568,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) { diff --git a/test/test_examples.py b/test/test_examples.py index 2658081fc..05efa0993 100755 --- a/test/test_examples.py +++ b/test/test_examples.py @@ -27,6 +27,11 @@ fuse_proto, fuse_caps, powerset, parse_kernel_version) from os.path import join as pjoin import logging +from enum import Enum + +class InodeCheck(Enum): + EXACT = 1 + NONZERO = 2 pytestmark = fuse_test_marker() @@ -138,7 +143,7 @@ def test_hello(tmpdir, name, options, cmdline_builder, output_checker): @pytest.mark.parametrize("writeback", (False, True)) @pytest.mark.parametrize("name", ('passthrough', 'passthrough_plus', - 'passthrough_fh', 'passthrough_ll')) + 'passthrough_fh', 'passthrough_ll', 'passthrough_zero_ino')) @pytest.mark.parametrize("debug", (False, True)) def test_passthrough(short_tmpdir, name, debug, output_checker, writeback): # Avoid false positives from libfuse debug messages @@ -153,10 +158,16 @@ def test_passthrough(short_tmpdir, name, debug, output_checker, writeback): mnt_dir = str(short_tmpdir.mkdir('mnt')) src_dir = str(short_tmpdir.mkdir('src')) + inode_check = InodeCheck.EXACT if name == 'passthrough_plus': cmdline = base_cmdline + \ [ pjoin(basename, 'example', 'passthrough'), '--plus', '-f', mnt_dir ] + elif name == 'passthrough_zero_ino': + cmdline = base_cmdline + \ + [ pjoin(basename, 'example', 'passthrough'), + '--plus', '--readdir-zero-inodes', '-f', mnt_dir ] + inode_check = InodeCheck.NONZERO elif name == 'passthrough_ll': cmdline = base_cmdline + \ [ pjoin(basename, 'example', name), @@ -189,12 +200,12 @@ def test_passthrough(short_tmpdir, name, debug, output_checker, writeback): work_dir = mnt_dir + src_dir tst_statvfs(work_dir) - tst_readdir(src_dir, work_dir) - tst_readdir_big(src_dir, work_dir) + tst_readdir(src_dir, work_dir, inode_check) + tst_readdir_big(src_dir, work_dir, inode_check) 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_passthrough(src_dir, work_dir, inode_check) tst_append(src_dir, work_dir) tst_seek(src_dir, work_dir) tst_mkdir(work_dir) @@ -207,7 +218,8 @@ def test_passthrough(short_tmpdir, name, debug, output_checker, writeback): # Underlying fs may not have full nanosecond resolution tst_utimens(work_dir, ns_tol=1000) - tst_link(work_dir) + if inode_check == InodeCheck.EXACT: + tst_link(work_dir) tst_truncate_path(work_dir) tst_truncate_fd(work_dir) tst_open_unlink(work_dir) @@ -757,7 +769,17 @@ def tst_link(mnt_dir): os.unlink(name1) -def tst_readdir(src_dir, mnt_dir): +def tst_inodes_nonzero(lines): + inode_nums = [int(line.split()[0]) for line in lines] + assert all(i != 0 for i in inode_nums), inode_nums + +def tst_inode(inode_check, actual, expected): + if inode_check == InodeCheck.EXACT: + assert expected == actual + elif inode_check == InodeCheck.NONZERO: + assert actual != 0 + +def tst_readdir(src_dir, mnt_dir, inode_check=InodeCheck.EXACT): newdir = name_generator() src_newdir = pjoin(src_dir, newdir) @@ -778,16 +800,18 @@ def tst_readdir(src_dir, mnt_dir): assert listdir_is == listdir_should inodes_is = readdir_inode(mnt_newdir) - inodes_should = readdir_inode(src_newdir) - assert inodes_is == inodes_should + if inode_check == InodeCheck.EXACT: + inodes_should = readdir_inode(src_newdir) + assert inodes_is == inodes_should + elif inode_check == InodeCheck.NONZERO: + tst_inodes_nonzero(inodes_is) os.unlink(file_) os.unlink(subfile) os.rmdir(subdir) os.rmdir(src_newdir) -def tst_readdir_big(src_dir, mnt_dir): - +def tst_readdir_big(src_dir, mnt_dir, inode_check=InodeCheck.EXACT): # Add enough entries so that readdir needs to be called # multiple times. fnames = [] @@ -803,13 +827,17 @@ def tst_readdir_big(src_dir, mnt_dir): assert listdir_is == listdir_should inodes_is = readdir_inode(mnt_dir) - inodes_should = readdir_inode(src_dir) - assert inodes_is == inodes_should + if inode_check == InodeCheck.EXACT: + inodes_should = readdir_inode(src_dir) + assert inodes_is == inodes_should + elif inode_check == InodeCheck.NONZERO: + tst_inodes_nonzero(inodes_is) for fname in fnames: + # A comment just to get a diff 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 + tst_inode(inode_check, stat_mnt.st_ino, stat_src.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 @@ -885,7 +913,7 @@ def tst_utimens(mnt_dir, ns_tol=0): 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): +def tst_passthrough(src_dir, mnt_dir, inode_check=InodeCheck.EXACT): name = name_generator() src_name = pjoin(src_dir, name) mnt_name = pjoin(mnt_dir, name) @@ -918,7 +946,7 @@ def tst_passthrough(src_dir, mnt_dir): src_stat = os.stat(src_name) mnt_stat = os.stat(mnt_name) assert src_stat.st_mode == mnt_stat.st_mode - assert src_stat.st_ino == mnt_stat.st_ino + tst_inode(inode_check, mnt_stat.st_ino, src_stat.st_ino) assert src_stat.st_size == mnt_stat.st_size assert src_stat.st_mtime == mnt_stat.st_mtime @@ -937,7 +965,7 @@ def tst_passthrough(src_dir, mnt_dir): src_stat = os.stat(src_name) mnt_stat = os.stat(mnt_name) assert src_stat.st_mode == mnt_stat.st_mode - assert src_stat.st_ino == mnt_stat.st_ino + tst_inode(inode_check, mnt_stat.st_ino, src_stat.st_ino) assert src_stat.st_size == mnt_stat.st_size assert abs(src_stat.st_mtime - mnt_stat.st_mtime) < 0.01 From e1525c4361f38278ad1b08595d7f9db37923b5ea Mon Sep 17 00:00:00 2001 From: Dave Vasilevsky <dave@vasilevsky.ca> Date: Mon, 1 Dec 2025 22:48:39 -0800 Subject: [PATCH 224/310] Clarify that use_ino takes priority over FUSE_FILL_DIR_PLUS The docs previously said that FUSE_FILL_DIR_PLUS makes "all file attributes" valid, but also that use_ino controls whether the st_ino field is honored. These docs conflict! Since it doesn't make sense for getattr() and readdir() to have different behavior for st_ino, it's best to decide that use_ino will always take priority. This should not break any filesystems, as use of non-zero st_ino with use_ino false already had weird and broken behavior before this PR. Signed-off-by: Dave Vasilevsky <dave@vasilevsky.ca> --- include/fuse.h | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/include/fuse.h b/include/fuse.h index 209102651..32d921b38 100644 --- a/include/fuse.h +++ b/include/fuse.h @@ -60,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) From c2fb67471e23ac70ed1cf56904199380ac7574a9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Dec 2025 22:05:17 +0000 Subject: [PATCH 225/310] build(deps): bump actions/checkout from 6.0.0 to 6.0.1 Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.0 to 6.0.1. - [Release notes](https://github.com/actions/checkout/releases) - [Commits](https://github.com/actions/checkout/compare/v6...v6.0.1) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 6.0.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> --- .github/workflows/abicheck.yml | 4 ++-- .github/workflows/bsd.yaml | 2 +- .github/workflows/checkpatch.yml | 2 +- .github/workflows/codeql.yml | 2 +- .github/workflows/codespell.yml | 2 +- .github/workflows/iwyi-check.yml | 2 +- .github/workflows/pr-ci.yml | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/abicheck.yml b/.github/workflows/abicheck.yml index 08ad24ddb..802431b2e 100644 --- a/.github/workflows/abicheck.yml +++ b/.github/workflows/abicheck.yml @@ -28,11 +28,11 @@ jobs: sudo apt-get update sudo apt-get -y install abigail-tools clang gcc liburing-dev libnuma-dev - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: path: current - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: path: previous ref: ${{ github.event.pull_request.base.ref }} diff --git a/.github/workflows/bsd.yaml b/.github/workflows/bsd.yaml index 0e50b4b25..8edb96bce 100644 --- a/.github/workflows/bsd.yaml +++ b/.github/workflows/bsd.yaml @@ -18,7 +18,7 @@ jobs: name: Build under FreeBSD steps: - name: Checkout - uses: actions/checkout@v6.0.0 + uses: actions/checkout@v6.0.1 - name: Build uses: vmactions/freebsd-vm@v1 with: diff --git a/.github/workflows/checkpatch.yml b/.github/workflows/checkpatch.yml index 947a457e8..2d17cf500 100644 --- a/.github/workflows/checkpatch.yml +++ b/.github/workflows/checkpatch.yml @@ -10,7 +10,7 @@ jobs: checkpatch: runs-on: ubuntu-latest steps: - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 0 - name: Install dependencies diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index d84baa685..701d8518d 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -49,7 +49,7 @@ jobs: build-mode: manual steps: - name: Checkout repository - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml index 2dd8a4e78..af68e2c9d 100644 --- a/.github/workflows/codespell.yml +++ b/.github/workflows/codespell.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Codespell uses: codespell-project/actions-codespell@8f01853be192eb0f849a5c7d721450e7a467c579 # v2.2 with: diff --git a/.github/workflows/iwyi-check.yml b/.github/workflows/iwyi-check.yml index a5a0f44b2..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@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - 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 0f65e275e..5a53cce58 100644 --- a/.github/workflows/pr-ci.yml +++ b/.github/workflows/pr-ci.yml @@ -33,7 +33,7 @@ jobs: gcc-multilib g++-multilib libc6-dev-i386 \ libpcap0.8-dev:i386 libudev-dev:i386 pkg-config:i386 \ liburing-dev libnuma-dev - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - uses: actions/setup-python@v6 with: python-version: '3.12' From 4f81b84d604e57a7c0bd33af375c4405d2ff689e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Dec 2025 22:05:09 +0000 Subject: [PATCH 226/310] build(deps): bump github/codeql-action from 4.31.6 to 4.31.7 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.31.6 to 4.31.7. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/fe4161a26a8629af62121b670040955b330f9af2...cf1bb45a277cb3c205638b2cd5c984db1c46a412) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 4.31.7 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 701d8518d..e054a8622 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -53,7 +53,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@fe4161a26a8629af62121b670040955b330f9af2 # v3.29.5 + uses: github/codeql-action/init@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v3.29.5 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -78,7 +78,7 @@ jobs: meson compile -C build - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@fe4161a26a8629af62121b670040955b330f9af2 # v3.29.5 + uses: github/codeql-action/analyze@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v3.29.5 with: category: "/language:${{matrix.language}}" From a6fdf8c4f795f671f818ac8f88d3fb89f18ab86c Mon Sep 17 00:00:00 2001 From: WekaJosh <80121792+WekaJosh@users.noreply.github.com> Date: Wed, 3 Dec 2025 10:03:07 -0800 Subject: [PATCH 227/310] Update fusermount.c Add WEKAFS to FS whitelist Signed-off-by: WekaJosh <80121792+WekaJosh@users.noreply.github.com> --- util/fusermount.c | 1 + 1 file changed, 1 insertion(+) diff --git a/util/fusermount.c b/util/fusermount.c index 7f19df7a8..f17b44f51 100644 --- a/util/fusermount.c +++ b/util/fusermount.c @@ -1219,6 +1219,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 From 033e0c3fbeb643bbd4e7775849975b6c3f754972 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 22:05:25 +0000 Subject: [PATCH 228/310] build(deps): bump github/codeql-action from 4.31.7 to 4.31.8 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.31.7 to 4.31.8. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/cf1bb45a277cb3c205638b2cd5c984db1c46a412...1b168cd39490f61582a9beae412bb7057a6b2c4e) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 4.31.8 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index e054a8622..914d81613 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -53,7 +53,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v3.29.5 + uses: github/codeql-action/init@1b168cd39490f61582a9beae412bb7057a6b2c4e # v3.29.5 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -78,7 +78,7 @@ jobs: meson compile -C build - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v3.29.5 + uses: github/codeql-action/analyze@1b168cd39490f61582a9beae412bb7057a6b2c4e # v3.29.5 with: category: "/language:${{matrix.language}}" From 2bb27d8197cb9a3c6f0e7f17aa0cd96f1053ff47 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Sun, 23 Nov 2025 22:55:58 +0100 Subject: [PATCH 229/310] fuse-io-uring: Handle EAGAIN/EINTR in fuse-io-uring Especially FUSE_IO_URING_CMD_REGISTER can easily get -EAGAIN - if it arrives in kernel before FUSE_INIT was finally handled. Easiest solution is to send it again. I'm not sure if FUSE_IO_URING_CMD_COMMIT_AND_FETCH might fail with -EINTR or -EGAIN - not from fuse client/kernel point of view, but possibly by io-uring. Better if we handle that as well. Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- lib/fuse_uring.c | 68 ++++++++++++++++++++++++++++++++++++++++++------ lib/util.h | 6 +++++ 2 files changed, 66 insertions(+), 8 deletions(-) diff --git a/lib/fuse_uring.c b/lib/fuse_uring.c index 1311f5a58..5b044b0c3 100644 --- a/lib/fuse_uring.c +++ b/lib/fuse_uring.c @@ -43,6 +43,8 @@ struct fuse_ring_ent { /* commit id of a fuse request */ uint64_t req_commit_id; + enum fuse_uring_cmd last_cmd; + /* header and payload */ struct iovec iov[2]; }; @@ -174,9 +176,8 @@ static int fuse_uring_commit_sqe(struct fuse_ring_pool *ring_pool, return -EIO; } - fuse_uring_sqe_prepare(sqe, ring_ent, - FUSE_IO_URING_CMD_COMMIT_AND_FETCH); - + 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); @@ -532,6 +533,48 @@ static struct fuse_ring_pool *fuse_create_ring(struct fuse_session *se) 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) { @@ -579,6 +622,7 @@ static int fuse_uring_queue_handle_cqes(struct fuse_ring_queue *queue) 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) { @@ -587,19 +631,27 @@ static int fuse_uring_queue_handle_cqes(struct fuse_ring_queue *queue) num_completed++; err = cqe->res; - if (err != 0) { + if (unlikely(err != 0)) { if (err > 0 && ((uintptr_t)io_uring_cqe_get_data(cqe) == (unsigned int)queue->eventfd)) { /* teardown from eventfd */ return -ENOTCONN; } - // XXX: Needs rate limited logs, otherwise log spam - //fuse_log(FUSE_LOG_ERR, "cqe res: %d\n", cqe->res); + + 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 != -EINTR && err != -EAGAIN && - err != -ENOTCONN) { + if (err != -ENOTCONN) { se->error = cqe->res; /* return first error */ diff --git a/lib/util.h b/lib/util.h index aae40e21d..107a2bfdd 100644 --- a/lib/util.h +++ b/lib/util.h @@ -40,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_ */ From 834a496b5ec0380db12f1e0d1d8435d118965e83 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Sun, 14 Dec 2025 22:04:33 +0100 Subject: [PATCH 230/310] fuse-io-uring: Function rename and split into two function The better name is fuse_uring_register_queue() and it also splits out registering the entries into a separate function for readability. Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- lib/fuse_uring.c | 64 +++++++++++++++++++++++++++++------------------- 1 file changed, 39 insertions(+), 25 deletions(-) diff --git a/lib/fuse_uring.c b/lib/fuse_uring.c index 5b044b0c3..0ed87495c 100644 --- a/lib/fuse_uring.c +++ b/lib/fuse_uring.c @@ -416,40 +416,54 @@ static void fuse_session_destruct_uring(struct fuse_ring_pool *fuse_ring) free(fuse_ring); } -static int fuse_uring_prepare_fetch_sqes(struct fuse_ring_queue *queue) +static int fuse_uring_register_ent(struct fuse_ring_queue *queue, + struct fuse_ring_ent *ent) { - struct fuse_ring_pool *ring_pool = queue->ring_pool; - unsigned int sq_ready; struct io_uring_sqe *sqe; - for (size_t idx = 0; idx < ring_pool->queue_depth; idx++) { - struct fuse_ring_ent *ent = &queue->ent[idx]; + 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; + } - sqe = io_uring_get_sqe(&queue->ring); - if (sqe == NULL) { - /* All SQEs are idle here - no good reason this - * could fail - */ + ent->last_cmd = FUSE_IO_URING_CMD_REGISTER; + fuse_uring_sqe_prepare(sqe, ent, ent->last_cmd); - fuse_log(FUSE_LOG_ERR, "Failed to get all ring SQEs"); - return -EIO; - } + /* only needed for fetch */ + ent->iov[0].iov_base = ent->req_header; + ent->iov[0].iov_len = queue->req_header_sz; - fuse_uring_sqe_prepare(sqe, ent, FUSE_IO_URING_CMD_REGISTER); + ent->iov[1].iov_base = ent->op_payload; + ent->iov[1].iov_len = ent->req_payload_sz; - /* only needed for fetch */ - ent->iov[0].iov_base = ent->req_header; - ent->iov[0].iov_len = queue->req_header_sz; + sqe->addr = (uint64_t)(ent->iov); + sqe->len = 2; - ent->iov[1].iov_base = ent->op_payload; - ent->iov[1].iov_len = ent->req_payload_sz; + /* 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); - sqe->addr = (uint64_t)(ent->iov); - sqe->len = 2; + return 0; - /* 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); +} + +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); @@ -757,7 +771,7 @@ static int fuse_uring_init_queue(struct fuse_ring_queue *queue) list_init_req(req); } - res = fuse_uring_prepare_fetch_sqes(queue); + res = fuse_uring_register_queue(queue); if (res != 0) { fuse_log( FUSE_LOG_ERR, From b9cae498ff9df22d785128576c0b13077452a7c4 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Sun, 14 Dec 2025 23:21:41 +0100 Subject: [PATCH 231/310] fuse-io-uring: Initialize queue->eventfd This was forgotten before. Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- lib/fuse_uring.c | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/fuse_uring.c b/lib/fuse_uring.c index 0ed87495c..a90a0a789 100644 --- a/lib/fuse_uring.c +++ b/lib/fuse_uring.c @@ -532,6 +532,7 @@ static struct fuse_ring_pool *fuse_create_ring(struct fuse_session *se) queue->numa_node = numa_node_of_cpu(qid); queue->qid = qid; queue->ring_pool = fuse_ring; + queue->eventfd = -1; } pthread_cond_init(&fuse_ring->thread_start_cond, NULL); From 8e536dcb767f31aae986cc4aff5a1a56420c7de5 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Sun, 14 Dec 2025 23:12:27 +0100 Subject: [PATCH 232/310] fuse-io-uring: Add a lock around io_uring_submit / io_uring_submit_and_wait There were two issues 1) If the application used an async thread outside of the ring thread to submit results, the ring thread might have submitted via io_uring_submit_and_wait() SQEs that were not ready yet. I.e. in fuse_uring_commit_sqe() it might have already called sqe = io_uring_get_sqe(&queue->ring); but then fuse_uring_sqe_prepare() and fuse_uring_sqe_prepare() might not have been run yet. 2) If run from a single thread only and without any corouting / reactor feature (only available in my private branch right now) the function fuse_uring_commit_sqe() didn't need to call io_uring_submit() at all. I.e. before this commit there were one system call per request too much. Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- lib/fuse_uring.c | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/lib/fuse_uring.c b/lib/fuse_uring.c index a90a0a789..0e5e8e9e1 100644 --- a/lib/fuse_uring.c +++ b/lib/fuse_uring.c @@ -59,6 +59,9 @@ struct fuse_ring_queue { 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[]; }; @@ -156,12 +159,20 @@ 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 = io_uring_get_sqe(&queue->ring); + 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. @@ -186,8 +197,11 @@ static int fuse_uring_commit_sqe(struct fuse_ring_pool *ring_pool, out->unique, ent_in_out->payload_sz); } - /* XXX: This needs to be a ring config option */ - io_uring_submit(&queue->ring); + if (!queue->cqe_processing) + io_uring_submit(&queue->ring); + + if (locked) + pthread_mutex_unlock(&queue->ring_lock); return 0; } @@ -408,6 +422,8 @@ static void fuse_session_destruct_uring(struct fuse_ring_pool *fuse_ring) 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); @@ -533,6 +549,7 @@ static struct fuse_ring_pool *fuse_create_ring(struct fuse_session *se) 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); @@ -822,7 +839,11 @@ static void *fuse_uring_thread(void *arg) 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; } From 4dea65b46e5fdcd33cf6575f2c030f68e4e37f0b Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Tue, 12 Aug 2025 17:20:45 +0200 Subject: [PATCH 233/310] Add signify verification in make_release_tarball.sh Signed-off-by: Bernd Schubert <bernd@bsbernd.com> (cherry picked from commit 8c8a62b1ef8b22641325ed8584d0e23e039ac42d) --- make_release_tarball.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/make_release_tarball.sh b/make_release_tarball.sh index a0040638b..ce2fe16c5 100755 --- a/make_release_tarball.sh +++ b/make_release_tarball.sh @@ -30,6 +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 echo "Contributors from ${PREV_TAG} to ${TAG}:" From 8bfc50dfcfa05a7e884ecae5e1cb0e88c4a4f67a Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bschubert@ddn.com> Date: Fri, 19 Dec 2025 00:05:19 +0100 Subject: [PATCH 234/310] Prepare 3.18.0 release Update AUTHORS and ChangeLog.rst, added signify/fuse-3.19.pub. Signed-off-by: Bernd Schubert <bschubert@ddn.com> --- AUTHORS | 21 ++++++++++- ChangeLog.rst | 81 +++++++++++++++++++++++++++++++++++++++++-- signify/fuse-3.19.pub | 2 ++ 3 files changed, 100 insertions(+), 4 deletions(-) create mode 100644 signify/fuse-3.19.pub diff --git a/AUTHORS b/AUTHORS index 837a62c84..e07747388 100644 --- a/AUTHORS +++ b/AUTHORS @@ -260,6 +260,25 @@ Tyler Hall <tylerwhall@gmail.com> yangyun <yangyun50@huawei.com> Abhishek <abhi_45645@yahoo.com> -# New authors since 666b2c3fa5159e3c72a0d08117507475117c9319 +# New authors since fuse-3.17.1 Luis Henriques <luis@igalia.com> Zegang <zegang.luo@qq.com> +swj <1186093704@qq.com> +Gleb Popov <6yearold@gmail.com> +WekaJosh <80121792+WekaJosh@users.noreply.github.com> +Alexander Monakov <amonakov@ispras.ru> +Ben Dooks <ben.dooks@codethink.co.uk> +Ben Linsay <blinsay@gmail.com> +Dave Vasilevsky <dave@vasilevsky.ca> +Darrick J. Wong <djwong@kernel.org> +Georgi Valkov <gvalkov@gmail.com> +Alik Aslanyan <inline0@pm.me> +jnr0006 <jacob.nick.riley@gmail.com> +Jingbo Xu <jefflexu@linux.alibaba.com> +Long Li <leo.lilong@huawei.com> +Maksim Harbachou <maksim.harbachou@resilio.com> +Meng Lu Wang <mwang@ddn.com> +Vassili Tchersky <vt+git@vbc.su> +Vassili Tchersky <vt+git@vbcy.org> +izxl007 <zeng.zheng@zte.com.cn> +Zeno Sebastian Endemann <zeno.endemann@mailbox.org> diff --git a/ChangeLog.rst b/ChangeLog.rst index 505d9dba8..15c998cf1 100644 --- a/ChangeLog.rst +++ b/ChangeLog.rst @@ -1,6 +1,81 @@ -libfuse 3.18 +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 + and don't try to update mtab if it does not exist +- Fix a build warning when HAVE_BACKTRACE is undefined +- fuse_loop_mt.c: fix close-on-exec flag on clone fd +- Remove struct size assertions from fuse_common.h + +libfuse 3.17.3 (2025-07-16) +=========================== +* more conn->want / conn->want_ext conversion fixes +* Fix feature detection for close_range +* Avoid double unmount on FUSE_DESTROY + +libfuse 3.17.2 (2025-04-23) +=========================== +* Fixed uninitized bufsize value (compilation warning and real + issue when HAVE_SPLICE was not defined) +* Fixed initialization races related to buffer realocation when + large buf sizes are used (/proc/sys/fs/fuse/max_pages_limit) +* Fix build with kernel < 5.9 +* Fix static_assert build failure with C++ version < 11 +* Compilation fix (remove second fuse_main_real_versioned declaration) +* Another conn.want flag conversion fix for high-level applications +* Check if pthread_setname_np() exists before use it +* fix example/memfs_ll rename deadlock error +* signal handlers: Store fuse_session unconditionally and restore + previous behavior that with multiple sessions the last session + was used for the signal exist handler + +libfuse 3.17.1 (2025-03-24) +=========================== +* fuse: Fix want conn.want flag conversion +* Prevent re-usage of stdio FDs for fusermount +* PanFS added to fusermount whitelist + +libfuse 3.17.1-rc1 (2025-02-18) +=============================== +* several BSD fixes +* x86 (32bit) build fixes +* nested declarations moved out of the inlined functions to avoid + build warnings +* signify public key added for future 3.18 -libfuse 3.17.1-rc0 (2024-02.10) +libfuse 3.17.1-rc0 (2025-02.10) =============================== * Fix libfuse build with FUSE_USE_VERSION 30 @@ -16,7 +91,7 @@ libfuse 3.17.1-rc0 (2024-02.10) exported to be able to silence leak checkers -libfuse 3.17 (2024-01-01, not officially releaesed) +libfuse 3.17 (2025-01-01, not officially releaesed) ================================================== * 3.11 and 3.14.2 introduced ABI incompatibilities, the ABI is restored 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 From 9157d99f17384f362c65b02cec065e0aec5f0b56 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Fri, 19 Dec 2025 00:17:47 +0100 Subject: [PATCH 235/310] prepare 3.18.0 release v2 Forgot meson.build and also some updates to make_release_tarball.sh Signed-off-by: Bernd Schubert <bernd@bsbernd.com> --- make_release_tarball.sh | 2 +- meson.build | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/make_release_tarball.sh b/make_release_tarball.sh index ce2fe16c5..45438a5cd 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. diff --git a/meson.build b/meson.build index e3c7eeba6..e8e7c448b 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('libfuse3', ['c'], - version: '3.18.0-rc0', # Version with RC suffix + version: '3.18.0', meson_version: '>= 0.60.0', default_options: [ 'buildtype=debugoptimized', From 98f94e0b1d0b1479d833a3584442a95b3700ad19 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Sat, 20 Dec 2025 13:22:25 +0100 Subject: [PATCH 236/310] Fix fuse version script / 3.17.3 to 3.18 Several functions like fuse_set_feature_flag had been introduced some time ago in master (3.18-rc0) first and then backported to 3.17.3. In order to handle the backport a new section FUSE_3.17.3 was introduced in fuse_version_script. The master branch kept these symbols in the 3.18 section, which now causes an ABI issue with the 3.18 release. This commit attempts to fix that and creates the 3.17.3 section in the master and 3.18.x branch. Closes: https://github.com/libfuse/libfuse/issues/1397 Signed-off-by: Bernd Schubert <bernd@bsbernd.com> --- lib/fuse_versionscript | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/fuse_versionscript b/lib/fuse_versionscript index 2feafcf83..b58360feb 100644 --- a/lib/fuse_versionscript +++ b/lib/fuse_versionscript @@ -202,17 +202,21 @@ FUSE_3.17 { fuse_log_close_syslog; } FUSE_3.12; -FUSE_3.18 { +FUSE_3.17.3 { global: - fuse_req_is_uring; - fuse_req_get_payload; fuse_set_feature_flag; fuse_unset_feature_flag; fuse_get_feature_flag; - fuse_lowlevel_notify_increment_epoch; - # 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; From c35865b22ef55c38238a7d3630683aa47fdc76ff Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Sat, 20 Dec 2025 14:03:20 +0100 Subject: [PATCH 237/310] Add an ABI check for the last tag of the previous release The 3.18.0 ABI issue came up as we didn't have an ABI check to the last previous release. The supressions file needs to compare to the previous version is also used for the existing ABI check - will reduce false positives. This will fail ABI checks against libfuse-3.18.0, as some symbols in 3.18.0 were marked as the wrong version. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> (cherry picked from commit fef782261a53c72c2dfcb3a3b2ca3cd1abace92c) --- .github/workflows/abicheck.yml | 7 +- .github/workflows/abicheck_prev_release.yml | 76 +++++++++++++++++++ .../workflows/abidiff_suppressions.abignore | 36 +++++++++ .../workflows/find_previous_release_tag.sh | 69 +++++++++++++++++ 4 files changed, 185 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/abicheck_prev_release.yml create mode 100644 .github/workflows/abidiff_suppressions.abignore create mode 100755 .github/workflows/find_previous_release_tag.sh diff --git a/.github/workflows/abicheck.yml b/.github/workflows/abicheck.yml index 802431b2e..245b1bbc1 100644 --- a/.github/workflows/abicheck.yml +++ b/.github/workflows/abicheck.yml @@ -59,7 +59,8 @@ jobs: - name: Run abidiff run: abidiff --no-added-syms - --headers-dir1 previous/include/ - --headers-dir2 current/include/ - previous/build/lib/libfuse3.so + --suppressions current/.github/workflows/abidiff_suppressions.abignore + --headers-dir1 previous/include/ + --headers-dir2 current/include/ + previous/build/lib/libfuse3.so current/build/lib/libfuse3.so diff --git a/.github/workflows/abicheck_prev_release.yml b/.github/workflows/abicheck_prev_release.yml new file mode 100644 index 000000000..235341b55 --- /dev/null +++ b/.github/workflows/abicheck_prev_release.yml @@ -0,0 +1,76 @@ +--- +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 + chmod +x .github/workflows/find_previous_release_tag.sh + 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: Build current + working-directory: current + run: | + pip install -r requirements.txt + meson setup build --buildtype=debug + meson compile -C build + + - name: Build previous + working-directory: previous + run: | + echo "Previous release tag: ${{ steps.prev_release.outputs.prev_tag }}" + echo "Commit-id of previous release: $(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 + --suppressions current/.github/workflows/abidiff_suppressions.abignore + --headers-dir1 previous/include/ + --headers-dir2 current/include/ + previous/build/lib/libfuse3.so + current/build/lib/libfuse3.so diff --git a/.github/workflows/abidiff_suppressions.abignore b/.github/workflows/abidiff_suppressions.abignore new file mode 100644 index 000000000..949efda6b --- /dev/null +++ b/.github/workflows/abidiff_suppressions.abignore @@ -0,0 +1,36 @@ +# 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 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" From 2a6da8cb338b1718e899240d1d43d52e311e504a Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Sat, 20 Dec 2025 23:33:15 +0100 Subject: [PATCH 238/310] Prepare the 3.18.1 release --- ChangeLog.rst | 6 ++++++ meson.build | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/ChangeLog.rst b/ChangeLog.rst index 15c998cf1..b5e8402c7 100644 --- a/ChangeLog.rst +++ b/ChangeLog.rst @@ -1,3 +1,9 @@ +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) =========================== diff --git a/meson.build b/meson.build index e8e7c448b..4eb6c0032 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('libfuse3', ['c'], - version: '3.18.0', + version: '3.18.1', meson_version: '>= 0.60.0', default_options: [ 'buildtype=debugoptimized', From 7beb86c09b6ec5aab14dc25256ed8a5ad18554d7 Mon Sep 17 00:00:00 2001 From: Abhinav Agarwal <abhinavagarwal1996@gmail.com> Date: Tue, 17 Mar 2026 19:03:57 +0000 Subject: [PATCH 239/310] fuse-io-uring: Fix NULL deref and memory leak in fuse_uring_init_queue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs in fuse_uring_init_queue(): 1. numa_alloc_local() return values are not checked. On allocation failure the code proceeds with NULL pointers, causing a NULL pointer dereference when the SQE registration subsequently accesses the header/payload buffers. 2. When fuse_uring_register_queue() fails, the function falls through to return queue->ring.ring_fd (success) instead of propagating the error. The NUMA allocations are then leaked, and the caller uses a broken queue for I/O. Fix by returning -ENOMEM on allocation failure and returning the error from fuse_uring_register_queue() on registration failure. In both cases, cleanup (including NUMA frees and eventfd close) is delegated to fuse_session_destruct_uring() via the fuse_uring_start() error path — which is the intended cleanup owner for this subsystem. Fixes CVE-2026-33179 Signed-off-by: Abhinav Agarwal <abhinav.agarwal@rubrik.com> --- lib/fuse_uring.c | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/fuse_uring.c b/lib/fuse_uring.c index 0e5e8e9e1..e841abdcb 100644 --- a/lib/fuse_uring.c +++ b/lib/fuse_uring.c @@ -759,7 +759,7 @@ static int fuse_uring_init_queue(struct fuse_ring_queue *queue) if (res != 0) { fuse_log(FUSE_LOG_ERR, "qid=%d io_uring init failed\n", queue->qid); - goto err; + return res; } queue->req_header_sz = ROUND_UP(sizeof(struct fuse_ring_ent), @@ -777,10 +777,14 @@ static int fuse_uring_init_queue(struct fuse_ring_queue *queue) */ 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); @@ -796,13 +800,10 @@ static int fuse_uring_init_queue(struct fuse_ring_queue *queue) "Grave fuse-uring error on preparing SQEs, aborting\n"); se->error = -EIO; fuse_session_exit(se); + return res; } return queue->ring.ring_fd; - -err: - close(queue->eventfd); - return res; } static void *fuse_uring_thread(void *arg) From 49fcd891a58f622c098e2ca67d66086f7b213836 Mon Sep 17 00:00:00 2001 From: Abhinav Agarwal <abhinavagarwal1996@gmail.com> Date: Mon, 16 Mar 2026 23:14:44 +0000 Subject: [PATCH 240/310] fuse-io-uring: Fix UAF and NULL deref in startup error path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In fuse_uring_start(), the error path called fuse_session_destruct_uring() which frees fuse_ring, then stored the freed pointer in se->uring.pool. On session shutdown, the session loop cleanup checks if (se->uring.pool) and calls fuse_uring_stop() — dereferencing the freed memory (use-after-free). Fix by setting se->uring.pool = NULL in the error path so the cleanup check is skipped. Also add a NULL guard before the destruct call to handle the case where fuse_create_ring() itself returns NULL, which would cause a NULL pointer dereference at fuse_ring->nr_queues. Fixes CVE-2026-33150 Signed-off-by: Abhinav Agarwal <abhinav.agarwal@rubrik.com> --- lib/fuse_uring.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/fuse_uring.c b/lib/fuse_uring.c index e841abdcb..c3127b7e5 100644 --- a/lib/fuse_uring.c +++ b/lib/fuse_uring.c @@ -925,8 +925,9 @@ int fuse_uring_start(struct fuse_session *se) err: if (err) { /* Note all threads need to have been started */ - fuse_session_destruct_uring(fuse_ring); - se->uring.pool = fuse_ring; + if (fuse_ring) + fuse_session_destruct_uring(fuse_ring); + se->uring.pool = NULL; } return err; } From 033844748010a3b8265bf1c90b9ae8ffe4cd9ca7 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Wed, 18 Mar 2026 21:40:39 +0100 Subject: [PATCH 241/310] Prepare 3.18.2 release --- AUTHORS | 3 +++ ChangeLog.rst | 6 ++++++ meson.build | 2 +- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index e07747388..2c63b1feb 100644 --- a/AUTHORS +++ b/AUTHORS @@ -282,3 +282,6 @@ Vassili Tchersky <vt+git@vbc.su> Vassili Tchersky <vt+git@vbcy.org> izxl007 <zeng.zheng@zte.com.cn> Zeno Sebastian Endemann <zeno.endemann@mailbox.org> + +# New authors since fuse-3.18.1 +Abhinav Agarwal <abhinavagarwal1996@gmail.com> diff --git a/ChangeLog.rst b/ChangeLog.rst index b5e8402c7..9a364b518 100644 --- a/ChangeLog.rst +++ b/ChangeLog.rst @@ -1,3 +1,9 @@ +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+ diff --git a/meson.build b/meson.build index 4eb6c0032..f0570dc2c 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('libfuse3', ['c'], - version: '3.18.1', + version: '3.18.2', meson_version: '>= 0.60.0', default_options: [ 'buildtype=debugoptimized', From a486808eca4712da8a4be179c239bdbf565563a3 Mon Sep 17 00:00:00 2001 From: Miklos Szeredi <mszeredi@redhat.com> Date: Mon, 24 Aug 2026 16:34:29 +0200 Subject: [PATCH 242/310] fusermount: don't try opening mnt with privs during auto_umount Always switch to original uid/gid before testing if opening mountpoint would return ENOTCONN. Otherwise it is possible for a malicious actor to replace the mountpint with a symlink and allow opening/closing an attacker controlled path with elevated privileges, possibly causing side effects. Signed-off-by: Miklos Szeredi <mszeredi@redhat.com> --- util/fusermount.c | 25 +++---------------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/util/fusermount.c b/util/fusermount.c index f17b44f51..c9b9088c6 100644 --- a/util/fusermount.c +++ b/util/fusermount.c @@ -1389,11 +1389,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) { @@ -1448,7 +1446,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) { @@ -1461,23 +1458,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; From d9f2b717cb2f5fcfbd8e48a7831425289fd8b8c9 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Mon, 31 Aug 2026 13:57:42 +0200 Subject: [PATCH 243/310] checkpatch: move the call into a script and tune the ignore list Run the checker from .github/workflows/run-checkpatch.sh rather than inline workflow YAML, so any commit can be checked locally exactly the way CI checks it, and bring the IGNORE list in line with master's. Squashed from master's run-checkpatch.sh history, 6b7749e762cc ("Move the checkpatch call into a script") through e1ffa6efbe16 ("checkpatch: ignore BAD_REPORTED_BY_LINK"). The script here is identical to master's. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> --- .github/workflows/checkpatch.yml | 6 ++++-- .github/workflows/run-checkpatch.sh | 31 +++++++++++++++++++++++++++++ checkpatch.pl | 3 ++- 3 files changed, 37 insertions(+), 3 deletions(-) create mode 100755 .github/workflows/run-checkpatch.sh diff --git a/.github/workflows/checkpatch.yml b/.github/workflows/checkpatch.yml index 2d17cf500..c97f5acc3 100644 --- a/.github/workflows/checkpatch.yml +++ b/.github/workflows/checkpatch.yml @@ -27,7 +27,9 @@ jobs: 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 <merge>^..<merge> 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" @@ -35,7 +37,7 @@ jobs: fi subject=$(git log -1 --format=%s $commit) echo "Checking commit: $commit - $subject" - if ! ./checkpatch.pl --max-line-length=100 --no-tree --ignore MAINTAINERS,SPDX_LICENSE_TAG,COMMIT_MESSAGE,FILE_PATH_CHANGES,EMAIL_SUBJECT,AVOID_EXTERNS,GIT_COMMIT_ID,ENOSYS_SYSCALL,ENOSYS,FROM_SIGN_OFF_MISMATCH,QUOTED_COMMIT_ID,PREFER_ATTRIBUTE_ALWAYS_UNUSED,PREFER_DEFINED_ATTRIBUTE_MACRO -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/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. +# <commit>^..<commit> rather than -1 <commit>: 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/checkpatch.pl b/checkpatch.pl index 9eed3683a..2d44e0e6e 100755 --- a/checkpatch.pl +++ b/checkpatch.pl @@ -520,7 +520,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__}; From 54c9cde59f7835b71618d72faa534a8712fe9aca Mon Sep 17 00:00:00 2001 From: "Darrick J. Wong" <djwong@kernel.org> Date: Tue, 12 May 2026 10:46:33 -0700 Subject: [PATCH 244/310] checkpatch.pl: update to linux 7.0 Update this script to match the version from Linux 7.0, without clobbering Bernd's _Atomic additions. This fixes the complaints about "overly long" Fixes: commit ids. Signed-off-by: Darrick J. Wong <djwong@kernel.org> (cherry picked from commit d999bd63afee30949443dc237e3a1f210d0c43e0) --- checkpatch.pl | 226 +++++++++++++++++++++++++++++++++++++------------- 1 file changed, 168 insertions(+), 58 deletions(-) diff --git a/checkpatch.pl b/checkpatch.pl index 2d44e0e6e..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{$_}++ } @_; @@ -667,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; @@ -835,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 @@ -1085,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 = ( @@ -2625,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; @@ -2876,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-<architecture>\n" . "$here$rawline\n"); + "do not modify files in include/asm, change architecture specific files in arch/<architecture>/include/asm\n" . "$here$rawline\n"); } $found_file = 1; } @@ -3011,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; @@ -3231,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> (\"<title line>\")' - 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; } } } @@ -3283,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", @@ -3329,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") @@ -3492,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)}; @@ -3656,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"); } } @@ -3700,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*\"/) || @@ -3745,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*\//) { @@ -4828,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", @@ -5514,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; @@ -5844,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 @@ -5905,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; @@ -5960,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| @@ -5998,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"); } } @@ -6046,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"); } @@ -6702,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", @@ -6913,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; } @@ -7003,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); } @@ -7228,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/) { @@ -7254,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; } } } @@ -7677,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 From bea752553ad84432499cc3d7337c955acc6cac1c Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Sun, 5 Apr 2026 17:01:51 +0200 Subject: [PATCH 245/310] abi check: Move the actual check to a script Move the ABI check functionality from the GitHub workflow to a standalone script at .github/workflows/abicheck.sh. This makes the ABI check reusable and easier to run locally during development. The script: - Auto-detects the previous release tag using find_previous_release_tag.sh - Supports manual tag specification via --prev-tag - Creates temporary checkouts for comparison - Runs abidiff with appropriate suppressions for false positives Added abidiff_suppressions.abignore to suppress ABI-compatible changes: - const qualifier additions (source-level only, ABI compatible) - fuse_session size changes (pre-existing in branch) - fuse_conn_info and fuse_operations changes (backward compatible) Signed-off-by: Bernd Schubert <bernd@bsbernd.com> (cherry picked from commit e00878456789363909311f0f69b74311a4879cb2) --- .github/workflows/abicheck.sh | 126 ++++++++++++++++++++ .github/workflows/abicheck.yml | 24 +--- .github/workflows/abicheck_prev_release.yml | 25 +--- 3 files changed, 134 insertions(+), 41 deletions(-) create mode 100755 .github/workflows/abicheck.sh 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 <tag> Previous release tag/hash to compare against + (default: auto-detect latest release tag) + + --current-dir <path> Path to current version checkout + (default: create temporary checkout from current directory) + + --previous-dir <path> 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 245b1bbc1..169c22a52 100644 --- a/.github/workflows/abicheck.yml +++ b/.github/workflows/abicheck.yml @@ -41,26 +41,10 @@ jobs: 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 - --suppressions current/.github/workflows/abidiff_suppressions.abignore - --headers-dir1 previous/include/ - --headers-dir2 current/include/ - previous/build/lib/libfuse3.so - current/build/lib/libfuse3.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 index 235341b55..cc33ac12d 100644 --- a/.github/workflows/abicheck_prev_release.yml +++ b/.github/workflows/abicheck_prev_release.yml @@ -37,7 +37,6 @@ jobs: id: prev_release run: | cd current - chmod +x .github/workflows/find_previous_release_tag.sh PREV_TAG=$(.github/workflows/find_previous_release_tag.sh) echo "prev_tag=$PREV_TAG" >> $GITHUB_OUTPUT @@ -50,27 +49,11 @@ jobs: 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 "Previous release tag: ${{ steps.prev_release.outputs.prev_tag }}" - echo "Commit-id of previous release: $(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 - --suppressions current/.github/workflows/abidiff_suppressions.abignore - --headers-dir1 previous/include/ - --headers-dir2 current/include/ - previous/build/lib/libfuse3.so - current/build/lib/libfuse3.so + current/.github/workflows/abicheck.sh --current-dir current --previous-dir previous From d111104443a7566d06de9170d76c9b201bc733cd Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Wed, 29 Jul 2026 01:10:53 +0200 Subject: [PATCH 246/310] test: replace the pytest suite with the shell runner The security patches in this series carry test/ hunks written against master's shell runner, so port the cases to it, drop pytest, and import the CI build matrix that drives it. This is a squashed backport, not a per-commit port: master grows the shell runner and its cases over many commits from 46e18b7f47c9 ("test: add a shell test runner") onwards, and picking them one by one onto 3.18 is not practical. What lands here is a snapshot of master's test/ and .github/workflows/, so the individual master commits are not recorded. Four discovered cases are left out because their prerequisites are master-only: unit/test_loop_config, unit/test_mount_flags, ctests/teardown-watchdog and misc/printcap-caps. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> --- .../workflows/install-ubuntu-dependencies.sh | 232 +++ .github/workflows/pr-ci.yml | 203 +- .gitignore | 3 + README.md | 18 +- dev-docs/release-process.md | 2 +- example/hello_ll_uds.c | 26 +- requirements.txt | 3 +- test/cases/README.md | 418 +++++ test/cases/ctests/api-30.sh | 7 + test/cases/ctests/signals.sh | 7 + test/cases/ctests/write-cache-writeback.sh | 8 + test/cases/ctests/write-cache.sh | 11 + test/cases/examples/cuse.sh | 29 + .../examples/dev-auto-unmount-nonroot.sh | 15 + test/cases/examples/dev-auto-unmount-root.sh | 15 + test/cases/examples/hello-direct-clone_fd.sh | 8 + test/cases/examples/hello-direct.sh | 8 + .../cases/examples/hello-dropcaps-clone_fd.sh | 8 + test/cases/examples/hello-dropcaps.sh | 8 + .../examples/hello-ll-direct-clone_fd.sh | 8 + test/cases/examples/hello-ll-direct.sh | 8 + .../examples/hello-ll-dropcaps-clone_fd.sh | 8 + test/cases/examples/hello-ll-dropcaps.sh | 8 + .../examples/hello-ll-mount-fuse-clone_fd.sh | 8 + test/cases/examples/hello-ll-mount-fuse.sh | 8 + .../examples/hello-mount-fuse-clone_fd.sh | 8 + test/cases/examples/hello-mount-fuse.sh | 8 + test/cases/examples/ioctl-ll.sh | 24 + test/cases/examples/ioctl.sh | 23 + test/cases/examples/null.sh | 24 + test/cases/examples/passthrough-base-debug.sh | 13 + test/cases/examples/passthrough-base.sh | 13 + test/cases/examples/passthrough-fh-debug.sh | 13 + test/cases/examples/passthrough-fh.sh | 13 + test/cases/examples/passthrough-hp-nocache.sh | 19 + test/cases/examples/passthrough-hp.sh | 16 + test/cases/examples/passthrough-ll-debug.sh | 13 + .../passthrough-ll-writeback-debug.sh | 17 + .../examples/passthrough-ll-writeback.sh | 17 + test/cases/examples/passthrough-ll.sh | 13 + test/cases/examples/passthrough-plus-debug.sh | 13 + test/cases/examples/passthrough-plus.sh | 13 + .../examples/passthrough-zero-ino-debug.sh | 14 + test/cases/examples/passthrough-zero-ino.sh | 14 + test/cases/examples/poll.sh | 11 + test/cases/examples/release-unlink-race.sh | 24 + .../examples/test-hello-direct-clone_fd.sh | 8 + test/cases/examples/test-hello-direct.sh | 8 + .../examples/test-hello-dropcaps-clone_fd.sh | 8 + test/cases/examples/test-hello-dropcaps.sh | 8 + .../test-hello-mount-fuse-clone_fd.sh | 8 + test/cases/examples/test-hello-mount-fuse.sh | 8 + test/cases/lib/checks.py | 972 ++++++++++ test/cases/lib/common.sh | 553 ++++++ test/cases/lib/hello.sh | 63 + test/cases/lib/mountinfo.sh | 31 + test/cases/lib/notify.sh | 112 ++ test/cases/lib/passthrough.sh | 82 + test/cases/misc/hello-uds.sh | 26 + test/cases/misc/printcap.sh | 11 + test/cases/mount/attrs-default-hello-ll.sh | 12 + test/cases/mount/attrs-default-hello.sh | 12 + test/cases/mount/attrs-nodev-hello-ll.sh | 12 + test/cases/mount/attrs-nodev-hello.sh | 12 + test/cases/mount/attrs-nosuid-hello-ll.sh | 12 + test/cases/mount/attrs-nosuid-hello.sh | 12 + test/cases/mount/attrs-ro-hello-ll.sh | 14 + test/cases/mount/attrs-ro-hello.sh | 14 + test/cases/mount/attrs-root-dev-hello-ll.sh | 16 + test/cases/mount/attrs-root-dev-hello.sh | 16 + test/cases/mount/attrs-root-suid-hello-ll.sh | 16 + test/cases/mount/attrs-root-suid-hello.sh | 16 + test/cases/mount/blkdev-fsname-hello-ll.sh | 28 + test/cases/mount/blkdev-fsname-hello.sh | 28 + .../mount/mountinfo-baseline-hello-ll.sh | 12 + test/cases/mount/mountinfo-baseline-hello.sh | 12 + test/cases/mount/mountinfo-fsname-hello-ll.sh | 12 + test/cases/mount/mountinfo-fsname-hello.sh | 12 + .../mountinfo-subtype-fsname-hello-ll.sh | 13 + .../mount/mountinfo-subtype-fsname-hello.sh | 13 + .../cases/mount/mountinfo-subtype-hello-ll.sh | 12 + test/cases/mount/mountinfo-subtype-hello.sh | 12 + test/cases/notify/file-size-no-notify.sh | 8 + test/cases/notify/file-size.sh | 8 + .../notify/inval-entry-expire-no-notify.sh | 15 + test/cases/notify/inval-entry-expire.sh | 15 + .../notify/inval-entry-inc-epoch-no-notify.sh | 13 + test/cases/notify/inval-entry-inc-epoch.sh | 13 + .../inval-entry-invalidate-no-notify.sh | 9 + test/cases/notify/inval-entry-invalidate.sh | 9 + test/cases/notify/inval-inode-no-notify.sh | 8 + test/cases/notify/inval-inode.sh | 8 + .../cases/notify/invalidate-path-no-notify.sh | 8 + test/cases/notify/invalidate-path.sh | 8 + test/cases/notify/prune-no-notify.sh | 12 + test/cases/notify/prune.sh | 12 + test/cases/notify/store-retrieve-no-notify.sh | 8 + test/cases/notify/store-retrieve.sh | 8 + test/cases/unit/test_abi | 1 + test/ci-build.sh | 360 ++-- test/ci/doxygen.sh | 8 + test/ci/prepare-runner.sh | 48 + test/conftest.py | 100 - test/meson.build | 46 +- test/pytest.ini | 6 - test/run-tests.py | 1648 +++++++++++++++++ test/test_api_30.c | 256 +++ test/test_ctests.py | 161 -- test/test_custom_io.py | 81 - test/test_examples.py | 980 ---------- test/test_syscalls.c | 28 +- test/util.py | 211 --- test/wrong_command.c | 16 - 113 files changed, 5976 insertions(+), 1752 deletions(-) create mode 100755 .github/workflows/install-ubuntu-dependencies.sh create mode 100644 test/cases/README.md create mode 100755 test/cases/ctests/api-30.sh create mode 100755 test/cases/ctests/signals.sh create mode 100755 test/cases/ctests/write-cache-writeback.sh create mode 100755 test/cases/ctests/write-cache.sh create mode 100755 test/cases/examples/cuse.sh create mode 100755 test/cases/examples/dev-auto-unmount-nonroot.sh create mode 100755 test/cases/examples/dev-auto-unmount-root.sh create mode 100755 test/cases/examples/hello-direct-clone_fd.sh create mode 100755 test/cases/examples/hello-direct.sh create mode 100755 test/cases/examples/hello-dropcaps-clone_fd.sh create mode 100755 test/cases/examples/hello-dropcaps.sh create mode 100755 test/cases/examples/hello-ll-direct-clone_fd.sh create mode 100755 test/cases/examples/hello-ll-direct.sh create mode 100755 test/cases/examples/hello-ll-dropcaps-clone_fd.sh create mode 100755 test/cases/examples/hello-ll-dropcaps.sh create mode 100755 test/cases/examples/hello-ll-mount-fuse-clone_fd.sh create mode 100755 test/cases/examples/hello-ll-mount-fuse.sh create mode 100755 test/cases/examples/hello-mount-fuse-clone_fd.sh create mode 100755 test/cases/examples/hello-mount-fuse.sh create mode 100755 test/cases/examples/ioctl-ll.sh create mode 100755 test/cases/examples/ioctl.sh create mode 100755 test/cases/examples/null.sh create mode 100755 test/cases/examples/passthrough-base-debug.sh create mode 100755 test/cases/examples/passthrough-base.sh create mode 100755 test/cases/examples/passthrough-fh-debug.sh create mode 100755 test/cases/examples/passthrough-fh.sh create mode 100755 test/cases/examples/passthrough-hp-nocache.sh create mode 100755 test/cases/examples/passthrough-hp.sh create mode 100755 test/cases/examples/passthrough-ll-debug.sh create mode 100755 test/cases/examples/passthrough-ll-writeback-debug.sh create mode 100755 test/cases/examples/passthrough-ll-writeback.sh create mode 100755 test/cases/examples/passthrough-ll.sh create mode 100755 test/cases/examples/passthrough-plus-debug.sh create mode 100755 test/cases/examples/passthrough-plus.sh create mode 100755 test/cases/examples/passthrough-zero-ino-debug.sh create mode 100755 test/cases/examples/passthrough-zero-ino.sh create mode 100755 test/cases/examples/poll.sh create mode 100755 test/cases/examples/release-unlink-race.sh create mode 100755 test/cases/examples/test-hello-direct-clone_fd.sh create mode 100755 test/cases/examples/test-hello-direct.sh create mode 100755 test/cases/examples/test-hello-dropcaps-clone_fd.sh create mode 100755 test/cases/examples/test-hello-dropcaps.sh create mode 100755 test/cases/examples/test-hello-mount-fuse-clone_fd.sh create mode 100755 test/cases/examples/test-hello-mount-fuse.sh create mode 100755 test/cases/lib/checks.py create mode 100644 test/cases/lib/common.sh create mode 100644 test/cases/lib/hello.sh create mode 100644 test/cases/lib/mountinfo.sh create mode 100644 test/cases/lib/notify.sh create mode 100644 test/cases/lib/passthrough.sh create mode 100755 test/cases/misc/hello-uds.sh create mode 100755 test/cases/misc/printcap.sh create mode 100755 test/cases/mount/attrs-default-hello-ll.sh create mode 100755 test/cases/mount/attrs-default-hello.sh create mode 100755 test/cases/mount/attrs-nodev-hello-ll.sh create mode 100755 test/cases/mount/attrs-nodev-hello.sh create mode 100755 test/cases/mount/attrs-nosuid-hello-ll.sh create mode 100755 test/cases/mount/attrs-nosuid-hello.sh create mode 100755 test/cases/mount/attrs-ro-hello-ll.sh create mode 100755 test/cases/mount/attrs-ro-hello.sh create mode 100755 test/cases/mount/attrs-root-dev-hello-ll.sh create mode 100755 test/cases/mount/attrs-root-dev-hello.sh create mode 100755 test/cases/mount/attrs-root-suid-hello-ll.sh create mode 100755 test/cases/mount/attrs-root-suid-hello.sh create mode 100755 test/cases/mount/blkdev-fsname-hello-ll.sh create mode 100755 test/cases/mount/blkdev-fsname-hello.sh create mode 100755 test/cases/mount/mountinfo-baseline-hello-ll.sh create mode 100755 test/cases/mount/mountinfo-baseline-hello.sh create mode 100755 test/cases/mount/mountinfo-fsname-hello-ll.sh create mode 100755 test/cases/mount/mountinfo-fsname-hello.sh create mode 100755 test/cases/mount/mountinfo-subtype-fsname-hello-ll.sh create mode 100755 test/cases/mount/mountinfo-subtype-fsname-hello.sh create mode 100755 test/cases/mount/mountinfo-subtype-hello-ll.sh create mode 100755 test/cases/mount/mountinfo-subtype-hello.sh create mode 100755 test/cases/notify/file-size-no-notify.sh create mode 100755 test/cases/notify/file-size.sh create mode 100755 test/cases/notify/inval-entry-expire-no-notify.sh create mode 100755 test/cases/notify/inval-entry-expire.sh create mode 100755 test/cases/notify/inval-entry-inc-epoch-no-notify.sh create mode 100755 test/cases/notify/inval-entry-inc-epoch.sh create mode 100755 test/cases/notify/inval-entry-invalidate-no-notify.sh create mode 100755 test/cases/notify/inval-entry-invalidate.sh create mode 100755 test/cases/notify/inval-inode-no-notify.sh create mode 100755 test/cases/notify/inval-inode.sh create mode 100755 test/cases/notify/invalidate-path-no-notify.sh create mode 100755 test/cases/notify/invalidate-path.sh create mode 100755 test/cases/notify/prune-no-notify.sh create mode 100755 test/cases/notify/prune.sh create mode 100755 test/cases/notify/store-retrieve-no-notify.sh create mode 100755 test/cases/notify/store-retrieve.sh create mode 100644 test/cases/unit/test_abi create mode 100755 test/ci/doxygen.sh create mode 100755 test/ci/prepare-runner.sh delete mode 100644 test/conftest.py delete mode 100644 test/pytest.ini create mode 100755 test/run-tests.py create mode 100644 test/test_api_30.c delete mode 100644 test/test_ctests.py delete mode 100644 test/test_custom_io.py delete mode 100755 test/test_examples.py delete mode 100644 test/util.py delete mode 100644 test/wrong_command.c 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/pr-ci.yml b/.github/workflows/pr-ci.yml index 5a53cce58..f7026286a 100644 --- a/.github/workflows/pr-ci.yml +++ b/.github/workflows/pr-ci.yml @@ -17,26 +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 \ - liburing-dev libnuma-dev - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - - uses: actions/setup-python@v6 + ./.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 <category>/<leaf>, so its logs/ sits three + # levels under fuse-tests/run/: <config>/<category>/<leaf>/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/.gitignore b/.gitignore index 0412bc73f..e7fdd120d 100644 --- a/.gitignore +++ b/.gitignore @@ -40,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/README.md b/README.md index eaf1308d5..0596f8a90 100644 --- a/README.md +++ b/README.md @@ -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 --------------------- diff --git a/dev-docs/release-process.md b/dev-docs/release-process.md index fe4c225d1..c5b8a0d96 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/` diff --git a/example/hello_ll_uds.c b/example/hello_ll_uds.c index b0a6e9cd4..aa906b71a 100644 --- a/example/hello_ll_uds.c +++ b/example/hello_ll_uds.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/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/test/cases/README.md b/test/cases/README.md new file mode 100644 index 000000000..eb8807e4e --- /dev/null +++ b/test/cases/README.md @@ -0,0 +1,418 @@ +# 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_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 + +# 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`. + +`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..8cf6bbd3e --- /dev/null +++ b/test/cases/lib/checks.py @@ -0,0 +1,972 @@ +#!/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 + + +# ------------------------------------------------------------------ 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_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..9efa9e6f0 --- /dev/null +++ b/test/cases/lib/common.sh @@ -0,0 +1,553 @@ +# 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 +# _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 + [ -e /dev/fuse ] || + _notrun "the FUSE kernel module does not seem to be loaded" + [ "$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/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 2bced379c..3006795f8 100755 --- a/test/ci-build.sh +++ b/test/ci-build.sh @@ -1,168 +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} -) - -# Sanitized with io-uring -export CC=clang -export CXX=clang++ -export FUSE_URING_ENABLE=1 -sanitized_build -unset FUSE_URING_ENABLE - -# 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" - -# Sanitized build without fuse-io-uring -export CC=clang -export CXX=clang++ -sanitized_build "-Denable-io-uring=false" - -# Build without any sanitizer -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 291c9199b..000000000 --- a/test/conftest.py +++ /dev/null @@ -1,100 +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: - # Skip FUSE error messages in the format "unique: X, error: -Y (...), outsize: Z" - # These are no errors, but just fuse debug messages with the return code - if re.search(r'unique: \d+, error: -\d+ \(.*\), outsize: \d+', hit.group(0)): - continue - raise AssertionError(f'Suspicious output to stderr (matched "{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/meson.build b/test/meson.build index 9f6f409cc..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 ], @@ -26,20 +26,36 @@ 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/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 b3863e058..000000000 --- a/test/test_ctests.py +++ /dev/null @@ -1,161 +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") - -def test_signals(output_checker): - """Test for proper signal handling (issue #1182)""" - logger = logging.getLogger(__name__) - logger.debug("Testing signal handling") - cmdline = [ pjoin(basename, 'test', 'test_signals') ] - logger.debug(f"Command line: {' '.join(cmdline)}") - subprocess.run(cmdline, stdout=output_checker.fd, \ - stderr=output_checker.fd, timeout=10, check=True) - logger.debug("Signal handling test completed successfully") - 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 05efa0993..000000000 --- a/test/test_examples.py +++ /dev/null @@ -1,980 +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 -from enum import Enum - -class InodeCheck(Enum): - EXACT = 1 - NONZERO = 2 - -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', 'passthrough_zero_ino')) -@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')) - - inode_check = InodeCheck.EXACT - if name == 'passthrough_plus': - cmdline = base_cmdline + \ - [ pjoin(basename, 'example', 'passthrough'), - '--plus', '-f', mnt_dir ] - elif name == 'passthrough_zero_ino': - cmdline = base_cmdline + \ - [ pjoin(basename, 'example', 'passthrough'), - '--plus', '--readdir-zero-inodes', '-f', mnt_dir ] - inode_check = InodeCheck.NONZERO - elif name == 'passthrough_ll': - cmdline = base_cmdline + \ - [ pjoin(basename, 'example', name), - '-f', mnt_dir, '-o', 'timeout=0' ] - else: # passthrough and passthrough_fh - cmdline = base_cmdline + \ - [ pjoin(basename, 'example', name), - '-f', mnt_dir ] - - # Set all timeouts to 0 for everything except passthrough_ll - # (this includes passthrough, passthrough_plus, and passthrough_fh) - if name != 'passthrough_ll': - cmdline.extend(['-o', 'entry_timeout=0,negative_timeout=0,attr_timeout=0,ac_attr_timeout=0']) - - 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') - - print(f"\nDebug: 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) - work_dir = mnt_dir + src_dir - - tst_statvfs(work_dir) - tst_readdir(src_dir, work_dir, inode_check) - tst_readdir_big(src_dir, work_dir, inode_check) - tst_open_read(src_dir, work_dir) - tst_open_write(src_dir, work_dir) - tst_create(work_dir) - tst_passthrough(src_dir, work_dir, inode_check) - 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) - - if inode_check == InodeCheck.EXACT: - 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", "inc_epoch")) -@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') - elif only_expire == "inc_epoch": - cmdline.append('--inc-epoch') - if fuse_proto < (7,44): - pytest.skip('inc-epoch 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_inodes_nonzero(lines): - inode_nums = [int(line.split()[0]) for line in lines] - assert all(i != 0 for i in inode_nums), inode_nums - -def tst_inode(inode_check, actual, expected): - if inode_check == InodeCheck.EXACT: - assert expected == actual - elif inode_check == InodeCheck.NONZERO: - assert actual != 0 - -def tst_readdir(src_dir, mnt_dir, inode_check=InodeCheck.EXACT): - 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) - if inode_check == InodeCheck.EXACT: - inodes_should = readdir_inode(src_newdir) - assert inodes_is == inodes_should - elif inode_check == InodeCheck.NONZERO: - tst_inodes_nonzero(inodes_is) - - os.unlink(file_) - os.unlink(subfile) - os.rmdir(subdir) - os.rmdir(src_newdir) - -def tst_readdir_big(src_dir, mnt_dir, inode_check=InodeCheck.EXACT): - # 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) - if inode_check == InodeCheck.EXACT: - inodes_should = readdir_inode(src_dir) - assert inodes_is == inodes_should - elif inode_check == InodeCheck.NONZERO: - tst_inodes_nonzero(inodes_is) - - for fname in fnames: - # A comment just to get a diff - stat_src = os.stat(pjoin(src_dir, fname)) - stat_mnt = os.stat(pjoin(mnt_dir, fname)) - tst_inode(inode_check, stat_mnt.st_ino, stat_src.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, inode_check=InodeCheck.EXACT): - name = name_generator() - src_name = pjoin(src_dir, name) - mnt_name = pjoin(mnt_dir, name) - - print(f"\nDebug: Creating file {name}") - print(f"Debug: src_name={src_name}") - print(f"Debug: mnt_name={mnt_name}") - - # First test: write to source directory - 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') - - print(f"Debug: File written to src_name") - - start_time = time.time() - while time.time() - start_time < 10: # 10 second timeout - if name in os.listdir(mnt_dir): - break - print(f"Debug: Waiting for file to appear... ({time.time() - start_time:.1f}s)") - time.sleep(0.1) - else: - pytest.fail("File did not appear in mount directory within 10 seconds") - - assert name in os.listdir(src_dir) - assert name in os.listdir(mnt_dir) - - # Compare relevant stat attributes - src_stat = os.stat(src_name) - mnt_stat = os.stat(mnt_name) - assert src_stat.st_mode == mnt_stat.st_mode - tst_inode(inode_check, mnt_stat.st_ino, src_stat.st_ino) - assert src_stat.st_size == mnt_stat.st_size - assert src_stat.st_mtime == mnt_stat.st_mtime - - # Second test: write to mount directory - name = name_generator() - src_name = pjoin(src_dir, name) - mnt_name = pjoin(mnt_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) - - # Compare relevant stat attributes - src_stat = os.stat(src_name) - mnt_stat = os.stat(mnt_name) - assert src_stat.st_mode == mnt_stat.st_mode - tst_inode(inode_check, mnt_stat.st_ino, src_stat.st_ino) - assert src_stat.st_size == mnt_stat.st_size - assert abs(src_stat.st_mtime - mnt_stat.st_mtime) < 0.01 - - -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_syscalls.c b/test/test_syscalls.c index 61ee953b9..115eab8d3 100644 --- a/test/test_syscalls.c +++ b/test/test_syscalls.c @@ -85,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]) @@ -109,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; } @@ -2107,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]); diff --git a/test/util.py b/test/util.py deleted file mode 100644 index 125fd50fe..000000000 --- a/test/util.py +++ /dev/null @@ -1,211 +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: - if test_fn(mnt_dir): - return True - 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 -} From dea927deb73a3fc696420376158d27f7898cdd5d Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Sun, 30 Aug 2026 23:04:19 +0200 Subject: [PATCH 247/310] lib: pass UMOUNT_NOFOLLOW on the kernel unmount paths These run with the daemon's own privileges, so a swapped symlink gains nothing, but the library has no reason to follow one on unmount either. fuse_kern_umount_mountfd() is left alone - it unmounts through /proc/self/fd/N, which is a symlink by construction. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> (cherry picked from commit ceb46e91864506011acb760b597e41a4d852f16a) --- lib/mount.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/mount.c b/lib/mount.c index 7a856c101..40f3317a4 100644 --- a/lib/mount.c +++ b/lib/mount.c @@ -332,7 +332,7 @@ 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; @@ -618,7 +618,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); @@ -692,7 +692,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) { From d28f9e31638617e508ce3ca94462292c7f982c8a Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Sun, 30 Aug 2026 23:02:28 +0200 Subject: [PATCH 248/310] fusermount: pass UMOUNT_NOFOLLOW on the non-setuid unmount path The geteuid() != 0 branch of do_unmount is taken when the helper is not installed setuid-root, so it is not exploitable, but there is no reason for it to follow a symlink either. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> (cherry picked from commit bafe10299acf716c0653ad9ab208868ee5a7929c) --- util/fusermount.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util/fusermount.c b/util/fusermount.c index c9b9088c6..45478b89a 100644 --- a/util/fusermount.c +++ b/util/fusermount.c @@ -1743,7 +1743,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", From 6fde84a49a13ef721d7981566a05052fb44aa038 Mon Sep 17 00:00:00 2001 From: Abhinav Agarwal <abhinavagarwal1996@gmail.com> Date: Wed, 15 Apr 2026 10:15:16 -0700 Subject: [PATCH 249/310] fusermount: Check fstat return value for comm fd validation The fstat() call on the communication fd from _FUSE_COMMFD was not checking its return value. On failure (e.g., invalid fd number), statbuf remains uninitialized and the subsequent S_ISSOCK check operates on garbage stack data, potentially bypassing the socket type validation. Check the return value and fail closed on error. Signed-off-by: Abhinav Agarwal <abhinavagarwal1996@gmail.com> (cherry picked from commit 349b83231bb9edaf1f05bc773fb6ca049e1c7276) --- util/fusermount.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/util/fusermount.c b/util/fusermount.c index 45478b89a..9ac24e457 100644 --- a/util/fusermount.c +++ b/util/fusermount.c @@ -1670,7 +1670,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", From b80e5307de693a04247e732be1711dc07525b3b5 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Sun, 30 Aug 2026 22:48:42 +0200 Subject: [PATCH 250/310] fusermount: pass UMOUNT_NOFOLLOW on the send_fd cleanup path This umount2() runs with euid 0 on a caller-supplied path, so an unprivileged user can swap the mountpoint for a symlink and make the setuid-root helper detach an unrelated mount. unmount_fuse_locked() already passes the flag. Reported-by: Vivek Parikh <vivek.parikh@breachx.ai> Signed-off-by: Bernd Schubert <bernd@bsbernd.com> (cherry picked from commit 81e46d5eb0ff55dbd62bad6e7a90865527a0461a) --- util/fusermount.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/util/fusermount.c b/util/fusermount.c index 9ac24e457..d2fcff01e 100644 --- a/util/fusermount.c +++ b/util/fusermount.c @@ -1693,7 +1693,12 @@ int main(int argc, char *argv[]) res = send_fd(cfd, fd); if (res != 0) { - umount2(mnt, MNT_DETACH); /* lazy umount */ + /* + * euid 0 here and mnt is caller supplied - without + * UMOUNT_NOFOLLOW a swapped symlink detaches an + * unrelated mount. + */ + umount2(mnt, MNT_DETACH | UMOUNT_NOFOLLOW); /* lazy umount */ goto err_out; } close(fd); From 8dea5a6d222e63f049244896532fb274dc5066b8 Mon Sep 17 00:00:00 2001 From: Abhinav Agarwal <abhinavagarwal1996@gmail.com> Date: Wed, 15 Apr 2026 11:58:29 -0700 Subject: [PATCH 251/310] fusermount: Guard trailing comma removal against empty string get_mnt_opts() accesses (*mnt_optsp)[l-1] without checking that l > 0. If the option string is empty (reachable via read-only mount with no additional options), this is an out-of-bounds read at index -1. Signed-off-by: Abhinav Agarwal <abhinavagarwal1996@gmail.com> (cherry picked from commit 8eda8f9566094c6503c96b704985f6bf99983c1c) --- util/fusermount.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util/fusermount.c b/util/fusermount.c index d2fcff01e..7c54ef2e1 100644 --- a/util/fusermount.c +++ b/util/fusermount.c @@ -852,7 +852,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(); From 1dae9ce83413f663e3868e6b7d1ca056eeecd0de Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Mon, 31 Aug 2026 02:13:26 +0200 Subject: [PATCH 252/310] util: avoid a pointer underflow when trimming fuse.conf lines A blank or all-whitespace line makes the trailing-whitespace loop in strip_line() form line - 1 and then compare that pointer against line, which is undefined behaviour even though the store that follows lands inside the buffer. Count a length down instead, and cover the parser with a unit test that feeds it exactly those lines. The cherry pick to 3.18 differs in respect to the unit test, which is left out, because strip_line() is still part of util/fusermount.c here and the test builds against util/fuser_conf.c. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> (cherry picked from commit 1daf891f0daa18264ec5e500dc78b40a386cacfd) --- util/fusermount.c | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/util/fusermount.c b/util/fusermount.c index 7c54ef2e1..4d58682e5 100644 --- a/util/fusermount.c +++ b/util/fusermount.c @@ -685,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); From 3973490ee2928d24d6638025062c6eb1d8f05d5a Mon Sep 17 00:00:00 2001 From: Abhinav Agarwal <abhinavagarwal1996@gmail.com> Date: Mon, 31 Aug 2026 02:13:44 +0200 Subject: [PATCH 253/310] fusermount: Reject invalid negative mount_max in fuse.conf mount_max is parsed with %i (signed) from /etc/fuse.conf. The value -1 is the documented sentinel for "no limit", but any other negative value (e.g., -2 from a typo) passes the mount_max != -1 guard and makes the mount_count >= mount_max comparison always true, blocking all non-root FUSE mounts. Reject values below -1 with a warning rather than silently accepting them. The cherry pick to 3.18 differs in respect to the file it changes, because parse_line() is still part of util/fusermount.c here and reads progname from file scope instead of taking it as an argument. Signed-off-by: Abhinav Agarwal <abhinavagarwal1996@gmail.com> Signed-off-by: Bernd Schubert <bernd@bsbernd.com> (cherry picked from commit 5e3f016beff43cc0dfd3649f75cb6d0d19748f49) --- util/fusermount.c | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/util/fusermount.c b/util/fusermount.c index 4d58682e5..ba70ea241 100644 --- a/util/fusermount.c +++ b/util/fusermount.c @@ -708,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", From e1af09138f887cfb0d5f804da0102f4a60e7fd96 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Wed, 29 Apr 2026 14:26:44 +0200 Subject: [PATCH 254/310] Fix a sign bug in prepare_fuse_fd() fcntl() returns -1 on error. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> (cherry picked from commit f8abf5d1baa9fb689255f7091937081025749158) --- util/mount.fuse.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util/mount.fuse.c b/util/mount.fuse.c index f1a90fe8a..7dfff1b54 100644 --- a/util/mount.fuse.c +++ b/util/mount.fuse.c @@ -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); From 303a27c5fb30a90230a0ec43323571c150137627 Mon Sep 17 00:00:00 2001 From: Keerthana KT <keerthana@labs.digiscrypt.com> Date: Mon, 31 Aug 2026 02:14:31 +0200 Subject: [PATCH 255/310] mount_util: terminate the mount and umount argv with -- fsname reaches add_mount() straight from -ofsname= in setuid-root fusermount3, and from the SOURCE command on the mount.service socket, so an unprivileged caller controls a positional operand of /bin/mount. fuse_mount_spawn() raises the real uid to 0 just before posix_spawn(), so mount(8) is not in restricted mode either, and a value such as -oro,--target=/etc is parsed as another option instead of as the source. add -- after the options, and do the same in exec_umount() and remove_mount(), which build their argv the same way. The cherry pick to 3.18 differs in respect to the umount argv, which needed -l moved ahead of the operand, because this branch still forks and calls execle() rather than fuse_mount_spawn() and has no mount.service socket. Signed-off-by: Keerthana KT <keerthana@labs.digiscrypt.com> Signed-off-by: Bernd Schubert <bernd@bsbernd.com> (cherry picked from commit 9d63190c9c56a24e1095144e8ffaf66124fa0550) --- lib/mount_util.c | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/lib/mount_util.c b/lib/mount_util.c index 8c0cdf72d..10dc3d363 100644 --- a/lib/mount_util.c +++ b/lib/mount_util.c @@ -117,8 +117,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 +184,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)); @@ -249,7 +256,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); From b192f05ef1f1eb5c3e8b913eba92b9c6d667856d Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Mon, 31 Aug 2026 02:18:21 +0200 Subject: [PATCH 256/310] mount_util: skip mtab updates for option-like mount arguments Commit 9d63190c9c56 ("mount_util: terminate the mount and umount argv with --") appends "--" so that a caller-controlled fsname cannot be read as an option. That only holds where mount(8) honours "--", which BusyBox does not, as of now. Mounting as root with -ofsname=-oro: 1. mount(2) succeeds 2. add_mount() execs /bin/mount -i -f ... -- -oro <mnt> to write the mtab/utab record 3. BusyBox mount(8) folds every "--xxx" argument into its option string before getopt runs, so the "--" is swallowed and -oro is parsed as -o ro 4. <mnt> is then the only operand left, so BusyBox mount(8) looks it up in /etc/fstab, does not find it, and exits non-zero 5. add_mount() returns -1, and fuse_kern_do_mount(), fuse_kern_fsmount() and fusermount3's do_mount() unmount and fail POSIX advises against a leading <hyphen-minus> in a filename for exactly this reason (XBD 3.264, "Portable Filename"), so declining to record such an fsname loses little. mount -f and umount --fake only record a mount(2) or umount2() that has already happened, so add_mount() and remove_mount() return 0 for such an operand, and for a NULL that would truncate argv. fuse_mnt_umount() runs a real umount(8), which cannot be skipped, so it unmounts with umount2(). The cherry pick to 3.18 differs in respect to the fusermount3 sources, which gain lib/fuse_log.c for fuse_log(), because fusermount3 compiles mount_util.c without libfuse here. fuse_kern_fsmount() does not exist on this branch. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> (cherry picked from commit 18c74977e7c7f0095717bdccac2ab75b55ff59ac) --- lib/mount_util.c | 31 ++++++++++++++++++++++++++++++- util/meson.build | 3 ++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/lib/mount_util.c b/lib/mount_util.c index 10dc3d363..9a02745dd 100644 --- a/lib/mount_util.c +++ b/lib/mount_util.c @@ -10,6 +10,7 @@ #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); @@ -213,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", @@ -231,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); 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'), From 9ac67ca9d1e00c4ec3c96428a1297e27d0891486 Mon Sep 17 00:00:00 2001 From: Jingbo Xu <jefflexu@linux.alibaba.com> Date: Wed, 19 Aug 2026 17:13:55 +0800 Subject: [PATCH 257/310] fuse_lowlevel: fix io_uring notify retrieve reply buffer handling In io_uring mode, the notify reply specific header (fuse_notify_retrieve_in) is at the start of the payload buffer, not in the ring header's op_in area. Pass op_payload (instead of op_in) as inarg to do_notify_reply, and only subtract sizeof(fuse_notify_retrieve_in) from the buffer size since fuse_in_header is not included in the payload buffer. This follows the same pattern as _do_write_buf / do_write_buf where the io_uring path receives the op struct and payload already separated. Signed-off-by: Jingbo Xu <jefflexu@linux.alibaba.com> (cherry picked from commit 3b25c1d39620b4b0f445c3e3b042edc4a6e4c6b3) --- lib/fuse_lowlevel.c | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index d420b257b..0cc505e7e 100644 --- a/lib/fuse_lowlevel.c +++ b/lib/fuse_lowlevel.c @@ -3036,7 +3036,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; @@ -3057,6 +3057,15 @@ 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) { @@ -3276,8 +3285,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"); @@ -3766,7 +3774,7 @@ void fuse_session_process_uring_cqe(struct fuse_session *se, } else if (in->opcode == FUSE_NOTIFY_REPLY) { struct fuse_buf buf = { .size = payload_len, .mem = op_payload }; - do_notify_reply(req, in->nodeid, op_in, &buf); + _do_notify_reply(req, in->nodeid, op_payload, &buf); } else { fuse_ll_ops2[in->opcode].func(req, in->nodeid, op_in, op_payload); From 99d7e061d3dd63decda307c9cc0db60381437e7a Mon Sep 17 00:00:00 2001 From: Jingbo Xu <jefflexu@linux.alibaba.com> Date: Fri, 12 Jun 2026 10:02:58 +0800 Subject: [PATCH 258/310] fuse-io-uring: fix req_header_sz size calculation The header buffer is described by "struct fuse_uring_req_header" rather than "struct fuse_ring_ent". Signed-off-by: Jingbo Xu <jefflexu@linux.alibaba.com> (cherry picked from commit a59ffa424fd607ee43e2a373371a2dc48d48525e) --- lib/fuse_uring.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/fuse_uring.c b/lib/fuse_uring.c index c3127b7e5..1941c4fd5 100644 --- a/lib/fuse_uring.c +++ b/lib/fuse_uring.c @@ -762,7 +762,7 @@ static int fuse_uring_init_queue(struct fuse_ring_queue *queue) return res; } - queue->req_header_sz = ROUND_UP(sizeof(struct fuse_ring_ent), + queue->req_header_sz = ROUND_UP(sizeof(struct fuse_uring_req_header), page_sz); for (size_t idx = 0; idx < ring->queue_depth; idx++) { From af4fc9dccd93b2b9d2cf59257fd7868df9de0830 Mon Sep 17 00:00:00 2001 From: Abhinav Agarwal <abhinavagarwal1996@gmail.com> Date: Wed, 15 Apr 2026 10:15:27 -0700 Subject: [PATCH 259/310] fuse_lowlevel: Add O_CLOEXEC to pipe-max-size procfs open grow_pipe_to_max() opens /proc/sys/fs/pipe-max-size without O_CLOEXEC. In a multi-threaded FUSE daemon, a concurrent fork+exec (e.g., to invoke fusermount) between the open() and close() calls would leak this fd into the child process. Add the same O_CLOEXEC fallback guard used in mount.c and fuse_loop_mt.c for portability to older headers. Signed-off-by: Abhinav Agarwal <abhinavagarwal1996@gmail.com> (cherry picked from commit 79aefd4fd92135b7fe016567ace56b26553dc957) --- lib/fuse_lowlevel.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index 0cc505e7e..b3e0557bc 100644 --- a/lib/fuse_lowlevel.c +++ b/lib/fuse_lowlevel.c @@ -49,6 +49,10 @@ #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 @@ -774,7 +778,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; From 1fdc46b8731e15c9e9ecaaf8779633aa6a88f47f Mon Sep 17 00:00:00 2001 From: Abhinav Agarwal <abhinavagarwal1996@gmail.com> Date: Wed, 15 Apr 2026 11:58:03 -0700 Subject: [PATCH 260/310] iconv: Fix copy-paste error checking wrong iconv descriptor After opening the fromfs iconv descriptor, the error check on line 725 tests ic->tofs (already verified above) instead of ic->fromfs. A failed iconv_open for the fromfs direction is silently ignored, leaving an invalid descriptor that causes undefined behavior in iconv_convpath. Signed-off-by: Abhinav Agarwal <abhinavagarwal1996@gmail.com> (cherry picked from commit ee093d226cf4ecf4a97b768b3d631115225cc4db) --- lib/modules/iconv.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/iconv.c b/lib/modules/iconv.c index 417c9043d..5c055a7ec 100644 --- a/lib/modules/iconv.c +++ b/lib/modules/iconv.c @@ -722,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; From 8dc964a4dd50b41e36e81ef2231292970f451a3a Mon Sep 17 00:00:00 2001 From: Abhinav Agarwal <abhinavagarwal1996@gmail.com> Date: Wed, 15 Apr 2026 11:02:09 -0700 Subject: [PATCH 261/310] fuse_loop_mt: Remove duplicate pthread_mutex_destroy of mt_lock fuse_session_loop_mt_312() unconditionally destroys se->mt_lock before returning. The caller then invokes fuse_session_destroy(), which destroys the same mutex again. This is undefined behavior per POSIX on every MT shutdown. fuse_session_reset() does not re-initialize mt_lock between these calls, so the second destroy always operates on an already-destroyed mutex. Remove the destroy from fuse_session_loop_mt_312 and let fuse_session_destroy remain the sole owner of mutex teardown, consistent with how se->mt_finish (the semaphore) is already handled. Signed-off-by: Abhinav Agarwal <abhinavagarwal1996@gmail.com> (cherry picked from commit b6cb345591881d82b344911d8c77a39449b14e28) --- lib/fuse_loop_mt.c | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/fuse_loop_mt.c b/lib/fuse_loop_mt.c index ec4bb0b2b..113ec36b5 100644 --- a/lib/fuse_loop_mt.c +++ b/lib/fuse_loop_mt.c @@ -420,7 +420,6 @@ int err; fuse_uring_stop(se); } - pthread_mutex_destroy(&se->mt_lock); if(se->error != 0) err = se->error; From 590d18ca42d93f2f304418e129932850123d8ed3 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Sat, 4 Apr 2026 21:55:03 +0200 Subject: [PATCH 262/310] lib: Fix -ENOMEM null-dereferences in fuse_loop_mt.c Check if fuse_loop_cfg_create() returns NULL before dereferencing the config pointer. This fixes potential NULL dereferences in: - fuse_session_loop_mt_312() when accessing config->clone_fd - fuse_session_loop_mt_31() when calling fuse_loop_cfg_set_clone_fd() Reported by: Abhinav Agarwal <abhinav.agarwal@rubrik.com> Signed-off-by: Bernd Schubert <bernd@bsbernd.com> (cherry picked from commit b74ee530553629b9d456b68c6543913f12854036) --- lib/fuse_loop_mt.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/fuse_loop_mt.c b/lib/fuse_loop_mt.c index 113ec36b5..7bdb1c76f 100644 --- a/lib/fuse_loop_mt.c +++ b/lib/fuse_loop_mt.c @@ -381,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; } @@ -462,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); From facef06f617e4972612a55aa0ba6d8e9e9c4528d Mon Sep 17 00:00:00 2001 From: Jingbo Xu <jefflexu@linux.alibaba.com> Date: Thu, 18 Jun 2026 15:51:13 +0800 Subject: [PATCH 263/310] fuse_lowlevel: check fuse_ll_ops2 in uring CQE dispatch The uring CQE path dispatches handlers from fuse_ll_ops2[]. Signed-off-by: Jingbo Xu <jefflexu@linux.alibaba.com> (cherry picked from commit 4230b910815453b1a9764659fb4a141f9e1929f4) --- lib/fuse_lowlevel.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index b3e0557bc..539255d39 100644 --- a/lib/fuse_lowlevel.c +++ b/lib/fuse_lowlevel.c @@ -3755,7 +3755,7 @@ void fuse_session_process_uring_cqe(struct fuse_session *se, goto reply_err; err = ENOSYS; - if (in->opcode >= FUSE_MAXOP || !fuse_ll_ops[in->opcode].func) + if (in->opcode >= FUSE_MAXOP || !fuse_ll_ops2[in->opcode].func) goto reply_err; if (se->debug) { From 1ddceafe66c285dae6760775c2f8fcdda473bdb6 Mon Sep 17 00:00:00 2001 From: Mikhail Novosyolov <m.novosyolov@rosa.ru> Date: Sat, 28 Feb 2026 17:34:19 +0300 Subject: [PATCH 264/310] fix: add assert to prevent NULL pointer dereference in try_get_path Svace reported a DEREF_AFTER_NULL.EX defect at lib/fuse.c:1022: The function try_get_path() can be called with name == NULL (this is valid, checked at line 1013). However, if wnodep != NULL, it calls lookup_node(f, nodeid, name) without checking if name is NULL. lookup_node() then calls name_hash() which dereferences *name in a loop: for (; *name; name++) hash = hash * 31 + (unsigned char) *name; If name == NULL, this results in NULL pointer dereference. While current code doesn't trigger this bug (all callers with wnodep != NULL pass non-NULL name), adding an assert documents the function contract and catches potential misuse during development. Co-developed-by: Z.AI GLM-5 <noreplay@z.ai> Signed-off-by: Mikhail Novosyolov <m.novosyolov@rosa.ru> (cherry picked from commit 73db1b26a4c759ecf6cfce495d8be85d5e5085f4) --- lib/fuse.c | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/fuse.c b/lib/fuse.c index 9607bb0d2..991b4e5da 100644 --- a/lib/fuse.c +++ b/lib/fuse.c @@ -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) { From 60b62ddfc98154bf273cb9cf703c8bd3f130caf3 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Tue, 28 Jul 2026 19:32:49 +0200 Subject: [PATCH 265/310] example/cuse_client: cap the transfer size do_rw() passed the SIZE argument straight to calloc(), so a mistyped value asked the allocator for as much memory as the command line named. Reject anything above 16 MiB, which is far more than the fioc example ever transfers. Also removes the cpp/uncontrolled-allocation-size alert: the query treats a relational check on the size variable as a barrier. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> (cherry picked from commit a58566c741e5407f0295623abfeb816e98bba617) --- example/cuse_client.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/example/cuse_client.c b/example/cuse_client.c index de8244473..50f70f3bf 100644 --- a/example/cuse_client.c +++ b/example/cuse_client.c @@ -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); From f9cdb206bd82011900f8c5796c419650dda58d92 Mon Sep 17 00:00:00 2001 From: Abhinav Agarwal <abhinavagarwal1996@gmail.com> Date: Mon, 31 Aug 2026 02:20:03 +0200 Subject: [PATCH 266/310] fuse_lowlevel: Free mountpoint in fuse_session_destroy If fuse_session_mount() succeeds but fuse_session_unmount() is never called (e.g., on an error path), the mountpoint string allocated by strdup in fuse_session_mount() is leaked. Free it in fuse_session_destroy() using the same atomic_exchange pattern as fuse_session_unmount() to handle concurrent access safely. Signed-off-by: Abhinav Agarwal <abhinavagarwal1996@gmail.com> Signed-off-by: Bernd Schubert <bernd@bsbernd.com> (cherry picked from commit 6c055dddf7dd5e0ee01b5725d649742d03af37c3) --- lib/fuse_lowlevel.c | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index 539255d39..e9d7e4f60 100644 --- a/lib/fuse_lowlevel.c +++ b/lib/fuse_lowlevel.c @@ -3844,6 +3844,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); } From 18dc5ead242c43dcae95a7ae9a42974881a03017 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Mon, 31 Aug 2026 12:32:35 +0200 Subject: [PATCH 267/310] examples: reformat notify_* with clang-format These three files predate .clang-format and still use spaces, which trips checkpatch's LEADING_SPACE warning on any line touched nearby. Run clang-format so the following localtime_r() fix lands cleanly. The cherry pick to 3.18 differs in respect to the reformatting, which was regenerated with clang-format, because the upstream diff does not apply over this branch's tfs_init(). Signed-off-by: Bernd Schubert <bernd@bsbernd.com> (cherry picked from commit 0c6884b66acd020f25974b91126be2f2fcad4e88) --- example/notify_inval_entry.c | 506 ++++++++++++++-------------- example/notify_inval_inode.c | 472 +++++++++++++------------- example/notify_store_retrieve.c | 575 ++++++++++++++++---------------- 3 files changed, 785 insertions(+), 768 deletions(-) diff --git a/example/notify_inval_entry.c b/example/notify_inval_entry.c index 5ab3c32d1..a4a6fcd86 100644 --- a/example/notify_inval_entry.c +++ b/example/notify_inval_entry.c @@ -81,7 +81,6 @@ * \include notify_inval_entry.c */ - #define FUSE_USE_VERSION FUSE_MAKE_VERSION(3, 12) #include <fuse_lowlevel.h> @@ -105,319 +104,328 @@ static pthread_t main_thread; /* Command line parsing */ struct options { - int no_notify; - float timeout; - int update_interval; - int only_expire; - int inc_epoch; + 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, - .inc_epoch = 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), - OPTION("--inc-epoch", inc_epoch), - 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 *now; + ssize_t ret; - t = time(NULL); - now = localtime(&t); - assert(now != NULL); + t = time(NULL); + now = localtime(&t); + 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; - 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("fuse_lowlevel_notify_expire_entry not supported by kernel\n"); - 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("fuse_lowlevel_notify_increment_epoch not supported by kernel\n"); - 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 *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" - " --inc-epoch Increment epoch, invalidating all dentries\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; - } - 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 { +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 1dcecee14..41760fdd1 100644 --- a/example/notify_inval_inode.c +++ b/example/notify_inval_inode.c @@ -58,7 +58,6 @@ * \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,272 @@ 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 *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_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 23fdecc28..d72dad629 100644 --- a/example/notify_store_retrieve.c +++ b/example/notify_store_retrieve.c @@ -57,7 +57,6 @@ * \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,165 @@ 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 *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_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 From f38c880a41a18a884a0ad9dc39db3c418144a624 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Mon, 31 Aug 2026 02:20:49 +0200 Subject: [PATCH 268/310] examples: use localtime_r() in update_fs() update_fs() is called from the update_fs_loop() thread while the FUSE session serves requests on other threads. localtime() returns a pointer to a single shared static struct tm, so those calls race and strftime() can format a partially overwritten time. Also silences the CodeQL cpp/potentially-dangerous-function alerts. The cherry pick to 3.18 differs in respect to notify_prune.c, which is left unchanged, because that file does not exist on this branch. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> (cherry picked from commit 92c9f3b2b51f5a3b62bb46e61ceca5f1d1dc3139) --- example/invalidate_path.c | 3 ++- example/notify_inval_entry.c | 3 ++- example/notify_inval_inode.c | 3 ++- example/notify_store_retrieve.c | 3 ++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/example/invalidate_path.c b/example/invalidate_path.c index fad43ea77..3e2d24509 100644 --- a/example/invalidate_path.c +++ b/example/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/notify_inval_entry.c b/example/notify_inval_entry.c index a4a6fcd86..69ce39329 100644 --- a/example/notify_inval_entry.c +++ b/example/notify_inval_entry.c @@ -262,11 +262,12 @@ static const struct fuse_lowlevel_ops tfs_oper = { static void update_fs(void) { time_t t; + struct tm tmbuf; struct tm *now; ssize_t ret; t = time(NULL); - now = localtime(&t); + now = localtime_r(&t, &tmbuf); assert(now != NULL); ret = strftime(file_name, MAX_STR_LEN, "Time_is_%Hh_%Mm_%Ss", now); diff --git a/example/notify_inval_inode.c b/example/notify_inval_inode.c index 41760fdd1..c02853084 100644 --- a/example/notify_inval_inode.c +++ b/example/notify_inval_inode.c @@ -272,10 +272,11 @@ static const struct fuse_lowlevel_ops tfs_oper = { static void update_fs(void) { + struct tm tmbuf; struct tm *now; time_t t; t = time(NULL); - now = localtime(&t); + now = localtime_r(&t, &tmbuf); assert(now != NULL); file_size = strftime(file_contents, MAX_STR_LEN, diff --git a/example/notify_store_retrieve.c b/example/notify_store_retrieve.c index d72dad629..23d433318 100644 --- a/example/notify_store_retrieve.c +++ b/example/notify_store_retrieve.c @@ -326,10 +326,11 @@ static const struct fuse_lowlevel_ops tfs_oper = { static void update_fs(void) { + struct tm tmbuf; struct tm *now; time_t t; t = time(NULL); - now = localtime(&t); + now = localtime_r(&t, &tmbuf); assert(now != NULL); file_size = strftime(file_contents, MAX_STR_LEN, From 487e5b384062dd8ce0b6127dfc74e1b010c5ce4f Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Wed, 17 Jun 2026 11:49:51 +0200 Subject: [PATCH 269/310] memfs_ll: fix the locking, refcounting and bounds bugs The example served as a template often enough that its races were worth fixing: unsynchronised Inodes access, self-deadlock and a dangling dentry in link(), rename() taking its two directories in caller order, use-after-free on find() and on overwrite, an nlink double-decrement, missing error cleanup in create/mkdir, and unchecked read/write offsets. Squashed from master commits 237af7634726, 00b126812816, 9fbe6a0f172a, 8aa8ba677d93, b137cc849ea9, 7952b4b69db0, a22569e617ab, 2f6f54ba4cfc and 2137d4b0cc05. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> --- example/memfs_ll.cc | 227 +++++++++++++++++++++++++++----------------- 1 file changed, 138 insertions(+), 89 deletions(-) diff --git a/example/memfs_ll.cc b/example/memfs_ll.cc index 7055a434a..e0e428352 100644 --- a/example/memfs_ll.cc +++ b/example/memfs_ll.cc @@ -43,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) { @@ -138,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; } @@ -193,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()) { @@ -210,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; @@ -225,6 +229,7 @@ class Inode { bool is_empty() const { + std::lock_guard<std::mutex> lock(mutex); return dentries.empty(); } @@ -243,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; @@ -258,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)) { } @@ -298,7 +308,7 @@ class Dentry { Inode *get_inode() const { - return inode; + return inode.get(); } void inc_lookup() @@ -309,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); @@ -348,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)); @@ -378,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(); } }; @@ -462,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; @@ -483,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); @@ -495,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); } @@ -524,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; @@ -540,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; @@ -551,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; @@ -564,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)); @@ -581,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; @@ -592,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); } @@ -599,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()) { @@ -625,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; @@ -639,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; @@ -670,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); @@ -718,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); @@ -732,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); } @@ -752,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) @@ -775,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); } @@ -784,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; @@ -823,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; @@ -863,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; @@ -898,11 +935,13 @@ 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)) { @@ -923,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) { @@ -936,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(); @@ -966,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; @@ -995,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; From 072a64620283a1985349b224a67e50f1230672aa Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Thu, 6 Aug 2026 12:24:03 +0200 Subject: [PATCH 270/310] example/passthrough_hp: protect the dir stream with a DirHandle lock do_readdir() serialised on the directory's Inode::m although the state it protects is the DirHandle's DIR * and offset. Borrowing the inode lock made READDIR block every other operation on that directory and forced it to hold an Inode::m across do_lookup() and forget_one(), which both take fs.mutex. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> (cherry picked from commit 002d3e2a26620695c543e0fd5c9966e4e49d6e2f) --- example/passthrough_hp.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/example/passthrough_hp.cc b/example/passthrough_hp.cc index 2dc0f9503..893aacba5 100644 --- a/example/passthrough_hp.cc +++ b/example/passthrough_hp.cc @@ -680,6 +680,7 @@ static void sfs_readlink(fuse_req_t req, fuse_ino_t ino) struct DirHandle { DIR *dp{ nullptr }; off_t offset; + std::mutex m; // serialises readdir() on this handle DirHandle() = default; DirHandle(const DirHandle &) = delete; @@ -750,8 +751,7 @@ 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 }; + lock_guard<mutex> g{ d->m }; char *p; auto rem = size; int err = 0, count = 0; From 0d76d3a4534cb516405ee95704a0abbe8fa2c51d Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Thu, 6 Aug 2026 12:24:13 +0200 Subject: [PATCH 271/310] example/passthrough_hp: take fs.mutex when link() raises nlookup Every other lookup-count increment happens under fs.mutex, which is what lets forget_one() decide that a count of zero really means the inode is unreferenced. link() incremented without it, so a forget racing a link on the same inode could erase an inode the kernel had just been handed. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> (cherry picked from commit 77897d4586d25b7d36a0cb68e45b4d32ed19a7a3) --- example/passthrough_hp.cc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/example/passthrough_hp.cc b/example/passthrough_hp.cc index 893aacba5..8e450aa3d 100644 --- a/example/passthrough_hp.cc +++ b/example/passthrough_hp.cc @@ -548,6 +548,9 @@ static void sfs_link(fuse_req_t req, fuse_ino_t ino, fuse_ino_t parent, } 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__ << " " From 565550f82105d9fda477ae6133e73564d61d4065 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Thu, 6 Aug 2026 12:24:53 +0200 Subject: [PATCH 272/310] example/passthrough_hp: acquire fs.mutex before Inode::m forget_one() and sfs_unlink() took the inode lock first and fs.mutex second, while both lookup paths take them the other way round, so a FUSE_LOOKUP and a FUSE_FORGET on the same inode deadlock and the lookup is never answered. forget_one() needs no inode lock once the count has reached zero, so it drops it and erases by key under fs.mutex alone. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> (cherry picked from commit e9223eff5bc72f93afa11a3284ccacba94ec9b45) --- example/passthrough_hp.cc | 63 ++++++++++++++++++++++----------------- 1 file changed, 35 insertions(+), 28 deletions(-) diff --git a/example/passthrough_hp.cc b/example/passthrough_hp.cc index 8e450aa3d..063ee2499 100644 --- a/example/passthrough_hp.cc +++ b/example/passthrough_hp.cc @@ -150,7 +150,7 @@ struct Inode { }; struct Fs { - // Must be acquired *after* any Inode.m locks. + // Must be acquired *before* any Inode.m locks. std::mutex mutex; InodeMap inodes; // protected by mutex Inode root; @@ -426,10 +426,8 @@ static int do_lookup(fuse_ino_t parent, const char *name, fuse_entry_param *e) fs_lock.unlock(); 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 + /* 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; @@ -600,13 +598,13 @@ static void sfs_unlink(fuse_req_t req, fuse_ino_t parent, const char *name) } if (e.attr.st_nlink == 1) { Inode &inode = get_inode(e.ino); + 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; - lock_guard<mutex> g_fs{ fs.mutex }; close(inode.fd); inode.fd = -ENOENT; inode.generation++; @@ -623,32 +621,41 @@ static void sfs_unlink(fuse_req_t req, fuse_ino_t parent, const char *name) static void forget_one(fuse_ino_t ino, uint64_t n) { Inode &inode = get_inode(ino); - unique_lock<mutex> l{ inode.m }; + SrcId id; - if (n > inode.nlookup) { - cerr << "INTERNAL ERROR: Negative lookup count for inode " - << inode.src_ino << endl; - abort(); + { + 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 }; } - inode.nlookup -= n; - if (fs.debug) - cerr << "DEBUG:" << __func__ << ":" << __LINE__ << " " - << "inode " << inode.src_ino << " count " << inode.nlookup - << endl; + /* 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 (!inode.nlookup) { - lock_guard<mutex> g_fs{ fs.mutex }; - l.unlock(); - if (!inode.nlookup) { - if (fs.debug) - cerr << "DEBUG: forget: cleaning up inode " - << inode.src_ino << endl; - 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; + 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) From e93350b7415031eff265f29c896e6fd2efc6a950 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Thu, 6 Aug 2026 12:31:57 +0200 Subject: [PATCH 273/310] example/passthrough_hp: drop the object lock before replying fuse_reply_*() can be the point where the kernel drops its last reference, so a FORGET or a RELEASEDIR may arrive and destroy the inode or the dir handle - and with it the mutex - while the replying thread is still unwinding the scope guard that holds it. rmdir() took the parent lock for nothing but the unlinkat() and loses it entirely. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> (cherry picked from commit c7d7e73902850e24e2435454c5b7fcad49c0fba8) --- example/passthrough_hp.cc | 107 ++++++++++++++++++++++---------------- 1 file changed, 62 insertions(+), 45 deletions(-) diff --git a/example/passthrough_hp.cc b/example/passthrough_hp.cc index 063ee2499..e84e13c8f 100644 --- a/example/passthrough_hp.cc +++ b/example/passthrough_hp.cc @@ -132,6 +132,9 @@ struct Inode { 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 @@ -563,7 +566,6 @@ static void sfs_link(fuse_req_t req, fuse_ino_t ino, fuse_ino_t parent, 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); } @@ -690,7 +692,9 @@ static void sfs_readlink(fuse_req_t req, fuse_ino_t ino) struct DirHandle { DIR *dp{ nullptr }; off_t offset; - std::mutex m; // serialises readdir() on this handle + // serialises readdir(); drop before fuse_reply_*, the reply may let + // RELEASEDIR delete this handle + std::mutex m; DirHandle() = default; DirHandle(const DirHandle &) = delete; @@ -717,22 +721,24 @@ static void sfs_opendir(fuse_req_t req, fuse_ino_t ino, fuse_file_info *fi) 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 }; + { + // 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; + 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; + // 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; + d->offset = 0; + } fi->fh = reinterpret_cast<uint64_t>(d); if (fs.timeout) { @@ -761,7 +767,6 @@ 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); - lock_guard<mutex> g{ d->m }; char *p; auto rem = size; int err = 0, count = 0; @@ -777,6 +782,8 @@ static void do_readdir(fuse_req_t req, fuse_ino_t ino, size_t size, } p = buf; + unique_lock<mutex> l{ d->m }; + if (offset != d->offset) { if (fs.debug) cerr << "DEBUG: readdir(): seeking to " << offset @@ -854,11 +861,13 @@ static void do_readdir(fuse_req_t req, fuse_ino_t ino, size_t 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; @@ -968,13 +977,15 @@ static void sfs_create(fuse_req_t req, fuse_ino_t parent, const char *name, } Inode &inode = get_inode(e.ino); - lock_guard<mutex> g{ inode.m }; - inode.nopen++; + { + lock_guard<mutex> g{ inode.m }; + inode.nopen++; - sfs_create_open_flags(fi); + sfs_create_open_flags(fi); - if (fs.passthrough) - do_passthrough_open(req, e.ino, fd, fi); + if (fs.passthrough) + do_passthrough_open(req, e.ino, fd, fi); + } fuse_reply_create(req, &e, fi); } @@ -1052,12 +1063,14 @@ static void sfs_tmpfile(fuse_req_t req, fuse_ino_t parent, mode_t mode, return; } - lock_guard<mutex> g{ inode->m }; + { + lock_guard<mutex> g{ inode->m }; - sfs_create_open_flags(fi); + sfs_create_open_flags(fi); - if (fs.passthrough) - do_passthrough_open(req, e.ino, fd, fi); + if (fs.passthrough) + do_passthrough_open(req, e.ino, fd, fi); + } fuse_reply_create(req, &e, fi); } @@ -1110,35 +1123,39 @@ static void sfs_open(fuse_req_t req, fuse_ino_t ino, fuse_file_info *fi) return; } - lock_guard<mutex> g{ inode.m }; - inode.nopen++; + { + lock_guard<mutex> g{ inode.m }; + inode.nopen++; - sfs_create_open_flags(fi); + sfs_create_open_flags(fi); - fi->fh = fd; - if (fs.passthrough) - do_passthrough_open(req, ino, fd, 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; + { + 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; } - inode.backing_id = 0; } close(fi->fh); From bdcd0ed94f8c5450036297a6a6ab0e0b68a25fa2 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Thu, 6 Aug 2026 12:40:46 +0200 Subject: [PATCH 274/310] example/passthrough_hp: do not hold inode locks across syscalls opendir(), unlink() and release() ran openat()/fdopendir(), close() and fuse_passthrough_close() with an inode or handle lock held, blocking every other operation on that object for the duration of a kernel call. Each one only needs the lock for the field update around it. unlink() also gains from the split: the fd is unpublished before it is closed, so no other thread can pass the number to a syscall after it is reused. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> (cherry picked from commit 1c59592190051685bb9d52ee35ff10028543e14d) --- example/passthrough_hp.cc | 78 ++++++++++++++++++++++++--------------- 1 file changed, 48 insertions(+), 30 deletions(-) diff --git a/example/passthrough_hp.cc b/example/passthrough_hp.cc index e84e13c8f..64f1ae5be 100644 --- a/example/passthrough_hp.cc +++ b/example/passthrough_hp.cc @@ -600,17 +600,27 @@ static void sfs_unlink(fuse_req_t req, fuse_ino_t parent, const char *name) } if (e.attr.st_nlink == 1) { Inode &inode = get_inode(e.ino); - 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; - close(inode.fd); - inode.fd = -ENOENT; - inode.generation++; + 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 @@ -715,28 +725,32 @@ static DirHandle *get_dir_handle(fuse_file_info *fi) 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{ 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; + lock_guard<mutex> g{ d->m }; + d->dp = dp; d->offset = 0; } @@ -1139,25 +1153,29 @@ static void sfs_open(fuse_req_t req, fuse_ino_t ino, fuse_file_info *fi) 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) { - 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; - } + 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); } From 9ce07a2c932c8b69a0038a731e0f279029a5b410 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Mon, 31 Aug 2026 02:23:54 +0200 Subject: [PATCH 275/310] fusermount: resolve the mountpoint without a second lookup check_perm() named the mountpoint twice: lstat() decided the type, then chdir() or open() looked the same path up again. A symlink swapped in between the two got its target checked and mounted on. Pin the inode once with O_PATH and take the type, the ownership checks, the working directory and the write test from that descriptor. O_NOFOLLOW refuses a symlink outright for an unprivileged caller; root keeps following them, as before. Rewrite of commit 280c80e5, not a backport of it: master's version is built on sync-init, the new mount API and the util/ code unification, none of which are on this branch. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> --- util/fusermount.c | 121 ++++++++++++++++++++++++++++++---------------- 1 file changed, 78 insertions(+), 43 deletions(-) diff --git a/util/fusermount.c b/util/fusermount.c index ba70ea241..fb2a78664 100644 --- a/util/fusermount.c +++ b/util/fusermount.c @@ -1105,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 @@ -1193,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 @@ -1242,13 +1271,19 @@ 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; + +out_close: + close(fd); + return res; } static int open_fuse_device(const char *dev) From fe1ed268b661cdcd60e8233e12e7d5a0c21c6058 Mon Sep 17 00:00:00 2001 From: nikola-sh <nikola-sh@narod.ru> Date: Mon, 23 Mar 2026 18:18:57 +0300 Subject: [PATCH 276/310] Fix hang on statx call If libfuse is compiled with no `HAVE_STATX` statx call hangs. It happens because `_do_statx` doesn't reply anything to the kernel. Let's fix it. Signed-off-by: nikola-sh <nikola-sh@narod.ru> (cherry picked from commit ae57c09f19a02a42dd16eb6b23633effb6d9bfe5) --- lib/fuse_lowlevel.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index e9d7e4f60..6460dba88 100644 --- a/lib/fuse_lowlevel.c +++ b/lib/fuse_lowlevel.c @@ -2542,9 +2542,9 @@ 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)req; (void)nodeid; (void)op_in; + fuse_reply_err(req, ENOSYS); } #endif From ce539e5d4e6fa04b186240f04f96b0f545f801b2 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Sun, 2 Aug 2026 02:44:09 +0200 Subject: [PATCH 277/310] fix missing fuse_loop_mt_312 declararion without symbol versioning Without symbol versioning, fuse_loop_mt() expands to fuse_loop_mt_312(), but the header does not declare that target. API 3.12 and newer callers therefore fail with -Werror=implicit-function-declaration. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> (cherry picked from commit 661af89955cdb8dd28071c2bfe1fba9aa6698d24) --- include/fuse.h | 1 + 1 file changed, 1 insertion(+) diff --git a/include/fuse.h b/include/fuse.h index 32d921b38..9dfd79e39 100644 --- a/include/fuse.h +++ b/include/fuse.h @@ -1170,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 From 4cb9f8e36873e9a9c63bc644ed50f7a37d4b8a5f Mon Sep 17 00:00:00 2001 From: wangrzneu <wangrzneu@gmail.com> Date: Sat, 28 Mar 2026 09:17:51 +0800 Subject: [PATCH 278/310] Fix Clang 21 compile error: non-assignable lvalue in ST_MTIM_NSEC Close https://github.com/libfuse/libfuse/issues/1434 Signed-off-by: wangrzneu <wangrzneu@gmail.com> (cherry picked from commit 031f37db7e57e5b82a6c1f90a3254e804f260472) --- lib/fuse.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/fuse.c b/lib/fuse.c index 991b4e5da..8b05d39b8 100644 --- a/lib/fuse.c +++ b/lib/fuse.c @@ -4454,8 +4454,8 @@ static void fuse_lib_statx(fuse_req_t req, fuse_ino_t ino, int flags, int mask, if (f->conf.auto_cache) { struct stat stbuf; - stbuf.st_mtime = stxbuf.stx_mtime.tv_nsec; - ST_MTIM_NSEC(&stbuf) = stxbuf.stx_mtime.tv_nsec; + 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); } From c12d9e55542645ef9da2422852225f52962afe63 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Sat, 4 Apr 2026 21:53:36 +0200 Subject: [PATCH 279/310] lib: Fix fd-leak in helper.c fuse_daemonize() Close pipe file descriptors in error paths when fork() or setsid() fail. Without this fix, waiter[0] and waiter[1] file descriptors leak when these system calls fail. Reported by: CodeChecker (gcc-fd-leak) Signed-off-by: Bernd Schubert <bernd@bsbernd.com> (cherry picked from commit 23b5c371c5d489cd5918e0c988fba0fb16c76144) --- lib/helper.c | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/helper.c b/lib/helper.c index 5c13b93a4..357a0b6b6 100644 --- a/lib/helper.c +++ b/lib/helper.c @@ -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; } From 8747bc3307f78cd01e1a8908ffba281bfa6e15e6 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Sat, 4 Apr 2026 21:58:26 +0200 Subject: [PATCH 280/310] lib: Fix leak of mountpoint in fuse_session_mount For the code where the caller already opened /dev/fuse the mount point was not stored into se->mountpoint. Reported by: CodeChecker (gcc-malloc-leak) Signed-off-by: Bernd Schubert <bernd@bsbernd.com> (cherry picked from commit 8738882033a92b87f351b7da5e1cd827adec39f2) --- lib/fuse_lowlevel.c | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index 6460dba88..d54d7ae37 100644 --- a/lib/fuse_lowlevel.c +++ b/lib/fuse_lowlevel.c @@ -4371,17 +4371,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; From 9a9b2a3bb951f28228f609edbe766dc2c241248b Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Sun, 5 Apr 2026 00:07:08 +0200 Subject: [PATCH 281/310] lib/fuse.c: Fix memory leak when pthread_setspecific fails In fuse_create_context(), we allocate memory for the context but don't check if pthread_setspecific() succeeds. If it fails, the allocated memory would leak since it's not stored anywhere. The fix is to check the return value of pthread_setspecific() and free the allocated memory before aborting if it fails. This fixes CodeChecker warning about leak of 'c'. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> (cherry picked from commit a09c3cdae9b1dac5d91c6b10b872b75ddb9f07d5) --- lib/fuse.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/fuse.c b/lib/fuse.c index 8b05d39b8..a3a3f8d56 100644 --- a/lib/fuse.c +++ b/lib/fuse.c @@ -2532,7 +2532,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)); } From 2881ac4d05cfa710b8bbd517787992139d3c0671 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Sat, 4 Apr 2026 23:54:39 +0200 Subject: [PATCH 282/310] lib/fuse_lowlevel.c: Fix memory leak when pipe size cannot be grown When fuse_ll_get_pipe() successfully allocates a pipe but then we determine that the pipe size is insufficient and cannot be grown, we were jumping to fallback without freeing the pipe. This left the pipe allocated in thread-local storage but unused. The fix is to call fuse_ll_clear_pipe() before goto fallback to properly free the pipe and remove it from thread-local storage. This fixes CodeChecker warning about leak of 'llp'. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> (cherry picked from commit f64b826259f1afe283387dc8b1dc051d0fd15d6a) --- lib/fuse_lowlevel.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index d54d7ae37..a1d600077 100644 --- a/lib/fuse_lowlevel.c +++ b/lib/fuse_lowlevel.c @@ -3928,12 +3928,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) { From aa6ed3a92acd2ed001c3e50e4d68a135ab1e0b94 Mon Sep 17 00:00:00 2001 From: Abhinav Agarwal <abhinavagarwal1996@gmail.com> Date: Tue, 7 Apr 2026 22:51:36 +0000 Subject: [PATCH 283/310] Fix memory leak in print_module_help() If the second fuse_opt_add_arg() fails, the function returns without calling fuse_opt_free_args(), leaking the argv allocated by the first successful call. Found by Facebook Infer (MEMORY_LEAK_C). Signed-off-by: Abhinav Agarwal <abhinavagarwal1996@gmail.com> (cherry picked from commit 46b987bb6d14717a7f0bb5a6130b49dd4bbfee6a) --- lib/fuse.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/fuse.c b/lib/fuse.c index a3a3f8d56..f08a5e201 100644 --- a/lib/fuse.c +++ b/lib/fuse.c @@ -4795,8 +4795,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); From 6c45a95ae68d9e94278e56751e0a7f2f8bf6ecca Mon Sep 17 00:00:00 2001 From: Alexander Mikhalitsyn <aleksandr.mikhalitsyn@futurfusion.io> Date: Tue, 12 May 2026 15:28:30 +0200 Subject: [PATCH 284/310] fuse: prevent possible fuse_pollhandle leak Documentation for poll() callback says that: > The callee is responsible for destroying ph with > fuse_pollhandle_destroy() when no longer in use. In fuse_lib_poll() ((struct fuse_lowlevel_ops*)->poll) we need to be more careful: 1. If get_path_nullok() fails, we need to free fuse_pollhandle 2. If we passed execution down to fuse_fs_poll(), then it must release fuse_pollhandle resources when no (struct fuse_operations*)->poll provided by the filesystem driver. Found this by myself while reading the code as a part of [1] review. This is not critical at all, because once we return ENOSYS once, kernel never sends FUSE_POLL again. So memleak is unnoticable in practice. Alternatively, we can leak if get_path_nullok() fails all the time, but then we are in a much more serious troubles... Link: https://github.com/lxc/lxcfs/pull/726 [1] Signed-off-by: Alexander Mikhalitsyn <aleksandr.mikhalitsyn@futurfusion.io> (cherry picked from commit baebe3cde9519f25574ec3fdcc829d01b5ffdd51) --- lib/fuse.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/fuse.c b/lib/fuse.c index f08a5e201..a00098173 100644 --- a/lib/fuse.c +++ b/lib/fuse.c @@ -2266,8 +2266,12 @@ int fuse_fs_poll(struct fuse_fs *fs, const char *path, int res; fuse_get_context()->private_data = fs->user_data; - if (!fs->op.poll) + + if (!fs->op.poll) { + fuse_pollhandle_destroy(ph); return -ENOSYS; + } + if (fs->debug) fuse_log(FUSE_LOG_DEBUG, "poll[%llu] ph: %p, events 0x%x\n", (unsigned long long) fi->fh, ph, @@ -4333,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); From d6840992dcb29d10b1edbafd0beb50dd9002839a Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Sat, 1 Aug 2026 16:01:44 +0200 Subject: [PATCH 285/310] lib: document which loop fuse_session_exit() wakes The comment only described the behaviour of the loop that is not woken, so callers had no way to tell that the signal nudge is pointless for fuse_session_loop_mt(). Link: https://github.com/libfuse/libfuse/issues/1572 Signed-off-by: Bernd Schubert <bernd@bsbernd.com> (cherry picked from commit 27c03e8df023722d6c71c449d973b44aae4fe48f) --- include/fuse_lowlevel.h | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/include/fuse_lowlevel.h b/include/fuse_lowlevel.h index 6be0295b7..9f90c9ba0 100644 --- a/include/fuse_lowlevel.h +++ b/include/fuse_lowlevel.h @@ -2258,12 +2258,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 */ From a12a896d2315b2704fb19875b3a9acd17047615c Mon Sep 17 00:00:00 2001 From: Jan Engelhardt <jengelh@inai.de> Date: Tue, 13 Jan 2026 19:40:13 +0100 Subject: [PATCH 286/310] doc: replace "futur irrealis"-like tense in manpages Do not make sentences more complicated than they need to be. Signed-off-by: Jan Engelhardt <jengelh@inai.de> (cherry picked from commit fa03307c656326733182ab7e30a3cd9e05da43f4) --- doc/fusermount3.1 | 2 +- doc/mount.fuse3.8 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/fusermount3.1 b/doc/fusermount3.1 index 1455742b4..3a5c81f21 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 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 From 830a26a8e3d2fdee54ccec82dcf0643a411f77db Mon Sep 17 00:00:00 2001 From: wilkis <marvin+github@alruno.eu> Date: Sat, 6 Jun 2026 10:09:15 +0200 Subject: [PATCH 287/310] Update homepage at man page (#1511) Signed-off-by: wilkis <marvin@alruno.eu> (cherry picked from commit 1cb5878d263183d80baf83186e1ca3e0c091e8ab) --- doc/fusermount3.1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/fusermount3.1 b/doc/fusermount3.1 index 3a5c81f21..d45f061be 100644 --- a/doc/fusermount3.1 +++ b/doc/fusermount3.1 @@ -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 From 1718d51d781d28397d09f98eabffdd508090f4f8 Mon Sep 17 00:00:00 2001 From: Benjamin Peterson <benjamin@python.org> Date: Sat, 18 Apr 2026 13:07:36 -0700 Subject: [PATCH 288/310] update kernel FUSE io_uring doc URL The previous one 404ed. Signed-off-by: Benjamin Peterson <benjamin@python.org> (cherry picked from commit ff7aa456d426d89eb19661da7b4c171153bac516) --- doc/README.fuse-io-uring | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/README.fuse-io-uring b/doc/README.fuse-io-uring index 51348903d..62c71bf75 100644 --- a/doc/README.fuse-io-uring +++ b/doc/README.fuse-io-uring @@ -1,5 +1,5 @@ fuse-over-io-uring uses io-uring for transport of kernel/userspace -messages. See also https://docs.kernel.org/filesystems/fuse-io-uring.html +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 From 55f90385202e3c80ff1aa7972755096d8770ebb5 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Tue, 7 Apr 2026 18:28:14 +0200 Subject: [PATCH 289/310] Fix a README typo. Reported-by: Yaroslav Ibrahimov <51054594+yaroslavibr@users.noreply.github.com> Signed-off-by: Bernd Schubert <bernd@bsbernd.com> (cherry picked from commit 173cf15fd512b5fbb3fe479e0b0c4c6215606deb) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0596f8a90..05738efc8 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 ../ From b53a0c2cbaec5ad60de8ab684d401d3c9cbda0d1 Mon Sep 17 00:00:00 2001 From: Ashish Ahuja <shivashish.ahuja@gmail.com> Date: Wed, 3 Jun 2026 11:03:44 +0530 Subject: [PATCH 290/310] fix typo in comments; FUSE_LOOP_MT_DEF_IDLE_THREADS is -1, not 10 Signed-off-by: Ashish Ahuja <shivashish.ahuja@gmail.com> (cherry picked from commit ba68751e565afaeaa5018d40e1815569d928a5c7) --- lib/fuse_i.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/fuse_i.h b/lib/fuse_i.h index d35e1e51d..fa8d26b42 100644 --- a/lib/fuse_i.h +++ b/lib/fuse_i.h @@ -172,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 From dba568e778952d6f81706757c24a41f8701b0dd8 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Sat, 27 Dec 2025 14:38:21 +0100 Subject: [PATCH 291/310] make_release_tarball.sh: Fix signify verification Signed-off-by: Bernd Schubert <bernd@bsbernd.com> (cherry picked from commit b2852e09afe38e318b50bd4a14f8a3fc0f27612c) --- make_release_tarball.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/make_release_tarball.sh b/make_release_tarball.sh index 45438a5cd..bd97125f5 100755 --- a/make_release_tarball.sh +++ b/make_release_tarball.sh @@ -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}:" From bc0852b27581eef839d145ab8351d6364a9824c3 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Mon, 31 Aug 2026 20:55:37 +0200 Subject: [PATCH 292/310] docs: the fuse-devel list moved to lists.linux.dev The sourceforge address in the README is the only contact the project gives, and the release announcement template mails it too. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> (cherry picked from commit 22f3036050f181067a3ae8d948ea80157f9392fe) --- README.md | 7 ++++--- dev-docs/release-process.md | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 05738efc8..156e49621 100644 --- a/README.md +++ b/README.md @@ -156,9 +156,10 @@ directory and at http://libfuse.github.io/doxygen. Getting Help ------------ -If you need help, please ask on the <fuse-devel@lists.sourceforge.net> -mailing list (subscribe at -https://lists.sourceforge.net/lists/listinfo/fuse-devel). +If you need help, please ask on the <fuse-devel@lists.linux.dev> +mailing list (subscribe by mailing +<fuse-devel+subscribe@lists.linux.dev>, 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/dev-docs/release-process.md b/dev-docs/release-process.md index c5b8a0d96..d96c87306 100644 --- a/dev-docs/release-process.md +++ b/dev-docs/release-process.md @@ -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, From 04e7278cf2a1c3110d52561f5697158d5090b375 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Sun, 30 Aug 2026 23:59:35 +0200 Subject: [PATCH 293/310] test: split _require_fuse_device out of _require_fuse A case that exercises the fusermount3 mountpoint checks needs the kernel module but never completes a mount, so it must not be gated on a setuid fusermount3. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> (cherry picked from commit 0622998d7ca66482dce4134312234a80c1e8a906) --- test/cases/lib/common.sh | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/test/cases/lib/common.sh b/test/cases/lib/common.sh index 9efa9e6f0..4516620c2 100644 --- a/test/cases/lib/common.sh +++ b/test/cases/lib/common.sh @@ -111,6 +111,15 @@ _require_cap() _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 @@ -122,8 +131,7 @@ _require_fuse() # BSD has no fusermount3 at all; vfs.usermount decides there. _is_linux || return 0 - [ -e /dev/fuse ] || - _notrun "the FUSE kernel module does not seem to be loaded" + _require_fuse_device [ "$FUSE_UID" != 0 ] || return 0 fusermount=$(command -v fusermount3) || _notrun "no fusermount3 on \$PATH" From 90cb0c9d8a94e9f5a2cb25023c6a6fde734083ab Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Sun, 30 Aug 2026 23:59:35 +0200 Subject: [PATCH 294/310] test: cover the fusermount3 mountpoint checks Nothing exercised check_perm()'s refusals, so the symlink rule and the write-access rule could regress silently, and nothing pinned that only the final path component is protected -- a mountpoint under a symlinked parent has to keep working. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> (cherry picked from commit 7e69009a6c7b6d5d865fe42482d33bad9a6dde9d) --- test/cases/README.md | 5 ++- test/cases/lib/checks.py | 44 +++++++++++++++++++ test/cases/mount/mountpoint-symlink-parent.sh | 25 +++++++++++ test/cases/mount/mountpoint-validation.sh | 33 ++++++++++++++ 4 files changed, 106 insertions(+), 1 deletion(-) create mode 100755 test/cases/mount/mountpoint-symlink-parent.sh create mode 100755 test/cases/mount/mountpoint-validation.sh diff --git a/test/cases/README.md b/test/cases/README.md index eb8807e4e..3c0d0206b 100644 --- a/test/cases/README.md +++ b/test/cases/README.md @@ -175,6 +175,7 @@ _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] @@ -288,6 +289,7 @@ 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 @@ -300,7 +302,8 @@ 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`. +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`, diff --git a/test/cases/lib/checks.py b/test/cases/lib/checks.py index 8cf6bbd3e..53c8bc56c 100755 --- a/test/cases/lib/checks.py +++ b/test/cases/lib/checks.py @@ -875,6 +875,48 @@ def _recvall(sock, bufsize): 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')} @@ -945,6 +987,8 @@ def add(name, func, *positional, **options): ('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') 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 From 7f6824d6226388b5c6c33af8ec15ee2990f234e7 Mon Sep 17 00:00:00 2001 From: Miklos Szeredi <mszeredi@redhat.com> Date: Fri, 20 Mar 2026 10:53:37 +0100 Subject: [PATCH 295/310] mount: check if CMSG_FIRSTHDR() returned NULL Don't know how it can happen, but apparently it can. Check NULL cmsg to prevent crash. Reporter is content with that, since it's a stray fuse startup anyway. If it happens "for real", then we'll have better diagnostic. Signed-off-by: Miklos Szeredi <mszeredi@redhat.com> (cherry picked from commit 9d78424f148fd1701c998c690c79bec099e401fa) --- lib/mount.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/mount.c b/lib/mount.c index 40f3317a4..20f6b1c55 100644 --- a/lib/mount.c +++ b/lib/mount.c @@ -294,6 +294,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); From 803e4eadbe87397de1e2d6a5657592f42d7f4933 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Tue, 23 Jun 2026 00:25:26 +0200 Subject: [PATCH 296/310] fuse_uring: continue submission on a failing commit SQE Replies are not submitted as they are produced: while a queue thread is in fuse_uring_queue_handle_cqes() (cqe_processing set), each reply only prepares a COMMIT_AND_FETCH SQE and the per-reply io_uring_submit() is skipped. The whole batch, plus the eventfd poll re-arm, is flushed by the next io_uring_submit_and_wait(). Without IORING_SETUP_SUBMIT_ALL the kernel aborts io_submit_sqes() at the first SQE whose prep fails, leaving the rest of the batch unconsumed until the following loop iteration -- head-of-line blocking the other in-flight replies behind one bad commit. Set the flag so the kernel drains the whole batch and only the failing SQE gets an error CQE. This might have resulted in fuse-over-io-uring stalls, if applications were waiting for results and libfuse was waiting for CQEs, but not all SQEs had been submitted yet. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> (cherry picked from commit a19bb4f0c62583f7e6483f46e83eaa7e855c9125) --- lib/fuse_uring.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/fuse_uring.c b/lib/fuse_uring.c index 1941c4fd5..8268fc8bb 100644 --- a/lib/fuse_uring.c +++ b/lib/fuse_uring.c @@ -351,6 +351,11 @@ static int fuse_queue_setup_io_uring(struct io_uring *ring, size_t qid, 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; From 00caa960eed56ba1ad3d7a115005bc484267f964 Mon Sep 17 00:00:00 2001 From: Robert Doebbelin <robert@quobyte.com> Date: Mon, 10 Aug 2026 13:47:43 +0200 Subject: [PATCH 297/310] lib: commit the ring entry in fuse_reply_none() Over io_uring a ring entry only goes back to the kernel via COMMIT_AND_FETCH. fuse_reply_none() merely freed the request, so a FORGET answered this way leaked the entry and left the kernel-side request outstanding. Signed-off-by: Robert Doebbelin <robert@quobyte.com> (cherry picked from commit 758c7aa51b812dd44abf6bc2a14672f8cdaea293) --- lib/fuse_lowlevel.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index a1d600077..2df1990b7 100644 --- a/lib/fuse_lowlevel.c +++ b/lib/fuse_lowlevel.c @@ -417,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); } From edb730356464d98f1a34ec1b706ab2d5b6f9da5f Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Tue, 1 Sep 2026 10:03:26 +0200 Subject: [PATCH 298/310] fusermount: treat ECONNABORTED as a dead connection too A daemon that dies with io-uring registered leaves the mountpoint returning ECONNABORTED, not ENOTCONN: the probe open() is queued on a ring the kernel has not finished tearing down yet, and the teardown ends the request rather than refusing it. Accepting only ENOTCONN skips the auto-unmount and leaves the mount behind. The cherry pick to 3.18 differs in respect to should_auto_unmount(), because commit a486808eca47 removed the privileged open() from should_auto_unmount(); only check_ENOTCONN_as_owner() needs ECONNABORTED. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> (cherry picked from commit 60d2180bb9429fb6a404235cdc98ecf226b8cda7) --- util/fusermount.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util/fusermount.c b/util/fusermount.c index fb2a78664..33f7bc728 100644 --- a/util/fusermount.c +++ b/util/fusermount.c @@ -1459,7 +1459,7 @@ static int check_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); From 4afc4cc2c0071c96eb5138c923fcc8fab379b2e8 Mon Sep 17 00:00:00 2001 From: Miklos Szeredi <mszeredi@redhat.com> Date: Tue, 1 Sep 2026 16:37:12 +0200 Subject: [PATCH 299/310] fusermount: use unmount_fuse() on send_fd() failure This is safer than using umount2() directly, since it does the all the "fusermount -u" checks. Signed-off-by: Miklos Szeredi <mszeredi@redhat.com> (cherry picked from commit ff2727850ad0773f1bc487970956af5b7013bba8) --- util/fusermount.c | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/util/fusermount.c b/util/fusermount.c index 33f7bc728..11b912b47 100644 --- a/util/fusermount.c +++ b/util/fusermount.c @@ -1742,12 +1742,7 @@ int main(int argc, char *argv[]) res = send_fd(cfd, fd); if (res != 0) { - /* - * euid 0 here and mnt is caller supplied - without - * UMOUNT_NOFOLLOW a swapped symlink detaches an - * unrelated mount. - */ - umount2(mnt, MNT_DETACH | UMOUNT_NOFOLLOW); /* lazy umount */ + unmount_fuse(mnt, 1, 1); /* lazy umount */ goto err_out; } close(fd); From c4ffa6438525c53e6d213f3376c8ab667f16d46e Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Sun, 30 Aug 2026 16:57:14 +0200 Subject: [PATCH 300/310] abicheck: allow claiming fuse_file_info padding bits struct fuse_file_info reserves padding bits so new flags can be added without an ABI break, but abidiff reports the resulting member insertion and padding offset shift as a change and fails the check. Bound the suppression to size-preserving changes so a real size change still fails. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> (cherry picked from commit a2c34a7ec38575741340424d6e2411491248447b) --- .github/workflows/abidiff_suppressions.abignore | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/abidiff_suppressions.abignore b/.github/workflows/abidiff_suppressions.abignore index 949efda6b..cf53bb3c2 100644 --- a/.github/workflows/abidiff_suppressions.abignore +++ b/.github/workflows/abidiff_suppressions.abignore @@ -34,3 +34,10 @@ has_size_change = yes 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 From e9c88a3efad86bda9866da7addebc3cdd4f83f2c Mon Sep 17 00:00:00 2001 From: Robert Doebbelin <robert@quobyte.com> Date: Fri, 3 May 2024 11:03:10 +0200 Subject: [PATCH 301/310] lib: relay the KILLPRIV_V2 kill-suidgid flags to the filesystem With FUSE_CAP_HANDLE_KILLPRIV_V2 the kernel stops clearing suid/sgid itself and leaves that to the filesystem. Clearing them depends on CAP_FSETID, held by the application that issued the syscall - the filesystem's own privileges do not matter, and it cannot see the application's, so the kernel signals the decision per request. Of the three flags carrying it only FATTR_KILL_SUIDGID made it through, because FUSE_SET_ATTR_KILL_SUID happens to be the same bit. FUSE_OPEN_KILL_SUIDGID and FUSE_WRITE_KILL_SUIDGID were dropped, so a server that negotiated KILLPRIV_V2 never saw the request on O_TRUNC open and on write, and suid/sgid survived. Decode both into a new fuse_file_info::kill_suidgid, taken from the padding bits, in open, create, write and write_buf. Gate it on the negotiated capability and not on proto_minor: a filesystem that did not ask for KILLPRIV_V2 must never see fi->kill_suidgid set, and negotiating KILLPRIV_V2 implies 7.31, so fuse_create_in::open_flags is there to read. Added by Bernd: - fix the comment style for checkpatch - drop the XXX markers for unhandled open_flags, this patch handles them - replace the proto_minor guard with the capability check - drop security.capability from the field's documentation The cherry pick to 3.18 differs in respect to ChangeLog.rst, because prepare-3.18.3-rc1 records fuse_file_info::kill_suidgid in the 3.18.3 release notes. Signed-off-by: Robert Doebbelin <robert@quobyte.com> Signed-off-by: Bernd Schubert <bernd@bsbernd.com> (cherry picked from commit 9895469abeed0d2d58f09735cd61b5bdd173525a) --- include/fuse_common.h | 10 +++++++++- lib/fuse_lowlevel.c | 16 ++++++++++++++-- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/include/fuse_common.h b/include/fuse_common.h index 041188ec7..b6934599e 100644 --- a/include/fuse_common.h +++ b/include/fuse_common.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; diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index 2df1990b7..82553ba94 100644 --- a/lib/fuse_lowlevel.c +++ b/lib/fuse_lowlevel.c @@ -1670,7 +1670,9 @@ static void _do_create(fuse_req_t req, const fuse_ino_t nodeid, if (req->se->conn.proto_minor >= 12) req->ctx.umask = arg->umask; - /* XXX: fuse_create_in::open_flags */ + 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 { @@ -1700,7 +1702,9 @@ static void _do_open(fuse_req_t req, const fuse_ino_t nodeid, const void *op_in, memset(&fi, 0, sizeof(fi)); fi.flags = arg->flags; - /* XXX: fuse_open_in::open_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); @@ -1751,6 +1755,10 @@ static void _do_write(fuse_req_t req, const fuse_ino_t nodeid, fi.fh = arg->fh; fi.writepage = (arg->write_flags & FUSE_WRITE_CACHE) != 0; + 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; @@ -1787,6 +1795,10 @@ static void _do_write_buf(fuse_req_t req, const fuse_ino_t nodeid, 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; From 0c338357783a924b8581a37843aca89250956f44 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Mon, 31 Aug 2026 13:29:25 +0200 Subject: [PATCH 302/310] Prepare 3.18.3-rc1 Bump the version and record the backported fixes. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> --- ChangeLog.rst | 106 ++++++++++++++++++++++++++++++++++++++++++++++++++ meson.build | 2 +- 2 files changed, 107 insertions(+), 1 deletion(-) diff --git a/ChangeLog.rst b/ChangeLog.rst index 9a364b518..b6b303539 100644 --- a/ChangeLog.rst +++ b/ChangeLog.rst @@ -1,3 +1,109 @@ +libfuse 3.18.3-rc1 (unreleased) +=============================== + +Security Fixes +-------------- + +* 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 diff --git a/meson.build b/meson.build index f0570dc2c..fc1c2c42d 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('libfuse3', ['c'], - version: '3.18.2', + version: '3.18.3-rc1', meson_version: '>= 0.60.0', default_options: [ 'buildtype=debugoptimized', From b1a30539fb3407650541509007f7e6e2b26c5040 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Mon, 31 Aug 2026 19:23:52 +0200 Subject: [PATCH 303/310] doc: describe custom io and what libfuse does not validate fuse_session_custom_io() moves the FUSE protocol onto a descriptor whose peer is not the kernel, and the only writeup of that is an example. Say what the callbacks owe the session, what a session loses without a mount, and which request fields reach the handlers with no bounds check, since a peer that skips those checks crashes the filesystem process. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> (cherry picked from commit f1df035b90d0ab871eaa64e04407b8ff9a9d58b4) --- doc/README.custom-io | 187 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 doc/README.custom-io 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 From 006c0b19f0a75296505a5889e726976b574c9ddc Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Mon, 31 Aug 2026 19:09:25 +0200 Subject: [PATCH 304/310] build: add enable-custom-io, off by default fuse_session_custom_io() puts a peer of the caller's choosing where the kernel would be, and libfuse parses what arrives trusting that a kernel has already validated it -- unvalidated lengths and counts reach the handlers otherwise. No in-tree user asks for that transport but the hello_ll_uds example, so it is built only where the option is. The symbols stay exported and refuse at runtime instead of being compiled out: dropping fuse_session_custom_io@FUSE_3.17 from the default build would break every binary already linked against it. The cherry pick to 3.18 differs in respect to the summary() hunk in meson.build, which is dropped because 3.18 has no configuration summary block. The ChangeLog entry goes into the Security Fixes section of 3.18.3-rc1 rather than under Unreleased Changes. Signed-off-by: Bernd Schubert <bernd@bsbernd.com> (cherry picked from commit cbd6679ddff039f4742f83e647f990534077ea67) --- ChangeLog.rst | 8 ++++++++ example/meson.build | 6 +++++- include/fuse_lowlevel.h | 8 ++++++++ lib/fuse_lowlevel.c | 9 +++++++++ meson.build | 10 ++++++++++ meson_options.txt | 3 +++ 6 files changed, 43 insertions(+), 1 deletion(-) diff --git a/ChangeLog.rst b/ChangeLog.rst index b6b303539..d7f4d9de8 100644 --- a/ChangeLog.rst +++ b/ChangeLog.rst @@ -4,6 +4,14 @@ libfuse 3.18.3-rc1 (unreleased) 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. diff --git a/example/meson.build b/example/meson.build index 21e8317a1..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() diff --git a/include/fuse_lowlevel.h b/include/fuse_lowlevel.h index 9f90c9ba0..c8009b89e 100644 --- a/include/fuse_lowlevel.h +++ b/include/fuse_lowlevel.h @@ -2170,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 @@ -2178,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 diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c index 82553ba94..00c281502 100644 --- a/lib/fuse_lowlevel.c +++ b/lib/fuse_lowlevel.c @@ -4303,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); @@ -4337,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, diff --git a/meson.build b/meson.build index fc1c2c42d..745274eab 100644 --- a/meson.build +++ b/meson.build @@ -180,6 +180,16 @@ if get_option('enable-io-uring') and liburing.found() and libnuma.found() 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 c1f8fe694..969686eac 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -27,3 +27,6 @@ option('enable-usdt', type : 'boolean', value : false, 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') From 7cad94bd2a07bffa653eb11b3a474a707de23c82 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Tue, 8 Sep 2026 23:55:15 +0200 Subject: [PATCH 305/310] Released fuse-3.18.3 Signed-off-by: Bernd Schubert <bernd@bsbernd.com> --- AUTHORS | 12 ++++++++++++ ChangeLog.rst | 4 ++-- meson.build | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/AUTHORS b/AUTHORS index 2c63b1feb..946beef4d 100644 --- a/AUTHORS +++ b/AUTHORS @@ -285,3 +285,15 @@ Zeno Sebastian Endemann <zeno.endemann@mailbox.org> # New authors since fuse-3.18.1 Abhinav Agarwal <abhinavagarwal1996@gmail.com> + +# New authors since fuse-3.18.2 +Robert Doebbelin <robert@quobyte.com> +Ashish Ahuja <shivashish.ahuja@gmail.com> +Benjamin Peterson <benjamin@python.org> +wilkis <marvin+github@alruno.eu> +Jan Engelhardt <jengelh@inai.de> +Alexander Mikhalitsyn <aleksandr.mikhalitsyn@futurfusion.io> +wangrzneu <wangrzneu@gmail.com> +nikola-sh <nikola-sh@narod.ru> +Mikhail Novosyolov <m.novosyolov@rosa.ru> +Keerthana KT <keerthana@labs.digiscrypt.com> diff --git a/ChangeLog.rst b/ChangeLog.rst index d7f4d9de8..c28e2d917 100644 --- a/ChangeLog.rst +++ b/ChangeLog.rst @@ -1,5 +1,5 @@ -libfuse 3.18.3-rc1 (unreleased) -=============================== +libfuse 3.18.3 (2026-09-08) +=========================== Security Fixes -------------- diff --git a/meson.build b/meson.build index 745274eab..1e6cbb745 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('libfuse3', ['c'], - version: '3.18.3-rc1', + version: '3.18.3', meson_version: '>= 0.60.0', default_options: [ 'buildtype=debugoptimized', From 8b641f61b73a9734b27d3ab877ac6790aa7a7e6e Mon Sep 17 00:00:00 2001 From: wangrzneu <wangrzneu@gmail.com> Date: Sat, 28 Mar 2026 09:17:56 +0800 Subject: [PATCH 306/310] example: refactor poll_client to remove FSEL_FILES macro Replace the FSEL_FILES macro and explicit char array with a string literal for hex_map, deriving the file count from sizeof. Use size_t for loop variables and fix the printf format specifier accordingly. Signed-off-by: wangrzneu <wangrzneu@gmail.com> (cherry picked from commit 22d0fcd4c757a86377bc258296e55e2900af2c3c) --- example/poll_client.c | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/example/poll_client.c b/example/poll_client.c index 96f14190f..6dba3e71c 100644 --- a/example/poll_client.c +++ b/example/poll_client.c @@ -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"); From bae0cba1958932df567944b3cd56fe5a8d6d9801 Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Wed, 9 Sep 2026 00:16:41 +0200 Subject: [PATCH 307/310] Set unreleased in the changelog, one backport missing Compilation fails on Ubutu 26.06, we need another backport. --- ChangeLog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ChangeLog.rst b/ChangeLog.rst index c28e2d917..2aa3434cf 100644 --- a/ChangeLog.rst +++ b/ChangeLog.rst @@ -1,4 +1,4 @@ -libfuse 3.18.3 (2026-09-08) +libfuse 3.18.3 (unreleased) =========================== Security Fixes From 3dbcfce64ba0dd529cec7b94305662e7901a991b Mon Sep 17 00:00:00 2001 From: Bernd Schubert <bernd@bsbernd.com> Date: Wed, 9 Sep 2026 00:28:46 +0200 Subject: [PATCH 308/310] Released fuse-3.18.3 Signed-off-by: Bernd Schubert <bernd@bsbernd.com> --- ChangeLog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ChangeLog.rst b/ChangeLog.rst index 2aa3434cf..dabe1318f 100644 --- a/ChangeLog.rst +++ b/ChangeLog.rst @@ -1,4 +1,4 @@ -libfuse 3.18.3 (unreleased) +libfuse 3.18.3 (2026-09-09) =========================== Security Fixes From 5bf9273cca8f9135e7d9911ca55f81e5fbed7aae Mon Sep 17 00:00:00 2001 From: Mohamad Chaarawi <mohamad.chaarawi@hpe.com> Date: Mon, 2 Nov 2026 22:37:39 +0000 Subject: [PATCH 309/310] Use fused versioning: 1.2.0 (synced with fuse-3.18.3) Signed-off-by: Mohamad Chaarawi <mohamad.chaarawi@hpe.com> --- lib/meson.build | 2 +- meson.build | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/meson.build b/lib/meson.build index d683684e3..91f51392e 100644 --- a/lib/meson.build +++ b/lib/meson.build @@ -44,7 +44,7 @@ fusermount_path = join_paths(get_option('prefix'), get_option('bindir')) libfuse = library('fused', libfuse_sources, version: base_version, - soversion: '4', + soversion: '1', include_directories: include_dirs, dependencies: deps, install: true, diff --git a/meson.build b/meson.build index 6810c6eda..01cd1a372 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('libfused', ['c'], - version: '3.18.3', + version: '1.2.0', # synced with fuse-3.18.3 meson_version: '>= 0.60.0', default_options: [ 'buildtype=debugoptimized', From 0b7c2394ab939e75105ca0fa1fa7ffddf757f106 Mon Sep 17 00:00:00 2001 From: Mohamad Chaarawi <mohamad.chaarawi@hpe.com> Date: Mon, 2 Nov 2026 23:07:32 +0000 Subject: [PATCH 310/310] DAOS: use fork+exec instead of posix_spawn for fusermount3 posix_spawn() creates its child with clone(CLONE_VM|CLONE_VFORK). Under valgrind (as used by DAOS NLT) the child never reaches execve(), so fusermount3 is not run and the mount fails. Go back to fork+exec, as fused v1.0.0/v1.1.0 did, keeping the rest of upstream 3.18.3 mount.c unchanged. Signed-off-by: Mohamad Chaarawi <mohamad.chaarawi@hpe.com> --- lib/mount.c | 82 ++++++++++++++++++++++------------------------------- 1 file changed, 34 insertions(+), 48 deletions(-) diff --git a/lib/mount.c b/lib/mount.c index 20f6b1c55..f521090da 100644 --- a/lib/mount.c +++ b/lib/mount.c @@ -8,7 +8,6 @@ See the file LGPL2.txt. */ -/* For environ */ #define _GNU_SOURCE #include "fuse_config.h" @@ -25,7 +24,6 @@ #include <fcntl.h> #include <errno.h> #include <poll.h> -#include <spawn.h> #include <sys/socket.h> #include <sys/un.h> #include <sys/wait.h> @@ -123,31 +121,42 @@ static const struct fuse_opt fuse_mount_opts[] = { }; /* - * Running fusermount by calling 'posix_spawn' + * 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_posix_spawn(posix_spawn_file_actions_t *action, - char const * const argv[], pid_t *out_pid) +static int fusermount_fork_exec(char const * const argv[], int quiet, + int close_fd, pid_t *out_pid) { - const char *full_path = FUSERMOUNT_DIR "/" FUSERMOUNT_PROG; - pid_t pid; + pid_t pid = fork(); - /* See man 7 environ for the global environ pointer */ + if (pid == -1) { + int err = errno; - /* first try the install path */ - int status = posix_spawn(&pid, full_path, action, NULL, - (char * const *) argv, environ); - if (status != 0) { - /* if that fails, try a system install */ - status = posix_spawnp(&pid, FUSERMOUNT_PROG, action, NULL, - (char * const *) argv, environ); + fuse_log(FUSE_LOG_ERR, "fork() for '%s' failed: %s\n", + FUSERMOUNT_PROG, strerror(err)); + return -err; } - if (status != 0) { - fuse_log(FUSE_LOG_ERR, "Failed to call '%s': %s\n", - FUSERMOUNT_PROG, strerror(status)); - return -status; + 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) @@ -161,7 +170,7 @@ static int fusermount_posix_spawn(posix_spawn_file_actions_t *action, void fuse_mount_version(void) { char const *const argv[] = {FUSERMOUNT_PROG, "--version", NULL}; - int status = fusermount_posix_spawn(NULL, argv, NULL); + int status = fusermount_fork_exec(argv, 0, -1, NULL); if(status != 0) fuse_log(FUSE_LOG_ERR, "Running '%s --version' failed", @@ -343,7 +352,7 @@ void fuse_kern_unmount(const char *mountpoint, int fd) char const * const argv[] = { FUSERMOUNT_PROG, "--unmount", "--quiet", "--lazy", "--", mountpoint, NULL }; - int status = fusermount_posix_spawn(NULL, argv, 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)); @@ -389,28 +398,16 @@ static int setup_auto_unmount(const char *mountpoint, int quiet) NULL, }; - // TODO: add error handling for all manipulations of action. - posix_spawn_file_actions_t action; - posix_spawn_file_actions_init(&action); - - if (quiet) { - posix_spawn_file_actions_addopen(&action, STDOUT_FILENO, "/dev/null", O_WRONLY, 0); - posix_spawn_file_actions_addopen(&action, STDERR_FILENO, "/dev/null", O_WRONLY, 0); - } - posix_spawn_file_actions_addclose(&action, fds[1]); - /* * auto-umount runs in the background - it is not waiting for the * process */ - int status = fusermount_posix_spawn(&action, argv, &pid); - - posix_spawn_file_actions_destroy(&action); + 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 (spawn): %s", + fuse_log(FUSE_LOG_ERR, "fuse: Setting up auto-unmount failed (fork): %s", strerror(-status)); return -1; } @@ -463,23 +460,12 @@ static int fuse_mount_fusermount(const char *mountpoint, struct mount_opts *mo, }; - posix_spawn_file_actions_t action; - posix_spawn_file_actions_init(&action); - - if (quiet) { - posix_spawn_file_actions_addopen(&action, STDOUT_FILENO, "/dev/null", O_WRONLY, 0); - posix_spawn_file_actions_addopen(&action, STDERR_FILENO, "/dev/null", O_WRONLY, 0); - } - posix_spawn_file_actions_addclose(&action, fds[1]); - - int status = fusermount_posix_spawn(&action, argv, &pid); - - posix_spawn_file_actions_destroy(&action); + int status = fusermount_fork_exec(argv, quiet, fds[1], &pid); if(status != 0) { close(fds[0]); close(fds[1]); - fuse_log(FUSE_LOG_ERR, "posix_spawn(p)() for %s failed: %s", + fuse_log(FUSE_LOG_ERR, "fork() for %s failed: %s", FUSERMOUNT_PROG, strerror(-status)); return -1; }