From 8a89fedb720112ce2cfbc08da15fe5ce65a594f0 Mon Sep 17 00:00:00 2001 From: Vincent Pelletier Date: Sun, 30 Jul 2023 13:33:06 +0000 Subject: [PATCH] WIP functionfs: Add support for systemd.socket-style gadget initialisation WIP: untested, subject to changes See (5)systemd.socket, ListenUSBFunction. In such setup, systemd opens endpoint zero, writes the descriptors and opens all endpoints. As a result: - endpoints are received as already-open file descriptors - neither function nor string descriptors may be written to endpoint 0 upon function activation Design decision: requiring the developer to provide a python representation of the function, which must always consistent with the descriptor provided to the kernel (which must be stored in a file prior to the function process starting) would only cause confusion. Instead, expect to receive the content of the file containing the function descriptor as received by systemd, and parse it to get the pieces of information necessary for event monitoring. All this is done without adding any dependency on systemd code, only making it possible to use in such context. --- functionfs/__init__.py | 198 ++++++++++++++++++++++++++++++++++------- 1 file changed, 165 insertions(+), 33 deletions(-) diff --git a/functionfs/__init__.py b/functionfs/__init__.py index 565c4bd..e5b4b67 100644 --- a/functionfs/__init__.py +++ b/functionfs/__init__.py @@ -129,6 +129,11 @@ ), } +_ENDPOINT_DESCRIPTOR_DICT = { + USB_DT_ENDPOINT_SIZE: USBEndpointDescriptorNoAudio, + USB_DT_ENDPOINT_AUDIO_SIZE: USBEndpointDescriptor, +} + _MARKER = object() _EMPTY_DICT = {} # For internal ** falback usage def getInterfaceInAllSpeeds(interface, endpoint_list, class_descriptor_list=()): @@ -651,8 +656,8 @@ class EndpointINFile(EndpointFile): """ def __init__(self, path, submit, eventfd): """ - path (string) - Endpoint file path. + path (string, int) + Endpoint file path or file descriptor. submit (AIOContext.submit) To submit AIOBlocks. eventfd (EventFD) @@ -807,8 +812,8 @@ class EndpointOUTFile(EndpointFile): """ def __init__(self, path, submit, release, aio_block_list): """ - path (string) - Endpoint file path. + path (string, int) + Endpoint file path or file descriptor. submit (AIOContext.submit) To submit AIOBlocks to after completion. release ((AIOBlock) -> None) @@ -937,6 +942,8 @@ def __init__( in_aio_blocks_max=32, out_aio_blocks_per_endpoint=2, out_aio_blocks_max_packet_count=10, + function_descriptor=None, + function_strings=None, ): """ path (string) @@ -972,33 +979,137 @@ def __init__( out_aio_blocks_per_endpoint * sum_OUT_wMaxPacketSize * out_aio_blocks_max_packet_count So by default 10kB per 512-bytes OUT endpoint will be allocated. + function_descriptor (bytes) + When provided, this value is parsed as a serialised function + descriptor in order to prepare AIO blocks, and no descriptor + (neither function nor string) will be written to endpoint 0 during + __enter__. The following arguments are ignored: + fs_list, hs_list, ss_list + os_list + lang_dict + all_ctrl_recip, config0_setup + This is intended to be used with systemd ListenUSBFunction feature, + in which case the value provided here should be the content of the + file provided as USBFunctionDescriptors. + function_strings (bytes) + When function_descriptor, this value is stored in the + _function_strings property without any processing. + Otherwise, it is ignored. """ self._path = path self._ep_address_dict = ep_address_dict = {} self._eventfd = eventfd = libaio.EventFD(flags=libaio.EFD_NONBLOCK) + if function_descriptor is None: + self._need_descriptors = True + cannot_use_eventfd = self.quirks_ffs_unsafe_eventfd + flags = 0 + if all_ctrl_recip: + flags |= ALL_CTRL_RECIP + if config0_setup: + flags |= CONFIG0_SETUP + self._function_descriptor = getDescsV2( + flags, + fs_list=fs_list, + hs_list=hs_list, + ss_list=ss_list, + os_list=os_list, + eventfd=( + None + if cannot_use_eventfd else + eventfd + ), + ) + self._function_strings = getStrings(dict(lang_dict)) + else: + self._need_descriptors = False + cannot_use_eventfd = True + self._function_descriptor = function_descriptor + self._function_strings = function_strings + # Extract Endpoint descriptors from function_descriptor. + # This code assumes that the kernel validated the content of + # function_descriptor, so explicit checks are only done to detect + # cases where this code: + # - could be stale (ex: new magic) + # - cannot handle what the kernel received (ex: eventfd flag set) + # - would severely misbehave (ex: infinite loop) + # For other errors, rely on python raising generic exceptions. + def parse(ctype_class): + return ( + getattr(ctype_class, 'from_buffer_copy')( + function_descriptor, + offset, + ), + offset + ctypes.sizeof(ctype_class), + ) + def parseUSBInterfaceList(count): + # Only append endpoint descriptors to result, as this is all + # this method needs, and they will not be used outside of it. + result = [] + local_offset = 0 + for _ in range(count): + need_interface = True + while True: + head = USBDescriptorHeader.from_buffer_copy( + function_descriptor, + offset + local_offset, + ) + if need_interface: + if head.bDescriptorType != ch9.USB_DT_INTERFACE: + raise ValueError( + 'First descriptor is expected to be of type USB_DT_INTERFACE' + ) + need_interface = False + else: + if head.bDescriptorType == ch9.USB_DT_INTERFACE: + break + if head.bDescriptorType == ch9.USB_DT_ENDPOINT: + result.append( + _ENDPOINT_DESCRIPTOR_DICT[ + head.bLength + ].from_buffer_copy( + function_descriptor, + offset + local_offset, + ) + ) + local_offset += head.bLength + return ( + result, + offset + local_offset, + ) + head, offset = parse(DescsHeadV2) + if head.magic != DESCRIPTORS_MAGIC_V2: + raise ValueError( + 'Unknown function_descriptor magic: %#08x, kernel=%r ' + 'Please report this error.' % (head.magic, _KERNEL_VERSION), + ) + if head.flags & EVENTFD: + # XXX: is it even possible to add support for this ? + raise ValueError( + 'function_descriptor has unsupported EVENTFD flag set', + ) + all_ctrl_recip = bool(head.flags & ALL_CTRL_RECIP) + config0_setup = bool(head.flags & CONFIG0_SETUP) + if head.flags & HAS_FS_DESC: + fs_count, offset = parse(le32) + else: + fs_count = 0 + if head.flags & HAS_HS_DESC: + hs_count, offset = parse(le32) + else: + hs_count = 0 + if head.flags & HAS_SS_DESC: + ss_count, offset = parse(le32) + else: + ss_count = 0 + if head.flags & HAS_MS_OS_DESC: + # Do not parse OS descriptors + _, offset = parse(le32) + fs_list, offset = parseUSBInterfaceList(fs_count) + hs_list, offset = parseUSBInterfaceList(hs_count) + ss_list, offset = parseUSBInterfaceList(ss_count) + self.__cannot_use_eventfd = cannot_use_eventfd self.all_ctrl_recip = all_ctrl_recip self.config0_setup = config0_setup - flags = 0 - if all_ctrl_recip: - flags |= ALL_CTRL_RECIP - if config0_setup: - flags |= CONFIG0_SETUP - self.__quirks_ffs_unsafe_eventfd = quirks_ffs_unsafe_eventfd = ( - self.quirks_ffs_unsafe_eventfd - ) - self._function_descriptor = getDescsV2( - flags, - fs_list=fs_list, - hs_list=hs_list, - ss_list=ss_list, - os_list=os_list, - eventfd=( - None - if quirks_ffs_unsafe_eventfd else - eventfd - ), - ) - self._function_strings = getStrings(dict(lang_dict)) self._out_aio_block_list = out_aio_block_list = [] self._out_aio_block_dict = out_aio_block_dict = {} self._ep_descriptor_list = ep_descriptor_list = [] @@ -1035,7 +1146,7 @@ def __init__( ep_aio_block_list.append(out_block) out_aio_block_list.append(out_block) self._ep0_event_list = _EP0_EVENT_LIST_TYPE() - if quirks_ffs_unsafe_eventfd: + if cannot_use_eventfd: # Piggy-back on the "in" AIO context for ep0 poll AIO block in_aio_blocks_max += 1 self.__ep0_aio_block = libaio.AIOBlock( @@ -1056,21 +1167,42 @@ def __init__( in_aio_blocks_max, ) - def __enter__(self): + def __enter__(self, ep_name_list=()): """ Sends descriptor to kernel and opens endpoint files. + + ep_name_list (list of int) + If provided, this must be the file descriptors of the files + to use for all endpoints managed by the function, starting with + endpoint 0, followed by every endpoint referenced in the + function's descriptor, in the same order as their descriptor. + This is intended to be used with systemd ListenUSBFunction feature, + filled with the list provided by (3)sd_listen_fds. """ if self._open: raise RuntimeError('Context manager already active') try: - ep0 = Endpoint0File(os.path.join(self._path, 'ep0')) + ep_descriptor_list = self._ep_descriptor_list + if ep_name_list: + if len(ep_name_list) != len(ep_descriptor_list) + 1: + raise ValueError( + 'Expected %i endpoint file descriptors, got %i', + ) + ep0_name = ep_name_list[0] + else: + ep0_name = os.path.join(self._path, 'ep0') + ep0 = Endpoint0File(ep0_name) self._ep_list = ep_list = [ep0] - ep0.write(serialise(self._function_descriptor)) - ep0.write(serialise(self._function_strings)) + if self._need_descriptors: + ep0.write(serialise(self._function_descriptor)) + ep0.write(serialise(self._function_strings)) out_aio_block_dict = self._out_aio_block_dict for descriptor in self._ep_descriptor_list: index = len(ep_list) - endpoint_path = os.path.join(self._path, 'ep%u' % (index, )) + if ep_name_list: + endpoint_path = ep_name_list[index] + else: + endpoint_path = os.path.join(self._path, 'ep%u' % (index, )) is_in = bool(descriptor.bEndpointAddress & ch9.USB_DIR_IN) endpoint_class = self.getEndpointClass( is_in=is_in, @@ -1097,7 +1229,7 @@ def __enter__(self): fcntl.F_SETFL, fcntl.fcntl(ep0, fcntl.F_GETFL) | os.O_NONBLOCK, ) - if self.__quirks_ffs_unsafe_eventfd: + if self.__cannot_use_eventfd: self.__ep0_aio_block.target_file = ep0 self._in_aio_context.submit((self.__ep0_aio_block, )) except: @@ -1110,7 +1242,7 @@ def __unenter(self): """ Undo what __enter__ did. """ - if self.__quirks_ffs_unsafe_eventfd: + if self.__cannot_use_eventfd: self.__ep0_aio_block.target_file = None self._in_aio_context.cancelAll() out_aio_context = self._out_aio_context