Skip to content

Commit ce1291f

Browse files
committed
LibCore+Userland+Ladybird: Explicitly manage Core::Process's end-state
This removes the implicit `disown`ing of the Process in the destructor in favor of up front requests in the constructor. We also force users to exclusively and explicitly disown, wait or take the pid of the child. I hope that it will a good way to prevent API misuses, like calling C functions with the pid or `wait_for_termination` on a disowned child. It unfortunately prevents the usage of the "old" API with KeepAsChild::Yes, this is why there are some refactorings that come with the patch. This also should make it possible to implement KeepAsChild::No on other platforms.
1 parent bf4163d commit ce1291f

File tree

7 files changed

+136
-55
lines changed

7 files changed

+136
-55
lines changed

Ladybird/WebDriver/main.cpp

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,22 @@
1818

1919
static Vector<ByteString> certificates;
2020

21-
static ErrorOr<pid_t> launch_process(StringView application, ReadonlySpan<char const*> arguments)
21+
static ErrorOr<pid_t> launch_process(StringView application, Vector<ByteString> arguments)
2222
{
2323
auto paths = TRY(get_paths_for_helper_process(application));
2424

25-
ErrorOr<pid_t> result = -1;
2625
for (auto const& path : paths) {
2726
auto path_view = path.view();
28-
result = Core::Process::spawn(path_view, arguments, {}, Core::Process::KeepAsChild::Yes);
29-
if (!result.is_error())
30-
break;
27+
auto maybe_process = Core::Process::spawn(Core::ProcessSpawnOptions { .executable = path_view, .arguments = arguments, .keep_as_child = Core::KeepAsChild::Yes });
28+
if (!maybe_process.is_error())
29+
return maybe_process.value().take_pid();
3130
}
32-
return result;
31+
return -1;
3332
}
3433

3534
static ErrorOr<pid_t> launch_browser(ByteString const& socket_path)
3635
{
37-
auto arguments = Vector {
36+
auto arguments = Vector<ByteString> {
3837
"--webdriver-content-path",
3938
socket_path.characters(),
4039
};
@@ -57,7 +56,7 @@ static ErrorOr<pid_t> launch_headless_browser(ByteString const& socket_path)
5756
{
5857
auto resources = ByteString::formatted("{}/res", s_serenity_resource_root);
5958
return launch_process("headless-browser"sv,
60-
Array {
59+
Vector<ByteString> {
6160
"--resources",
6261
resources.characters(),
6362
"--webdriver-ipc-path",
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright (c) 2025, Lucas Chollet <[email protected]>
3+
*
4+
* SPDX-License-Identifier: BSD-2-Clause
5+
*/
6+
7+
#include <LibCore/Process.h>
8+
#include <LibTest/TestCase.h>
9+
10+
TEST_CASE(crash_on_api_misuse)
11+
{
12+
{
13+
auto process = TRY_OR_FAIL(Core::Process::spawn({ .executable = "/bin/true",
14+
.keep_as_child = Core::KeepAsChild::No }));
15+
16+
EXPECT_CRASH("calling wait_for_termination() on disowned child", [&] {
17+
EXPECT(!process.wait_for_termination().is_error());
18+
return Test::Crash::Failure::DidNotCrash;
19+
});
20+
21+
EXPECT_CRASH("calling take_pid() on disowned child", [&] {
22+
process.take_pid();
23+
return Test::Crash::Failure::DidNotCrash;
24+
});
25+
}
26+
27+
{
28+
auto process = TRY_OR_FAIL(Core::Process::spawn({ .executable = "/bin/true",
29+
.keep_as_child = Core::KeepAsChild::Yes }));
30+
31+
EXPECT_CRASH("calling take_pid() after wait_for_termination()", [&] {
32+
EXPECT(!process.wait_for_termination().is_error());
33+
process.take_pid();
34+
return Test::Crash::Failure::DidNotCrash;
35+
});
36+
37+
EXPECT_CRASH("calling wait_for_termination() after take_pid()", [&] {
38+
EXPECT(!process.wait_for_termination().is_error());
39+
process.take_pid();
40+
return Test::Crash::Failure::DidNotCrash;
41+
});
42+
// This creates a zombie process.
43+
process.take_pid();
44+
}
45+
46+
EXPECT_CRASH("Require explicit call to wait_for_termination() of take_pid()", [&] {
47+
{
48+
auto maybe_process = Core::Process::spawn(
49+
{ .executable = "/bin/true",
50+
.keep_as_child = Core::KeepAsChild::Yes });
51+
EXPECT(!maybe_process.is_error());
52+
}
53+
return Test::Crash::Failure::DidNotCrash;
54+
});
55+
}
56+
57+
TEST_CASE(no_crash)
58+
{
59+
{
60+
auto process = TRY_OR_FAIL(Core::Process::spawn({ .executable = "/bin/true",
61+
.keep_as_child = Core::KeepAsChild::No }));
62+
}
63+
{
64+
auto process = TRY_OR_FAIL(Core::Process::spawn({ .executable = "/bin/true",
65+
.keep_as_child = Core::KeepAsChild::Yes }));
66+
TRY_OR_FAIL(process.wait_for_termination());
67+
}
68+
{
69+
auto process = TRY_OR_FAIL(Core::Process::spawn({ .executable = "/bin/true",
70+
.keep_as_child = Core::KeepAsChild::Yes }));
71+
// This creates a zombie process.
72+
process.take_pid();
73+
}
74+
}

Tests/LibELF/TestOrder.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ static ByteBuffer run(ByteString executable)
2626
.fd = 1,
2727
},
2828
},
29+
.keep_as_child = Core::KeepAsChild::Yes,
2930
}));
3031
MUST(process.wait_for_termination());
3132
auto output = MUST(Core::File::open(path_to_captured_output.string(), Core::File::OpenMode::Read));

Userland/Applications/Run/RunWindow.cpp

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,11 +108,15 @@ void RunWindow::do_run()
108108
bool RunWindow::run_as_command(ByteString const& run_input)
109109
{
110110
// TODO: Query and use the user's preferred shell.
111-
auto maybe_child_pid = Core::Process::spawn("/bin/Shell"sv, Array { "-c", run_input.characters() }, {}, Core::Process::KeepAsChild::Yes);
112-
if (maybe_child_pid.is_error())
111+
auto maybe_child_process = Core::Process::spawn(Core::ProcessSpawnOptions {
112+
.executable = "/bin/Shell"sv,
113+
.arguments = { "-c", run_input.characters() },
114+
.keep_as_child = Core::KeepAsChild::Yes });
115+
116+
if (maybe_child_process.is_error())
113117
return false;
114118

115-
pid_t child_pid = maybe_child_pid.release_value();
119+
pid_t child_pid = maybe_child_process.value().take_pid();
116120

117121
// The child shell was able to start. Let's save it to the history immediately so users can see it as the first entry the next time they run this program.
118122
prepend_history(run_input);

Userland/Applications/Terminal/main.cpp

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ class TerminalChangeListener : public Config::Listener {
103103
static ErrorOr<void> utmp_update(StringView tty, pid_t pid, bool create)
104104
{
105105
auto pid_string = String::number(pid);
106-
Array utmp_update_command {
106+
Vector<ByteString> utmp_update_command {
107107
"-f"sv,
108108
"Terminal"sv,
109109
"-p"sv,
@@ -112,7 +112,11 @@ static ErrorOr<void> utmp_update(StringView tty, pid_t pid, bool create)
112112
tty,
113113
};
114114

115-
auto utmpupdate_pid = TRY(Core::Process::spawn("/bin/utmpupdate"sv, utmp_update_command, {}, Core::Process::KeepAsChild::Yes));
115+
auto utmpupdate_pid = TRY(Core::Process::spawn(Core::ProcessSpawnOptions {
116+
.executable = "/bin/utmpupdate"sv,
117+
.arguments = utmp_update_command,
118+
.keep_as_child = Core::KeepAsChild::Yes }))
119+
.take_pid();
116120

117121
Core::System::WaitPidResult status;
118122
auto wait_successful = false;

Userland/Libraries/LibCore/Process.cpp

Lines changed: 21 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,16 @@ ErrorOr<Process> Process::spawn(ProcessSpawnOptions const& options)
114114
} else {
115115
pid = TRY(System::posix_spawn(options.executable.view(), &spawn_actions, nullptr, const_cast<char**>(argv_list.get().data()), Core::Environment::raw_environ()));
116116
}
117-
return Process { pid };
117+
118+
auto process = Process { pid };
119+
if (options.keep_as_child == KeepAsChild::No) {
120+
dbgln("Disowning");
121+
TRY(process.disown());
122+
} else {
123+
dbgln("Or not");
124+
}
125+
126+
return process;
118127
}
119128

120129
ErrorOr<pid_t> Process::spawn(StringView path, ReadonlySpan<ByteString> arguments, ByteString working_directory, KeepAsChild keep_as_child)
@@ -123,14 +132,9 @@ ErrorOr<pid_t> Process::spawn(StringView path, ReadonlySpan<ByteString> argument
123132
.executable = path,
124133
.arguments = Vector<ByteString> { arguments },
125134
.working_directory = working_directory.is_empty() ? Optional<ByteString> {} : Optional<ByteString> { working_directory },
135+
.keep_as_child = keep_as_child,
126136
}));
127137

128-
if (keep_as_child == KeepAsChild::No)
129-
TRY(process.disown());
130-
else {
131-
// FIXME: This won't be needed if return value is changed to Process.
132-
process.m_should_disown = false;
133-
}
134138
return process.m_pid;
135139
}
136140

@@ -145,12 +149,9 @@ ErrorOr<pid_t> Process::spawn(StringView path, ReadonlySpan<StringView> argument
145149
.executable = path,
146150
.arguments = backing_strings,
147151
.working_directory = working_directory.is_empty() ? Optional<ByteString> {} : Optional<ByteString> { working_directory },
152+
.keep_as_child = keep_as_child,
148153
}));
149154

150-
if (keep_as_child == KeepAsChild::No)
151-
TRY(process.disown());
152-
else
153-
process.m_should_disown = false;
154155
return process.m_pid;
155156
}
156157

@@ -165,12 +166,9 @@ ErrorOr<pid_t> Process::spawn(StringView path, ReadonlySpan<char const*> argumen
165166
.executable = path,
166167
.arguments = backing_strings,
167168
.working_directory = working_directory.is_empty() ? Optional<ByteString> {} : Optional<ByteString> { working_directory },
169+
.keep_as_child = keep_as_child,
168170
}));
169171

170-
if (keep_as_child == KeepAsChild::No)
171-
TRY(process.disown());
172-
else
173-
process.m_should_disown = false;
174172
return process.m_pid;
175173
}
176174

@@ -320,22 +318,20 @@ void Process::wait_for_debugger_and_break()
320318

321319
ErrorOr<void> Process::disown()
322320
{
323-
if (m_pid != 0 && m_should_disown) {
324321
#ifdef AK_OS_SERENITY
325-
TRY(System::disown(m_pid));
322+
TRY(System::disown(m_pid));
326323
#else
327-
// FIXME: Support disown outside Serenity.
324+
// FIXME: Support disown outside Serenity.
328325
#endif
329-
m_should_disown = false;
330-
return {};
331-
} else {
332-
return Error::from_errno(EINVAL);
333-
}
326+
m_was_managed = true;
327+
return {};
334328
}
335329

336330
ErrorOr<bool> Process::wait_for_termination()
337331
{
338332
VERIFY(m_pid > 0);
333+
VERIFY(!m_was_managed);
334+
m_was_managed = true;
339335

340336
bool exited_with_code_0 = true;
341337
int status;
@@ -353,7 +349,6 @@ ErrorOr<bool> Process::wait_for_termination()
353349
VERIFY_NOT_REACHED();
354350
}
355351

356-
m_should_disown = false;
357352
return exited_with_code_0;
358353
}
359354

@@ -457,7 +452,7 @@ ErrorOr<IPCProcess::ProcessPaths> IPCProcess::paths_for_process(StringView proce
457452
ErrorOr<IPCProcess::ProcessAndIPCSocket> IPCProcess::spawn_singleton_and_connect_to_process(ProcessSpawnOptions const& options)
458453
{
459454
auto [socket_path, pid_path] = TRY(paths_for_process(options.name));
460-
Process process { -1 };
455+
Optional<Process> process;
461456

462457
if (auto existing_pid = TRY(get_process_pid(options.name, pid_path)); existing_pid.has_value()) {
463458
process = Process { *existing_pid };
@@ -504,7 +499,7 @@ ErrorOr<IPCProcess::ProcessAndIPCSocket> IPCProcess::spawn_singleton_and_connect
504499
auto ipc_socket = TRY(LocalSocket::connect(socket_path));
505500
TRY(ipc_socket->set_blocking(true));
506501

507-
return ProcessAndIPCSocket { move(process), move(ipc_socket) };
502+
return ProcessAndIPCSocket { process.release_value(), move(ipc_socket) };
508503
}
509504

510505
}

Userland/Libraries/LibCore/Process.h

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ struct CloseFile {
3535

3636
}
3737

38+
enum class KeepAsChild {
39+
Yes,
40+
No
41+
};
42+
3843
struct ProcessSpawnOptions {
3944
StringView name {};
4045
ByteString executable {};
@@ -44,6 +49,8 @@ struct ProcessSpawnOptions {
4449

4550
using FileActionType = Variant<FileAction::OpenFile, FileAction::CloseFile>;
4651
Vector<FileActionType> file_actions {};
52+
53+
KeepAsChild keep_as_child = KeepAsChild::No;
4754
};
4855

4956
class IPCProcess;
@@ -52,27 +59,18 @@ class Process {
5259
AK_MAKE_NONCOPYABLE(Process);
5360

5461
public:
55-
enum class KeepAsChild {
56-
Yes,
57-
No
58-
};
59-
6062
Process(Process&& other)
6163
: m_pid(exchange(other.m_pid, 0))
62-
, m_should_disown(exchange(other.m_should_disown, false))
64+
, m_was_managed(exchange(other.m_was_managed, true))
6365
{
6466
}
6567

66-
Process& operator=(Process&& other)
67-
{
68-
m_pid = exchange(other.m_pid, 0);
69-
m_should_disown = exchange(other.m_should_disown, false);
70-
return *this;
71-
}
68+
Process& operator=(Process&& other) = delete;
7269

7370
~Process()
7471
{
75-
(void)disown();
72+
// We want users to explicitly call `disown()`, `take_pid()` or `wait_for_termination()`.
73+
VERIFY(m_was_managed);
7674
}
7775

7876
static ErrorOr<Process> spawn(ProcessSpawnOptions const& options);
@@ -97,19 +95,25 @@ class Process {
9795
// FIXME: Make it return an exit code.
9896
ErrorOr<bool> wait_for_termination();
9997

98+
pid_t take_pid()
99+
{
100+
VERIFY(!m_was_managed);
101+
m_was_managed = true;
102+
return exchange(m_pid, 0);
103+
}
104+
100105
private:
101106
friend IPCProcess;
102107

103108
ErrorOr<void> disown();
104109

105110
Process(pid_t pid)
106111
: m_pid(pid)
107-
, m_should_disown(true)
108112
{
109113
}
110114

111-
pid_t m_pid;
112-
bool m_should_disown;
115+
pid_t m_pid { 0 };
116+
bool m_was_managed { false };
113117
};
114118

115119
class IPCProcess {

0 commit comments

Comments
 (0)