From e3e132967cfa65fc4c7544faa9b68f19b09104a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Mon, 17 Aug 2026 12:24:20 -0300 Subject: [PATCH 1/2] fix(xcatd): stop a noderange from running a command through the ^ operator The ^ noderange operator reads node names from a file. NodeRange opened that file with a two-argument open. A two-argument open reads shell metacharacters in the path, so a noderange such as ^"id|" ran a command. xcatd expands a noderange while it processes a request, so the command ran on the management node. Use a three-argument open with an explicit read mode. The value is then only ever a file name. The ^ operator keeps working: ^/tmp/nodes still reads the file. This fix was recovered from the lenovobuild branch. The original there (commit for "Remove load from file in noderange support") removed the ^ operator. This keeps the documented operator and closes the command path instead. --- perl-xCAT/xCAT/NodeRange.pm | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/perl-xCAT/xCAT/NodeRange.pm b/perl-xCAT/xCAT/NodeRange.pm index f79fcec5e..fad97cd24 100644 --- a/perl-xCAT/xCAT/NodeRange.pm +++ b/perl-xCAT/xCAT/NodeRange.pm @@ -678,8 +678,9 @@ sub noderange { if ($atom eq '') { next; } if ($atom =~ /^\^(.*)$/) { # get a list of nodes from a file - open(NRF, $1); - while () { + my $nrfile = $1; + open(my $nrf, '<', $nrfile) or next; + while (<$nrf>) { my $line = $_; unless ($line =~ m/^[\^#]/) { $line =~ m/^([^: ]*)/; @@ -692,7 +693,7 @@ sub noderange { } } } - close(NRF); + close($nrf); next; } From dd930681a5283c8d3669b9fa8917484cbab62280 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Mon, 17 Aug 2026 12:24:20 -0300 Subject: [PATCH 2/2] test(xcatd): cover the noderange ^ file operator The test runs the real NodeRange code. It sends a ^ range that a two-argument open would run as a command and checks the command does not run. It also reads a real comment-only file to show the operator still works. The command assertion fails against the previous behavior. --- xCAT-test/unit/noderange_file_operator.t | 51 ++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 xCAT-test/unit/noderange_file_operator.t diff --git a/xCAT-test/unit/noderange_file_operator.t b/xCAT-test/unit/noderange_file_operator.t new file mode 100644 index 000000000..8e228ffeb --- /dev/null +++ b/xCAT-test/unit/noderange_file_operator.t @@ -0,0 +1,51 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use FindBin; +use File::Spec; +use File::Temp qw(tempfile); +use Test::More; + +my $libdir = "$FindBin::Bin/../../perl-xCAT"; +plan skip_all => 'NodeRange.pm not found' unless -r "$libdir/xCAT/NodeRange.pm"; + +# NodeRange requires xCAT::Table at load and creates a nodelist handle once, on +# entry. Stub Table so the module loads and that init succeeds without a +# database. The ^file path under test never calls a Table method. +BEGIN { + $INC{'xCAT/Table.pm'} = 1; + package xCAT::Table; + sub new { return bless {}, shift } + sub _set_use_cache { } + sub _build_cache { } + our $AUTOLOAD; + sub AUTOLOAD { return } + sub DESTROY { } +} +eval { require Text::Balanced; 1 } or plan skip_all => 'Text::Balanced is required'; + +push @INC, $libdir; +require xCAT::NodeRange; + +# The command an injected ^file range would run. The marker must never appear. +my $marker = File::Spec->catfile(File::Spec->tmpdir, "xcat_nr_marker_$$"); +unlink $marker; +END { unlink $marker if defined $marker } + +# The ^ operator reads node names from a file. A two-argument open would treat +# the value as a shell pipe and run the command; assert it does not. +my $evil = '^touch ' . $marker . ' |'; +eval { xCAT::NodeRange::noderange($evil, 0, 0) }; +ok(!-e $marker, 'a piped ^file range does not execute a command'); + +# A real file that holds only comment and blank lines is opened and read +# without error and yields no nodes. +my ($fh, $listfile) = tempfile('xcat_nrlist_XXXXXX', TMPDIR => 1, UNLINK => 1); +print {$fh} "# a comment line\n\n"; +close $fh; +my @res = eval { xCAT::NodeRange::noderange('^' . $listfile, 0, 0) }; +is($@, '', 'a real ^file is read without error'); +is_deeply(\@res, [], 'a comment-only ^file yields no nodes'); + +done_testing();