async def reconnect(self, process: Process) -> None: """Using the passed-in process to establish the connection, reconnect to this PersistentProcess """ if not self.prepped_for_reconnect: # It does work to reconnect without prep_for_reconnect, except for one nitpick: # If the underlying process for the PersistentProcess dies while we're in the # middle of reconnecting to it, the file descriptors opened by the C code # running in the process will leak if the process is in a shared fd table. # That's annoying on its own, but also means we won't get an EOF from our # communication with the process, and we'll just hang forever. await self.prep_for_reconnect() await self.task.run_fd_table_gc(use_self=False) if not isinstance(self.task.sysif, PersistentSyscallConnection): raise Exception("self.task.sysif of unexpected type", self.task.sysif) await self.task.sysif.shutdown_current_connection() [(access_syscall_sock, syscall_sock), (access_data_sock, data_sock)] = await process.open_async_channels(2) serverfd, remote_data_sock = await _connect_and_send(self, process, syscall_sock, data_sock) await syscall_sock.close() await data_sock.close() # Set up the new SyscallConnection conn = SyscallConnection( self.task.sysif.logger, access_syscall_sock, serverfd, ) await self.task.sysif.set_new_conn(conn) # close remote fds we don't have handles to; this includes the old interface fds. await self.task.run_fd_table_gc()
async def clone_child_task( parent: ForkThread, flags: CLONE, trampoline_func: t.Callable[[FileDescriptor], Trampoline], ) -> t.Tuple[AsyncChildProcess, Task]: """Clone a new child process and setup the sysif and task to manage it We rely on trampoline_func to take a socket and give us a native function call with arguments that will speak the rsyscall protocol over that socket. We also create a futex process, which we use to monitor the ctid futex. This process allows us to detect when the child successfully finishes an exec; see the docstring of ChildSyscallInterface. Because we set CLONE.CHILD_CLEARTID, the ctid futex will receive a FUTEX_WAKE when the child process exits or execs, and the futex process will accordingly exit. """ # Open a channel which we'll use for the rsyscall connection [(access_sock, remote_sock)] = await parent.connection.open_async_channels(1) # Create a trampoline that will start the new process running an rsyscall server trampoline = trampoline_func(remote_sock) # Force these flags to be used flags |= CLONE.VM|CLONE.FILES|CLONE.IO|CLONE.SYSVSEM # TODO it is unclear why we sometimes need to make a new mapping here, instead of # allocating with our normal allocator; all our memory is already MAP.SHARED, I think. # We should resolve this so we can use the normal allocator. arena = Arena(await parent.task.mmap(4096*2, PROT.READ|PROT.WRITE, MAP.SHARED)) async def op(sem: RAM) -> t.Tuple[t.Tuple[Pointer[Stack], WrittenPointer[Stack]], WrittenPointer[FutexNode]]: stack_value = parent.loader.make_trampoline_stack(trampoline) stack_buf = await sem.malloc(Stack, 4096) stack = await stack_buf.write_to_end(stack_value, alignment=16) futex_pointer = await sem.ptr(FutexNode(None, Int32(0))) return stack, futex_pointer # Create the stack we'll need, and the zero-initialized futex stack, futex_pointer = await parent.ram.perform_batch(op, arena) # it's important to start the processes in this order, so that the thread # process is the first process started; this is relevant in several # situations, including unshare(NEWPID) and manipulation of ns_last_pid child_process = await parent.monitor.clone(flags|CLONE.CHILD_CLEARTID, stack, ctid=futex_pointer) futex_process = await launch_futex_monitor( parent.ram, parent.loader, parent.monitor, futex_pointer) # Create the new syscall interface, which needs to use not just the connection, # but also the child process and the futex process. syscall = ChildSyscallInterface(SyscallConnection(access_sock, access_sock), child_process, futex_process) # Set up the new task with appropriately inherited namespaces, tables, etc. # TODO correctly track all the namespaces we're in if flags & CLONE.NEWPID: pidns = far.PidNamespace(child_process.process.near.id) else: pidns = parent.task.pidns task = Task(syscall, child_process.process, parent.task.fd_table, parent.task.address_space, pidns) task.sigmask = parent.task.sigmask # Move ownership of the remote sock into the task and store it so it isn't closed remote_sock_handle = remote_sock.move(task) syscall.store_remote_side_handles(remote_sock_handle, remote_sock_handle) return child_process, task
async def reconnect(self, thread: Thread) -> None: """Using the passed-in thread to establish the connection, reconnect to this PersistentThread """ if not self.prepped_for_reconnect: # It does work to reconnect without prep_for_reconnect, except for one nitpick: # If the underlying process for the PersistentThread dies while we're in the # middle of reconnecting to it, the file descriptors opened by the C code # running in the process will leak if the process is in a shared fd table. # That's annoying on its own, but also means we won't get an EOF from our # communication with the process, and we'll just hang forever. await self.prep_for_reconnect() await self.task.run_fd_table_gc(use_self=False) if not isinstance(self.task.sysif, PersistentSyscallConnection): raise Exception("self.task.sysif of unexpected type", self.task.sysif) await self.task.sysif.shutdown_current_connection() [(access_syscall_sock, syscall_sock), (access_data_sock, data_sock)] = await thread.open_async_channels(2) [infd, outfd, remote_data_sock ] = await _connect_and_send(self, thread, [syscall_sock, syscall_sock, data_sock]) await syscall_sock.close() await data_sock.close() # Set up the new SyscallConnection conn = SyscallConnection( self.task.sysif.logger, access_syscall_sock, access_syscall_sock, infd, outfd, ) self.task.sysif.set_new_conn(conn) # Fix up RAM with new transport # TODO technically this could still be in the same address space - that's the case in our tests. # we should figure out a way to use a LocalMemoryTransport here so it can copy efficiently transport = SocketMemoryTransport(access_data_sock, remote_data_sock) self.ram.transport = transport self.transport = transport # close remote fds we don't have handles to; this includes the old interface fds. await self.task.run_fd_table_gc()
async def reconnect(self, thread: Thread) -> None: """Using the passed-in thread to establish the connection, reconnect to this PersistentThread """ await run_fd_table_gc(self.task.fd_table) if not isinstance(self.task.sysif, (ChildSyscallInterface, NonChildSyscallInterface)): raise Exception("self.task.sysif of unexpected type", self.task.sysif) await self.task.sysif.close_interface() # TODO should check that no transport requests are in flight [(access_syscall_sock, syscall_sock), (access_data_sock, data_sock)] = await thread.open_async_channels(2) [infd, outfd, remote_data_sock ] = await _connect_and_send(self, thread, [syscall_sock, syscall_sock, data_sock]) await syscall_sock.close() await data_sock.close() # Fix up Task's sysif with new SyscallConnection self.task.sysif.rsyscall_connection = SyscallConnection( access_syscall_sock, access_syscall_sock) self.task.sysif.store_remote_side_handles(infd, outfd) # Fix up RAM with new transport # TODO technically this could still be in the same address space - that's the case in our tests. # we should figure out a way to use a LocalMemoryTransport here so it can copy efficiently transport = SocketMemoryTransport(access_data_sock, remote_data_sock, self.ram.allocator) self.ram.transport = transport self.transport = transport # Fix up epoller with new activity fd def devnull(event: EPOLL) -> None: pass await self.epoller.register( infd, EPOLL.IN | EPOLL.OUT | EPOLL.RDHUP | EPOLL.PRI | EPOLL.ERR | EPOLL.HUP, devnull) # close remote fds we don't have handles to; this includes the old interface fds. await run_fd_table_gc(self.task.fd_table)
async def stdin_bootstrap( parent: Thread, bootstrap_command: Command, ) -> t.Tuple[AsyncChildProcess, Thread]: """Create a thread from running an arbitrary command which must run rsyscall-stdin-bootstrap bootstrap_command can be any arbitrary command, but it must eventually exec rsyscall-stdin-bootstrap, and pass down stdin when it does. We'll fork and exec bootstrap_command, passing down a socketpair for stdin, and try to bootstrap over the other end of the socketpair. Once rsyscall-stdin-bootstrap starts, it will respond to our bootstrap and we'll create a new thread. """ #### fork and exec into the bootstrap command child = await parent.fork() # create the socketpair that will be used as stdin stdin_pair = await (await parent.task.socketpair( AF.UNIX, SOCK.STREAM, 0, await parent.ram.malloc(Socketpair))).read() parent_sock = stdin_pair.first child_sock = stdin_pair.second.move(child.task) # set up stdin with socketpair await child.unshare_files(going_to_exec=True) await child.stdin.replace_with(child_sock) # exec child_process = await child.exec(bootstrap_command) #### set up all the fds we'll want to pass over # the basic connections [(access_syscall_sock, passed_syscall_sock), (access_data_sock, passed_data_sock)] = await parent.open_async_channels(2) # memfd for setting up the futex futex_memfd = await parent.task.memfd_create( await parent.ram.ptr(Path("child_robust_futex_list"))) # send the fds to the new process connection_fd, make_connection = await parent.connection.prep_fd_transfer() async def sendmsg_op(sem: RAM) -> WrittenPointer[SendMsghdr]: iovec = await sem.ptr(IovecList([await sem.malloc(bytes, 1)])) cmsgs = await sem.ptr(CmsgList([CmsgSCMRights([ passed_syscall_sock, passed_data_sock, futex_memfd, connection_fd])])) return await sem.ptr(SendMsghdr(None, iovec, cmsgs)) _, [] = await parent_sock.sendmsg(await parent.ram.perform_batch(sendmsg_op), SendmsgFlags.NONE) # close our reference to fds that only the new process needs await passed_syscall_sock.close() await passed_data_sock.close() # close the socketpair await parent_sock.close() #### read describe to get all the information we need from the new process describe_buf = AsyncReadBuffer(access_data_sock) describe_struct = await describe_buf.read_cffi('struct rsyscall_stdin_bootstrap') environ = await describe_buf.read_envp(describe_struct.envp_count) #### build the new task pid = describe_struct.pid fd_table = far.FDTable(pid) address_space = far.AddressSpace(pid) # we assume pid namespace is shared # TODO include namespace inode numbers numbers in describe # note: if we start dealing with namespace numbers then we need to # have a Kernel namespace which tells us which kernel we get those # numbers from. # oh hey we can conveniently dump the inode numbers with getdents! pidns = parent.task.pidns process = near.Process(pid) remote_syscall_fd = near.FileDescriptor(describe_struct.syscall_fd) syscall = NonChildSyscallInterface(SyscallConnection(access_syscall_sock, access_syscall_sock), process) base_task = Task(syscall, process, fd_table, address_space, pidns) handle_remote_syscall_fd = base_task.make_fd_handle(remote_syscall_fd) syscall.store_remote_side_handles(handle_remote_syscall_fd, handle_remote_syscall_fd) allocator = memory.AllocatorClient.make_allocator(base_task) # we assume our SignalMask is zero'd before being started, so we don't inherit it ram = RAM(base_task, SocketMemoryTransport(access_data_sock, base_task.make_fd_handle(near.FileDescriptor(describe_struct.data_fd)), allocator), allocator) # TODO I think I can maybe elide creating this epollcenter and instead inherit it or share it, maybe? epoller = await Epoller.make_root(ram, base_task) child_monitor = await ChildProcessMonitor.make(ram, base_task, epoller) connection = make_connection(base_task, ram, base_task.make_fd_handle(near.FileDescriptor(describe_struct.connecting_fd))) new_parent = Thread( task=base_task, ram=ram, connection=connection, loader=NativeLoader.make_from_symbols(base_task, describe_struct.symbols), epoller=epoller, child_monitor=child_monitor, environ=Environment(base_task, ram, environ), stdin=base_task.make_fd_handle(near.FileDescriptor(0)), stdout=base_task.make_fd_handle(near.FileDescriptor(1)), stderr=base_task.make_fd_handle(near.FileDescriptor(2)), ) #### TODO set up futex I guess remote_futex_memfd = near.FileDescriptor(describe_struct.futex_memfd) return child_process, new_parent
async def ssh_bootstrap( parent: Process, # the actual ssh command to run ssh_command: SSHCommand, # the local path we'll use for the socket local_socket_path: Path, # the directory we're bootstrapping out of tmp_path_bytes: bytes, ) -> t.Tuple[AsyncChildPid, Process]: "Over ssh, run the bootstrap executable, " # identify local path local_data_addr = await parent.ram.ptr( await SockaddrUn.from_path(parent, local_socket_path)) # start port forwarding; we'll just leak this process, no big deal # TODO we shouldn't leak processes; we should be GCing processes at some point forward_child_pid = await ssh_forward( parent, ssh_command, local_socket_path, (tmp_path_bytes + b"/data").decode()) # start bootstrap bootstrap_process = await parent.fork() bootstrap_child_pid = await bootstrap_process.exec(ssh_command.args( "-n", f"cd {tmp_path_bytes.decode()}; exec ./bootstrap rsyscall" )) # TODO should unlink the bootstrap after I'm done execing. # it would be better if sh supported fexecve, then I could unlink it before I exec... # Connect to local socket 4 times async def make_async_connection() -> AsyncFileDescriptor: sock = await parent.make_afd(await parent.socket(AF.UNIX, SOCK.STREAM|SOCK.NONBLOCK)) await sock.connect(local_data_addr) return sock async_local_syscall_sock = await make_async_connection() async_local_data_sock = await make_async_connection() # Read description off of the data sock describe_buf = AsyncReadBuffer(async_local_data_sock) describe_struct = await describe_buf.read_cffi('struct rsyscall_bootstrap') new_pid = describe_struct.pid environ = await describe_buf.read_envp(describe_struct.envp_count) # Build the new task! new_address_space = far.AddressSpace(new_pid) # TODO the pid namespace will probably be common for all connections... # TODO we should get this from the SSHHost, this is usually going # to be common for all connections and we should express that new_pid_namespace = far.PidNamespace(new_pid) new_pid = near.Pid(new_pid) new_base_task = Task( new_pid, handle.FDTable(new_pid), new_address_space, new_pid_namespace, ) handle_remote_syscall_fd = new_base_task.make_fd_handle(near.FileDescriptor(describe_struct.syscall_sock)) new_base_task.sysif = SyscallConnection( logger.getChild(str(new_pid)), async_local_syscall_sock, async_local_syscall_sock, handle_remote_syscall_fd, handle_remote_syscall_fd, ) handle_remote_data_fd = new_base_task.make_fd_handle(near.FileDescriptor(describe_struct.data_sock)) handle_listening_fd = new_base_task.make_fd_handle(near.FileDescriptor(describe_struct.listening_sock)) new_allocator = memory.AllocatorClient.make_allocator(new_base_task) new_transport = SocketMemoryTransport(async_local_data_sock, handle_remote_data_fd) # we don't inherit SignalMask; we assume ssh zeroes the sigmask before starting us new_ram = RAM(new_base_task, new_transport, new_allocator) epoller = await Epoller.make_root(new_ram, new_base_task) child_monitor = await ChildPidMonitor.make(new_ram, new_base_task, epoller) await handle_listening_fd.fcntl(F.SETFL, O.NONBLOCK) connection = ListeningConnection( parent.task, parent.ram, parent.epoller, local_data_addr, new_base_task, new_ram, await AsyncFileDescriptor.make(epoller, new_ram, handle_listening_fd), ) new_process = Process( task=new_base_task, ram=new_ram, connection=connection, loader=NativeLoader.make_from_symbols(new_base_task, describe_struct.symbols), epoller=epoller, child_monitor=child_monitor, environ=Environment.make_from_environ(new_base_task, new_ram, environ), stdin=new_base_task.make_fd_handle(near.FileDescriptor(0)), stdout=new_base_task.make_fd_handle(near.FileDescriptor(1)), stderr=new_base_task.make_fd_handle(near.FileDescriptor(2)), ) return bootstrap_child_pid, new_process
async def clone_child_task( task: Task, ram: RAM, connection: Connection, loader: NativeLoader, monitor: ChildProcessMonitor, flags: CLONE, trampoline_func: t.Callable[[FileDescriptor], Trampoline], ) -> t.Tuple[AsyncChildProcess, Task]: """Clone a new child process and setup the sysif and task to manage it We rely on trampoline_func to take a socket and give us a native function call with arguments that will speak the rsyscall protocol over that socket. We want to see EOF on our local socket if that remote socket is no longer being read; for example, if the process exits or execs. This is not automatic for us: Since the process might share its file descriptor table with other processes, remote_sock might not be closed when the process exits or execs. To ensure that we get an EOF, we use the ctid futex, which, thanks to CLONE.CHILD_CLEARTID, will be cleared and receive a futex wakeup when the child process exits or execs. When we see that futex wakeup (from Python, with the futex integrated into our event loop through launch_futex_monitor), we call shutdown(SHUT.RDWR) on the local socket from the parent. This results in future reads returning EOF. """ # These flags are mandatory; if we don't use CLONE_VM then CHILD_CLEARTID doesn't work # properly and our only other recourse to detect exec is to abuse robust futexes. flags |= CLONE.VM | CLONE.CHILD_CLEARTID # Open a channel which we'll use for the rsyscall connection [(access_sock, remote_sock)] = await connection.open_async_channels(1) # Create a trampoline that will start the new process running an rsyscall server trampoline = trampoline_func(remote_sock) # TODO it is unclear why we sometimes need to make a new mapping here, instead of # allocating with our normal allocator; all our memory is already MAP.SHARED, I think. # We should resolve this so we can use the normal allocator. arena = Arena(await task.mmap(4096 * 2, PROT.READ | PROT.WRITE, MAP.SHARED)) async def op( sem: RAM ) -> t.Tuple[t.Tuple[Pointer[Stack], WrittenPointer[Stack]], WrittenPointer[FutexNode]]: stack_value = loader.make_trampoline_stack(trampoline) stack_buf = await sem.malloc(Stack, 4096) stack = await stack_buf.write_to_end(stack_value, alignment=16) futex_pointer = await sem.ptr(FutexNode(None, Int32(1))) return stack, futex_pointer # Create the stack we'll need, and the zero-initialized futex stack, futex_pointer = await ram.perform_batch(op, arena) # it's important to start the processes in this order, so that the thread # process is the first process started; this is relevant in several # situations, including unshare(NEWPID) and manipulation of ns_last_pid child_process = await monitor.clone(flags, stack, ctid=futex_pointer) # We want to be able to rely on getting an EOF if the other side of the syscall # connection is no longer being read (e.g., if the process exits or execs). Since the # process might share its file descriptor table with other processes, remote_sock # might not be closed when the process exits or execs. To ensure that we get an EOF, # we use the ctid futex, which will be cleared on process exit or exec; we shutdown # access_sock when the ctid futex is cleared, to get an EOF. # We do this with launch_futex_monitor and a background coroutine. futex_process = await launch_futex_monitor(ram, loader, monitor, futex_pointer) async def shutdown_access_sock_on_futex_process_exit(): try: await futex_process.waitpid(W.EXITED) except SyscallError: # if the parent of the futex_process dies, this syscall # connection is broken anyway, so shut it down. pass await access_sock.handle.shutdown(SHUT.RDWR) # Running this in the background, without an associated object, is a bit dubious... reset(shutdown_access_sock_on_futex_process_exit()) # Set up the new task with appropriately inherited namespaces, tables, etc. # TODO correctly track all the namespaces we're in if flags & CLONE.NEWPID: pidns = far.PidNamespace(child_process.process.near.id) else: pidns = task.pidns if flags & CLONE.FILES: fd_table = task.fd_table else: fd_table = handle.FDTable(child_process.process.near.id, task.fd_table) child_task = Task(child_process.process, fd_table, task.address_space, pidns) child_task.sigmask = task.sigmask # Move ownership of the remote sock into the task and store it so it isn't closed remote_sock_handle = remote_sock.inherit(child_task) await remote_sock.invalidate() # Create the new syscall interface, which needs to use not just the connection, # but also the futex process. child_task.sysif = SyscallConnection( logger.getChild(str(child_process.process.near)), access_sock, access_sock, remote_sock_handle, remote_sock_handle, ) return child_process, child_task
async def _setup_stub( thread: Thread, bootstrap_sock: FileDescriptor, ) -> t.Tuple[t.List[str], Thread]: "Setup a stub thread" [(access_syscall_sock, passed_syscall_sock), (access_data_sock, passed_data_sock) ] = await thread.open_async_channels(2) # memfd for setting up the futex futex_memfd = await thread.task.memfd_create(await thread.ram.ptr( Path("child_robust_futex_list"))) # send the fds to the new process connection_fd, make_connection = await thread.connection.prep_fd_transfer() async def sendmsg_op(sem: RAM) -> WrittenPointer[SendMsghdr]: iovec = await sem.ptr(IovecList([await sem.malloc(bytes, 1)])) cmsgs = await sem.ptr( CmsgList([ CmsgSCMRights([ passed_syscall_sock, passed_data_sock, futex_memfd, connection_fd ]) ])) return await sem.ptr(SendMsghdr(None, iovec, cmsgs)) _, [] = await bootstrap_sock.sendmsg( await thread.ram.perform_batch(sendmsg_op), SendmsgFlags.NONE) # close our reference to fds that only the new process needs await passed_syscall_sock.invalidate() await passed_data_sock.invalidate() # close the socketpair await bootstrap_sock.invalidate() #### read describe to get all the information we need from the new process describe_buf = AsyncReadBuffer(access_data_sock) describe_struct = await describe_buf.read_cffi('struct rsyscall_unix_stub') argv_raw = await describe_buf.read_length_prefixed_array( describe_struct.argc) argv = [os.fsdecode(arg) for arg in argv_raw] environ = await describe_buf.read_envp(describe_struct.envp_count) #### build the new task pid = describe_struct.pid fd_table = handle.FDTable(pid) address_space = far.AddressSpace(pid) # we assume pid namespace is shared pidns = thread.task.pidns process = near.Process(pid) # we assume net namespace is shared - that's dubious... # we should make it possible to control the namespace sharing more, hmm. # TODO maybe the describe should contain the net namespace number? and we can store our own as well? # then we can automatically do it right base_task = Task(process, fd_table, address_space, pidns) remote_syscall_fd = base_task.make_fd_handle( near.FileDescriptor(describe_struct.syscall_fd)) base_task.sysif = SyscallConnection( logger.getChild(str(process)), access_syscall_sock, access_syscall_sock, remote_syscall_fd, remote_syscall_fd, ) allocator = memory.AllocatorClient.make_allocator(base_task) base_task.sigmask = Sigset( {SIG(bit) for bit in rsyscall.struct.bits(describe_struct.sigmask)}) ram = RAM( base_task, SocketMemoryTransport( access_data_sock, base_task.make_fd_handle( near.FileDescriptor(describe_struct.data_fd))), allocator) # TODO I think I can maybe elide creating this epollcenter and instead inherit it or share it, maybe? # I guess I need to write out the set too in describe epoller = await Epoller.make_root(ram, base_task) child_monitor = await ChildProcessMonitor.make(ram, base_task, epoller) connection = make_connection( base_task, ram, base_task.make_fd_handle( near.FileDescriptor(describe_struct.connecting_fd))) new_thread = Thread( task=base_task, ram=ram, connection=connection, loader=NativeLoader.make_from_symbols(base_task, describe_struct.symbols), epoller=epoller, child_monitor=child_monitor, environ=Environment.make_from_environ(base_task, ram, environ), stdin=base_task.make_fd_handle(near.FileDescriptor(0)), stdout=base_task.make_fd_handle(near.FileDescriptor(1)), stderr=base_task.make_fd_handle(near.FileDescriptor(2)), ) #### TODO set up futex I guess remote_futex_memfd = near.FileDescriptor(describe_struct.futex_memfd) return argv, new_thread
async def stdin_bootstrap( parent: Process, bootstrap_command: Command, ) -> t.Tuple[AsyncChildPid, Process]: """Create a process from running an arbitrary command which must run rsyscall-stdin-bootstrap bootstrap_command can be any arbitrary command, but it must eventually exec rsyscall-stdin-bootstrap, and pass down stdin when it does. We'll clone and exec bootstrap_command, passing down a socketpair for stdin, and try to bootstrap over the other end of the socketpair. Once rsyscall-stdin-bootstrap starts, it will respond to our bootstrap and we'll create a new process. """ #### clone and exec into the bootstrap command # create the socketpair that will be used as stdin stdin_pair = await (await parent.task.socketpair( AF.UNIX, SOCK.STREAM, 0, await parent.task.malloc(Socketpair))).read() parent_sock = stdin_pair.first child = await parent.fork() # set up stdin with socketpair await child.task.inherit_fd(stdin_pair.second).dup2(child.stdin) await stdin_pair.second.close() # exec child_pid = await child.exec(bootstrap_command) #### set up all the fds we'll want to pass over # the basic connections [(access_syscall_sock, passed_syscall_sock), (access_data_sock, passed_data_sock) ] = await parent.open_async_channels(2) # send the fds to the new process connection_fd, make_connection = await parent.connection.prep_fd_transfer() iovec = await parent.ptr(IovecList([await parent.malloc(bytes, 1)])) cmsgs = await parent.ptr( CmsgList([ CmsgSCMRights( [passed_syscall_sock, passed_data_sock, connection_fd]) ])) _, [] = await parent_sock.sendmsg( await parent.ptr(SendMsghdr(None, iovec, cmsgs)), SendmsgFlags.NONE) # close our reference to fds that only the new process needs await passed_syscall_sock.close() await passed_data_sock.close() # close the socketpair await parent_sock.close() #### read describe to get all the information we need from the new process describe_buf = AsyncReadBuffer(access_data_sock) describe_struct = await describe_buf.read_cffi( 'struct rsyscall_stdin_bootstrap') environ = await describe_buf.read_envp(describe_struct.envp_count) #### build the new task pid = describe_struct.pid fd_table = handle.FDTable(pid) address_space = far.AddressSpace(pid) # we assume pid namespace is shared # TODO include namespace inode numbers numbers in describe # note: if we start dealing with namespace numbers then we need to # have a Kernel namespace which tells us which kernel we get those # numbers from. # oh hey we can conveniently dump the inode numbers with getdents! pidns = parent.task.pidns # we assume mount namespace is not shared (can't hurt) mountns = far.MountNamespace(pid) pid = near.Pid(pid) base_task = Task(pid, fd_table, address_space, pidns, mountns) remote_syscall_fd = base_task.make_fd_handle( near.FileDescriptor(describe_struct.syscall_fd)) base_task.sysif = SyscallConnection( logger.getChild(str(pid)), access_syscall_sock, remote_syscall_fd, ) base_task.allocator = await memory.AllocatorClient.make_allocator(base_task ) # we assume our SignalMask is zero'd before being started, so we don't inherit it # TODO I think I can maybe elide creating this epollcenter and instead inherit it or share it, maybe? epoller = await Epoller.make_root(base_task) child_monitor = await ChildPidMonitor.make(base_task, epoller) connection = make_connection( base_task, base_task.make_fd_handle( near.FileDescriptor(describe_struct.connecting_fd))) new_parent = Process( task=base_task, connection=connection, loader=NativeLoader.make_from_symbols(base_task, describe_struct.symbols), epoller=epoller, child_monitor=child_monitor, environ=Environment.make_from_environ(base_task, environ), stdin=base_task.make_fd_handle(near.FileDescriptor(0)), stdout=base_task.make_fd_handle(near.FileDescriptor(1)), stderr=base_task.make_fd_handle(near.FileDescriptor(2)), ) return child_pid, new_parent