Skip to content

Commit 31d3eeb

Browse files
committed
Merge bitcoin/bitcoin#32343: common: Close non-std fds before exec in RunCommandJSON
a0eed55 run_command: Enable close_fds option to avoid lingering fds (Luke Dashjr) c7c356a cpp-subprocess: Iterate through /proc/self/fd for close_fds option on Linux (Luke Dashjr) 4f5e04d Revert "remove unneeded close_fds option from cpp-subprocess" (Luke Dashjr) Pull request description: Picks up stale #30756, while addressing my fallback comment (bitcoin/bitcoin#30756 (comment)). > Currently, RunCommandParseJSON runs its target with whatever fds happen to be open inherited on POSIX platforms. I don't think there's any practical scenario where this is a problem right now, but there's a lot of potential for weird problems (eg, if a process manages to outlive bitcoind - perhaps it's hanging - the listening port(s) won't get released and starting bitcoind again will fail). It's also a potential security issue if a child process is intended to be sandboxed at some point. Not to mention plain ugly :) > > cpp-subprocess has a feature to address this called close_fds. Not sure why it was removed in bitcoin/bitcoin#29961 rather than fixing this during the migration, but this PR restores it, enables it for RunCommandParseJSON, and optimises it by iterating over /proc/self/fd/ like most other libraries do these days ([eg, glib]> (https://gitlab.gnome.org/GNOME/glib/blob/487b1fd20c5e494366a82ddc0fa6b53b8bd779ad/glib/gspawn.c#L1094)) since iterating all possible fd numbers [has been found to be problematic](https://bugzilla.redhat.com/show_bug.cgi?id=1537564). > > (Equivalent to bitcoin/bitcoin#22417 was for boost::process) ACKs for top commit: achow101: ACK a0eed55 hebasto: ACK a0eed55, tested on Ubuntu 25.04: vasild: ACK a0eed55 Tree-SHA512: 7dc1cb6cc1f45ff7c4f53512e400baad1a033b4ebf14ba6f6ffa38588314932d6d01ef67b197f081e8202bb802659ac6a87998277797721d6d7b20efde8e9a6b
2 parents 4b26ca0 + a0eed55 commit 31d3eeb

File tree

2 files changed

+59
-1
lines changed

2 files changed

+59
-1
lines changed

src/common/run_command.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ UniValue RunCommandParseJSON(const std::string& str_command, const std::string&
2424

2525
if (str_command.empty()) return UniValue::VNULL;
2626

27-
auto c = sp::Popen(str_command, sp::input{sp::PIPE}, sp::output{sp::PIPE}, sp::error{sp::PIPE});
27+
auto c = sp::Popen(str_command, sp::input{sp::PIPE}, sp::output{sp::PIPE}, sp::error{sp::PIPE}, sp::close_fds{true});
2828
if (!str_std_in.empty()) {
2929
c.send(str_std_in);
3030
}

src/util/subprocess.h

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ Documentation for C++ subprocessing library.
3636
#ifndef BITCOIN_UTIL_SUBPROCESS_H
3737
#define BITCOIN_UTIL_SUBPROCESS_H
3838

39+
#include <util/fs.h>
40+
#include <util/strencodings.h>
3941
#include <util/syserror.h>
4042

4143
#include <algorithm>
@@ -536,6 +538,20 @@ namespace util
536538
* -------------------------------
537539
*/
538540

541+
/*!
542+
* Option to close all file descriptors
543+
* when the child process is spawned.
544+
* The close fd list does not include
545+
* input/output/error if they are explicitly
546+
* set as part of the Popen arguments.
547+
*
548+
* Default value is false.
549+
*/
550+
struct close_fds {
551+
explicit close_fds(bool c): close_all(c) {}
552+
bool close_all = false;
553+
};
554+
539555
/*!
540556
* Base class for all arguments involving string value.
541557
*/
@@ -733,6 +749,7 @@ struct ArgumentDeducer
733749
void set_option(input&& inp);
734750
void set_option(output&& out);
735751
void set_option(error&& err);
752+
void set_option(close_fds&& cfds);
736753

737754
private:
738755
Popen* popen_ = nullptr;
@@ -1026,6 +1043,8 @@ class Popen
10261043
std::future<void> cleanup_future_;
10271044
#endif
10281045

1046+
bool close_fds_ = false;
1047+
10291048
std::string exe_name_;
10301049

10311050
// Command in string format
@@ -1269,6 +1288,10 @@ namespace detail {
12691288
if (err.rd_ch_ != -1) popen_->stream_.err_read_ = err.rd_ch_;
12701289
}
12711290

1291+
inline void ArgumentDeducer::set_option(close_fds&& cfds) {
1292+
popen_->close_fds_ = cfds.close_all;
1293+
}
1294+
12721295

12731296
inline void Child::execute_child() {
12741297
#ifndef __USING_WINDOWS__
@@ -1315,6 +1338,41 @@ namespace detail {
13151338
if (stream.err_write_ != -1 && stream.err_write_ > 2)
13161339
subprocess_close(stream.err_write_);
13171340

1341+
// Close all the inherited fd's except the error write pipe
1342+
if (parent_->close_fds_) {
1343+
// If possible, try to get the list of open file descriptors from the
1344+
// operating system. This is more efficient, but not guaranteed to be
1345+
// available.
1346+
#ifdef __linux__
1347+
// For Linux, enumerate /proc/<pid>/fd.
1348+
try {
1349+
std::vector<int> fds_to_close;
1350+
for (const auto& it : fs::directory_iterator(strprintf("/proc/%d/fd", getpid()))) {
1351+
auto fd{ToIntegral<uint64_t>(it.path().filename().native())};
1352+
if (!fd || *fd > std::numeric_limits<int>::max()) continue;
1353+
if (*fd <= 2) continue; // leave std{in,out,err} alone
1354+
if (*fd == static_cast<uint64_t>(err_wr_pipe_)) continue;
1355+
fds_to_close.push_back(*fd);
1356+
}
1357+
for (const int fd : fds_to_close) {
1358+
close(fd);
1359+
}
1360+
} catch (const fs::filesystem_error &e) {
1361+
throw OSError("/proc/<pid>/fd iteration failed", e.code().value());
1362+
}
1363+
#else
1364+
// On other operating systems, iterate over all file descriptor slots
1365+
// and try to close them all.
1366+
int max_fd = sysconf(_SC_OPEN_MAX);
1367+
if (max_fd == -1) throw OSError("sysconf failed", errno);
1368+
1369+
for (int i = 3; i < max_fd; i++) {
1370+
if (i == err_wr_pipe_) continue;
1371+
close(i);
1372+
}
1373+
#endif
1374+
}
1375+
13181376
// Replace the current image with the executable
13191377
sys_ret = execvp(parent_->exe_name_.c_str(), parent_->cargv_.data());
13201378

0 commit comments

Comments
 (0)