Пример #1
0
    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()
Пример #2
0
    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)
Пример #3
0
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
Пример #4
0
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
Пример #5
0
async def rsyscall_exec(
    parent: Thread,
    child: ChildThread,
    executable: RsyscallServerExecutable,
) -> None:
    """exec rsyscall-server and repair the thread to continue working after the exec

    This is of fairly limited use except as a stress-test for our primitives.

    We need to know about our parent thread because we need to create a new futex child
    process to wait for the child calling exec. We can't have the child itself create this
    futex process because the whole point of the futex process is to monitor for the child
    calling exec or exit; see ChildSyscallInterface. That futex process can be a child of
    anyone, so technically `parent` doesn't have to be our parent, it just needs to
    currently share its fd table with us.

    For the new futex process, we need to use a robust futex, registered on the
    robust_list.  The robust_list is, unfortunately, the only truly robust way to get
    notified of a process calling exec. We use ctid elsewhere, but the kernel has an
    irritating check where it only does a futex wakeup on ctid if the process's memory
    space is shared. The kernel always does the robust_list wakeups, so we can rely on the
    robust list even when we're working with processes that don't share address space.

    """
    [(access_data_sock, passed_data_sock)] = await child.open_async_channels(1)
    # create this guy and pass him down to the new thread
    child_futex_memfd = await child.task.memfd_create(await child.ram.ptr(
        Path("child_robust_futex_list")))
    parent_futex_memfd = child_futex_memfd.for_task(parent.task)
    if isinstance(child.task.sysif, ChildSyscallInterface):
        syscall = child.task.sysif
    else:
        raise Exception("can only exec in ChildSyscallInterface sysifs, not",
                        child.task.sysif)
    # unshare files so we can unset cloexec on fds to inherit
    await child.unshare_files(going_to_exec=True)
    # unset cloexec on all the fds we want to copy to the new space
    for fd in child.task.fd_handles:
        await fd.fcntl(F.SETFD, 0)

    def encode(fd: FileDescriptor) -> bytes:
        return str(int(fd.near)).encode()

    #### call exec and set up the new task
    await child.exec(
        executable.command.args(
            encode(passed_data_sock),
            encode(syscall.infd),
            encode(syscall.outfd),
            *[encode(fd) for fd in child.task.fd_handles],
        ), [child.monitor.sigfd.signal_block])
    if len(syscall.rsyscall_connection.pending_responses) == 1:
        # remove execve from pending_responses, we're never going to get a response to it
        syscall.rsyscall_connection.pending_responses = []
    else:
        raise Exception(
            "syscall connection in bad state; " +
            "expected one pending response for execve, instead got",
            syscall.rsyscall_connection.pending_responses)
    # a new address space needs a new allocator and transport; we mutate the RAM so things
    # that have stored the RAM continue to work.
    child.ram.allocator = memory.AllocatorClient.make_allocator(child.task)
    child.ram.transport = SocketMemoryTransport(access_data_sock,
                                                passed_data_sock,
                                                child.ram.allocator)
    # rsyscall-server will write the symbol table to passed_data_sock, and we'll read it
    # from access_data sock to set up the symbol table for the new address space
    child.loader = NativeLoader.make_from_symbols(
        child.task, await AsyncReadBuffer(access_data_sock).read_cffi(
            'struct rsyscall_symbol_table'))

    #### make new futex process
    # The futex process we used before is dead now that we've exec'd. We need to make some
    # syscalls in the child to set up the new futex process. ChildSyscallInterface would
    # throw immediately on seeing the current dead futex_process, so we need to null it out.
    syscall.futex_process = None
    # We have to use a robust futex now for our futex_process, see docstring
    parent_futex_ptr, child_futex_ptr = await setup_shared_memory_robust_futex(
        parent, parent_futex_memfd, child, child_futex_memfd)
    syscall.futex_process = await launch_futex_monitor(parent.ram,
                                                       parent.loader,
                                                       parent.monitor,
                                                       parent_futex_ptr)
    # now we are alive and fully working again, we can be used for GC
    child.task._add_to_active_fd_table_tasks()
Пример #6
0
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