Skip to content
API Documentation
BastionGuard™
ArchiveSandbox.cpp File Reference

Go to the source code of this file.

Namespaces

namespace  anonymous_namespace{ArchiveSandbox.cpp}

Functions

bool anonymous_namespace{ArchiveSandbox.cpp}::drain_fd_nonblocking (int fd, std::string &out, bool &got_eof, bool &truncated)
bool anonymous_namespace{ArchiveSandbox.cpp}::set_nonblocking (int fd)
void anonymous_namespace{ArchiveSandbox.cpp}::set_worker_rlimits ()
bool anonymous_namespace{ArchiveSandbox.cpp}::json_has_true (const std::string &jsonjson, const std::string &key)
int anonymous_namespace{ArchiveSandbox.cpp}::json_read_int (const std::string &jsonjson, const std::string &key, int fallback=0)
std::string anonymous_namespace{ArchiveSandbox.cpp}::json_read_string (const std::string &jsonjson, const std::string &key)
SandboxExecResult run_archive_worker_bwrap (const std::string &worker_path, const std::string &input_path, int timeout_seconds)
ArchiveInspectionResult analyze_archive_in_sandbox (const std::string &worker_path, const std::string &input_path, int timeout_seconds)

Function Documentation

◆ analyze_archive_in_sandbox()

ArchiveInspectionResult analyze_archive_in_sandbox ( const std::string & worker_path,
const std::string & input_path,
int timeout_seconds )

Definition at line 390 of file ArchiveSandbox.cpp.

394{
396
397 const auto exec = run_archive_worker_bwrap(worker_path, input_path, timeout_seconds);
398
399 if (!exec.launched) {
400 r.success = false;
401 r.sandbox_failed = true;
402 r.reason_code = "ARCHIVE_SANDBOX_LAUNCH_FAILED";
403 r.detail = exec.stderr_text;
404 return r;
405 }
406
407 if (exec.output_truncated) {
408 r.success = false;
409 r.sandbox_failed = true;
410 r.reason_code = "ARCHIVE_SANDBOX_OUTPUT_LIMIT";
411 r.detail = "archive worker exceeded output limit";
412 return r;
413 }
414
415 if (exec.timed_out) {
416 r.success = false;
417 r.sandbox_timeout = true;
418 r.reason_code = "ARCHIVE_SANDBOX_TIMEOUT";
419 r.detail = "archive worker timeout";
420 r.raw_json = exec.stdout_text;
421 return r;
422 }
423
424 if (!exec.exited || exec.exit_code != 0) {
425 r.success = false;
426 r.sandbox_failed = true;
427 r.reason_code = "ARCHIVE_SANDBOX_ANALYSIS_FAILED";
428 r.detail = exec.stderr_text.empty() ? "archive worker failed" : exec.stderr_text;
429 r.raw_json = exec.stdout_text;
430 return r;
431 }
432
433 r.success = true;
434 r.raw_json = exec.stdout_text;
435 r.is_zip = json_has_true(exec.stdout_text, "is_zip");
436 r.stored_size_mismatch = json_has_true(exec.stdout_text, "stored_size_mismatch");
437 r.header_mismatch = json_has_true(exec.stdout_text, "header_mismatch");
438 r.invalid_offset = json_has_true(exec.stdout_text, "invalid_offset");
439 r.overlapping_entries = json_has_true(exec.stdout_text, "overlapping_entries");
440 r.nested_archive = json_has_true(exec.stdout_text, "nested_archive");
441 r.risk_score = json_read_int(exec.stdout_text, "risk_score", 0);
442 r.reason_code = json_read_string(exec.stdout_text, "reason_code");
443 r.detail = json_read_string(exec.stdout_text, "detail");
444
445 const std::string risk = json_read_string(exec.stdout_text, "risk");
446 if (risk == "EVASIVE") {
447 r.risk = ArchiveRisk::EVASIVE;
448 } else if (risk == "MALFORMED") {
450 } else if (risk == "SUSPICIOUS") {
452 } else {
453 r.risk = ArchiveRisk::CLEAN;
454 }
455
456 return r;
457}
SandboxExecResult run_archive_worker_bwrap(const std::string &worker_path, const std::string &input_path, int timeout_seconds)
int json_read_int(const std::string &json, const std::string &key, int fallback=0)
std::string json_read_string(const std::string &json, const std::string &key)
bool json_has_true(const std::string &json, const std::string &key)

References analyze_archive_in_sandbox(), CLEAN, EVASIVE, anonymous_namespace{ArchiveSandbox.cpp}::json_has_true(), anonymous_namespace{ArchiveSandbox.cpp}::json_read_int(), anonymous_namespace{ArchiveSandbox.cpp}::json_read_string(), MALFORMED, run_archive_worker_bwrap(), and SUSPICIOUS.

Referenced by analyze_archive_in_sandbox(), and ArchiveInspector::inspect().

◆ run_archive_worker_bwrap()

SandboxExecResult run_archive_worker_bwrap ( const std::string & worker_path,
const std::string & input_path,
int timeout_seconds )

Definition at line 171 of file ArchiveSandbox.cpp.

175{
176 SandboxExecResult result;
177
178 if (!fs::exists(worker_path) || !fs::is_regular_file(worker_path)) {
179 result.stderr_text = "invalid worker_path";
180 return result;
181 }
182
183 if (!fs::exists(input_path) || !fs::is_regular_file(input_path)) {
184 result.stderr_text = "invalid input_path";
185 return result;
186 }
187
188 char tmp_template[] = "/tmp/bastionguard-archive-XXXXXX";
189 char* tmp_dir = ::mkdtemp(tmp_template);
190 if (!tmp_dir) {
191 result.stderr_text = "mkdtemp failed";
192 return result;
193 }
194
195 const fs::path sandbox_root(tmp_dir);
196 const fs::path work_dir = sandbox_root / "work";
197 std::error_code ec;
198 fs::create_directories(work_dir, ec);
199 if (ec) {
200 result.stderr_text = "create_directories failed";
201 fs::remove_all(sandbox_root, ec);
202 return result;
203 }
204
205 int out_pipe[2] = {-1, -1};
206 int err_pipe[2] = {-1, -1};
207
208 if (::pipe(out_pipe) != 0 || ::pipe(err_pipe) != 0) {
209 result.stderr_text = "pipe failed";
210 for (int fd : {out_pipe[0], out_pipe[1], err_pipe[0], err_pipe[1]}) {
211 if (fd >= 0) ::close(fd);
212 }
213 fs::remove_all(sandbox_root, ec);
214 return result;
215 }
216
217 const pid_t pid = ::fork();
218 if (pid < 0) {
219 result.stderr_text = "fork failed";
220 ::close(out_pipe[0]);
221 ::close(out_pipe[1]);
222 ::close(err_pipe[0]);
223 ::close(err_pipe[1]);
224 fs::remove_all(sandbox_root, ec);
225 return result;
226 }
227
228 if (pid == 0) {
229 ::close(out_pipe[0]);
230 ::close(err_pipe[0]);
231
232 ::dup2(out_pipe[1], STDOUT_FILENO);
233 ::dup2(err_pipe[1], STDERR_FILENO);
234
235 ::close(out_pipe[1]);
236 ::close(err_pipe[1]);
237
239
240 std::vector<std::string> args = {
241 "bwrap",
242 "--die-with-parent",
243 "--new-session",
244 "--unshare-ipc",
245 "--unshare-pid",
246 "--unshare-uts",
247 "--unshare-cgroup-try",
248 "--proc", "/proc",
249 "--dev", "/dev",
250 "--ro-bind", "/usr", "/usr",
251 "--ro-bind", "/bin", "/bin",
252 "--ro-bind", "/lib", "/lib",
253 "--ro-bind", "/lib64", "/lib64",
254 "--tmpfs", "/tmp",
255 "--dir", "/work",
256 "--ro-bind", input_path, "/work/input.zip",
257 "--ro-bind", worker_path, "/worker",
258 "--chdir", "/work",
259 "--setenv", "HOME", "/tmp",
260 "--setenv", "TMPDIR", "/tmp",
261 "/worker",
262 "/work/input.zip"
263 };
264
265 std::vector<char*> argv;
266 argv.reserve(args.size() + 1);
267 for (auto& s : args) {
268 argv.push_back(s.data());
269 }
270 argv.push_back(nullptr);
271
272 ::execvp("bwrap", argv.data());
273 _exit(127);
274 }
275
276 result.launched = true;
277
278 ::close(out_pipe[1]);
279 ::close(err_pipe[1]);
280
281 // Mettiamo entrambe le pipe in non-blocking così possiamo drenarle
282 // mentre il child sta ancora girando. Se non lo facessimo, un child
283 // che scrive più di PIPE_BUF si bloccherebbe su write() e non uscirebbe
284 // mai -> deadlock fino al timeout.
285 set_nonblocking(out_pipe[0]);
286 set_nonblocking(err_pipe[0]);
287
288 const auto start = std::chrono::steady_clock::now();
289 bool child_done = false;
290 int status = 0;
291
292 bool out_eof = false;
293 bool err_eof = false;
294
295 const int effective_timeout = std::max(1, timeout_seconds);
296 while (true) {
297 // Se il child è già uscito E entrambe le pipe sono in EOF,
298 // abbiamo finito.
299 if (!child_done) {
300 const pid_t w = ::waitpid(pid, &status, WNOHANG);
301 if (w == pid) {
302 child_done = true;
303 } else if (w < 0 && errno != EINTR) {
304 result.stderr_text = "waitpid failed";
305 // Do not exit leaving an untracked child behind.
306 ::kill(pid, SIGKILL);
307 ::waitpid(pid, &status, 0);
308 break;
309 }
310 }
311
312 // Timeout: SIGKILL e reap sincrono.
313 const auto now = std::chrono::steady_clock::now();
314 const auto elapsed =
315 std::chrono::duration_cast<std::chrono::seconds>(now - start).count();
316 if (elapsed >= effective_timeout && (!child_done || !out_eof || !err_eof)) {
317 // A forked grandchild may still own an output pipe after its
318 // direct parent exits. Bound the entire operation, not only waitpid.
319 result.timed_out = true;
320 if (!child_done) {
321 ::kill(pid, SIGKILL);
322 ::waitpid(pid, &status, 0);
323 child_done = true;
324 }
325 break;
326 }
327
328 // Drena le pipe disponibili (non-blocking).
329 struct pollfd fds[2];
330 int nfds = 0;
331 if (!out_eof) { fds[nfds].fd = out_pipe[0]; fds[nfds].events = POLLIN; ++nfds; }
332 if (!err_eof) { fds[nfds].fd = err_pipe[0]; fds[nfds].events = POLLIN; ++nfds; }
333
334 if (nfds > 0) {
335 // Poll breve: se il child non è ancora morto, controlliamo poi
336 // waitpid al prossimo giro; se è morto, drenaiamo finché non c'è
337 // più nulla da leggere.
338 const int poll_timeout_ms = child_done ? 100 : 50;
339 const int pr = ::poll(fds, nfds, poll_timeout_ms);
340
341 if (pr > 0) {
342 for (int i = 0; i < nfds; ++i) {
343 if (!(fds[i].revents & (POLLIN | POLLHUP | POLLERR))) continue;
344
345 bool eof_here = false;
346 if (fds[i].fd == out_pipe[0]) {
347 drain_fd_nonblocking(out_pipe[0], result.stdout_text, eof_here, result.output_truncated);
348 if (eof_here) out_eof = true;
349 } else if (fds[i].fd == err_pipe[0]) {
350 drain_fd_nonblocking(err_pipe[0], result.stderr_text, eof_here, result.output_truncated);
351 if (eof_here) err_eof = true;
352 }
353 }
354 }
355 // pr == 0 (timeout poll) o pr < 0 con EINTR: proseguiamo il loop.
356 }
357
358 // Condizione di uscita: child morto e nessun ulteriore dato in arrivo.
359 if (child_done && (out_eof || nfds == 0) && (err_eof || nfds == 0)) {
360 break;
361 }
362
363 // Sanity: se non c'è più nulla da drenare ma il child non è morto,
364 // aspettiamo un pochino per non bruciare CPU.
365 if (!child_done && nfds == 0) {
366 std::this_thread::sleep_for(std::chrono::milliseconds(50));
367 }
368 }
369
370 // Drena residui (le EOF dovrebbero essere già arrivate, ma per sicurezza).
371 if (!out_eof) { bool e; drain_fd_nonblocking(out_pipe[0], result.stdout_text, e, result.output_truncated); }
372 if (!err_eof) { bool e; drain_fd_nonblocking(err_pipe[0], result.stderr_text, e, result.output_truncated); }
373
374 ::close(out_pipe[0]);
375 ::close(err_pipe[0]);
376
377 if (child_done) {
378 result.exited = true;
379 if (WIFEXITED(status)) {
380 result.exit_code = WEXITSTATUS(status);
381 } else if (WIFSIGNALED(status)) {
382 result.exit_code = 128 + WTERMSIG(status);
383 }
384 }
385
386 fs::remove_all(sandbox_root, ec);
387 return result;
388}
bool drain_fd_nonblocking(int fd, std::string &out, bool &got_eof, bool &truncated)
std::string stderr_text
std::string stdout_text

References anonymous_namespace{ArchiveSandbox.cpp}::drain_fd_nonblocking(), SandboxExecResult::exit_code, SandboxExecResult::exited, SandboxExecResult::launched, SandboxExecResult::output_truncated, run_archive_worker_bwrap(), anonymous_namespace{ArchiveSandbox.cpp}::set_nonblocking(), anonymous_namespace{ArchiveSandbox.cpp}::set_worker_rlimits(), SandboxExecResult::stderr_text, SandboxExecResult::stdout_text, and SandboxExecResult::timed_out.

Referenced by analyze_archive_in_sandbox(), and run_archive_worker_bwrap().