From cc05957d0d0614cbbdfa5ed76b1e22b2c3ba554c Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Mon, 3 Aug 2026 01:09:38 +0200 Subject: [PATCH 1/2] Add Btrfs qgroup management APIs Add Linux quota helpers for detecting Btrfs filesystems, reporting full and simple quota status, inspecting subvolume qgroups, and managing limits and qgroup hierarchies. Support older btrfs-progs releases through a compatibility fallback and add focused coverage for parsing, validation, command construction, and error propagation. --- quota/linux-lib.pl | 386 ++++++++++++++++++++++++++++++++++++++++++++ quota/t/run-tests.t | 247 ++++++++++++++++++++++++++++ 2 files changed, 633 insertions(+) create mode 100644 quota/t/run-tests.t diff --git a/quota/linux-lib.pl b/quota/linux-lib.pl index a0cf7d08d..3b62e1461 100755 --- a/quota/linux-lib.pl +++ b/quota/linux-lib.pl @@ -1084,6 +1084,392 @@ my $out = &backquote_logged( &error($out) if ($?); } +=head2 is_btrfs_fs(path) + +Returns 1 if a path is on a mounted Btrfs filesystem, 0 otherwise. + +=cut +sub is_btrfs_fs +{ +my ($path) = @_; +return 0 if (!&valid_btrfs_path($path)); + +# Btrfs subvolumes can have a different st_dev from the filesystem mount, so +# select the longest containing mount path instead of comparing device numbers. +my $best; +foreach my $m (&mount::list_mounted()) { + next if (!defined($m->[0]) || !defined($m->[2])); + next if (!&is_under_directory($m->[0], $path)); + $best = $m if (!$best || length($m->[0]) > length($best->[0])); + } +return $best && $best->[2] eq "btrfs" ? 1 : 0; +} + +# valid_btrfs_path(path) +# Returns 1 for an absolute path that is safe to pass to Btrfs tools. +sub valid_btrfs_path +{ +return defined($_[0]) && $_[0] =~ /^\// && $_[0] !~ /[\r\n\0]/ ? 1 : 0; +} + +# run_btrfs_command(logged, arg, ...) +# Runs a Btrfs command with a clean locale. Returns the output and undef on +# success, or undef and an error message on failure. +sub run_btrfs_command +{ +my ($logged, @args) = @_; +my $btrfs = &has_command("btrfs"); +return (undef, "The btrfs command was not found") if (!$btrfs); +my $cmd = quotemeta($btrfs)." ". + join(" ", map { quotemeta($_) } @args)." 2>&1"; +&clean_language(); +my $out = $logged ? &backquote_logged($cmd) : &backquote_command($cmd); +my $ex = $?; +&reset_environment(); +$out =~ s/\s+$//; +return $ex ? (undef, $out || "The btrfs command failed") : ($out, undef); +} + +=head2 parse_btrfs_quota_status(output) + +Parses output from C and returns a hash reference with +enabled, mode, inconsistent and other status fields, or undef for invalid +output. This function is mainly intended for internal use. + +=cut +sub parse_btrfs_quota_status +{ +my ($out) = @_; +my %rv; +return undef if ($out !~ /^\s*Enabled:\s*(yes|no)\s*$/mi); +$rv{'enabled'} = lc($1) eq "yes" ? 1 : 0; +if ($out =~ /^\s*Mode:\s*(\S+)(?:\s+\(([^\)]*)\))?\s*$/mi) { + $rv{'mode'} = lc($1); + $rv{'mode_description'} = $2 if (defined($2)); + } +if ($out =~ /^\s*Inconsistent:\s*(yes|no)\s*$/mi) { + $rv{'inconsistent'} = lc($1) eq "yes" ? 1 : 0; + } +if ($out =~ /^\s*Override limits:\s*(yes|no)\s*$/mi) { + $rv{'override_limits'} = lc($1) eq "yes" ? 1 : 0; + } +if ($out =~ /^\s*Drop subtree threshold:\s*(\d+)\s*$/mi) { + $rv{'drop_subtree_threshold'} = int($1); + } +if ($out =~ /^\s*Total count:\s*(\d+)\s*$/mi) { + $rv{'total_count'} = int($1); + } +my %levels; +while($out =~ /^\s*Level\s+(\d+):\s*(\d+)\s*$/gmi) { + $levels{$1} = int($2); + } +$rv{'levels'} = \%levels if (%levels); +return \%rv; +} + +=head2 btrfs_quota_status(path) + +Returns a hash reference describing the Btrfs quota status for a path. The +hash always contains C, and may contain C, C, +C and C. Returns undef when the path is not on Btrfs. +Older btrfs-progs releases without C are supported using +C as a fallback. + +=cut +sub btrfs_quota_status +{ +my ($path) = @_; +return undef if (!&is_btrfs_fs($path)); +my ($out, $err) = &run_btrfs_command(0, "quota", "status", $path); +if (defined($out)) { + my $rv = &parse_btrfs_quota_status($out); + if ($rv) { + $rv->{'supported'} = 1; + return $rv; + } + } + +# Older versions have no quota status command. qgroup show succeeds when +# quotas are enabled, and reports a missing quota root when disabled. +my ($qout, $qerr) = &run_btrfs_command(0, "qgroup", "show", "--raw", $path); +if (defined($qout)) { + return { 'supported' => 1, + 'enabled' => 1 }; + } +elsif ($qerr =~ /(?:quota root does not exist|quotas? (?:are |is )?not enabled)/i) { + return { 'supported' => 1, + 'enabled' => 0 }; + } +return { 'supported' => 1, + 'error' => $qerr || $err || "Unable to read Btrfs quota status" }; +} + +=head2 parse_btrfs_qgroup_output(output) + +Parses raw output from C and returns an array +reference of qgroup hashes. Numeric sizes are returned in bytes, missing +limits are undef, and parent and child qgroups are returned as array refs. +This function is mainly intended for internal use. + +=cut +sub parse_btrfs_qgroup_output +{ +my ($out) = @_; +my @rv; +foreach my $line (split(/\r?\n/, $out)) { + $line =~ s/^\s+//; + $line =~ s/\s+$//; + next if ($line !~ /^(\d+\/\d+)\s+/); + my @cols = split(/\s+/, $line, 8); + next if (@cols < 7); + my ($id, $referenced, $exclusive, $max_referenced, $max_exclusive, + $parents, $children, $path) = @cols; + push(@rv, { + 'id' => $id, + 'referenced' => int($referenced), + 'exclusive' => int($exclusive), + 'max_referenced' => $max_referenced eq "none" ? undef : + int($max_referenced), + 'max_exclusive' => $max_exclusive eq "none" ? undef : + int($max_exclusive), + 'parents' => $parents =~ /^-+$/ ? [ ] : + [ split(/,/, $parents) ], + 'children' => $children =~ /^-+$/ ? [ ] : + [ split(/,/, $children) ], + 'path' => defined($path) ? $path : "", + }); + } +return \@rv; +} + +=head2 list_btrfs_qgroups(path, [sync], [&error]) + +Returns an array reference containing all Btrfs qgroups on the filesystem +that contains path. Each entry contains id, referenced and exclusive usage, +limits, parents, children and path. Returns undef on failure and optionally +saves the error message to the final scalar reference. If sync is true, the +filesystem is synchronized before usage is read. + +=cut +sub list_btrfs_qgroups +{ +my ($path, $sync, $errref) = @_; +if (!&valid_btrfs_path($path)) { + $$errref = "Invalid Btrfs path" if ($errref); + return undef; + } +my @args = ( "qgroup", "show", "--raw", "-r", "-e", "-p", "-c" ); +push(@args, "--sync") if ($sync); +push(@args, $path); +my ($out, $err) = &run_btrfs_command(0, @args); +if (!defined($out)) { + $$errref = $err if ($errref); + return undef; + } +my $rv = &parse_btrfs_qgroup_output($out); +if (!@$rv && $out =~ /\S/) { + $$errref = "Unable to parse Btrfs qgroup output" if ($errref); + return undef; + } +$$errref = undef if ($errref); +return $rv; +} + +=head2 btrfs_subvolume_id(path, [&error]) + +Returns the numeric Btrfs subvolume ID for path, or undef if the path is not +a subvolume or the ID cannot be read. The optional scalar reference receives +the command error. + +=cut +sub btrfs_subvolume_id +{ +my ($path, $errref) = @_; +if (!&valid_btrfs_path($path)) { + $$errref = "Invalid Btrfs path" if ($errref); + return undef; + } +my ($out, $err) = &run_btrfs_command(0, "subvolume", "show", $path); +if (defined($out) && $out =~ /^\s*Subvolume ID:\s*(\d+)\s*$/mi) { + $$errref = undef if ($errref); + return int($1); + } +$$errref = $err || "Unable to read the Btrfs subvolume ID" if ($errref); +return undef; +} + +=head2 get_btrfs_qgroup(path, [sync], [&error]) + +Returns the level-0 qgroup for a Btrfs subvolume path, or undef on failure. +The returned hash has the same fields as entries from list_btrfs_qgroups. + +=cut +sub get_btrfs_qgroup +{ +my ($path, $sync, $errref) = @_; +my $id = &btrfs_subvolume_id($path, $errref); +return undef if (!defined($id)); +my $qgroups = &list_btrfs_qgroups($path, $sync, $errref); +return undef if (!$qgroups); +foreach my $q (@$qgroups) { + return $q if ($q->{'id'} eq "0/$id"); + } +$$errref = "No Btrfs qgroup exists for subvolume $id" if ($errref); +return undef; +} + +=head2 enable_btrfs_quotas(path, [simple]) + +Enables Btrfs quotas on the filesystem containing path. If simple is true, +simple quotas (squotas) are requested. Returns undef on success or an error +message on failure. + +=cut +sub enable_btrfs_quotas +{ +my ($path, $simple) = @_; +return "Invalid Btrfs path" if (!&valid_btrfs_path($path)); +my @args = ( "quota", "enable" ); +push(@args, "--simple") if ($simple); +push(@args, $path); +my ($out, $err) = &run_btrfs_command(1, @args); +return $err; +} + +=head2 disable_btrfs_quotas(path) + +Disables Btrfs quotas on the filesystem containing path. This removes all +qgroup configuration. Returns undef on success or an error message on +failure. + +=cut +sub disable_btrfs_quotas +{ +my ($path) = @_; +return "Invalid Btrfs path" if (!&valid_btrfs_path($path)); +my ($out, $err) = &run_btrfs_command(1, "quota", "disable", $path); +return $err; +} + +=head2 rescan_btrfs_quotas(path, [wait]) + +Starts a Btrfs quota rescan. If wait is true, waits for the rescan to finish. +Returns undef on success or an error message on failure. + +=cut +sub rescan_btrfs_quotas +{ +my ($path, $wait) = @_; +return "Invalid Btrfs path" if (!&valid_btrfs_path($path)); +my @args = ( "quota", "rescan" ); +push(@args, "-w") if ($wait); +push(@args, $path); +my ($out, $err) = &run_btrfs_command(1, @args); +return $err; +} + +# valid_btrfs_qgroup_id(id) +# Returns 1 for a syntactically valid qgroup ID. +sub valid_btrfs_qgroup_id +{ +return defined($_[0]) && $_[0] =~ /^\d+\/\d+$/ ? 1 : 0; +} + +=head2 set_btrfs_qgroup_limit(path, [qgroup], [bytes], [exclusive]) + +Sets the referenced or exclusive byte limit for a Btrfs qgroup. If qgroup is +undef, path must be a subvolume and its level-0 qgroup is changed. If bytes +is undef, the limit is removed. Returns undef on success or an error message +on failure. + +=cut +sub set_btrfs_qgroup_limit +{ +my ($path, $qgroup, $bytes, $exclusive) = @_; +return "Invalid Btrfs path" if (!&valid_btrfs_path($path)); +return "Invalid Btrfs qgroup ID" + if (defined($qgroup) && !&valid_btrfs_qgroup_id($qgroup)); +return "Invalid Btrfs qgroup limit" + if (defined($bytes) && $bytes !~ /^\d+$/); +my @args = ( "qgroup", "limit" ); +push(@args, "-e") if ($exclusive); +push(@args, defined($bytes) ? $bytes : "none"); +push(@args, $qgroup) if (defined($qgroup)); +push(@args, $path); +my ($out, $err) = &run_btrfs_command(1, @args); +return $err; +} + +=head2 create_btrfs_qgroup(path, qgroup) + +Creates a Btrfs qgroup on the filesystem containing path. Returns undef on +success or an error message on failure. + +=cut +sub create_btrfs_qgroup +{ +my ($path, $qgroup) = @_; +return "Invalid Btrfs path" if (!&valid_btrfs_path($path)); +return "Invalid Btrfs qgroup ID" if (!&valid_btrfs_qgroup_id($qgroup)); +my ($out, $err) = &run_btrfs_command( + 1, "qgroup", "create", $qgroup, $path); +return $err; +} + +=head2 delete_btrfs_qgroup(path, qgroup) + +Deletes an unassigned Btrfs qgroup. Returns undef on success or an error +message on failure. + +=cut +sub delete_btrfs_qgroup +{ +my ($path, $qgroup) = @_; +return "Invalid Btrfs path" if (!&valid_btrfs_path($path)); +return "Invalid Btrfs qgroup ID" if (!&valid_btrfs_qgroup_id($qgroup)); +my ($out, $err) = &run_btrfs_command( + 1, "qgroup", "destroy", $qgroup, $path); +return $err; +} + +=head2 assign_btrfs_qgroup(path, child, parent) + +Assigns a child Btrfs qgroup to a parent qgroup. Returns undef on success or +an error message on failure. + +=cut +sub assign_btrfs_qgroup +{ +my ($path, $child, $parent) = @_; +return "Invalid Btrfs path" if (!&valid_btrfs_path($path)); +return "Invalid child Btrfs qgroup ID" + if (!&valid_btrfs_qgroup_id($child)); +return "Invalid parent Btrfs qgroup ID" + if (!&valid_btrfs_qgroup_id($parent)); +my ($out, $err) = &run_btrfs_command( + 1, "qgroup", "assign", $child, $parent, $path); +return $err; +} + +=head2 unassign_btrfs_qgroup(path, child, parent) + +Removes a child Btrfs qgroup from a parent qgroup. Returns undef on success +or an error message on failure. + +=cut +sub unassign_btrfs_qgroup +{ +my ($path, $child, $parent) = @_; +return "Invalid Btrfs path" if (!&valid_btrfs_path($path)); +return "Invalid child Btrfs qgroup ID" + if (!&valid_btrfs_qgroup_id($child)); +return "Invalid parent Btrfs qgroup ID" + if (!&valid_btrfs_qgroup_id($parent)); +my ($out, $err) = &run_btrfs_command( + 1, "qgroup", "remove", $child, $parent, $path); +return $err; +} + =head2 can_quotacheck(fs) Returns 1 if some FS supports quota checking diff --git a/quota/t/run-tests.t b/quota/t/run-tests.t new file mode 100644 index 000000000..361fd30fb --- /dev/null +++ b/quota/t/run-tests.t @@ -0,0 +1,247 @@ +#!/usr/bin/perl +use strict; +use warnings; +no warnings 'once'; +use Test::More; +use Cwd qw(abs_path); +use File::Basename qw(dirname); + +my $root = abs_path(dirname(__FILE__)."/../..") or die "rootdir: $!"; +my @commands; +my @responses; +our @mounted = ( + [ "/", "/dev/root", "ext4", "rw" ], + [ "/srv/btrfs", "/dev/loop0", "btrfs", "rw" ], + [ "/srv/btrfs/external", "/dev/loop1", "ext4", "rw" ], + ); + +sub has_command +{ +return $_[0] eq "btrfs" ? "/usr/bin/btrfs" : undef; +} + +sub clean_language { } +sub reset_environment { } + +sub is_under_directory +{ +my ($dir, $path) = @_; +return 1 if ($dir eq "/"); +$dir =~ s/\/*$/\//; +return $path eq substr($dir, 0, -1) || index($path, $dir) == 0; +} + +sub next_response +{ +my ($cmd) = @_; +push(@commands, $cmd); +my $response = shift(@responses) || { 'out' => "", 'status' => 0 }; +$? = $response->{'status'}; +return $response->{'out'}; +} + +sub backquote_command +{ +return &next_response($_[0]); +} + +sub backquote_logged +{ +return &next_response($_[0]); +} + +sub error +{ +die join("", @_); +} + +{ +package mount; +sub list_mounted +{ +return @main::mounted; +} +sub filesystem_for_dir +{ +# Btrfs subvolumes can have a different st_dev from their containing mount, +# which prevents filesystem_for_dir from finding the mount by device number. +return @{$main::mounted[0]}; +} +} + +do "$root/quota/linux-lib.pl" or die "linux-lib.pl: $@ $!"; + +ok(main::is_btrfs_fs("/srv/btrfs"), + "Btrfs mount point is detected"); +ok(main::is_btrfs_fs("/srv/btrfs/domain1"), + "path inside Btrfs is detected"); +ok(!main::is_btrfs_fs("/srv/btrfs/external/file"), + "nested non-Btrfs mount takes precedence"); +ok(!main::is_btrfs_fs("/"), + "non-Btrfs path is rejected"); +ok(!defined(main::btrfs_quota_status("/")), + "quota status is unavailable for non-Btrfs paths"); + +my $status_text = <<'EOF'; +Quotas on /srv/btrfs: + Enabled: yes + Mode: qgroup (full accounting) + Inconsistent: no + Override limits: no + Drop subtree threshold: 3 + Total count: 4 + Level 0: 3 + Level 1: 1 +EOF +my $status = main::parse_btrfs_quota_status($status_text); +is_deeply($status, { + 'enabled' => 1, + 'mode' => 'qgroup', + 'mode_description' => 'full accounting', + 'inconsistent' => 0, + 'override_limits' => 0, + 'drop_subtree_threshold' => 3, + 'total_count' => 4, + 'levels' => { 0 => 3, 1 => 1 }, + }, "full Btrfs quota status is parsed"); + +my $simple_status = main::parse_btrfs_quota_status(<<'EOF'); +Quotas on /srv/btrfs: + Enabled: yes + Mode: squota (simple accounting) + Inconsistent: yes +EOF +is($simple_status->{'mode'}, "squota", "simple quota mode is parsed"); +ok($simple_status->{'inconsistent'}, "inconsistent status is parsed"); +is_deeply(main::parse_btrfs_quota_status(" Enabled: no\n"), + { 'enabled' => 0 }, "disabled status is parsed"); +ok(!defined(main::parse_btrfs_quota_status("invalid output\n")), + "invalid status output is rejected"); + +@commands = ( ); +@responses = ({ 'out' => $status_text, 'status' => 0 }); +$status = main::btrfs_quota_status("/srv/btrfs"); +ok($status->{'supported'} && $status->{'enabled'}, + "status command reports enabled quotas"); +is(scalar(@commands), 1, "successful status does not run fallback"); + +@responses = ( + { 'out' => "ERROR: unknown token 'status'\n", 'status' => 1 }, + { 'out' => "qgroupid rfer excl\n0/5 16384 16384\n", 'status' => 0 }, + ); +$status = main::btrfs_quota_status("/srv/btrfs"); +ok($status->{'enabled'}, "legacy qgroup fallback detects enabled quotas"); + +@responses = ( + { 'out' => "ERROR: unknown token 'status'\n", 'status' => 1 }, + { 'out' => "ERROR: quota root does not exist\n", 'status' => 1 }, + ); +$status = main::btrfs_quota_status("/srv/btrfs"); +is($status->{'enabled'}, 0, "legacy fallback detects disabled quotas"); + +my $qgroup_text = <<'EOF'; +Qgroupid Referenced Exclusive Max referenced Max exclusive Parent Child Path +-------- ---------- --------- -------------- ------------- ------ ----- ---- +0/5 16384 16384 none none --- --- +0/256 16384 16384 67108864 none 1/100 - domain1 +0/257 0 0 none 33554432 1/100 - domain two +1/100 16384 16384 100663296 none - 0/256,0/257 <0 member qgroups> +EOF +my $qgroups = main::parse_btrfs_qgroup_output($qgroup_text); +is(scalar(@$qgroups), 4, "all qgroups are parsed"); +is_deeply($qgroups->[0]->{'parents'}, [ ], + "legacy empty parent marker is parsed"); +is_deeply($qgroups->[0]->{'children'}, [ ], + "legacy empty child marker is parsed"); +is($qgroups->[1]->{'max_referenced'}, 67108864, + "referenced limit is parsed as bytes"); +ok(!defined($qgroups->[1]->{'max_exclusive'}), + "missing exclusive limit is undef"); +is_deeply($qgroups->[1]->{'parents'}, [ "1/100" ], + "parent qgroup is parsed"); +is($qgroups->[2]->{'path'}, "domain two", + "subvolume paths containing spaces are preserved"); +is_deeply($qgroups->[3]->{'children'}, [ "0/256", "0/257" ], + "multiple child qgroups are parsed"); + +@commands = ( ); +@responses = ({ 'out' => $qgroup_text, 'status' => 0 }); +my $list_error; +$qgroups = main::list_btrfs_qgroups("/srv/btrfs", 1, \$list_error); +is(scalar(@$qgroups), 4, "qgroup list command output is returned"); +ok(!defined($list_error), "successful qgroup list clears the error"); +like($commands[0], qr/qgroup show .*\\-\\-sync .*srv.*btrfs/, + "synchronized qgroup listing requests --sync"); + +@responses = ({ 'out' => "ERROR: quotas not enabled\n", 'status' => 1 }); +$qgroups = main::list_btrfs_qgroups("/srv/btrfs", 0, \$list_error); +ok(!defined($qgroups), "failed qgroup listing returns undef"); +like($list_error, qr/quotas not enabled/, + "failed qgroup listing returns the command error"); +@responses = ({ 'out' => "unexpected output\n", 'status' => 0 }); +$qgroups = main::list_btrfs_qgroups("/srv/btrfs", 0, \$list_error); +ok(!defined($qgroups), "unparseable qgroup listing returns undef"); +is($list_error, "Unable to parse Btrfs qgroup output", + "unparseable qgroup output is reported"); + +@responses = ({ + 'out' => "domain1\n\tSubvolume ID:\t\t256\n", + 'status' => 0, + }); +is(main::btrfs_subvolume_id("/srv/btrfs/domain1"), 256, + "subvolume ID is parsed"); + +@responses = ( + { 'out' => "domain1\n\tSubvolume ID:\t\t256\n", 'status' => 0 }, + { 'out' => $qgroup_text, 'status' => 0 }, + ); +my $qgroup = main::get_btrfs_qgroup("/srv/btrfs/domain1", 0); +is($qgroup->{'id'}, "0/256", "subvolume qgroup is selected by ID"); + +@commands = ( ); +@responses = map { { 'out' => "", 'status' => 0 } } 1 .. 9; +is(main::enable_btrfs_quotas("/srv/btrfs", 1), undef, + "simple quotas can be enabled"); +is(main::set_btrfs_qgroup_limit("/srv/btrfs", "1/100", 1048576, 0), + undef, "referenced qgroup limit can be set"); +is(main::set_btrfs_qgroup_limit("/srv/btrfs/domain1", undef, undef, 1), + undef, "exclusive subvolume limit can be removed"); +is(main::create_btrfs_qgroup("/srv/btrfs", "1/101"), undef, + "parent qgroup can be created"); +is(main::assign_btrfs_qgroup("/srv/btrfs", "0/256", "1/101"), undef, + "child qgroup can be assigned"); +is(main::unassign_btrfs_qgroup("/srv/btrfs", "0/256", "1/101"), undef, + "child qgroup can be unassigned"); +is(main::delete_btrfs_qgroup("/srv/btrfs", "1/101"), undef, + "parent qgroup can be deleted"); +is(main::rescan_btrfs_quotas("/srv/btrfs", 1), undef, + "quota rescan can run and wait"); +is(main::disable_btrfs_quotas("/srv/btrfs"), undef, + "Btrfs quotas can be disabled"); +like($commands[0], qr/quota enable .*\\-\\-simple/, + "simple enable command uses supported long option"); +like($commands[1], qr/qgroup limit .*1048576 .*1\\\/100/, + "referenced limit command contains size and qgroup"); +like($commands[2], qr/qgroup limit .*\-e .*none/, + "exclusive limit removal uses -e and none"); +like($commands[7], qr/quota rescan .*\\-w/, + "quota rescan uses the portable short wait option"); + +is(main::set_btrfs_qgroup_limit("/srv/btrfs", "bad", 1024), + "Invalid Btrfs qgroup ID", "invalid qgroup IDs are rejected"); +is(main::disable_btrfs_quotas("relative/path"), + "Invalid Btrfs path", "relative paths are rejected"); +is(main::disable_btrfs_quotas("/srv/btrfs\n/etc"), + "Invalid Btrfs path", "paths with control characters are rejected"); +ok(!main::is_btrfs_fs("/srv/btrfs\n/etc"), + "paths with control characters are not detected as Btrfs"); +is(main::set_btrfs_qgroup_limit("/srv/btrfs", "1/100", "1M"), + "Invalid Btrfs qgroup limit", "non-byte limits are rejected"); +is(main::assign_btrfs_qgroup("/srv/btrfs", "bad", "1/100"), + "Invalid child Btrfs qgroup ID", "invalid child assignment is rejected"); + +@responses = ({ 'out' => "ERROR: qgroup exists\n", 'status' => 1 }); +is(main::create_btrfs_qgroup("/srv/btrfs", "1/100"), + "ERROR: qgroup exists", "Btrfs command errors are returned to callers"); + +done_testing(); From 99ab9159c322407c7172a94d706eea99f1af1bde Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Mon, 3 Aug 2026 02:44:49 +0200 Subject: [PATCH 2/2] Fix to use formal params https://github.com/webmin/webmin/pull/2804#pullrequestreview-4840092046 --- quota/linux-lib.pl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/quota/linux-lib.pl b/quota/linux-lib.pl index 3b62e1461..c9a3a3bc5 100755 --- a/quota/linux-lib.pl +++ b/quota/linux-lib.pl @@ -1109,7 +1109,8 @@ return $best && $best->[2] eq "btrfs" ? 1 : 0; # Returns 1 for an absolute path that is safe to pass to Btrfs tools. sub valid_btrfs_path { -return defined($_[0]) && $_[0] =~ /^\// && $_[0] !~ /[\r\n\0]/ ? 1 : 0; +my ($path) = @_; +return defined($path) && $path =~ /^\// && $path !~ /[\r\n\0]/ ? 1 : 0; } # run_btrfs_command(logged, arg, ...)