• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mendersoftware / mender / 2222576901

18 Dec 2025 03:12PM UTC coverage: 79.672% (-0.01%) from 79.682%
2222576901

push

gitlab-ci

vpodzime
fix: Improve handling of arguments in artifact-gen scripts

Fix how single-file-artifact-gen and directory-artifact-gen parse
arguments and pass them to mender-artifact. They need to be more
careful to prevent injected extra arguments going through the
script to mender-artifact.

Ticket: MEN-9115
Changelog: single-file-artifact-gen and directory-artifact-gen now handle arguments in a safer way
Signed-off-by: Vratislav Podzimek <vratislav.podzimek+auto-signed@northern.tech>

7870 of 9878 relevant lines covered (79.67%)

13898.74 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.13
/src/artifact/v3/scripts/executor.cpp
1
// Copyright 2023 Northern.tech AS
2
//
3
//    Licensed under the Apache License, Version 2.0 (the "License");
4
//    you may not use this file except in compliance with the License.
5
//    You may obtain a copy of the License at
6
//
7
//        http://www.apache.org/licenses/LICENSE-2.0
8
//
9
//    Unless required by applicable law or agreed to in writing, software
10
//    distributed under the License is distributed on an "AS IS" BASIS,
11
//    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
//    See the License for the specific language governing permissions and
13
//    limitations under the License.
14

15
#include <artifact/v3/scripts/executor.hpp>
16

17
#include <algorithm>
18
#include <chrono>
19
#include <regex>
20
#include <string>
21

22
#include <common/common.hpp>
23
#include <common/expected.hpp>
24
#include <common/path.hpp>
25

26

27
namespace mender {
28
namespace artifact {
29
namespace scripts {
30
namespace executor {
31

32
namespace expected = mender::common::expected;
33

34

35
using expected::ExpectedBool;
36

37
namespace processes = mender::common::processes;
38
namespace error = mender::common::error;
39
namespace path = mender::common::path;
40

41

42
const int state_script_retry_exit_code {21};
43

44
unordered_map<const State, string> state_map {
45
        {State::Idle, "Idle"},
46
        {State::Sync, "Sync"},
47
        {State::Download, "Download"},
48
        {State::ArtifactInstall, "ArtifactInstall"},
49
        {State::ArtifactReboot, "ArtifactReboot"},
50
        {State::ArtifactCommit, "ArtifactCommit"},
51
        {State::ArtifactRollback, "ArtifactRollback"},
52
        {State::ArtifactRollbackReboot, "ArtifactRollbackReboot"},
53
        {State::ArtifactFailure, "ArtifactFailure"},
54
};
55

56
unordered_map<const Action, string> action_map {
57
        {Action::Enter, "Enter"},
58
        {Action::Leave, "Leave"},
59
        {Action::Error, "Error"},
60
};
61

62
error::Error CorrectVersionFile(const string &path) {
1,438✔
63
        // Missing file is OK
64
        // This is because previous versions of the client wrote no
65
        // version file, so no-file=v3
66
        if (!path::FileExists(path)) {
1,438✔
67
                return error::NoError;
338✔
68
        }
69

70
        ifstream vf {path};
2,200✔
71

72
        if (!vf) {
1,100✔
73
                auto errnum {errno};
×
74
                return error::Error(
75
                        generic_category().default_error_condition(errnum), "Failed to open the version file");
×
76
        }
77

78
        string version;
79
        vf >> version;
1,100✔
80
        if (!vf) {
1,100✔
81
                auto errnum {errno};
×
82
                return error::Error(
83
                        generic_category().default_error_condition(errnum),
×
84
                        "Error reading the version number from the version file");
×
85
        }
86

87
        if (not common::VectorContainsString(supported_state_script_versions, version)) {
1,100✔
88
                return executor::MakeError(
89
                        executor::VersionFileError, "Unexpected Artifact script version found: " + version);
2✔
90
        }
91
        return error::NoError;
1,099✔
92
}
93

94
bool IsValidStateScript(const string &file, State state, Action action) {
11,912✔
95
        string expression {
96
                "(" + state_map.at(state) + ")" + "_(" + action_map.at(action) + ")_[0-9][0-9](_\\S+)?"};
23,824✔
97
        const regex artifact_script_regexp {expression, std::regex_constants::ECMAScript};
11,912✔
98
        return regex_match(path::BaseName(file), artifact_script_regexp);
35,736✔
99
}
100

101
function<bool(const string &)> Matcher(State state, Action action) {
×
102
        return [state, action](const string &file) {
11,912✔
103
                const bool is_valid {IsValidStateScript(file, state, action)};
11,912✔
104
                if (!is_valid) {
11,912✔
105
                        return false;
106
                }
107
                auto exp_executable = path::IsExecutable(file, true);
738✔
108
                if (!exp_executable) {
738✔
109
                        log::Debug("Issue figuring the executable bits of: " + exp_executable.error().String());
×
110
                        return false;
×
111
                }
112
                return is_valid and exp_executable.value();
738✔
113
        };
×
114
}
115

116
bool IsArtifactScript(State state) {
×
117
        switch (state) {
×
118
        case State::Idle:
119
        case State::Sync:
120
        case State::Download:
121
                return false;
122
        case State::ArtifactInstall:
×
123
        case State::ArtifactReboot:
124
        case State::ArtifactCommit:
125
        case State::ArtifactRollback:
126
        case State::ArtifactRollbackReboot:
127
        case State::ArtifactFailure:
128
                return true;
×
129
        }
130
        assert(false);
131
        return false;
132
}
133

134
string ScriptRunner::ScriptPath(State state) {
1,418✔
135
        if (IsArtifactScript(state)) {
136
                return this->artifact_script_path_;
712✔
137
        }
138
        return this->rootfs_script_path_;
706✔
139
}
140

141
string Name(const State state, const Action action) {
1,052✔
142
        return state_map.at(state) + action_map.at(action);
1,052✔
143
}
144

145
Error CheckScriptsCompatibility(const string &scripts_path) {
19✔
146
        return CorrectVersionFile(path::Join(scripts_path, "version"));
38✔
147
}
148

149
ScriptRunner::ScriptRunner(
3,275✔
150
        events::EventLoop &loop,
151
        chrono::milliseconds script_timeout,
152
        chrono::milliseconds retry_interval,
153
        chrono::milliseconds retry_timeout,
154
        const string &artifact_script_path,
155
        const string &rootfs_script_path,
156
        processes::OutputCallback stdout_callback,
157
        processes::OutputCallback stderr_callback) :
3,275✔
158
        loop_ {loop},
159
        script_timeout_ {script_timeout},
160
        retry_interval_ {retry_interval},
161
        retry_timeout_ {retry_timeout},
162
        artifact_script_path_ {artifact_script_path},
163
        rootfs_script_path_ {rootfs_script_path},
164
        stdout_callback_ {stdout_callback},
165
        stderr_callback_ {stderr_callback},
166
        error_script_error_ {error::NoError},
167
        retry_interval_timer_ {new events::Timer(loop_)},
3,275✔
168
        retry_timeout_timer_ {new events::Timer(loop_)} {};
6,550✔
169

170
void ScriptRunner::LogErrAndExecuteNext(
16✔
171
        Error err,
172
        vector<string>::iterator current_script,
173
        vector<string>::iterator end,
174
        bool ignore_error,
175
        HandlerFunction handler) {
176
        // Collect the error and carry on
177
        if (err.code == processes::MakeError(processes::NonZeroExitStatusError, "").code) {
16✔
178
                this->error_script_error_ = this->error_script_error_.FollowedBy(executor::MakeError(
16✔
179
                        executor::NonZeroExitStatusError,
180
                        "Got non zero exit code from script: " + *current_script));
32✔
181
        } else {
182
                this->error_script_error_ = this->error_script_error_.FollowedBy(err);
×
183
        }
184

185
        HandleScriptNext(current_script, end, ignore_error, handler);
16✔
186
}
16✔
187

188
void ScriptRunner::HandleScriptNext(
699✔
189
        vector<string>::iterator current_script,
190
        vector<string>::iterator end,
191
        bool ignore_error,
192
        HandlerFunction handler) {
193
        // Stop retry timer and start the next script execution
194
        if (this->retry_timeout_timer_->GetActive()) {
699✔
195
                this->retry_timeout_timer_->Cancel();
2✔
196
        }
197

198
        auto local_err = Execute(std::next(current_script), end, ignore_error, handler);
699✔
199
        if (local_err != error::NoError) {
699✔
200
                return handler(local_err);
×
201
        }
202
}
203

204
void ScriptRunner::HandleScriptError(Error err, HandlerFunction handler) {
1,223✔
205
        // Stop retry timer
206
        if (this->retry_timeout_timer_->GetActive()) {
1,223✔
207
                this->retry_timeout_timer_->Cancel();
1✔
208
        }
209
        if (err.code == processes::MakeError(processes::NonZeroExitStatusError, "").code) {
1,223✔
210
                return handler(executor::MakeError(
62✔
211
                        executor::NonZeroExitStatusError,
212
                        "Received error code: " + to_string(this->script_.get()->GetExitStatus())));
62✔
213
        }
214
        return handler(err);
2,384✔
215
}
216

217
void ScriptRunner::HandleScriptRetry(
30✔
218
        vector<string>::iterator current_script,
219
        vector<string>::iterator end,
220
        bool ignore_error,
221
        HandlerFunction handler) {
222
        log::Info(
30✔
223
                "Script returned Retry Later exit code, re-retrying in "
224
                + to_string(chrono::duration_cast<chrono::seconds>(this->retry_interval_).count()) + "s");
60✔
225

226
        this->retry_interval_timer_->AsyncWait(
30✔
227
                this->retry_interval_,
228
                [this, current_script, end, ignore_error, handler](error::Error err) {
60✔
229
                        if (err != error::NoError) {
30✔
230
                                return handler(this->error_script_error_.FollowedBy(err));
12✔
231
                        }
232

233
                        auto local_err = Execute(current_script, end, ignore_error, handler);
24✔
234
                        if (local_err != error::NoError) {
24✔
235
                                handler(local_err);
×
236
                        }
237
                });
60✔
238
}
30✔
239

240
void ScriptRunner::MaybeSetupRetryTimeoutTimer() {
30✔
241
        if (!this->retry_timeout_timer_->GetActive()) {
30✔
242
                log::Debug("Setting retry timer for " + to_string(this->retry_timeout_.count()) + "ms");
18✔
243
                // First run on this script
244
                this->retry_timeout_timer_->AsyncWait(this->retry_timeout_, [this](error::Error err) {
15✔
245
                        if (err.code == make_error_condition(errc::operation_canceled)) {
7✔
246
                                // The timer did not fire up. Do nothing
247
                        } else {
248
                                log::Error("Script Retry Later timeout out, cancelling and returning");
12✔
249
                                this->retry_interval_timer_->Cancel();
6✔
250
                                this->script_->Cancel();
6✔
251
                        }
252
                });
16✔
253
        }
254
}
30✔
255

256
Error ScriptRunner::Execute(
1,952✔
257
        vector<string>::iterator current_script,
258
        vector<string>::iterator end,
259
        bool ignore_error,
260
        HandlerFunction handler) {
261
        // No more scripts to execute
262
        if (current_script == end) {
1,952✔
263
                HandleScriptError(this->error_script_error_, handler); // Success
2,382✔
264
                return error::NoError;
1,191✔
265
        }
266

267
        log::Info("Running State Script: " + *current_script);
761✔
268

269
        this->script_.reset(new processes::Process({*current_script}));
2,283✔
270
        auto err {this->script_->Start(stdout_callback_, stderr_callback_)};
1,522✔
271
        if (err != error::NoError) {
761✔
272
                return err;
×
273
        }
274

275
        return this->script_.get()->AsyncWait(
276
                this->loop_,
277
                [this, current_script, end, ignore_error, handler](Error err) {
4,642✔
278
                        if (err != error::NoError) {
761✔
279
                                const bool is_script_retry_error =
280
                                        err.code == processes::MakeError(processes::NonZeroExitStatusError, "").code
156✔
281
                                        && this->script_->GetExitStatus() == state_script_retry_exit_code;
78✔
282
                                if (is_script_retry_error) {
78✔
283
                                        MaybeSetupRetryTimeoutTimer();
30✔
284
                                        return HandleScriptRetry(current_script, end, ignore_error, handler);
60✔
285
                                } else if (ignore_error) {
48✔
286
                                        return LogErrAndExecuteNext(err, current_script, end, ignore_error, handler);
32✔
287
                                }
288
                                return HandleScriptError(err, handler);
64✔
289
                        }
290
                        return HandleScriptNext(current_script, end, ignore_error, handler);
1,366✔
291
                },
292
                this->script_timeout_);
1,522✔
293
}
294

295
Error ScriptRunner::AsyncRunScripts(
1,419✔
296
        State state, Action action, HandlerFunction handler, OnError on_error) {
297
        // Verify the version in the version file (OK if no version file present)
298
        auto version_file_error {CorrectVersionFile(path::Join(
1,419✔
299
                IsArtifactScript(state) ? this->artifact_script_path_ : this->rootfs_script_path_,
300
                "version"))};
1,419✔
301
        if (version_file_error != error::NoError) {
1,419✔
302
                return version_file_error;
1✔
303
        }
304

305
        // Collect
306
        const auto script_path {ScriptPath(state)};
1,418✔
307
        auto exp_scripts {path::ListFiles(script_path, Matcher(state, action))};
2,836✔
308
        if (!exp_scripts) {
1,418✔
309
                // Missing directory is OK
310
                if (exp_scripts.error().IsErrno(ENOENT)) {
189✔
311
                        log::Debug("Found no state script directory (" + script_path + "). Continuing on");
378✔
312
                        handler(error::NoError);
189✔
313
                        return error::NoError;
189✔
314
                }
315
                return executor::MakeError(
316
                        executor::Code::CollectionError,
317
                        "Failed to get the scripts, error: " + exp_scripts.error().String());
×
318
        }
319

320
        // Sort
321
        {
322
                auto &unsorted_scripts {exp_scripts.value()};
1,229✔
323

324
                vector<string> sorted_scripts(unsorted_scripts.begin(), unsorted_scripts.end());
2,458✔
325

326
                sort(sorted_scripts.begin(), sorted_scripts.end());
1,229✔
327
                this->collected_scripts_ = std::move(sorted_scripts);
1,229✔
328
        }
329

330
        bool ignore_error = on_error == OnError::Ignore || action == Action::Error;
1,229✔
331

332
        // Execute
333
        auto scripts_iterator {this->collected_scripts_.begin()};
334
        auto scripts_iterator_end {this->collected_scripts_.end()};
335
        return Execute(scripts_iterator, scripts_iterator_end, ignore_error, handler);
2,458✔
336
}
337

338

339
Error ScriptRunner::RunScripts(State state, Action action, OnError on_error) {
358✔
340
        auto run_err {error::NoError};
358✔
341
        auto err = AsyncRunScripts(
342
                state,
343
                action,
344
                [this, &run_err](Error error) {
716✔
345
                        run_err = error;
358✔
346
                        this->loop_.Stop();
358✔
347
                },
358✔
348
                on_error);
358✔
349
        if (err != error::NoError) {
358✔
350
                return err;
×
351
        }
352
        this->loop_.Run();
358✔
353
        return run_err;
358✔
354
}
355

356
} // namespace executor
357
} // namespace scripts
358
} // namespace artifact
359
} // namespace mender
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc