Skip to content

Instantly share code, notes, and snippets.

@ellieayla
Created February 6, 2018 03:14
Show Gist options
  • Save ellieayla/69148d471bdd1b60ba66d17b26a02afd to your computer and use it in GitHub Desktop.
Save ellieayla/69148d471bdd1b60ba66d17b26a02afd to your computer and use it in GitHub Desktop.
#!/usr/bin/env python
# https://www.reddit.com/r/Python/comments/7v9kse/filesocket_descriptor_share_across_process/
# >>> hi, can we send a file/socket descriptor (with access and permissions) from one process to another ?
# >>> I mean not by forking but when process are already created .
"""
Demonstrate sending an open file descriptor between a pair of processes, the recipient of
which will read the file contents, depite not having permission to open(..., 'r') the file.
"""
import socket
import array
import sys
import multiprocessing
import os
import time
import pwd
import grp
import logging
logger = logging.getLogger()
logging.basicConfig(format='%(asctime)-15s pid=%(process)d %(side)s: %(message)s', level=logging.INFO)
# Function from https://docs.python.org/3/library/socket.html#socket.socket.recvmsg
def recv_fds(sock, msglen, maxfds):
fds = array.array("i") # Array of ints
msg, ancdata, flags, addr = sock.recvmsg(msglen, socket.CMSG_LEN(maxfds * fds.itemsize))
for cmsg_level, cmsg_type, cmsg_data in ancdata:
if (cmsg_level == socket.SOL_SOCKET and cmsg_type == socket.SCM_RIGHTS):
# Append data, ignoring any truncated integers at the end.
fds.fromstring(cmsg_data[:len(cmsg_data) - (len(cmsg_data) % fds.itemsize)])
return msg, list(fds)
# Function from https://docs.python.org/3/library/socket.html#socket.socket.sendmsg
def send_fds(sock, msg, fds):
return sock.sendmsg([msg], [(socket.SOL_SOCKET, socket.SCM_RIGHTS, array.array("i", fds))])
def receiver(socket_filename, source_filename):
"""
Drop permissions, open the socket file, block waiting for a file descriptor from socket, read from file descriptor.
"""
logger.info('Starting privledges: %d/%d = %s/%s' % \
(os.getuid(),
os.getgid(),
pwd.getpwuid(os.getuid())[0],
grp.getgrgid(os.getgid())[0]), extra={"side": "<== RECV"})
if os.getuid() == 0:
running_uid = pwd.getpwnam("nobody")[2]
running_gid = grp.getgrnam("nogroup")[2]
os.setgid(running_gid)
os.setuid(running_uid)
logger.info('Dropped privledges: currently %d/%d = %s/%s' % \
(os.getuid(),
os.getgid(),
pwd.getpwuid(os.getuid())[0],
grp.getgrgid(os.getgid())[0]), extra={"side": "<== RECV"})
try:
with open(source_filename):
logger.info("This process *does* have the ability to read the source file '%s' - it just won't." % source_filename, extra={"side": "<== RECV"})
except Exception as e:
logger.info("Proof that this process cannot open '%s': %s" % (source_filename, e), extra={"side": "<== RECV"})
logger.info("Receiver delaying creation of socket to demonstrate sender retries...", extra={"side": "<== RECV"})
time.sleep(2)
logger.info("Binding to (and creating) AF_UNIX socket socket file '%s'" % socket_filename, extra={"side": "<== RECV"})
sock = socket.socket(family=socket.AF_UNIX)
sock.bind(socket_filename)
sock.listen()
logger.info("Socket listening %s" % sock, extra={"side": "<== RECV"})
# Waste a file descriptor, so the fd numbers on source and receive sides don't match (they'll be both 6 by default)
leak_a_file_descriptor = open("/etc/hosts")
if not hasattr(sock, "recvmsg"):
raise RuntimeError("We don't have a `Socket.recvmsg` in this implementation of python (eg, system python 2.7 on OSX")
# Accept exactly 1 connection
client, info = sock.accept()
logger.info("Connected, client=%s" % client, extra={"side": "<== RECV"})
logger.info("Blocking until message is received", extra={"side": "<== RECV"})
msg, fds = recv_fds(client, 100, 4)
logger.info("Received message msg=%s fds=%s" % (msg, fds), extra={"side": "<== RECV"})
f = os.fdopen(fds[0])
logger.info("Opened fd %d => %s" % (fds[0], f), extra={"side": "<== RECV"})
logger.info("Printing first 5 lines of file content", extra={"side": "<== RECV"})
for line, n in zip(f, range(5)):
logger.info("%d ... %s" % (n, line.strip()), extra={"side": "<== RECV"})
f.close()
def sender(socket_filename, source_filename):
"""
Open socket file, open `source_filename`, send the open file descriptor to sibling process via socket.
"""
sock = socket.socket(family=socket.AF_UNIX)
logger.info("Connecting to socket file '%s'" % socket_filename, extra={"side": "SEND ==>"})
# Simple retry; receiver may not have created the socket_filename yet.
e = None
for _ in range(10):
try:
sock.connect(socket_filename)
break
except OSError as e:
logger.error("Socket file '%s' not available yet, try %d/10 (%s)" % (socket_filename, _, e), extra={"side": "SEND ==>"})
time.sleep(0.5)
pass
else: # nobreak
raise e
logger.info("Connected", extra={"side": "SEND ==>"})
logger.info("Sender delaying to demonstrate a blocking receiver...", extra={"side": "SEND ==>"})
time.sleep(4)
with open(source_filename, mode='r') as f:
logger.info("Opened source file %s" % source_filename, extra={"side": "SEND ==>"})
file_descriptor_int = f.fileno()
logger.info("Sending file descriptors %d" % file_descriptor_int, extra={"side": "SEND ==>"})
send_fds(sock, b"some payload", [file_descriptor_int])
if __name__ == "__main__":
socket_filename = sys.argv[1]
source_filename = sys.argv[2]
logger.info("Starting", extra={"side": "main"})
p_sender = multiprocessing.Process(target=sender, args=(socket_filename, source_filename))
p_sender.start()
try:
receiver(socket_filename, source_filename)
p_sender.join()
finally:
p_sender.terminate()
logger.info("Deleting socket file '%s'" % socket_filename, extra={"side": "main"})
os.unlink(socket_filename)
logger.info("Done", extra={"side": "main"})
@GreatBahram
Copy link

you can also use multiprocessing.reduction.sendfds function.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment