Skip to content

Commit 575fe16

Browse files
committed
feat engine: account for PATH in ProcessStarter::Exec
cd29b43fbef034256d01e7f0a1e0d48f6f188a72
1 parent 294af9d commit 575fe16

File tree

5 files changed

+282
-49
lines changed

5 files changed

+282
-49
lines changed

core/include/userver/engine/subprocess/environment_variables.hpp

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,10 @@ class EnvironmentVariables {
4848
/// @brief Constructs an instance from pairs: key, value taken from the map.
4949
explicit EnvironmentVariables(Map vars);
5050

51-
/// Constructs copy of the instance.
5251
EnvironmentVariables(const EnvironmentVariables&) = default;
52+
EnvironmentVariables& operator=(const EnvironmentVariables&) = default;
53+
EnvironmentVariables(EnvironmentVariables&&) noexcept = default;
54+
EnvironmentVariables& operator=(EnvironmentVariables&&) noexcept = default;
5355

5456
/// @brief Updates variable.
5557
/// @note If variable does not exist then it is added.
@@ -69,13 +71,17 @@ class EnvironmentVariables {
6971
/// Returns the reference to the value.
7072
std::string& operator[](const std::string& variable_name);
7173

74+
/// Compares equal if thee map of environment variables are equal.
75+
bool operator==(const EnvironmentVariables& rhs) const;
76+
7277
/// Checks whether the container is empty.
7378
auto empty() const { return vars_.empty(); }
7479

7580
/// Returns the number of elements.
7681
auto size() const { return vars_.size(); }
7782

7883
using const_iterator = Map::const_iterator;
84+
using iterator = const_iterator;
7985

8086
/// Returns a const iterator to the beginning.
8187
auto begin() const { return vars_.begin(); }
@@ -117,6 +123,17 @@ void SetEnvironmentVariable(const std::string& variable_name,
117123
/// @warning Not thread-safe.
118124
void UnsetEnvironmentVariable(const std::string& variable_name);
119125

126+
/// @brief RAII idiom guard of environment variables.
127+
/// @warning The constructor and destructor are not thread-safe.
128+
class EnvironmentVariablesScope {
129+
public:
130+
EnvironmentVariablesScope();
131+
~EnvironmentVariablesScope();
132+
133+
private:
134+
rcu::ReadablePtr<EnvironmentVariables> old_env_;
135+
};
136+
120137
} // namespace engine::subprocess
121138

122139
USERVER_NAMESPACE_END

core/include/userver/engine/subprocess/process_starter.hpp

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,29 @@ class ThreadControl;
2020

2121
namespace engine::subprocess {
2222

23+
/// @brief Structure of settings for executing commands in the OS.
24+
struct ExecOptions final {
25+
/// EnvironmentVariables for the environment in execution, or `std::nullopt`
26+
/// to use GetCurrentEnvironmentVariables()
27+
std::optional<EnvironmentVariables> env{};
28+
/// EnvironmentVariablesUpdate for the update environment before execution, or
29+
/// `std::nullopt` to leave `env` as is
30+
std::optional<EnvironmentVariablesUpdate> env_update{};
31+
/// File path to be redirected stdout, or `std::nullopt` to use the service's
32+
/// stdout
33+
std::optional<std::string> stdout_file{};
34+
/// File path to be redirected stderr, or `std::nullopt` to use the service's
35+
/// stderr
36+
std::optional<std::string> stderr_file{};
37+
/// If `false`, `command` is treated as absolute path or a relative path.
38+
/// If `true`, and `command` does not contain `/`, and PATH in environment
39+
/// variables, then it will be searched in the colon-separated list of
40+
/// directory pathnames specified in the PATH environment variable.
41+
/// If `true`, and `command` contains `/`, `command` is treated as absolute
42+
/// path or a relative path.
43+
bool use_path{false};
44+
};
45+
2346
/// @ingroup userver_clients
2447
///
2548
/// @brief Creates a new OS subprocess and executes a command in it.
@@ -29,23 +52,39 @@ class ProcessStarter {
2952
/// `main-task-processor is OK for this purpose.
3053
explicit ProcessStarter(TaskProcessor& task_processor);
3154

32-
/// @param command the absolute path to the executable
55+
/// @param command the absolute path or relative path. If `use_path` is
56+
/// `true`, and `command` does not contains `/`, then it will be searched in
57+
/// the colon-separated list of directory pathnames specified in the PATH
58+
/// environment variable. More details @ref ExecOptions::use_path
59+
/// @param args exact args passed to the executable
60+
/// @param options @ref ExecOptions settings
61+
/// @throws std::runtime_error if `use_path` is `true`, `command` contains `/`
62+
/// and PATH not in environment variables
63+
ChildProcess Exec(const std::string& command,
64+
const std::vector<std::string>& args,
65+
ExecOptions&& options = {});
66+
67+
/// @overload
68+
/// @param command the absolute path or relative path
3369
/// @param args exact args passed to the executable
3470
/// @param env redefines all environment variables
71+
/// @deprecated Use the `Exec` overload taking @ref ExecOptions
3572
ChildProcess Exec(
3673
const std::string& command, const std::vector<std::string>& args,
3774
const EnvironmentVariables& env,
3875
// TODO: use something like pipes instead of path to files
3976
const std::optional<std::string>& stdout_file = std::nullopt,
4077
const std::optional<std::string>& stderr_file = std::nullopt);
4178

42-
/// @param command the absolute path to the executable
79+
/// @overload
80+
/// @param command the absolute path or relative path
4381
/// @param args exact args passed to the executable
4482
/// @param env_update variables to add to the current environment, overwriting
4583
/// existing ones
84+
/// @deprecated Use the `Exec` overload taking @ref ExecOptions
4685
ChildProcess Exec(
4786
const std::string& command, const std::vector<std::string>& args,
48-
EnvironmentVariablesUpdate env_update = EnvironmentVariablesUpdate{{}},
87+
EnvironmentVariablesUpdate env_update,
4988
// TODO: use something like pipes instead of path to files
5089
const std::optional<std::string>& stdout_file = std::nullopt,
5190
const std::optional<std::string>& stderr_file = std::nullopt);

core/src/engine/subprocess/environment_variables.cpp

Lines changed: 52 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,30 @@ USERVER_NAMESPACE_BEGIN
1414
namespace engine::subprocess {
1515
namespace {
1616

17+
void UnsetEnvironmentVariableWithoutSync(const std::string& variable_name) {
18+
// NOLINTNEXTLINE(concurrency-mt-unsafe)
19+
::unsetenv(variable_name.c_str());
20+
}
21+
22+
void SetEnvironmentVariableWithoutSync(const std::string& variable_name,
23+
const std::string& value,
24+
Overwrite overwrite) {
25+
const auto read_ptr = GetCurrentEnvironmentVariablesPtr();
26+
const bool value_exist =
27+
(read_ptr->GetValueOptional(variable_name) != nullptr);
28+
if (value_exist) {
29+
UINVARIANT(overwrite != Overwrite::kForbidden,
30+
"variable with name= " + variable_name +
31+
" was set earlier and prohibits rewriting");
32+
if (overwrite == Overwrite::kIgnored) {
33+
return;
34+
}
35+
}
36+
37+
// NOLINTNEXTLINE(concurrency-mt-unsafe)
38+
::setenv(variable_name.c_str(), value.c_str(), 1);
39+
}
40+
1741
EnvironmentVariables::Map GetCurrentEnvironmentVariablesMap() {
1842
EnvironmentVariables::Map map;
1943

@@ -69,6 +93,10 @@ std::string& EnvironmentVariables::operator[](
6993
return vars_[variable_name];
7094
}
7195

96+
bool EnvironmentVariables::operator==(const EnvironmentVariables& rhs) const {
97+
return vars_ == rhs.vars_;
98+
}
99+
72100
EnvironmentVariables GetCurrentEnvironmentVariables() {
73101
return GetCurrentEnvironmentVariablesStorage().ReadCopy();
74102
}
@@ -82,30 +110,38 @@ void UpdateCurrentEnvironmentVariables() {
82110
EnvironmentVariables{GetCurrentEnvironmentVariablesMap()});
83111
}
84112

85-
void SetEnvironmentVariable(const std::string& variable_name,
86-
const std::string& value, Overwrite overwrite) {
87-
const auto read_ptr = GetCurrentEnvironmentVariablesPtr();
88-
const bool value_exist =
89-
(read_ptr->GetValueOptional(variable_name) != nullptr);
90-
if (value_exist) {
91-
UINVARIANT(overwrite != Overwrite::kForbidden,
92-
"variable with name= " + variable_name +
93-
" was set earlier and prohibits rewriting");
94-
if (overwrite == Overwrite::kIgnored) {
95-
return;
113+
EnvironmentVariablesScope::EnvironmentVariablesScope()
114+
: old_env_(GetCurrentEnvironmentVariablesPtr()) {}
115+
116+
EnvironmentVariablesScope::~EnvironmentVariablesScope() {
117+
const auto snapshot = GetCurrentEnvironmentVariablesStorage().Read();
118+
const auto& env = *snapshot;
119+
120+
for (const auto& [variable_name, value] : *old_env_) {
121+
if (!env.GetValueOptional(variable_name) ||
122+
env.GetValue(variable_name) != value) {
123+
SetEnvironmentVariableWithoutSync(variable_name, value,
124+
Overwrite::kAllowed);
96125
}
97126
}
98127

99-
// NOLINTNEXTLINE(concurrency-mt-unsafe)
100-
::setenv(variable_name.c_str(), value.c_str(), 1);
128+
for (const auto& [variable_name, value] : env) {
129+
if (!old_env_->GetValueOptional(variable_name)) {
130+
UnsetEnvironmentVariableWithoutSync(variable_name);
131+
}
132+
}
101133

102134
UpdateCurrentEnvironmentVariables();
103135
}
104136

105-
void UnsetEnvironmentVariable(const std::string& variable_name) {
106-
// NOLINTNEXTLINE(concurrency-mt-unsafe)
107-
::unsetenv(variable_name.c_str());
137+
void SetEnvironmentVariable(const std::string& variable_name,
138+
const std::string& value, Overwrite overwrite) {
139+
SetEnvironmentVariableWithoutSync(variable_name, value, overwrite);
140+
UpdateCurrentEnvironmentVariables();
141+
}
108142

143+
void UnsetEnvironmentVariable(const std::string& variable_name) {
144+
UnsetEnvironmentVariableWithoutSync(variable_name);
109145
UpdateCurrentEnvironmentVariables();
110146
}
111147

core/src/engine/subprocess/process_starter.cpp

Lines changed: 65 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,17 @@
2424
#include <userver/utils/algo.hpp>
2525
#include <utils/check_syscall.hpp>
2626

27+
extern char** environ;
28+
2729
USERVER_NAMESPACE_BEGIN
2830

2931
namespace engine::subprocess {
3032
namespace {
3133

32-
void DoExecve(const std::string& command, const std::vector<std::string>& args,
33-
const EnvironmentVariables& env,
34-
const std::optional<std::string>& stdout_file,
35-
const std::optional<std::string>& stderr_file) {
34+
void DoExec(const std::string& command, const std::vector<std::string>& args,
35+
const EnvironmentVariables& env,
36+
const std::optional<std::string>& stdout_file,
37+
const std::optional<std::string>& stderr_file, bool use_path) {
3638
if (stdout_file) {
3739
if (!std::freopen(stdout_file->c_str(), "a", stdout)) {
3840
utils::CheckSyscall(-1, "freopen stdout to {}", *stdout_file);
@@ -65,8 +67,33 @@ void DoExecve(const std::string& command, const std::vector<std::string>& args,
6567
}
6668
envp_ptrs.push_back(nullptr);
6769

68-
utils::CheckSyscall(
69-
execve(command.c_str(), argv_ptrs.data(), envp_ptrs.data()), "execve");
70+
environ = envp_ptrs.data(); // The variable is assigned to the environment to
71+
// use the execv, execvp functions
72+
73+
if (!use_path) {
74+
utils::CheckSyscall(execv(command.c_str(), argv_ptrs.data()), "execv");
75+
} else {
76+
utils::CheckSyscall(execvp(command.c_str(), argv_ptrs.data()), "execvp");
77+
}
78+
}
79+
80+
EnvironmentVariables ApplyEnviromentUpdate(
81+
std::optional<EnvironmentVariables>&& env,
82+
std::optional<EnvironmentVariablesUpdate>&& env_update) {
83+
if (env) {
84+
if (env_update) {
85+
return env->UpdateWith(std::move(env_update.value()));
86+
} else {
87+
return std::move(env.value());
88+
}
89+
} else {
90+
if (env_update) {
91+
return GetCurrentEnvironmentVariables().UpdateWith(
92+
std::move(env_update.value()));
93+
} else {
94+
return GetCurrentEnvironmentVariables();
95+
}
96+
}
7097
}
7198

7299
} // namespace
@@ -75,11 +102,19 @@ ProcessStarter::ProcessStarter(TaskProcessor& task_processor)
75102
: thread_control_(
76103
task_processor.EventThreadPool().GetEvDefaultLoopThread()) {}
77104

78-
ChildProcess ProcessStarter::Exec(
79-
const std::string& command, const std::vector<std::string>& args,
80-
const EnvironmentVariables& env,
81-
const std::optional<std::string>& stdout_file,
82-
const std::optional<std::string>& stderr_file) {
105+
ChildProcess ProcessStarter::Exec(const std::string& command,
106+
const std::vector<std::string>& args,
107+
ExecOptions&& options) {
108+
EnvironmentVariables env = ApplyEnviromentUpdate(
109+
std::move(options.env), std::move(options.env_update));
110+
111+
if (options.use_path && command.find('/') != std::string::npos &&
112+
!env.GetValueOptional("PATH")) {
113+
throw std::runtime_error(
114+
"execvp potential vulnerability. more details "
115+
"https://github.com/userver-framework/userver/issues/588");
116+
}
117+
83118
tracing::Span span("ProcessStarter::Exec");
84119
span.AddTag("command", command);
85120
Promise<ChildProcess> promise;
@@ -91,8 +126,9 @@ ChildProcess ProcessStarter::Exec(
91126
return key_value.first + '=' + key_value.second;
92127
});
93128
LOG_DEBUG() << fmt::format(
94-
"do fork() + execve(), command={}, args=[\'{}\'], env=[]",
95-
fmt::join(args, "' '"), fmt::join(keys, ", "));
129+
"do fork() + {}(), command={}, args=[\'{}\'], env=[]",
130+
options.use_path ? "execv" : "execvp", fmt::join(args, "' '"),
131+
fmt::join(keys, ", "));
96132

97133
const auto pid = utils::CheckSyscall(fork(), "fork");
98134
if (pid) {
@@ -116,15 +152,16 @@ ChildProcess ProcessStarter::Exec(
116152
// in child thread
117153
try {
118154
try {
119-
DoExecve(command, args, env, stdout_file, stderr_file);
155+
DoExec(command, args, env, options.stdout_file, options.stderr_file,
156+
options.use_path);
120157
} catch (const std::exception& ex) {
121158
std::cerr << "Cannot execute child: " << ex.what();
122159
}
123160
} catch (...) {
124161
// must not do anything in a child
125162
std::abort();
126163
}
127-
// on success execve does not return
164+
// on success execve or execvp does not return
128165
std::abort();
129166
}
130167
});
@@ -133,15 +170,24 @@ ChildProcess ProcessStarter::Exec(
133170
return future.get();
134171
}
135172

173+
ChildProcess ProcessStarter::Exec(
174+
const std::string& command, const std::vector<std::string>& args,
175+
const EnvironmentVariables& env,
176+
const std::optional<std::string>& stdout_file,
177+
const std::optional<std::string>& stderr_file) {
178+
ExecOptions options{std::move(env), std::nullopt, std::move(stdout_file),
179+
std::move(stderr_file), false};
180+
return Exec(command, args, std::move(options));
181+
}
182+
136183
ChildProcess ProcessStarter::Exec(
137184
const std::string& command, const std::vector<std::string>& args,
138185
EnvironmentVariablesUpdate env_update,
139186
const std::optional<std::string>& stdout_file,
140187
const std::optional<std::string>& stderr_file) {
141-
return Exec(command, args,
142-
EnvironmentVariables{GetCurrentEnvironmentVariables()}.UpdateWith(
143-
std::move(env_update)),
144-
stdout_file, stderr_file);
188+
ExecOptions options{std::nullopt, std::move(env_update),
189+
std::move(stdout_file), std::move(stderr_file), false};
190+
return Exec(command, args, std::move(options));
145191
}
146192

147193
} // namespace engine::subprocess

0 commit comments

Comments
 (0)