diff --git a/.github/workflows/xcat_test.yml b/.github/workflows/xcat_test.yml index cec518716..5ed312bd5 100644 --- a/.github/workflows/xcat_test.yml +++ b/.github/workflows/xcat_test.yml @@ -28,7 +28,8 @@ jobs: set -o pipefail docker run --rm -v "$PWD:/src:ro" -w /src fedora:42 bash -ec ' dnf -y -q install mock perl perl-File-Slurper >/dev/null - prove -v xCAT-test/native/mock_resultdir_owner.t' 2>&1 | tee mock.tap + prove -v xCAT-test/native/mock_resultdir_owner.t \ + xCAT-test/native/build_sources_other_uid.t' 2>&1 | tee mock.tap if grep -iE '# skip' mock.tap; then echo 'a mock result directory test was skipped' >&2 exit 1 diff --git a/xCAT-test/native/README.md b/xCAT-test/native/README.md index 9e0816eb8..eb1072c6e 100644 --- a/xCAT-test/native/README.md +++ b/xCAT-test/native/README.md @@ -39,10 +39,12 @@ sudo prove xCAT-test/native/ip_forwarding.t \ ``` The mock result directory case needs the mock Python library, and root for its -ownership checks. The `xcat_test` workflow runs it as root in a Fedora container: +ownership checks. The staged sources case needs root to read the sources as uid +1000. The `xcat_test` workflow runs both as root in a Fedora container: ``` -sudo prove xCAT-test/native/mock_resultdir_owner.t +sudo prove xCAT-test/native/mock_resultdir_owner.t \ + xCAT-test/native/build_sources_other_uid.t ``` The PostgreSQL case starts a private server with only a Unix socket. Run it as diff --git a/xCAT-test/native/build_sources_other_uid.t b/xCAT-test/native/build_sources_other_uid.t new file mode 100644 index 000000000..ea7f3d4c3 --- /dev/null +++ b/xCAT-test/native/build_sources_other_uid.t @@ -0,0 +1,50 @@ +#!/usr/bin/env perl +# mock reads the staged sources as the uid of the target's chrootuid while buildrpms.pl +# runs as root. A process with another uid must read them. CI runs this file as root. +use strict; +use warnings; +use FindBin; +use lib "$FindBin::Bin/../../build-utils/lib"; +use File::Path qw(make_path); +use File::Slurper qw(read_text write_text); +use File::Temp qw(tempdir); +use POSIX (); +use Test::More; +use XCAT::BuildUtils qw(build_sources_base prepare_build_sources_dir share_build_sources_dir); + +plan skip_all => 'requires root to switch to another uid' if $> != 0; + +# Flush before the fork below, or the child repeats this file's TAP output on exit. +$| = 1; + +my $checkout = tempdir(CLEANUP => 1); +chmod 0755, $checkout or die "chmod $checkout: $!\n"; +# A root $HOME the build uid cannot enter, as on the build hosts. +make_path("$checkout/root"); +chmod 0550, "$checkout/root" or die "chmod: $!\n"; +local $ENV{HOME} = "$checkout/root"; + +my $umask = umask 077; +my $dir = prepare_build_sources_dir(build_sources_base($checkout), $$, 'hosta'); +make_path("$dir/sub"); +write_text("$dir/xCAT-2.20.0.tar.gz", "payload\n"); +write_text("$dir/sub/xcat.conf", "conf\n"); +share_build_sources_dir($dir); +umask $umask; + +my $pid = fork // die "fork: $!\n"; +if ($pid == 0) { + POSIX::setgid(1000); + POSIX::setuid(1000); + POSIX::_exit(2) if $> != 1000; + my $ok = eval { + opendir(my $dh, $dir) or die; + read_text("$dir/xCAT-2.20.0.tar.gz") eq "payload\n" + && read_text("$dir/sub/xcat.conf") eq "conf\n"; + }; + POSIX::_exit($ok ? 0 : 1); +} +waitpid($pid, 0); +is($? >> 8, 0, 'uid 1000 lists the staging directory and reads every staged file'); + +done_testing; diff --git a/xCAT-test/unit/buildrpms_source_only.t b/xCAT-test/unit/buildrpms_source_only.t index 2868f185d..b528a588e 100644 --- a/xCAT-test/unit/buildrpms_source_only.t +++ b/xCAT-test/unit/buildrpms_source_only.t @@ -51,6 +51,7 @@ our @STAGES; sub createmockconfig { push @main::STAGES, 'createmockconfig'; } sub prepare_mock_resultdirs { push @main::STAGES, 'prepare_mock_resultdirs'; } sub buildsources { push @main::STAGES, 'buildsources'; } + sub share_build_sources_dir { push @main::STAGES, 'share_build_sources_dir'; } sub buildspkgs { push @main::STAGES, 'buildspkgs'; } sub buildpkgs { push @main::STAGES, 'buildpkgs'; } # buildall assigns a staging directory per build process. @@ -129,11 +130,11 @@ sub stages_for { } is_deeply( stages_for(0), - [qw(createmockconfig prepare_mock_resultdirs buildsources buildspkgs buildpkgs)], + [qw(createmockconfig prepare_mock_resultdirs buildsources share_build_sources_dir buildspkgs buildpkgs)], 'a normal run builds the source rpm and then rebuilds it into binaries' ); is_deeply( stages_for(1), - [qw(createmockconfig prepare_mock_resultdirs buildsources buildspkgs)], + [qw(createmockconfig prepare_mock_resultdirs buildsources share_build_sources_dir buildspkgs)], 'a source-only run stops once the source rpm exists' ); ok( !grep( { $_ eq 'buildpkgs' } @{ stages_for(1) } ), diff --git a/xCAT-test/unit/buildrpms_sources_per_package.t b/xCAT-test/unit/buildrpms_sources_per_package.t index 0a97d9380..a55a97efa 100644 --- a/xCAT-test/unit/buildrpms_sources_per_package.t +++ b/xCAT-test/unit/buildrpms_sources_per_package.t @@ -4,6 +4,7 @@ use strict; use warnings; +use Cwd (); use File::Path qw(make_path); use File::Slurper qw(write_text); use File::Temp qw(tempdir); @@ -12,11 +13,23 @@ use lib "$FindBin::Bin/../../build-utils/lib"; use Test::More; use XCAT::BuildUtils qw(build_sources_base build_sources_dir prepare_build_sources_dir - remove_build_sources_dir sweep_build_sources_dirs); + remove_build_sources_dir sweep_build_sources_dirs share_build_sources_dir); +# mock reads the staged sources as the uid of the target's chrootuid, and $HOME of a +# root build is /root, mode 0550. The base belongs to the checkout, not to $HOME. my $home = tempdir(CLEANUP => 1); +local $ENV{HOME} = "$home/home"; my $base = build_sources_base($home); -is($base, "$home/rpmbuild/sources", 'the base directory is under the rpmbuild tree'); +is($base, "$home/dist/sources", 'the base directory is under the dist tree of the checkout'); +unlike($base, qr{^\Q$ENV{HOME}\E/}, 'the base directory is not under $HOME'); +{ + my $cwd = Cwd::getcwd(); + chdir $home or die "chdir $home: $!\n"; + my $default = build_sources_base(); + chdir $cwd or die "chdir $cwd: $!\n"; + is($default, Cwd::abs_path($home) . "/dist/sources", + 'the base directory defaults to the dist tree of the working directory'); +} # Two live processes on one host never have the same pid. The host name keeps two # hosts apart when $HOME is on NFS. @@ -89,4 +102,23 @@ ok(!-e $reaped, 'the default check deletes the directory of a reaped process'); is_deeply([sweep_build_sources_dirs("$home/absent", 'hosta', $is_alive)], [], 'a missing base directory is not an error'); +# Another uid must traverse every directory and read every file, whatever the umask. +{ + my $root = tempdir(CLEANUP => 1); + my $umask = umask 077; + my $sbase = build_sources_base($root); + my $dir = prepare_build_sources_dir($sbase, 401, 'hosta'); + make_path("$dir/sub"); + write_text("$dir/xCAT-2.20.0.tar.gz", "payload\n"); + write_text("$dir/sub/xcat.conf", "conf\n"); + share_build_sources_dir($dir); + umask $umask; + for my $d ("$root/dist", $sbase, $dir, "$dir/sub") { + is((stat $d)[2] & 0055, 0055, "other users can list and enter $d"); + } + for my $f ("$dir/xCAT-2.20.0.tar.gz", "$dir/sub/xcat.conf") { + is((stat $f)[2] & 0044, 0044, "other users can read $f"); + } +} + done_testing;