Skip to content

Commit 6f69506

Browse files
Create argv before fork (#16286)
Reduces code executing between `fork` and `exec`. Most importantly, removes the unsafe allocation from `command_args.map`.
1 parent 465963a commit 6f69506

File tree

5 files changed

+43
-45
lines changed

5 files changed

+43
-45
lines changed

src/crystal/system/process.cr

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,10 @@ struct Crystal::System::Process
7474
# def self.fork(&)
7575

7676
# Launches a child process with the command + args.
77-
# def self.spawn(command_args : Args, env : Env?, clear_env : Bool, input : Stdio, output : Stdio, error : Stdio, chdir : Path | String?) : ProcessInformation
77+
# def self.spawn(prepared_args : Args, env : Env?, clear_env : Bool, input : Stdio, output : Stdio, error : Stdio, chdir : Path | String?) : ProcessInformation
7878

7979
# Replaces the current process with a new one.
80-
# def self.replace(command_args : Args, env : Env?, clear_env : Bool, input : Stdio, output : Stdio, error : Stdio, chdir : Path | String?) : NoReturn
80+
# def self.replace(command : String, prepared_args : Args, env : Env?, clear_env : Bool, input : Stdio, output : Stdio, error : Stdio, chdir : Path | String?) : NoReturn
8181

8282
# Converts a command and array of arguments to the system-specific representation.
8383
# def self.prepare_args(command : String, args : Enumerable(String)?, shell : Bool) : Args

src/crystal/system/unix/process.cr

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ struct Crystal::System::Process
254254
end
255255
end
256256

257-
def self.spawn(command_args, env, clear_env, input, output, error, chdir)
257+
def self.spawn(prepared_args, env, clear_env, input, output, error, chdir)
258258
r, w = FileDescriptor.system_pipe
259259

260260
pid = fork(will_exec: true) do
@@ -266,7 +266,7 @@ struct Crystal::System::Process
266266
if !pid
267267
LibC.close(r)
268268
begin
269-
self.try_replace(command_args, env, clear_env, input, output, error, chdir)
269+
self.try_replace(prepared_args, env, clear_env, input, output, error, chdir)
270270
byte = 1_u8
271271
errno = Errno.value.to_i32
272272
FileDescriptor.write_fully(w, pointerof(byte))
@@ -292,15 +292,15 @@ struct Crystal::System::Process
292292
when 0
293293
# Error message coming
294294
message = reader_pipe.gets_to_end
295-
raise RuntimeError.new("Error executing process: '#{command_args[0]}': #{message}")
295+
raise RuntimeError.new("Error executing process: '#{prepared_args[0]}': #{message}")
296296
when 1
297297
# Errno coming
298298
# can't use IO#read_bytes(Int32) because we skipped system/network
299299
# endianness check when writing the integer while read_bytes would;
300300
# we thus read it in the same as order as written
301301
buf = uninitialized StaticArray(UInt8, 4)
302302
reader_pipe.read_fully(buf.to_slice)
303-
raise_exception_from_errno(command_args[0], Errno.new(buf.unsafe_as(Int32)))
303+
raise_exception_from_errno(prepared_args[0], Errno.new(buf.unsafe_as(Int32)))
304304
else
305305
raise RuntimeError.new("BUG: Invalid error response received from subprocess")
306306
end
@@ -311,28 +311,30 @@ struct Crystal::System::Process
311311
pid
312312
end
313313

314-
def self.prepare_args(command : String, args : Enumerable(String)?, shell : Bool) : Array(String)
314+
def self.prepare_args(command : String, args : Enumerable(String)?, shell : Bool) : {String, LibC::Char**}
315315
if shell
316316
command = %(#{command} "${@}") unless command.includes?(' ')
317-
shell_args = ["/bin/sh", "-c", command, "sh"]
317+
argv_ary = ["/bin/sh", "-c", command, "sh"]
318318

319319
if args
320320
unless command.includes?(%("${@}"))
321321
raise ArgumentError.new(%(Can't specify arguments in both command and args without including "${@}" into your command))
322322
end
323-
324-
shell_args.concat(args)
325323
end
326324

327-
shell_args
325+
pathname = "/bin/sh"
328326
else
329-
command_args = [command]
330-
command_args.concat(args) if args
331-
command_args
327+
argv_ary = [command]
328+
pathname = command
332329
end
330+
331+
argv_ary.concat(args) if args
332+
333+
argv = argv_ary.map(&.check_no_null_byte.to_unsafe)
334+
{pathname, argv.to_unsafe}
333335
end
334336

335-
private def self.try_replace(command_args, env, clear_env, input, output, error, chdir)
337+
private def self.try_replace(prepared_args, env, clear_env, input, output, error, chdir)
336338
reopen_io(input, ORIGINAL_STDIN)
337339
reopen_io(output, ORIGINAL_STDOUT)
338340
reopen_io(error, ORIGINAL_STDERR)
@@ -348,11 +350,7 @@ struct Crystal::System::Process
348350

349351
::Dir.cd(chdir) if chdir
350352

351-
command = command_args[0]
352-
argv = command_args.map &.check_no_null_byte.to_unsafe
353-
argv << Pointer(UInt8).null
354-
355-
lock_write { execvpe(command, argv, LibC.environ) }
353+
lock_write { LibC.execvp(*prepared_args) }
356354
end
357355

358356
private def self.execvpe(command, argv, envp)
@@ -373,9 +371,9 @@ struct Crystal::System::Process
373371
LibC.execvp(command, argv)
374372
end
375373

376-
def self.replace(command_args, env, clear_env, input, output, error, chdir)
377-
try_replace(command_args, env, clear_env, input, output, error, chdir)
378-
raise_exception_from_errno(command_args[0])
374+
def self.replace(command, prepared_args, env, clear_env, input, output, error, chdir)
375+
try_replace(prepared_args, env, clear_env, input, output, error, chdir)
376+
raise_exception_from_errno(command)
379377
end
380378

381379
private def self.raise_exception_from_errno(command, errno = Errno.value)

src/crystal/system/wasi/process.cr

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,15 +88,15 @@ struct Crystal::System::Process
8888
raise NotImplementedError.new("Process.fork")
8989
end
9090

91-
def self.spawn(command_args, env, clear_env, input, output, error, chdir)
91+
def self.spawn(prepared_args, env, clear_env, input, output, error, chdir)
9292
raise NotImplementedError.new("Process.spawn")
9393
end
9494

9595
def self.prepare_args(command : String, args : Enumerable(String)?, shell : Bool) : Array(String)
9696
raise NotImplementedError.new("Process.prepare_args")
9797
end
9898

99-
def self.replace(command_args, env, clear_env, input, output, error, chdir)
99+
def self.replace(command, prepared_args, env, clear_env, input, output, error, chdir)
100100
raise NotImplementedError.new("Process.replace")
101101
end
102102

src/crystal/system/win32/process.cr

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ struct Crystal::System::Process
282282
{new_handle, dup_handle}
283283
end
284284

285-
def self.spawn(command_args, env, clear_env, input, output, error, chdir)
285+
def self.spawn(prepared_args, env, clear_env, input, output, error, chdir)
286286
startup_info = LibC::STARTUPINFOW.new
287287
startup_info.cb = sizeof(LibC::STARTUPINFOW)
288288
startup_info.dwFlags = LibC::STARTF_USESTDHANDLES
@@ -293,19 +293,19 @@ struct Crystal::System::Process
293293

294294
process_info = LibC::PROCESS_INFORMATION.new
295295

296-
command_args = ::Process.quote_windows(command_args) unless command_args.is_a?(String)
296+
prepared_args = ::Process.quote_windows(prepared_args) unless prepared_args.is_a?(String)
297297

298298
if LibC.CreateProcessW(
299-
nil, System.to_wstr(command_args), nil, nil, true, LibC::CREATE_SUSPENDED | LibC::CREATE_UNICODE_ENVIRONMENT,
299+
nil, System.to_wstr(prepared_args), nil, nil, true, LibC::CREATE_SUSPENDED | LibC::CREATE_UNICODE_ENVIRONMENT,
300300
make_env_block(env, clear_env), chdir.try { |str| System.to_wstr(str) } || Pointer(UInt16).null,
301301
pointerof(startup_info), pointerof(process_info)
302302
) == 0
303303
error = WinError.value
304304
case error.to_errno
305305
when Errno::EACCES, Errno::ENOENT, Errno::ENOEXEC
306-
raise ::File::Error.from_os_error("Error executing process", error, file: command_args)
306+
raise ::File::Error.from_os_error("Error executing process", error, file: prepared_args)
307307
else
308-
raise IO::Error.from_os_error("Error executing process: '#{command_args}'", error)
308+
raise IO::Error.from_os_error("Error executing process: '#{prepared_args}'", error)
309309
end
310310
end
311311

@@ -337,13 +337,13 @@ struct Crystal::System::Process
337337
raise ::File::Error.from_os_error("Error executing process", WinError::ERROR_BAD_EXE_FORMAT, file: command)
338338
end
339339

340-
command_args = [command]
341-
command_args.concat(args) if args
342-
command_args
340+
prepared_args = [command]
341+
prepared_args.concat(args) if args
342+
prepared_args
343343
end
344344
end
345345

346-
private def self.try_replace(command_args, env, clear_env, input, output, error, chdir)
346+
private def self.try_replace(command, prepared_args, env, clear_env, input, output, error, chdir)
347347
old_input_fd = reopen_io(input, ORIGINAL_STDIN)
348348
old_output_fd = reopen_io(output, ORIGINAL_STDOUT)
349349
old_error_fd = reopen_io(error, ORIGINAL_STDERR)
@@ -359,12 +359,12 @@ struct Crystal::System::Process
359359

360360
::Dir.cd(chdir) if chdir
361361

362-
if command_args.is_a?(String)
363-
command = System.to_wstr(command_args)
362+
if prepared_args.is_a?(String)
363+
command = System.to_wstr(prepared_args)
364364
argv = [command]
365365
else
366-
command = System.to_wstr(command_args[0])
367-
argv = command_args.map { |arg| System.to_wstr(arg) }
366+
command = System.to_wstr(prepared_args[0])
367+
argv = prepared_args.map { |arg| System.to_wstr(arg) }
368368
end
369369
argv << Pointer(LibC::WCHAR).null
370370

@@ -378,9 +378,9 @@ struct Crystal::System::Process
378378
errno
379379
end
380380

381-
def self.replace(command_args, env, clear_env, input, output, error, chdir) : NoReturn
382-
errno = try_replace(command_args, env, clear_env, input, output, error, chdir)
383-
raise_exception_from_errno(command_args.is_a?(String) ? command_args : command_args[0], errno)
381+
def self.replace(command, prepared_args, env, clear_env, input, output, error, chdir) : NoReturn
382+
errno = try_replace(command, prepared_args, env, clear_env, input, output, error, chdir)
383+
raise_exception_from_errno(prepared_args.is_a?(String) ? prepared_args : prepared_args[0], errno)
384384
end
385385

386386
private def self.raise_exception_from_errno(command, errno = Errno.value)

src/process.cr

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -215,13 +215,13 @@ class Process
215215
# Raises `IO::Error` if executing the command fails (for example if the executable doesn't exist).
216216
def self.exec(command : String, args : Enumerable(String)? = nil, env : Env = nil, clear_env : Bool = false, shell : Bool = false,
217217
input : ExecStdio = Redirect::Inherit, output : ExecStdio = Redirect::Inherit, error : ExecStdio = Redirect::Inherit, chdir : Path | String? = nil) : NoReturn
218-
command_args = Crystal::System::Process.prepare_args(command, args, shell)
218+
prepared_args = Crystal::System::Process.prepare_args(command, args, shell)
219219

220220
input = exec_stdio_to_fd(input, for: STDIN)
221221
output = exec_stdio_to_fd(output, for: STDOUT)
222222
error = exec_stdio_to_fd(error, for: STDERR)
223223

224-
Crystal::System::Process.replace(command_args, env, clear_env, input, output, error, chdir)
224+
Crystal::System::Process.replace(command, prepared_args, env, clear_env, input, output, error, chdir)
225225
end
226226

227227
private def self.exec_stdio_to_fd(stdio : ExecStdio, for dst_io : IO::FileDescriptor) : IO::FileDescriptor
@@ -280,13 +280,13 @@ class Process
280280
# Raises `IO::Error` if executing the command fails (for example if the executable doesn't exist).
281281
def initialize(command : String, args : Enumerable(String)? = nil, env : Env = nil, clear_env : Bool = false, shell : Bool = false,
282282
input : Stdio = Redirect::Close, output : Stdio = Redirect::Close, error : Stdio = Redirect::Close, chdir : Path | String? = nil)
283-
command_args = Crystal::System::Process.prepare_args(command, args, shell)
283+
prepared_args = Crystal::System::Process.prepare_args(command, args, shell)
284284

285285
fork_input = stdio_to_fd(input, for: STDIN)
286286
fork_output = stdio_to_fd(output, for: STDOUT)
287287
fork_error = stdio_to_fd(error, for: STDERR)
288288

289-
pid = Crystal::System::Process.spawn(command_args, env, clear_env, fork_input, fork_output, fork_error, chdir.try &.to_s)
289+
pid = Crystal::System::Process.spawn(prepared_args, env, clear_env, fork_input, fork_output, fork_error, chdir.try &.to_s)
290290
@process_info = Crystal::System::Process.new(pid)
291291

292292
fork_input.close unless fork_input.in?(input, STDIN)

0 commit comments

Comments
 (0)