diff --git a/spec/std/process_spec.cr b/spec/std/process_spec.cr index 382bbf3bb886..da9db791e781 100644 --- a/spec/std/process_spec.cr +++ b/spec/std/process_spec.cr @@ -159,24 +159,24 @@ describe Process do end end - describe "kill" do + describe "signal" do it "kills a process" do process = Process.new("yes") - process.kill(Signal::KILL).should be_nil + process.signal(Signal::KILL).should be_nil end it "kills many process" do process1 = Process.new("yes") process2 = Process.new("yes") - process1.kill(Signal::KILL).should be_nil - process2.kill(Signal::KILL).should be_nil + process1.signal(Signal::KILL).should be_nil + process2.signal(Signal::KILL).should be_nil end end it "gets the pgid of a process id" do process = Process.new("yes") Process.pgid(process.pid).should be_a(Int32) - process.kill(Signal::KILL) + process.signal(Signal::KILL) Process.pgid.should eq(Process.pgid(Process.pid)) end diff --git a/spec/std/signal_spec.cr b/spec/std/signal_spec.cr index 3811bdc65fdc..c98b4a706bea 100644 --- a/spec/std/signal_spec.cr +++ b/spec/std/signal_spec.cr @@ -11,7 +11,7 @@ describe "Signal" do Signal::USR1.trap do ran = true end - Process.kill Signal::USR1, Process.pid + Process.signal Signal::USR1, Process.pid 10.times do |i| break if ran sleep 0.1 @@ -21,7 +21,7 @@ describe "Signal" do it "ignores a signal" do Signal::USR2.ignore - Process.kill Signal::USR2, Process.pid + Process.signal Signal::USR2, Process.pid end it "CHLD.reset sets default Crystal child handler" do diff --git a/src/compiler/crystal/tools/playground/server.cr b/src/compiler/crystal/tools/playground/server.cr index 3ffc45145e4c..72aa61b63b5b 100644 --- a/src/compiler/crystal/tools/playground/server.cr +++ b/src/compiler/crystal/tools/playground/server.cr @@ -161,7 +161,7 @@ module Crystal::Playground @logger.info "Code execution killed (session=#{@session_key}, filename=#{@running_process_filename})." @process = nil File.delete @running_process_filename rescue nil - process.terminate_gracefully rescue nil + process.terminate rescue nil end end diff --git a/src/crystal/system/unix/process.cr b/src/crystal/system/unix/process.cr index 3c605f2ae4b5..6dbcda715f11 100644 --- a/src/crystal/system/unix/process.cr +++ b/src/crystal/system/unix/process.cr @@ -9,72 +9,69 @@ class Process @wait_count = 0 end - def wait(pid : LibC::PidT) : Channel(Int32) - return Crystal::SignalChildHandler.wait(pid) + protected def wait(pid : Int64) : Channel(Int32) + Crystal::SignalChildHandler.wait(pid) end protected def self.exit_system(status = 0) : NoReturn LibC.exit(status) end - protected def self.pid_system : LibC::PidT - LibC.getpid + protected def self.pid_system : Int64 + LibC.getpid.to_i64 end - # Returns `true` if the process identified by *pid* is valid for - # a currently registered process, `false` otherwise. Note that this - # returns `true` for a process in the zombie or similar state. - def self.exists_system(pid : Int) - ret = LibC.kill(pid, 0) - if ret == 0 - true - else - return false if Errno.value == Errno::ESRCH - raise Errno.new("kill") - end + # Returns the process group identifier of the current process. + def self.pgid : Int64 + self.pgid_system + end + + protected def self.exists_system(pid : Int64) + ret = LibC.kill(pid.to_i32, 0) + return true if ret == 0 + return false if Errno.value == Errno::ESRCH + raise Errno.new("kill") end # Returns the process group identifier of the current process. - def self.pgid : LibC::PidT - pgid(0) + protected def self.pgid_system : Int64 + pgid(0).to_i64 end # Returns the process group identifier of the process identified by *pid*. - def self.pgid(pid : Int32) : LibC::PidT + private def self.pgid(pid : Int64) : LibC::PidT ret = LibC.getpgid(pid) raise Errno.new("getpgid") if ret < 0 ret end - # Returns the process identifier of the parent process of the current process. - def self.ppid : LibC::PidT - LibC.getppid + protected def self.ppid_system : Int64 + LibC.getppid.to_i64 end - def terminate_system - kill Signal::TERM + protected def terminate_system + signal Signal::TERM end - def kill_system - kill Signal::KILL + protected def kill_system + signal Signal::KILL end - # See also: `Process.kill` - def kill(sig = Signal::TERM) - Process.kill sig, @pid + # See also: `Process.signal` + def signal(sig = Signal::TERM) + Process.signal sig, @pid end # Sends a *signal* to the processes identified by the given *pids*. - def self.kill(signal : Signal, *pids : Int) + def self.signal(signal : Signal, *pids : Int) pids.each do |pid| ret = LibC.kill(pid, signal.value) raise Errno.new("kill") if ret < 0 end - nil end # Creates a process, executes it - def create_and_exec(command : String, args : (Array | Tuple)?, env : Env?, clear_env : Bool, fork_input : IO::FileDescriptor, fork_output : IO::FileDescriptor, fork_error : IO::FileDescriptor, chdir : String?, reader_pipe, writer_pipe) + protected def create_and_exec(command : String, args : (Array | Tuple)?, env : Env?, clear_env : Bool, fork_input : IO::FileDescriptor, fork_output : IO::FileDescriptor, fork_error : IO::FileDescriptor, chdir : String?, reader_pipe, writer_pipe) if pid = Process.fork_internal(will_exec: true) pid else @@ -186,7 +183,7 @@ class Process raise Errno.new(error_message) end - def self.system_prepare_shell(command, args) + protected def self.system_prepare_shell(command, args) command = %(#{command} "${@}") unless command.includes?(' ') shell_args = ["-c", command, "--"] @@ -208,7 +205,7 @@ class Process end # :nodoc: - protected def self.fork_internal(*, will_exec : Bool) + protected def self.fork_internal(*, will_exec : Bool) : Int64? newmask = uninitialized LibC::SigsetT oldmask = uninitialized LibC::SigsetT @@ -241,10 +238,36 @@ class Process LibC.pthread_sigmask(LibC::SIG_SETMASK, pointerof(oldmask), nil) end - pid + pid.try &.to_i64 end - protected def system_close + protected def close_system + end + + # Changes the root directory and the current working directory for the current + # process. + # + # Security: `chroot` on its own is not an effective means of mitigation. At minimum + # the process needs to also drop privileges as soon as feasible after the `chroot`. + # Changes to the directory hierarchy or file descriptors passed via `recvmsg(2)` from + # outside the `chroot` jail may allow a restricted process to escape, even if it is + # unprivileged. + # + # ``` + # Process.chroot("/var/empty") + # ``` + def self.chroot(path : String) : Nil + path.check_no_null_byte + if LibC.chroot(path) != 0 + raise Errno.new("Failed to chroot") + end + + if LibC.chdir("/") != 0 + errno = Errno.new("chdir after chroot failed") + errno.callstack = CallStack.new + errno.inspect_with_backtrace(STDERR) + abort("Unresolvable state, exiting...") + end end end diff --git a/src/crystal/system/win32/env.cr b/src/crystal/system/win32/env.cr index 1bd71d020c7f..14ce096fa893 100644 --- a/src/crystal/system/win32/env.cr +++ b/src/crystal/system/win32/env.cr @@ -59,19 +59,22 @@ module Crystal::System::Env # Iterates all environment variables. def self.each(&block : String, String ->) - orig_pointer = pointer = LibC.GetEnvironmentStringsW + pointer = LibC.GetEnvironmentStringsW raise WinError.new("GetEnvironmentStringsW") if pointer.null? - begin - while !pointer.value.zero? - string, pointer = String.from_utf16(pointer) - key_value = string.split('=', 2) - key = key_value[0] - value = key_value[1]? || "" - yield key, value - end + self.parse_env_block(pointer) { |key, val| yield key, val } ensure - LibC.FreeEnvironmentStringsW(orig_pointer) + LibC.FreeEnvironmentStringsW(pointer) + end + end + + def self.parse_env_block(pointer : Pointer(UInt16), &block : String, String ->) + while !pointer.value.zero? + string, pointer = String.from_utf16(pointer) + key_value = string.split('=', 2) + key = key_value[0] + value = key_value[1]? || "" + yield key, value end end end diff --git a/src/crystal/system/win32/file_descriptor.cr b/src/crystal/system/win32/file_descriptor.cr index a297f1c8a6ef..42472f1aeed1 100644 --- a/src/crystal/system/win32/file_descriptor.cr +++ b/src/crystal/system/win32/file_descriptor.cr @@ -46,12 +46,21 @@ module Crystal::System::FileDescriptor false end - private def windows_handle - ret = LibC._get_osfhandle(@fd) + def self.windows_handle_for?(fd) + ret = LibC._get_osfhandle(fd) raise Errno.new("_get_osfhandle") if ret == -1 + return nil if ret == -2 LibC::HANDLE.new(ret) end + def windows_handle? + Crystal::System::FileDescriptor.windows_handle_for?(@fd) + end + + def windows_handle + windows_handle? || raise "FD isnt't associated with a stream" + end + private def system_info handle = windows_handle @@ -92,23 +101,9 @@ module Crystal::System::FileDescriptor end private def system_reopen(other : IO::FileDescriptor) - {% if LibC.methods.includes? "dup3".id %} - # dup doesn't copy the CLOEXEC flag, so copy it manually using dup3 - flags = other.close_on_exec? ? LibC::O_CLOEXEC : 0 - if LibC.dup3(other.fd, self.fd, flags) == -1 - raise Errno.new("Could not reopen file descriptor") - end - {% else %} - # dup doesn't copy the CLOEXEC flag, copy it manually to the new - if LibC.dup2(other.fd, self.fd) == -1 - raise Errno.new("Could not reopen file descriptor") - end - - if other.close_on_exec? - self.close_on_exec = true - end - {% end %} - + if LibC._dup2(other.fd, self.fd) == -1 + raise Errno.new("Could not reopen file descriptor") + end # Mark the handle open, since we had to have dup'd a live handle. @closed = false end @@ -125,9 +120,13 @@ module Crystal::System::FileDescriptor end end - def self.pipe(read_blocking, write_blocking) + def self.pipe(read_blocking, write_blocking, inheritable = true) pipe_fds = uninitialized StaticArray(LibC::Int, 2) - if LibC._pipe(pipe_fds, 8192, LibC::O_BINARY) != 0 + flags = LibC::O_BINARY + if !inheritable + flags |= LibC::O_NOINHERIT + end + if LibC._pipe(pipe_fds, 8192, flags) != 0 raise Errno.new("Could not create pipe") end @@ -139,9 +138,8 @@ module Crystal::System::FileDescriptor end def self.pread(fd, buffer, offset) - handle = LibC._get_osfhandle(fd) - raise Errno.new("_get_osfhandle") if handle == -1 - handle = LibC::HANDLE.new(handle) + handle = windows_handle_for?(fd) + raise Errno.new("_get_osfhandle") if !handle overlapped = LibC::OVERLAPPED.new overlapped.union.offset.offset = LibC::DWORD.new(offset) diff --git a/src/crystal/system/win32/process.cr b/src/crystal/system/win32/process.cr new file mode 100644 index 000000000000..a86c0e55f7e6 --- /dev/null +++ b/src/crystal/system/win32/process.cr @@ -0,0 +1,425 @@ +require "c/processthreadsapi" +require "c/winuser" +require "c/tlhelp32" + +POWERSHELL = "powershell.exe" +CMD = "cmd.exe" + +class Process + @@mutex = Mutex.new + + @@pending = {} of Int64 => LibC::DWORD + @@waiting = {} of Int64 => Channel(Int32) + + @handle : LibC::HANDLE? = nil + @pi = LibC::PROCESS_INFORMATION.new + @child_descriptors = Array(IO::FileDescriptor).new + + protected def self.exit_system(status = 0) : NoReturn + LibC.ExitProcess(status) + end + + protected def self.pid_system : Int64 + LibC.GetCurrentProcessId.to_i64 + end + + protected def self.ppid_system : Int64 + pid = LibC.GetCurrentProcessId + snapshot = LibC.CreateToolhelp32Snapshot(LibC::TH32CS_SNAPPROCESS, 0) + begin + if (snapshot == LibC::INVALID_HANDLE_VALUE) + raise WinError.new("CreateToolhelp32Snapshot") + end + + pe32 = LibC::PROCESSENTRY32.new + pe32.dwSize = sizeof(LibC::PROCESSENTRY32) + if (LibC.Process32First(snapshot, pointerof(pe32)) == LibC::FALSE) + raise WinError.new("Process32First") + end + + loop do + if (pe32.th32ProcessID == pid) + return pe32.th32ParentProcessID.to_i64 + end + break if LibC.Process32Next(snapshot, pointerof(pe32)) == LibC::FALSE + end + ensure + if (snapshot != LibC::INVALID_HANDLE_VALUE) + LibC.CloseHandle(snapshot) + end + end + -1_i64 + end + + def self.current_process_handle : LibC::HANDLE + LibC.GetCurrentProcess + end + + def self.current_thread_handle : LibC::HANDLE + LibC.GetCurrentThread + end + + protected def self.exists_system(pid : Int64) + handle = LibC.OpenProcess(LibC::PROCESS_QUERY_INFORMATION, LibC::FALSE, pid.to_u32) + if handle == LibC::NULL + return false + else + LibC.CloseHandle(handle) + return true + end + end + + protected def create_and_exec(command : String, args : (Array | Tuple)?, env : Env?, clear_env : Bool, fork_input : IO::FileDescriptor, fork_output : IO::FileDescriptor, fork_error : IO::FileDescriptor, chdir : String?, reader_pipe, writer_pipe) + # like in child process + self.exec_internal(command, args, env, clear_env, fork_input, fork_output, fork_error, chdir) + rescue ex : Errno + writer_pipe.write_bytes(ex.errno) + writer_pipe.write_bytes(ex.message.try(&.bytesize) || 0) + writer_pipe << ex.message + 0_i64 + rescue ex + ex.inspect_with_backtrace STDERR + STDERR.flush + 0_i64 + end + + def self.duplicate_handle(fd, inheritable) + ret = LibC._dup(fd.fd) + if ret == -1 + raise Errno.new("Could not duplicate file descriptor") + end + IO::FileDescriptor.new(ret) + end + + def exec_internal(command : String, args : (Array | Tuple)?, env : Env?, clear_env : Bool, input : IO::FileDescriptor, output : IO::FileDescriptor, error : IO::FileDescriptor, chdir : String?) + startupinfo = LibC::STARTUPINFOW.new + # only listed handlers are inherited + inherited_handle_list = Array(LibC::HANDLE).new + + if fd = Process.duplicate_handle(input, true) + @child_descriptors << fd + handle = fd.windows_handle + startupinfo.hStdInput = handle + inherited_handle_list << handle + end + + if fd = Process.duplicate_handle(output, true) + @child_descriptors << fd + handle = fd.windows_handle + startupinfo.hStdOutput = handle + inherited_handle_list << handle + end + + if fd = Process.duplicate_handle(error, true) + @child_descriptors << fd + handle = fd.windows_handle + startupinfo.hStdError = handle + inherited_handle_list << handle + end + + startupinfo.dwFlags = LibC::STARTF_USESTDHANDLES + + LibC.InitializeProcThreadAttributeList(LibC::NULL, 1_u32, 0, out lpSize) + attribute_list = Pointer(Void).malloc(lpSize.to_u64) + if LibC.InitializeProcThreadAttributeList(attribute_list, 1_u32, 0, pointerof(lpSize)) == LibC::FALSE + raise WinError.new("InitializeProcThreadAttributeList") + end + + if inherited_handle_list.size > 0 + if LibC.UpdateProcThreadAttribute(attribute_list, 0, LibC::PROC_THREAD_ATTRIBUTE_HANDLE_LIST, + inherited_handle_list, sizeof(LibC::HANDLE) * inherited_handle_list.size, nil, nil) == LibC::FALSE + raise WinError.new("UpdateProcThreadAttribute") + end + end + + startupinfoex = LibC::STARTUPINFOEXW.new + startupinfoex.startupInfo = startupinfo + startupinfoex.lpAttributeList = attribute_list + startupinfoex.startupInfo.cb = sizeof(LibC::STARTUPINFOEXW) + + args = args.nil? ? "" : "#{args.join(' ')}" + command_line = %("#{command}" #{args}) + ret = LibC.CreateProcessW( + nil, # module name + to_windows_path(command_line), # command line args + nil, # Process handle not inheritable + nil, # Thread handle not inheritable + LibC::TRUE, # Set handle inheritance to TRUE + LibC::CREATE_UNICODE_ENVIRONMENT | LibC::EXTENDED_STARTUPINFO_PRESENT, # Use UTF-16 ENV Block + Process.create_env_block(env, clear_env), + chdir ? to_windows_path(chdir) : nil, # Use chdir or parent's starting directory + pointerof(startupinfoex), # Pointer to STARTUPINFOEX structure + pointerof(@pi) ) # Pointer to PROCESS_INFORMATION structure + if ret != 0 + wait_install_res = LibC.RegisterWaitForSingleObject(out wait_handle, @pi.hProcess, ->Process.on_exited(Void*, Bool), Box.box(self), LibC::INFINITE, LibC::WT_EXECUTEONLYONCE) + if (wait_install_res == LibC::FALSE) + raise WinError.new("RegisterWaitForSingleObject") + end + @handle = wait_handle + @pi.dwProcessId.to_i64 + else + raise WinError.new("CreateProcessW") + end + end + + protected def self.on_exited(context : Void*, is_time_out : Bool) + process = Box(::Process).unbox(context) + process.on_exited + end + + protected def on_exited + @child_descriptors.each do |fd| + LibC._close(fd.fd) # ignore fail when already closed by child process + end + if LibC.GetExitCodeProcess(@pi.hProcess, out exit_code) == LibC::FALSE + raise WinError.new("GetExitCodeProcess") + end + # Close process and thread handles. + LibC.CloseHandle(@pi.hProcess) + LibC.CloseHandle(@pi.hThread) + @@mutex.lock + if channel = @@waiting.delete(@pid) + @@mutex.unlock + channel.send(exit_code.to_i32) + channel.close + else + @@pending[@pid] = exit_code + @@mutex.unlock + end + end + + protected def close_system + if @handle + LibC.UnregisterWait(@handle) + end + end + + private def self.shell + return ENV["comspec"]? || CMD + end + + protected def self.system_prepare_shell(command, args) + shell = self.shell + + if shell.ends_with?(POWERSHELL) + shell_args = ["-NoProfile", "-NonInteractive", "-NoLogo", "-Command", command] + elsif shell.ends_with?(CMD) + shell_args = ["/C", command] + else + raise "Unsupported shell #{shell}" + end + + if args + shell_args.concat(args) + end + + command = shell + args = shell_args + {command, args} + end + + # This function is used internally by :func:`CreateProcess` to convert + # the input to ``lpEnvironment`` to a string which the underlying C API + # call will understand. + # + # An environment block consists of a null-terminated block of null-terminated strings. Each string is in the following form: + # name=value\0 + # + # Because the equal sign is used as a separator, it must not be used in the name of an environment variable. + # + # An environment block can contain Unicode characters because we includes CREATE_UNICODE_ENVIRONMENT flag in dwCreationFlags + # A Unicode environment block is terminated by four zero bytes: two for the last string, two more to terminate the block. + protected def self.create_env_block(env : Env?, clear_env : Bool) + final_env : Env = {} of String => String + if LibC.CreateEnvironmentBlock(out pointer, nil, LibC::FALSE) == LibC::FALSE + raise WinError.new("CreateEnvironmentBlock") + end + env_block = pointer.as(Pointer(UInt16)) + begin + Crystal::System::Env.parse_env_block(env_block) do |key, val| + final_env[key] = val + end + ensure + LibC.DestroyEnvironmentBlock(pointer) + end + if !clear_env + ENV.each do |key, val| + final_env[key] = val + end + end + env.try &.each do |key, val| + final_env[key] = val + end + builder = WinEnvBuilder.new + final_env.each do |key, val| + add_to_env_block(builder, key, val) + end + # terminate the block + builder.write(0_u16) + builder.buffer + end + + private def self.add_to_env_block(block : WinEnvBuilder, key : String, val : String) + # From Microsoft's documentation on `lpEnvironment`: + # Because the equal sign is used as a separator, it must not be used + # in the name of an environment variable. + if !key.includes?('=') && !key.empty? + block.write(key, val) + end + end + + protected def wait(pid) : Channel(Int32) + channel = Channel(Int32).new(1) + @@mutex.lock + if exit_code = @@pending.delete(pid) + @@mutex.unlock + channel.send(exit_code.to_i32) + channel.close + else + @@waiting[pid] = channel + @@mutex.unlock + end + + channel + end + + protected def terminate_system + hwnd = find_main_window(@pi.dwProcessId) + if hwnd.address != 0 + LibC.PostMessageW(hwnd, LibC::WM_CLOSE, 0, 0) + else + LibC.PostThreadMessageW(@pi.dwThreadId, LibC::WM_QUIT, 0, 0) + end + end + + protected def kill_system + LibC.TerminateProcess(@pi.hProcess, -1) + end + + struct FindMainWindowParam + property process_id : LibC::DWORD = 0 + property window_handle : LibC::HANDLE = LibC::HANDLE.null + end + + protected def find_main_window(process_id : LibC::DWORD) : LibC::HWND + data = FindMainWindowParam.new + data.process_id = process_id + LibC.EnumWindows(->Process.enum_windows_callback(LibC::HWND, LibC::LPARAM), Box.box(data).address) + data.window_handle + end + + protected def self.enum_windows_callback(handle : LibC::HWND, lparam : LibC::LPARAM) : LibC::BOOL + data = Box(FindMainWindowParam).unbox(Pointer(Void).new(lparam)) + process_id = LibC::DWORD.new(0) + LibC.GetWindowThreadProcessId(handle, pointerof(process_id)) + if data.process_id != process_id || !is_main_window(handle) + return 1 + else + data.window_handle = handle + return 0 + end + end + + protected def self.is_main_window(handle : LibC::HWND) : Bool + LibC.GetWindow(handle, LibC::GW_OWNER).address == 0 && LibC.IsWindowVisible(handle) == 1 + end + + def self.run_ps(powershell_script : String, env : Env = nil, clear_env : Bool = false, + input : Stdio = Redirect::Close, output : Stdio = Redirect::Pipe, error : Stdio = Redirect::Inherit, chdir : String? = nil, &block : Process ->) + args = ["-NoProfile", "-NonInteractive", "-NoLogo"] + if input == Redirect::Close + args << "-Command" + args << "-" + else + slice_16 = powershell_script.to_utf16 + pointer = slice_16.to_unsafe + bytes = Slice(UInt8).new(pointer.as(Pointer(UInt8)), slice_16.size*2) + encoded = Base64.strict_encode(bytes) + args << "-encodedCommand" + args << encoded + end + passed_input = (input == Redirect::Close ? Redirect::Pipe : input) + process = new("powershell.exe", args, env, clear_env, false, passed_input, output, error, chdir) + if input == Redirect::Close + process.input << powershell_script + process.input << "\r\n" + process.input.close + end + begin + value = yield process + $? = process.wait + value + rescue ex + process.terminate + raise ex + end + end + + def self.run_ps(powershell_script : String, env : Env = nil, clear_env : Bool = false, chdir : String? = nil) : String + Process.run_ps(powershell_script, env: env, clear_env: clear_env, chdir: chdir) do |proc| + proc.output.gets_to_end + end + end + + private def to_windows_path(path : String) : LibC::LPWSTR + path.check_no_null_byte.to_utf16.to_unsafe + end + + private class WinEnvBuilder + getter wchar_size : Int32 + getter capacity : Int32 + getter buffer : Pointer(UInt16) + + def initialize(capacity : Int = 1) + @buffer = GC.malloc_atomic(capacity.to_u32*2).as(UInt16*) + @capacity = capacity.to_i + @wchar_size = 0 + end + + def slice + Slice.new(buffer, wchar_size) + end + + def bytes + Slice.new(buffer.as(Pointer(UInt8)), wchar_size*2) + end + + def write(key : String, val : String) + key_val_pair = "#{key}=#{val}" + write(key_val_pair.check_no_null_byte.to_utf16) + write(0_u16) + end + + private def write(slice : Slice(UInt16)) : Nil + return if slice.empty? + + count = slice.size + new_size = @wchar_size + count + if new_size > @capacity + resize_to_capacity(Math.pw2ceil(new_size)) + end + + slice.copy_to(@buffer + @wchar_size, count) + @wchar_size += count + + nil + end + + def write(wchar : UInt16) + new_size = @wchar_size + 1 + if new_size > @capacity + resize_to_capacity(Math.pw2ceil(new_size)) + end + + @buffer[@wchar_size] = wchar + @wchar_size += 1 + + nil + end + + private def resize_to_capacity(capacity) + @capacity = capacity + @buffer = @buffer.realloc(@capacity*2) + end + end +end diff --git a/src/lib_c/x86_64-windows-msvc/c/basetsd.cr b/src/lib_c/x86_64-windows-msvc/c/basetsd.cr index 9092e31ad946..9f09dffa8673 100644 --- a/src/lib_c/x86_64-windows-msvc/c/basetsd.cr +++ b/src/lib_c/x86_64-windows-msvc/c/basetsd.cr @@ -1,7 +1,22 @@ +require "./stddef" + lib LibC {% if flag?(:bits64) %} alias ULONG_PTR = UInt64 + alias SIZE_T = UInt64 {% else %} - alias ULONG_PTR = ULong + alias ULONG_PTR = UInt32 + alias SIZE_T = UInt32 {% end %} + + alias UINT = UInt32 + alias DWORD = UInt32 + alias BOOL = Int32 + alias BOOLEAN = BYTE + alias BYTE = UChar + alias LONG = Int32 + alias CHAR = UChar + alias ULONG = UInt32 + alias ULONG64 = UInt64 + alias DWORD64 = UInt64 end diff --git a/src/lib_c/x86_64-windows-msvc/c/int_safe.cr b/src/lib_c/x86_64-windows-msvc/c/int_safe.cr deleted file mode 100644 index c415f981e943..000000000000 --- a/src/lib_c/x86_64-windows-msvc/c/int_safe.cr +++ /dev/null @@ -1,3 +0,0 @@ -lib LibC - alias DWORD = UInt32 -end diff --git a/src/lib_c/x86_64-windows-msvc/c/io.cr b/src/lib_c/x86_64-windows-msvc/c/io.cr index b34fd20ba5f8..0727b9a7e077 100644 --- a/src/lib_c/x86_64-windows-msvc/c/io.cr +++ b/src/lib_c/x86_64-windows-msvc/c/io.cr @@ -14,5 +14,8 @@ lib LibC fun _wrename(oldname : WCHAR*, newname : WCHAR*) : Int fun _chsize(fd : Int, size : Long) : Int fun _get_osfhandle(fd : Int) : IntPtrT + fun _open_osfhandle(osfhandle : IntPtrT, flags : Int) : Int fun _pipe(pfds : Int*, psize : UInt, textmode : Int) : Int + fun _dup(fd : Int) : Int + fun _dup2(fd1 : Int, fd2 : Int) : Int end diff --git a/src/lib_c/x86_64-windows-msvc/c/libloaderapi.cr b/src/lib_c/x86_64-windows-msvc/c/libloaderapi.cr new file mode 100644 index 000000000000..8f352a630660 --- /dev/null +++ b/src/lib_c/x86_64-windows-msvc/c/libloaderapi.cr @@ -0,0 +1,11 @@ +# Libloaderapi.h +require "./win_def" + +lib LibC + alias HMODULE = Void* + fun GetModuleFileNameW( + hModule : HMODULE, + lpFilename : LPWSTR, + nSize : DWORD + ) : DWORD +end diff --git a/src/lib_c/x86_64-windows-msvc/c/processthreadsapi.cr b/src/lib_c/x86_64-windows-msvc/c/processthreadsapi.cr index b252b70d307a..ae8b9e43ec37 100644 --- a/src/lib_c/x86_64-windows-msvc/c/processthreadsapi.cr +++ b/src/lib_c/x86_64-windows-msvc/c/processthreadsapi.cr @@ -2,4 +2,100 @@ require "./basetsd" lib LibC fun GetCurrentThreadStackLimits(lowLimit : ULONG_PTR*, highLimit : ULONG_PTR*) : Void + fun ExitProcess(uExitCode : UINT) : NoReturn + fun GetCurrentProcessId : DWORD + fun GetCurrentProcess : HANDLE + fun GetCurrentThread : HANDLE + fun TerminateProcess(hProcess : HANDLE, uExitCode : UINT) : BOOL + + CREATE_SUSPENDED = 0x00000004_u32 + DETACHED_PROCESS = 0x00000008_u32 + CREATE_UNICODE_ENVIRONMENT = 0x00000400_u32 + CREATE_NO_WINDOW = 0x08000000_u32 + + struct PROCESS_INFORMATION + hProcess : HANDLE + hThread : HANDLE + dwProcessId : DWORD + dwThreadId : DWORD + end + + alias LPPROCESS_INFORMATION = PROCESS_INFORMATION* + + STARTF_USESTDHANDLES = 0x00000100_u32 + EXTENDED_STARTUPINFO_PRESENT = 0x00080000_u32 + + struct STARTUPINFOW + cb : DWORD + lpReserved : LPWSTR + lpDesktop : LPWSTR + lpTitle : LPWSTR + dwX : DWORD + dwY : DWORD + dwXSize : DWORD + dwYSize : DWORD + dwXCountChars : DWORD + dwYCountChars : DWORD + dwFillAttribute : DWORD + dwFlags : DWORD + wShowWindow : WORD + cbReserved2 : WORD + lpReserved2 : LPBYTE + hStdInput : HANDLE + hStdOutput : HANDLE + hStdError : HANDLE + end + + alias LPROC_THREAD_ATTRIBUTE_LIST = Void* + PROC_THREAD_ATTRIBUTE_HANDLE_LIST = 0x00020002_u32 + + struct STARTUPINFOEXW + startupInfo : STARTUPINFOW + lpAttributeList : LPROC_THREAD_ATTRIBUTE_LIST + end + + fun InitializeProcThreadAttributeList( + lpAttributeList : LPROC_THREAD_ATTRIBUTE_LIST, + dwAttributeCount : DWORD, + dwFlags : DWORD, + lpSize : SIZE_T* + ) : BOOL + + fun UpdateProcThreadAttribute( + lpAttributeList : LPROC_THREAD_ATTRIBUTE_LIST, + dwFlags : DWORD, + attribute : DWORD, + lpValue : PVOID, + cbSize : SIZE_T, + lpPreviousValue : PVOID, + lpReturnSize : SIZE_T* + ) : BOOL + + alias LPSTARTUPINFOW = Void* + + fun CreateProcessW( + lpApplicationName : LPWSTR, + lpCommandLine : LPWSTR, + lpProcessAttributes : SECURITY_ATTRIBUTES*, + lpThreadAttributes : SECURITY_ATTRIBUTES*, + bInheritHandles : BOOL, + dwCreationFlags : DWORD, + lpEnvironment : LPVOID, + lpCurrentDirectory : LPCWSTR, + lpStartupInfo : LPSTARTUPINFOW, + lpProcessInformation : LPPROCESS_INFORMATION + ) : BOOL + + fun GetExitCodeProcess( + hProcess : HANDLE, + lpExitCode : DWORD* + ) : BOOL + + PROCESS_QUERY_INFORMATION = 0x0400 + + fun OpenProcess( + dwDesiredAccess : DWORD, + bInheritHandle : BOOL, + dwProcessId : DWORD + ) : HANDLE end diff --git a/src/lib_c/x86_64-windows-msvc/c/stddef.cr b/src/lib_c/x86_64-windows-msvc/c/stddef.cr index 629703d69860..d32b69b71c57 100644 --- a/src/lib_c/x86_64-windows-msvc/c/stddef.cr +++ b/src/lib_c/x86_64-windows-msvc/c/stddef.cr @@ -1,3 +1,4 @@ lib LibC alias SizeT = UInt64 + NULL = Pointer(Void).null end diff --git a/src/lib_c/x86_64-windows-msvc/c/stdio.cr b/src/lib_c/x86_64-windows-msvc/c/stdio.cr index b9c57ae4133e..c30d95330d21 100644 --- a/src/lib_c/x86_64-windows-msvc/c/stdio.cr +++ b/src/lib_c/x86_64-windows-msvc/c/stdio.cr @@ -1,10 +1,29 @@ require "./stddef" +@[Link("legacy_stdio_definitions")] lib LibC fun printf(format : Char*, ...) : Int fun rename(old : Char*, new : Char*) : Int fun vsnprintf(str : Char*, size : SizeT, format : Char*, ap : VaList) : Int + fun vfprintf(stream : Void*, format : Char*, ap : VaList) : Int fun snprintf = __crystal_snprintf(str : Char*, size : SizeT, format : Char*, ...) : Int + fun dprintf = __crystal_dprintf(fd : Int, format : Char*, ...) : Int + fun _fdopen(fd : Int, mode : Char*) : Void* + fun fclose(stream : Void*) : Int + fun fflush(stream : Void*) : Int +end + +fun __crystal_dprintf(fd : LibC::Int, format : LibC::Char*, ...) : LibC::Int + f = LibC._fdopen(fd, "w") + if f == LibC::NULL + return -1 + end + res : LibC::Int = 0 + VaList.open do |varargs| + res = LibC.vfprintf(f, format, varargs) + end + LibC.fflush(f) + res end fun __crystal_snprintf(str : LibC::Char*, size : LibC::SizeT, format : LibC::Char*, ...) : LibC::Int diff --git a/src/lib_c/x86_64-windows-msvc/c/synchapi.cr b/src/lib_c/x86_64-windows-msvc/c/synchapi.cr index 3145aaa621f6..313edd676f82 100644 --- a/src/lib_c/x86_64-windows-msvc/c/synchapi.cr +++ b/src/lib_c/x86_64-windows-msvc/c/synchapi.cr @@ -1,5 +1,23 @@ -require "c/int_safe" +require "c/basetsd" lib LibC + INFINITE = -1 fun Sleep(dwMilliseconds : DWORD) + fun WaitForSingleObject( + hHandle : HANDLE, + dwMilliseconds : DWORD + ) : DWORD + + alias WAITORTIMERCALLBACK = (Void*, Bool -> Nil) + WT_EXECUTEONLYONCE = 0x00000008 + fun RegisterWaitForSingleObject( + phNewWaitObject : HANDLE*, + hObject : HANDLE, + callback : WAITORTIMERCALLBACK, + context : Void*, + dwMilliseconds : ULong, + dwFlags : ULong + ) : BOOL + + fun UnregisterWait(waitHandle : HANDLE) : BOOL end diff --git a/src/lib_c/x86_64-windows-msvc/c/sysinfoapi.cr b/src/lib_c/x86_64-windows-msvc/c/sysinfoapi.cr index 7fe6b54c6d37..3776e8bab081 100644 --- a/src/lib_c/x86_64-windows-msvc/c/sysinfoapi.cr +++ b/src/lib_c/x86_64-windows-msvc/c/sysinfoapi.cr @@ -1,6 +1,5 @@ require "c/winnt" require "c/win_def" -require "c/int_safe" lib LibC fun GetNativeSystemInfo(system_info : SYSTEM_INFO*) diff --git a/src/lib_c/x86_64-windows-msvc/c/tlhelp32.cr b/src/lib_c/x86_64-windows-msvc/c/tlhelp32.cr new file mode 100644 index 000000000000..5371bb961d65 --- /dev/null +++ b/src/lib_c/x86_64-windows-msvc/c/tlhelp32.cr @@ -0,0 +1,35 @@ +require "./basetsd" + +# TlHelp32.h +@[Link("Kernel32")] +lib LibC + TH32CS_SNAPPROCESS = 0x00000002 + + struct PROCESSENTRY32 + dwSize : DWORD + cntUsage : DWORD + th32ProcessID : DWORD + th32DefaultHeapID : ULONG_PTR + th32ModuleID : DWORD + cntThreads : DWORD + th32ParentProcessID : DWORD + pcPriClassBase : LONG + dwFlags : DWORD + szExeFile : CHAR[MAX_PATH] + end + + fun CreateToolhelp32Snapshot( + dwFlags : DWORD, + th32ProcessID : DWORD + ) : HANDLE + + fun Process32First( + hSnapshot : HANDLE, + lppe : PROCESSENTRY32* + ) : BOOL + + fun Process32Next( + hSnapshot : HANDLE, + lppe : PROCESSENTRY32* + ) : BOOL +end diff --git a/src/lib_c/x86_64-windows-msvc/c/win_def.cr b/src/lib_c/x86_64-windows-msvc/c/win_def.cr index 8117a19a9d0f..01b7718c1e78 100644 --- a/src/lib_c/x86_64-windows-msvc/c/win_def.cr +++ b/src/lib_c/x86_64-windows-msvc/c/win_def.cr @@ -1,5 +1,25 @@ +require "c/basetsd" + lib LibC alias WORD = UInt16 - alias BOOL = Int32 - alias BYTE = UChar + alias WCHAR = UInt16 + + alias LPSTR = CHAR* + alias PWSTR = WCHAR* + alias LPWSTR = WCHAR* + alias LPWCH = WCHAR* + alias LPCWSTR = LPWSTR + alias PCWSTR = WCHAR* + alias PVOID = Void* + alias LPVOID = PVOID + alias LPBYTE = UInt8* + alias HANDLE = Void* + alias HWND = Void* + + alias WPARAM = ULONG_PTR + alias LPARAM = ULONG_PTR + alias LPDWORD = DWORD* + + TRUE = 1 + FALSE = 0 end diff --git a/src/lib_c/x86_64-windows-msvc/c/winbase.cr b/src/lib_c/x86_64-windows-msvc/c/winbase.cr index 6fb4c3620bf0..871421b5a4aa 100644 --- a/src/lib_c/x86_64-windows-msvc/c/winbase.cr +++ b/src/lib_c/x86_64-windows-msvc/c/winbase.cr @@ -1,7 +1,7 @@ -require "c/winnt" require "c/win_def" -require "c/int_safe" +require "c/winnt" +@[Link("UserEnv")] lib LibC fun GetLastError : DWORD fun SetLastError(dwErrCode : DWORD) @@ -88,6 +88,8 @@ lib LibC fun GetEnvironmentVariableW(lpName : LPWSTR, lpBuffer : LPWSTR, nSize : DWORD) : DWORD fun GetEnvironmentStringsW : LPWCH + fun CreateEnvironmentBlock(lpEnvironment : LPVOID*, hToken : HANDLE, bInherit : BOOL) : BOOL + fun DestroyEnvironmentBlock(lpEnvironment : LPVOID) : BOOL fun FreeEnvironmentStringsW(lpszEnvironmentBlock : LPWCH) : BOOL fun SetEnvironmentVariableW(lpName : LPWSTR, lpValue : LPWSTR) : BOOL end diff --git a/src/lib_c/x86_64-windows-msvc/c/winnt.cr b/src/lib_c/x86_64-windows-msvc/c/winnt.cr index c440d9d7b547..b0cb71914128 100644 --- a/src/lib_c/x86_64-windows-msvc/c/winnt.cr +++ b/src/lib_c/x86_64-windows-msvc/c/winnt.cr @@ -1,15 +1,4 @@ lib LibC - alias BOOLEAN = BYTE - alias LONG = Int32 - - alias CHAR = UChar - alias WCHAR = UInt16 - alias LPSTR = CHAR* - alias LPWSTR = WCHAR* - alias LPWCH = WCHAR* - - alias HANDLE = Void* - INVALID_FILE_ATTRIBUTES = DWORD.new(-1) FILE_ATTRIBUTE_DIRECTORY = 0x10 FILE_ATTRIBUTE_READONLY = 0x1 diff --git a/src/lib_c/x86_64-windows-msvc/c/winuser.cr b/src/lib_c/x86_64-windows-msvc/c/winuser.cr new file mode 100644 index 000000000000..ce8713a2b087 --- /dev/null +++ b/src/lib_c/x86_64-windows-msvc/c/winuser.cr @@ -0,0 +1,43 @@ +# Winuser.h +require "c/win_def" + +@[Link("User32")] +lib LibC + WM_CLOSE = 0x0010 + WM_QUIT = 0x0012 + + fun PostMessageW( + hWnd : HWND, + msg : UINT, + wParam : WPARAM, + lParam : LPARAM + ) : BOOL + + fun PostThreadMessageW( + idThread : DWORD, + msg : UINT, + wParam : WPARAM, + lParam : LPARAM + ) : BOOL + + alias WNDENUMPROC = (HWND, LPARAM -> BOOL) + + fun EnumWindows( + lpEnumFunc : WNDENUMPROC, + lParam : LPARAM + ) : BOOL + + fun GetWindowThreadProcessId( + hWnd : HWND, + lpdwProcessId : LPDWORD + ) : DWORD + + GW_OWNER = 4 + + fun GetWindow( + hWnd : HWND, + uCmd : UINT + ) : HWND + + fun IsWindowVisible(hWnd : HWND) : BOOL +end diff --git a/src/process.cr b/src/process.cr index 84501fd7b929..c6278d38f2dd 100644 --- a/src/process.cr +++ b/src/process.cr @@ -11,15 +11,20 @@ class Process end # Returns the process identifier of the current process. - def self.pid - return self.pid_system + def self.pid : Int64 + self.pid_system + end + + # Returns the process identifier of the parent process of the current process. + def self.ppid : Int64 + self.ppid_system end # Returns `true` if the process identified by *pid* is valid for # a currently registered process, `false` otherwise. Note that this # returns `true` for a process in the zombie or similar state. def self.exists?(pid : Int) - return self.exists_system(pid) + self.exists_system(pid) end # A struct representing the CPU current times of the process, @@ -87,19 +92,19 @@ class Process $? = process.wait value rescue ex - process.terminate_gracefully + process.terminate raise ex end end # Terminate process gracefully - def terminate_gracefully - return self.terminate_system + def terminate + self.terminate_system end # Terminate process immediately (kill) - def terminate_immediately - return self.kill_system + def kill + self.kill_system end # Replaces the current process with a new one. This function never returns. @@ -133,7 +138,7 @@ class Process end end - getter pid : Int32 = 0 + getter pid : Int64 = 0 # A pipe to this process's input. Raises if a pipe wasn't asked when creating the process. getter! input : IO::FileDescriptor @@ -144,6 +149,7 @@ class Process # A pipe to this process's error. Raises if a pipe wasn't asked when creating the process. getter! error : IO::FileDescriptor + # channel of process exit code @waitpid : Channel(Int32) @wait_count = 0 @@ -256,7 +262,7 @@ class Process # Closes any pipes to the child process. def close close_io - system_close + close_system end def close_io @@ -316,32 +322,6 @@ class Process private def close_io(io) io.close if io end - - # Changes the root directory and the current working directory for the current - # process. - # - # Security: `chroot` on its own is not an effective means of mitigation. At minimum - # the process needs to also drop privileges as soon as feasible after the `chroot`. - # Changes to the directory hierarchy or file descriptors passed via `recvmsg(2)` from - # outside the `chroot` jail may allow a restricted process to escape, even if it is - # unprivileged. - # - # ``` - # Process.chroot("/var/empty") - # ``` - def self.chroot(path : String) : Nil - path.check_no_null_byte - if LibC.chroot(path) != 0 - raise Errno.new("Failed to chroot") - end - - if LibC.chdir("/") != 0 - errno = Errno.new("chdir after chroot failed") - errno.callstack = CallStack.new - errno.inspect_with_backtrace(STDERR) - abort("Unresolvable state, exiting...") - end - end end # Executes the given command in a subshell. diff --git a/src/process/executable_path.cr b/src/process/executable_path.cr index 717120d50c4f..7a02c9851921 100644 --- a/src/process/executable_path.cr +++ b/src/process/executable_path.cr @@ -90,6 +90,22 @@ end "/proc/self/exe" end end +{% elsif flag?(:win32) %} + require "c/libloaderapi" + + class Process + private def self.executable_path_impl + size = 512_u32 + buf = GC.malloc_atomic(size).as(UInt16*) + len = LibC.GetModuleFileNameW(LibC::NULL, buf, size) + if len == 0 + LibC.dprintf 2, "GetModuleFileNameW ERR: #{LibC.GetLastError}\n" + nil + else + String.from_utf16(Slice.new(buf, len)) + end + end + end {% else %} # openbsd, ... class Process diff --git a/src/signal.cr b/src/signal.cr index e9edd3de0448..0195c43adffa 100644 --- a/src/signal.cr +++ b/src/signal.cr @@ -270,11 +270,11 @@ module Crystal::SignalChildHandler # Process#wait through a channel, that may be created before or after the # child process exited. - @@pending = {} of LibC::PidT => Int32 - @@waiting = {} of LibC::PidT => Channel(Int32) + @@pending = {} of Int64 => Int32 + @@waiting = {} of Int64 => Channel(Int32) @@mutex = Mutex.new - def self.wait(pid : LibC::PidT) : Channel(Int32) + def self.wait(pid : Int64) : Channel(Int32) channel = Channel(Int32).new(1) @@mutex.lock @@ -292,7 +292,7 @@ module Crystal::SignalChildHandler def self.call : Nil loop do - pid = LibC.waitpid(-1, out exit_code, LibC::WNOHANG) + pid = LibC.waitpid(-1, out exit_code, LibC::WNOHANG).to_i64 case pid when 0