This is an automated email from the ASF dual-hosted git repository.

yiguolei pushed a commit to branch branch-4.1
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/branch-4.1 by this push:
     new 8da7d1b87b0 branch-4.1: [fix](be) Quote shell arguments in profiling 
and Python tools #70207 (#70368)
8da7d1b87b0 is described below

commit 8da7d1b87b0f33dfd1ba61de85d627423d19b38e
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Tue Nov 10 11:49:32 2026 +0800

    branch-4.1: [fix](be) Quote shell arguments in profiling and Python tools 
#70207 (#70368)
    
    Cherry-picked from #70207
    
    Co-authored-by: TengJianPing <[email protected]>
---
 be/src/agent/utils.cpp                  |  20 ++++
 be/src/agent/utils.h                    |   5 +-
 be/src/runtime/memory/heap_profiler.cpp |  30 +++---
 be/src/runtime/memory/heap_profiler.h   |   1 +
 be/src/udf/python/python_env.cpp        |  12 ++-
 be/src/util/pprof_utils.cpp             |  38 ++++---
 be/test/agent/utils_test.cpp            |  21 ++++
 be/test/udf/python/python_env_test.cpp  |  14 ++-
 be/test/util/profiler_command_test.cpp  | 170 ++++++++++++++++++++++++++++++++
 tools/jeprof                            |  23 ++++-
 10 files changed, 300 insertions(+), 34 deletions(-)

diff --git a/be/src/agent/utils.cpp b/be/src/agent/utils.cpp
index 8064eef7ac7..7d6cf48898d 100644
--- a/be/src/agent/utils.cpp
+++ b/be/src/agent/utils.cpp
@@ -227,7 +227,27 @@ Status MasterServerClient::confirm_unused_remote_files(
     return Status::OK();
 }
 
+std::string AgentUtils::shell_quote(const std::string& argument) {
+    std::string quoted = "'";
+    for (char ch : argument) {
+        // A single quote ends a quoted section. Insert it between two quoted 
sections.
+        // For example, a'b becomes 'a'\''b'.
+        if (ch == '\'') {
+            quoted += "'\\''";
+        } else {
+            quoted += ch;
+        }
+    }
+    quoted += '\'';
+    return quoted;
+}
+
 bool AgentUtils::exec_cmd(const std::string& command, std::string* errmsg, 
bool redirect_stderr) {
+    // popen reads a C string. A NUL byte would discard the rest of the 
command.
+    if (command.find('\0') != std::string::npos) {
+        *errmsg = "The shell command contains a NUL byte.\n";
+        return false;
+    }
     // The exit status of the command.
     uint32_t rc = 0;
 
diff --git a/be/src/agent/utils.h b/be/src/agent/utils.h
index 42c59718861..57c258e7a87 100644
--- a/be/src/agent/utils.h
+++ b/be/src/agent/utils.h
@@ -76,7 +76,10 @@ public:
     AgentUtils() = default;
     virtual ~AgentUtils() = default;
 
-    // Execute shell cmd
+    // Quote one shell argument. For example, "a;pwd" must remain one file 
name.
+    static std::string shell_quote(const std::string& argument);
+
+    // Execute a shell command. Use shell_quote for each variable argument.
     virtual bool exec_cmd(const std::string& command, std::string* errmsg,
                           bool redirect_stderr = true);
 
diff --git a/be/src/runtime/memory/heap_profiler.cpp 
b/be/src/runtime/memory/heap_profiler.cpp
index 97bfd15f654..31c066bb19f 100644
--- a/be/src/runtime/memory/heap_profiler.cpp
+++ b/be/src/runtime/memory/heap_profiler.cpp
@@ -66,14 +66,16 @@ bool HeapProfiler::get_prof_dump(const std::string& 
profile_file_name) {
 #endif
 }
 
-static std::string jeprof_profile_to_dot(const std::string& profile_file_name) 
{
+std::string HeapProfiler::jeprof_profile_to_dot(const std::string& 
profile_file_name) {
     AgentUtils util;
-    const static std::string jeprof_path = fmt::format("{}/bin/jeprof", 
std::getenv("DORIS_HOME"));
-    const static std::string binary_path =
-            fmt::format("{}/lib/doris_be", std::getenv("DORIS_HOME"));
+    const std::string jeprof_path = fmt::format("{}/bin/jeprof", 
std::getenv("DORIS_HOME"));
+    const std::string binary_path = fmt::format("{}/lib/doris_be", 
std::getenv("DORIS_HOME"));
     // 
https://doris.apache.org/community/developer-guide/debug-tool/#3-jeprof-parses-heap-profile
-    std::string jeprof_cmd =
-            fmt::format("{} --dot {} {}", jeprof_path, binary_path, 
profile_file_name);
+    // The configuration can contain shell syntax. Keep paths such as 
"log$(pwd)" literal.
+    // The -- separator also prevents a path from becoming a jeprof option.
+    std::string jeprof_cmd = fmt::format("{} --dot -- {} {}", 
AgentUtils::shell_quote(jeprof_path),
+                                         AgentUtils::shell_quote(binary_path),
+                                         
AgentUtils::shell_quote(profile_file_name));
     std::string msg;
     bool rc = util.exec_cmd(jeprof_cmd, &msg);
     if (!rc) {
@@ -117,16 +119,22 @@ bool HeapProfiler::check_enable_heap_profiler() {
 }
 
 std::string HeapProfiler::dump_heap_profile() {
-    if (!config::jeprofile_dir.empty()) {
-        auto st = 
io::global_local_filesystem()->create_directory(config::jeprofile_dir);
+    std::string profile_dir;
+    {
+        // A configuration update can replace this string during a dump.
+        // Copy it under the update lock. Use the same directory for both file 
operations.
+        std::lock_guard<std::mutex> 
lock(*config::get_mutable_string_config_lock());
+        profile_dir = config::jeprofile_dir;
+    }
+    if (!profile_dir.empty()) {
+        auto st = io::global_local_filesystem()->create_directory(profile_dir);
         if (!st.ok()) {
             LOG(WARNING) << "create jeprofile dir failed.";
             return "";
         }
     }
-    std::string profile_file_name =
-            fmt::format("{}/jeheap_dump.{}.{}.{}.heap", config::jeprofile_dir, 
std::time(nullptr),
-                        getpid(), rand());
+    std::string profile_file_name = 
fmt::format("{}/jeheap_dump.{}.{}.{}.heap", profile_dir,
+                                                std::time(nullptr), getpid(), 
rand());
     if (get_prof_dump(profile_file_name)) {
         return profile_file_name;
     } else {
diff --git a/be/src/runtime/memory/heap_profiler.h 
b/be/src/runtime/memory/heap_profiler.h
index 32ff20ca8e1..135081a2151 100644
--- a/be/src/runtime/memory/heap_profiler.h
+++ b/be/src/runtime/memory/heap_profiler.h
@@ -37,6 +37,7 @@ public:
     std::string dump_heap_profile_to_dot();
 
 private:
+    static std::string jeprof_profile_to_dot(const std::string& 
profile_file_name);
     void set_prof_active(bool prof);
     bool get_prof_dump(const std::string& profile_file_name);
 
diff --git a/be/src/udf/python/python_env.cpp b/be/src/udf/python/python_env.cpp
index 4ed05f0510d..bb25bcbe5a6 100644
--- a/be/src/udf/python/python_env.cpp
+++ b/be/src/udf/python/python_env.cpp
@@ -25,6 +25,7 @@
 #include <regex>
 #include <vector>
 
+#include "agent/utils.h"
 #include "common/status.h"
 #include "gen_cpp/BackendService_types.h"
 #include "udf/python/python_server.h"
@@ -54,7 +55,8 @@ static Status extract_python_version(const std::string& 
python_path, std::string
         return Status::NotFound("Python executable not found: {}", 
python_path);
     }
 
-    std::string cmd = fmt::format("\"{}\" --version", python_path);
+    // Double quotes still expand shell expressions such as $(pwd). Keep the 
path literal.
+    std::string cmd = fmt::format("{} --version", 
AgentUtils::shell_quote(python_path));
     FILE* pipe = popen(cmd.c_str(), "r");
     if (!pipe) {
         return Status::InternalError("Failed to run: {}", cmd);
@@ -177,7 +179,8 @@ Status PythonEnvironment::scan_from_venv_root_path(
             // Use --system-site-packages to inherit packages from system 
Python
             // This ensures pandas and pyarrow are available if installed in 
system
             std::string create_venv_cmd = fmt::format("{} -m venv 
--system-site-packages {}",
-                                                      interpreter_path, 
env_base_path);
+                                                      
AgentUtils::shell_quote(interpreter_path),
+                                                      
AgentUtils::shell_quote(env_base_path));
 
             if (system(create_venv_cmd.c_str()) != 0 || 
!fs::exists(python_path)) {
                 return Status::RuntimeError("Failed to create python virtual 
environment, cmd: {}",
@@ -340,8 +343,9 @@ Status list_installed_packages(const PythonVersion& version,
     }
 
     // Run pip list --format=json to get installed packages
-    std::string cmd =
-            fmt::format("\"{}\" -m pip list --format=json 2>/dev/null", 
version.executable_path);
+    // Use the same path quoting as version discovery and environment creation.
+    std::string cmd = fmt::format("{} -m pip list --format=json 2>/dev/null",
+                                  
AgentUtils::shell_quote(version.executable_path));
     FILE* pipe = popen(cmd.c_str(), "r");
     if (!pipe) {
         return Status::InternalError("Failed to run pip list for python 
version: {}",
diff --git a/be/src/util/pprof_utils.cpp b/be/src/util/pprof_utils.cpp
index 8f8cd7578e4..eadf07192d5 100644
--- a/be/src/util/pprof_utils.cpp
+++ b/be/src/util/pprof_utils.cpp
@@ -35,15 +35,17 @@ extern std::string pprof_profile_dir;
 
 Status PprofUtils::get_pprof_cmd(std::string* cmd) {
     AgentUtils util;
-    // check if pprof cmd exist
-    const static std::string tools_path = 
std::string(std::getenv("DORIS_HOME")) + "/tools/bin/";
+    // The startup script sets DORIS_HOME before the BE starts.
+    const char* doris_home = std::getenv("DORIS_HOME");
+    DORIS_CHECK(doris_home != nullptr);
+    const std::string tools_path = std::string(doris_home) + "/tools/bin/";
     std::string pprof_cmd = tools_path + "pprof";
     std::string msg;
-    bool rc = util.exec_cmd(pprof_cmd + " --version", &msg);
+    bool rc = util.exec_cmd(AgentUtils::shell_quote(pprof_cmd) + " --version", 
&msg);
     if (!rc) {
         // not found in BE tools dir, found in system
         pprof_cmd = "pprof";
-        rc = util.exec_cmd(pprof_cmd + " --version", &msg);
+        rc = util.exec_cmd(AgentUtils::shell_quote(pprof_cmd) + " --version", 
&msg);
         if (!rc) {
             return Status::NotSupported(
                     "pprof: command not found in systemp PATH or 
be/tools/bin/. Install gperftools "
@@ -59,7 +61,7 @@ Status PprofUtils::get_perf_cmd(std::string* cmd) {
     // check if perf cmd exist
     std::string perf_cmd = "perf";
     std::string msg;
-    bool rc = util.exec_cmd(perf_cmd + " --version", &msg);
+    bool rc = util.exec_cmd(AgentUtils::shell_quote(perf_cmd) + " --version", 
&msg);
     if (!rc) {
         return Status::NotSupported("perf: command not found in systemp PATH");
     }
@@ -111,13 +113,16 @@ Status PprofUtils::get_readable_profile(const 
std::string& file_or_content, bool
 
     // parse raw with "pprof --text cmdline raw_file"
     std::string cmd_output;
-    std::string final_cmd = pprof_cmd + absl::Substitute(" --text $0 $1", 
self_cmdline, final_file);
+    // Quote each path. For example, "log;pwd/profile" is one path, not two 
commands.
+    std::string final_cmd = absl::Substitute(
+            "$0 --text -- $1 $2", AgentUtils::shell_quote(pprof_cmd),
+            AgentUtils::shell_quote(self_cmdline), 
AgentUtils::shell_quote(final_file));
     AgentUtils util;
     LOG(INFO) << "begin to run command: " << final_cmd;
     bool rc = util.exec_cmd(final_cmd, &cmd_output, false);
 
     // delete raw file
-    
static_cast<void>(io::global_local_filesystem()->delete_file(file_or_content));
+    static_cast<void>(io::global_local_filesystem()->delete_file(final_file));
 
     if (!rc) {
         return Status::InternalError("Failed to execute command: {}", 
cmd_output);
@@ -153,8 +158,9 @@ Status PprofUtils::generate_flamegraph(int32_t 
sample_seconds,
 
     // sample
     std::stringstream cmd;
-    cmd << perf_cmd << " record -m 2 -g -p " << getpid() << " -o " << 
tmp_file.str() << " -- sleep "
-        << sample_seconds;
+    // Keep configured paths as arguments, including paths with spaces or 
shell syntax.
+    cmd << AgentUtils::shell_quote(perf_cmd) << " record -m 2 -g -p " << 
getpid() << " -o "
+        << AgentUtils::shell_quote(tmp_file.str()) << " -- sleep " << 
sample_seconds;
 
     AgentUtils util;
     std::string cmd_output;
@@ -173,8 +179,12 @@ Status PprofUtils::generate_flamegraph(int32_t 
sample_seconds,
         graph_file << config::pprof_profile_dir << "/flamegraph." << getpid() 
<< "." << rand()
                    << ".svg";
         std::stringstream gen_cmd;
-        gen_cmd << perf_cmd << " script -i " << tmp_file.str() << " | " << 
stackcollapse_perf_pl
-                << " | " << flamegraph_pl << " > " << graph_file.str();
+        // Only these fixed pipe and redirection operators are shell syntax.
+        gen_cmd << AgentUtils::shell_quote(perf_cmd) << " script -i "
+                << AgentUtils::shell_quote(tmp_file.str()) << " | "
+                << AgentUtils::shell_quote(stackcollapse_perf_pl) << " | "
+                << AgentUtils::shell_quote(flamegraph_pl) << " > "
+                << AgentUtils::shell_quote(graph_file.str());
         LOG(INFO) << "begin to run command: " << gen_cmd.str();
         rc = util.exec_cmd(gen_cmd.str(), &res_content);
         if (!rc) {
@@ -185,8 +195,10 @@ Status PprofUtils::generate_flamegraph(int32_t 
sample_seconds,
         *svg_file_or_content = graph_file.str();
     } else {
         std::stringstream gen_cmd;
-        gen_cmd << perf_cmd << " script -i " << tmp_file.str() << " | " << 
stackcollapse_perf_pl
-                << " | " << flamegraph_pl;
+        gen_cmd << AgentUtils::shell_quote(perf_cmd) << " script -i "
+                << AgentUtils::shell_quote(tmp_file.str()) << " | "
+                << AgentUtils::shell_quote(stackcollapse_perf_pl) << " | "
+                << AgentUtils::shell_quote(flamegraph_pl);
         LOG(INFO) << "begin to run command: " << gen_cmd.str();
         rc = util.exec_cmd(gen_cmd.str(), &res_content, false);
         if (!rc) {
diff --git a/be/test/agent/utils_test.cpp b/be/test/agent/utils_test.cpp
index dd32a219a0c..658997f31dd 100644
--- a/be/test/agent/utils_test.cpp
+++ b/be/test/agent/utils_test.cpp
@@ -45,4 +45,25 @@ TEST(AgentUtilsTest, Test) {
     }
 }
 
+// The shell must return each input unchanged. No profiler or table data is 
required.
+TEST(AgentUtilsTest, ShellQuotePreservesOneArgument) {
+    for (const std::string& value :
+         {"", "a b", "a'b'c", "\"$HOME\"`printf injected`$(printf 
injected);|&<>*?\\\n#"}) {
+        std::string output;
+        AgentUtils util;
+        ASSERT_TRUE(util.exec_cmd("printf '<%s>' " + 
AgentUtils::shell_quote(value), &output));
+        EXPECT_EQ("<" + value + ">", output);
+    }
+}
+
+TEST(AgentUtilsTest, RejectNulBeforeRunningCommand) {
+    std::string command = "printf unexpected";
+    command += '\0';
+    command += "printf ignored";
+    std::string output;
+    AgentUtils util;
+    EXPECT_FALSE(util.exec_cmd(command, &output));
+    EXPECT_EQ("The shell command contains a NUL byte.\n", output);
+}
+
 } // namespace doris
diff --git a/be/test/udf/python/python_env_test.cpp 
b/be/test/udf/python/python_env_test.cpp
index ed0f2406925..455e5a8386b 100644
--- a/be/test/udf/python/python_env_test.cpp
+++ b/be/test/udf/python/python_env_test.cpp
@@ -184,8 +184,9 @@ TEST_F(PythonEnvTest, 
ScanFromVenvRootPathNonExistentInterpreter) {
 }
 
 TEST_F(PythonEnvTest, ScanFromVenvRootPathCreatesAndValidatesEnvironment) {
-    const std::string root_path = test_dir_ + "/venv_root";
-    const std::string interpreter_path = test_dir_ + "/python3.11";
+    // One environment is sufficient to test version discovery and creation 
with literal paths.
+    const std::string root_path = test_dir_ + "/venv 'root' $(printf 
injected)";
+    const std::string interpreter_path = test_dir_ + "/python '3.11' $(printf 
injected)";
     fs::create_directories(root_path);
 
     // Model the two interpreter operations used by the production VENV path: 
version discovery
@@ -577,6 +578,15 @@ TEST_F(PythonEnvTest, 
PythonVersionManagerInitCondaSuccess) {
 // list_installed_packages tests
 // ============================================================================
 
+TEST_F(PythonEnvTest, ListInstalledPackagesKeepsPathLiteral) {
+    const auto version = create_fake_python_version_for_pip_list(
+            test_dir_ + "/python 'path' $(printf injected)", "printf '[]'\n", 
"3.11.9");
+    std::vector<std::pair<std::string, std::string>> packages;
+    const auto status = list_installed_packages(version, &packages);
+    ASSERT_TRUE(status.ok()) << status;
+    EXPECT_TRUE(packages.empty());
+}
+
 TEST_F(PythonEnvTest, ListInstalledPackagesInvalidPythonVersion) {
     PythonVersion invalid_version;
     std::vector<std::pair<std::string, std::string>> packages;
diff --git a/be/test/util/profiler_command_test.cpp 
b/be/test/util/profiler_command_test.cpp
new file mode 100644
index 00000000000..e1cc31a26ed
--- /dev/null
+++ b/be/test/util/profiler_command_test.cpp
@@ -0,0 +1,170 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include <gtest/gtest.h>
+
+#include <cstdlib>
+#include <filesystem>
+#include <fstream>
+#include <sstream>
+#include <string>
+
+#include "agent/utils.h"
+#include "common/config.h"
+#include "runtime/memory/heap_profiler.h"
+#include "util/pprof_utils.h"
+
+namespace doris {
+
+// Scripts check the shell boundary. The jeprof integration test uses the 
bundled tool.
+class ProfilerCommandTest : public testing::Test {
+protected:
+    void SetUp() override {
+        _old_home = getenv("DORIS_HOME");
+        _old_path = getenv("PATH");
+        _old_profile_dir = config::pprof_profile_dir;
+        std::string pattern =
+                (std::filesystem::current_path() / 
"profiler_command.XXXXXX").string();
+        ASSERT_NE(mkdtemp(pattern.data()), nullptr);
+        _root = pattern;
+        _home = _root / "tools 'one''two' $(printf injected)";
+        _profile_dir = _root / "profiles 'one''two' $(printf injected);#";
+        std::filesystem::create_directories(_profile_dir);
+        config::pprof_profile_dir = _profile_dir.string();
+        ASSERT_EQ(setenv("DORIS_HOME", _home.c_str(), 1), 0);
+        ASSERT_EQ(setenv("PATH", (_home.string() + "/bin:" + 
_old_path).c_str(), 1), 0);
+        write_tool(_home / "bin/jeprof", "printf '%s\\n' \"$@\"\n");
+        write_tool(_home / "tools/bin/pprof",
+                   "if [ \"$1\" = --version ]; then exit 0; fi\nprintf '%s\\n' 
\"$@\"\n");
+        write_tool(_home / "bin/perf", R"(case "$1" in
+--version) exit 0 ;;
+record)
+    while [ "$#" -gt 0 ]; do
+        if [ "$1" = -o ]; then
+            shift
+            printf 'sample\n' > "$1"
+            exit
+        fi
+        shift
+    done
+    exit 1 ;;
+script) cat -- "$3" ;;
+esac
+)");
+        write_tool(_home / "FlameGraph/stackcollapse-perf.pl", "cat\n");
+        write_tool(_home / "FlameGraph/flamegraph.pl", "cat\n");
+    }
+
+    void TearDown() override {
+        EXPECT_EQ(setenv("DORIS_HOME", _old_home.c_str(), 1), 0);
+        EXPECT_EQ(setenv("PATH", _old_path.c_str(), 1), 0);
+        config::pprof_profile_dir = _old_profile_dir;
+        std::filesystem::remove_all(_root);
+    }
+
+    static void write_tool(const std::filesystem::path& path, const 
std::string& body) {
+        std::filesystem::create_directories(path.parent_path());
+        std::ofstream file(path);
+        file << "#!/bin/sh\n" << body;
+        file.close();
+        ASSERT_TRUE(file.good());
+        std::filesystem::permissions(path, std::filesystem::perms::owner_all);
+    }
+
+    std::filesystem::path _root;
+    std::filesystem::path _home;
+    std::filesystem::path _profile_dir;
+    std::string _old_home;
+    std::string _old_path;
+    std::string _old_profile_dir;
+};
+
+TEST_F(ProfilerCommandTest, JeprofKeepsPathsLiteral) {
+    const std::string profile = (_profile_dir / "jeheap_dump.heap").string();
+    EXPECT_EQ("--dot\n--\n" + (_home / "lib/doris_be").string() + "\n" + 
profile + "\n",
+              HeapProfiler::jeprof_profile_to_dot(profile));
+}
+
+TEST_F(ProfilerCommandTest, BundledJeprofKeepsLocalProfile) {
+    const auto jeprof = std::filesystem::current_path() / "tools/jeprof";
+    // Keep failure output in this fixture. The curl stub prevents remote 
requests.
+    write_tool(_home / "bin/jeprof",
+               "export JEPROF_TMPDIR=" + 
AgentUtils::shell_quote(_root.string()) + "\nexec " +
+                       AgentUtils::shell_quote(jeprof.string()) + " \"$@\"\n");
+    write_tool(_home / "bin/curl", "echo 'Unexpected remote profile' >&2\nexit 
1\n");
+    std::filesystem::create_directories(_home / "lib");
+    std::filesystem::create_symlink("/bin/true", _home / "lib/doris_be");
+    const auto profile_dir = _profile_dir / "safe@1";
+    std::filesystem::create_directories(profile_dir);
+    const std::string profile = (profile_dir / "jeheap_dump.heap").string();
+    // Two lines describe a valid profile with no samples. No process sampling 
is needed.
+    std::ofstream(profile) << "heap_v2/1\n  t*: 0: 0 [0: 0]\n";
+    EXPECT_EQ("Using local file " + (_home / "lib/doris_be").string() + 
".\nUsing local file " +
+                      profile + ".\nNo nodes to print\n",
+              HeapProfiler::jeprof_profile_to_dot(profile));
+}
+
+TEST_F(ProfilerCommandTest, BundledJeprofUnitTests) {
+    const auto jeprof = std::filesystem::current_path() / "tools/jeprof";
+    std::string output;
+    AgentUtils util;
+    ASSERT_TRUE(util.exec_cmd(AgentUtils::shell_quote(jeprof.string()) + " 
--test", &output))
+            << output;
+    EXPECT_NE(output.find("ShellEscape round trip passed\n"), 
std::string::npos);
+}
+
+TEST_F(ProfilerCommandTest, PprofKeepsFilePathLiteral) {
+    const std::string profile = (_profile_dir / "cpu_profile").string();
+    std::ofstream(profile) << "sample";
+    std::string binary;
+    ASSERT_TRUE(PprofUtils::get_self_cmdline(&binary).ok());
+    std::stringstream output;
+    const auto status = PprofUtils::get_readable_profile(profile, true, 
&output);
+    ASSERT_TRUE(status.ok()) << status;
+    EXPECT_EQ("Profile(Sample 30 seconds)\n--text\n--\n" + binary + "\n" + 
profile + "\n\n",
+              output.str());
+    EXPECT_FALSE(std::filesystem::exists(profile));
+}
+
+TEST_F(ProfilerCommandTest, PprofKeepsGeneratedPathLiteral) {
+    std::stringstream output;
+    const auto status = PprofUtils::get_readable_profile("sample", false, 
&output);
+    ASSERT_TRUE(status.ok()) << status;
+    EXPECT_NE(output.str().find(_profile_dir.string() + "/pprof_profile."), 
std::string::npos);
+    EXPECT_TRUE(std::filesystem::is_empty(_profile_dir));
+}
+
+TEST_F(ProfilerCommandTest, FlamegraphKeepsPathsLiteral) {
+    // Both response modes use the same profile path. File mode also tests 
shell redirection.
+    for (bool return_file : {false, true}) {
+        std::string result;
+        const auto status = PprofUtils::generate_flamegraph(0, (_home / 
"FlameGraph").string(),
+                                                            return_file, 
&result);
+        ASSERT_TRUE(status.ok()) << status;
+        if (return_file) {
+            EXPECT_EQ(std::filesystem::path(result).parent_path(), 
_profile_dir);
+            std::ifstream file(result);
+            std::stringstream content;
+            content << file.rdbuf();
+            EXPECT_EQ("sample\n", content.str());
+        } else {
+            EXPECT_EQ("sample\n", result);
+        }
+    }
+}
+
+} // namespace doris
diff --git a/tools/jeprof b/tools/jeprof
index 620bff916bb..756951c9eae 100755
--- a/tools/jeprof
+++ b/tools/jeprof
@@ -550,7 +550,10 @@ sub Init() {
 
   # Parse profile file/location arguments
   foreach my $farg (@ARGV) {
-    if ($farg =~ m/(.*)\@([0-9]+)(|\/.*)$/ ) {
+    # A local directory can contain @1. Do not treat its files as remote 
groups.
+    if (-f $farg) {
+      unshift(@main::pfile_args, $farg);
+    } elsif ($farg =~ m/(.*)\@([0-9]+)(|\/.*)$/ ) {
       my $machine = $1;
       my $num_machines = $2;
       my $path = $3;
@@ -5305,7 +5308,8 @@ sub ShellEscape {
   foreach my $word (@_) {
     my $escaped_word = $word;
     if ($word =~ m![^a-zA-Z0-9/.,_=-]!) {  # check for anything not in 
whitelist
-      $escaped_word =~ s/'/'\\''/;
+      # Escape each quote. For example, a'b'c contains two quotes.
+      $escaped_word =~ s/'/'\\''/g;
       $escaped_word = "'$escaped_word'";
     }
     push(@escaped_words, $escaped_word);
@@ -5672,8 +5676,20 @@ sub AddressIncUnitTest {
 }
 
 
+# Check that the shell returns a path with two quotes unchanged.
+sub ShellEscapeUnitTest {
+  my $word = q{a'b'c};
+  my $command = "printf '%s' " . ShellEscape($word);
+  my $result = `$command`;
+  if ($? != 0 || $result ne $word) {
+    print STDERR "ShellEscape round trip failed\n";
+    return 1;
+  }
+  print STDERR "ShellEscape round trip passed\n";
+  return 0;
+}
+
 # Driver for unit tests.
-# Currently just the address add/subtract/increment routines for 64-bit.
 sub RunUnitTests {
   my $error_count = 0;
 
@@ -5714,6 +5730,7 @@ sub RunUnitTests {
   $error_count += AddressAddUnitTest($unit_test_data_8, $unit_test_data_16);
   $error_count += AddressSubUnitTest($unit_test_data_8, $unit_test_data_16);
   $error_count += AddressIncUnitTest($unit_test_data_8, $unit_test_data_16);
+  $error_count += ShellEscapeUnitTest();
   if ($error_count > 0) {
     print STDERR $error_count, " errors: FAILED\n";
   } else {


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to