Пример #1
0
async def ssh_forward(process: Process, ssh_command: SSHCommand,
                      local_path: Path, remote_path: str) -> AsyncChildPid:
    "Forward Unix socket connections to local_path to the socket at remote_path, over ssh"
    stdout_pipe = await (await
                         process.task.pipe(await
                                           process.task.malloc(Pipe))).read()
    async_stdout = await process.make_afd(stdout_pipe.read, set_nonblock=True)
    child = await process.fork()
    await child.task.inherit_fd(stdout_pipe.write).dup2(child.stdout)
    await child.task.chdir(await child.ptr(local_path.parent))
    child_pid = await child.exec(
        ssh_command.local_forward(
            "./" + local_path.name,
            remote_path,
            # TODO I optimistically assume that I'll have established a
            # connection through the tunnel before 1 minute has passed;
            # that connection will then keep the tunnel open.
        ).args("-n", "echo forwarded; exec sleep 60"))
    lines_buf = AsyncReadBuffer(async_stdout)
    forwarded = await lines_buf.read_line()
    if forwarded != b"forwarded":
        raise Exception(
            "ssh forwarding violated protocol, got instead of forwarded:",
            forwarded)
    await async_stdout.close()
    return child_pid
Пример #2
0
async def ssh_forward(thread: Thread, ssh_command: SSHCommand,
                      local_path: Path, remote_path: str) -> AsyncChildProcess:
    "Forward Unix socket connections to local_path to the socket at remote_path, over ssh"
    stdout_pipe = await (await
                         thread.task.pipe(await
                                          thread.ram.malloc(Pipe))).read()
    async_stdout = await thread.make_afd(stdout_pipe.read)
    child = await thread.fork()
    stdout = stdout_pipe.write.move(child.task)
    await child.unshare_files()
    await child.stdout.replace_with(stdout)
    child_process = await child.exec(
        ssh_command.local_forward(
            local_path,
            remote_path,
            # TODO I optimistically assume that I'll have established a
            # connection through the tunnel before 1 second has passed;
            # that connection will then keep the tunnel open.
        ).args("-n", "echo forwarded; exec sleep 1"))
    lines_buf = AsyncReadBuffer(async_stdout)
    forwarded = await lines_buf.read_line()
    if forwarded != b"forwarded":
        raise Exception(
            "ssh forwarding violated protocol, got instead of forwarded:",
            forwarded)
    await async_stdout.close()
    return child_process
Пример #3
0
 async def _run_responses(self) -> None:
     buffer = AsyncReadBuffer(self.fd)
     while True:
         req, cb = await self.response_queue.get_one()
         if isinstance(req, RsyscallSyscall):
             syscall = req
             self.logger.debug("going to read_result for syscall: %s %s", syscall, self.fd.handle.near)
             try:
                 value = (await buffer.read_struct(SyscallResponse)).value
             except Exception as exn:
                 hangup_exn = SyscallHangup()
                 hangup_exn.__cause__ = exn
                 cb.throw(hangup_exn)
             else:
                 cb.send(value)
         elif isinstance(req, Read):
             read = req
             self.logger.debug("going to read_length for data read of size %s", read.count)
             try:
                 data = await buffer.read_length(read.count)
             except Exception as exn:
                 hangup_exn = SyscallHangup()
                 hangup_exn.__cause__ = exn
                 cb.throw(hangup_exn)
             else:
                 cb.send(data)
         elif isinstance(req, Barrier):
             # We don't do anything - we just make sure this barrier is
             # sequenced relative to all other operations.
             cb.send(None)
         else:
             raise RuntimeError("invalid request", req)
Пример #4
0
async def make_bootstrap_dir(
    parent: Thread,
    ssh_command: SSHCommand,
    bootstrap_executable: FileDescriptor,
) -> t.AsyncGenerator[bytes, None]:
    """Over ssh, make a temporary directory containing the bootstrap executable, and start the socket bootstrap server

    The socket bootstrap server listens on two sockets in this temporary directory. One of
    them, we'll ssh forward back to the local host. The other, the main bootstrap process
    will connect to, to grab the listening socket fd for the former, so we can accept
    connections.

    We'll also use the bootstrap executable left in the temporary directory in
    ssh_bootstrap: we'll executed it to start the main bootstrap process.

    """
    stdout_pipe = await (await
                         parent.task.pipe(await
                                          parent.ram.malloc(Pipe))).read()
    async_stdout = await parent.make_afd(stdout_pipe.read)
    child = await parent.fork()
    async with child:
        await child.unshare_files_and_replace({
            child.stdout:
            stdout_pipe.write,
            child.stdin:
            bootstrap_executable,
        })
        child_process = await child.exec(
            ssh_command.args(ssh_bootstrap_script_contents))
        # from... local?
        # I guess this throws into sharper relief the distinction between core and module.
        # The ssh bootstrapping stuff should come from a different class,
        # which hardcodes the path,
        # and which works only for local tasks.
        # So in the meantime we'll continue to get it from task.filesystem.

        # sigh, openssh doesn't close its local stdout when it sees HUP/EOF on
        # the remote stdout. so we can't use EOF to signal end of our lines, and
        # instead have to have a sentinel to tell us when to stop reading.
        lines_buf = AsyncReadBuffer(async_stdout)
        tmp_path_bytes = await lines_buf.read_line()
        done = await lines_buf.read_line()
        if done != b"done":
            raise Exception(
                "socket binder violated protocol, got instead of done:", done)
        await async_stdout.close()
        logger.info("socket bootstrap done, got tmp path %s", tmp_path_bytes)
        yield tmp_path_bytes
        await child_process.check()
Пример #5
0
 async def _run_responses(self) -> None:
     buffer = AsyncReadBuffer(self.fromfd)
     while True:
         syscall, cb = await self.response_queue.get_one()
         self.logger.debug("going to read_result for syscall: %s %s",
                           syscall, self.fromfd.handle.near)
         try:
             value = (await buffer.read_struct(SyscallResponse)).value
         except Exception as exn:
             hangup_exn = SyscallHangup()
             hangup_exn.__cause__ = exn
             cb.throw(hangup_exn)
         else:
             cb.send(value)
Пример #6
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
Пример #7
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
Пример #8
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
Пример #9
0
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