Foundry/tt/ft_tt_udrv_pci_sysfs.c

384 lines
12 KiB
C

/* SPDX-License-Identifier: AGPL-3.0-or-later */
/* Copyright (C) 2026 David (Corey) Herrera */
/**
* @file ft_tt_udrv_pci_sysfs.c
* @brief Implementation of sysfs-based PCI discovery for the userspace driver.
*
* @status Discovery and parsing are fixture-gated; ft_tt_udrv_pci_map_bar()
* has mapped BAR0 of the live p150a (2026-08-16).
* @issues None
* @todo None
*/
#define _POSIX_C_SOURCE 200809L
#include "ft_tt_udrv_pci_sysfs.h"
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
#include "ft_tt_udrv.h"
/* Linux `include/linux/ioport.h` resource flags, the ones `resource` can carry
* for a BAR. Restated rather than included so this compiles on the Mac, where
* the code is written. */
#define IORESOURCE_IO 0x00000100u
#define IORESOURCE_MEM 0x00000200u
#define IORESOURCE_PREFETCH 0x00002000u
#define IORESOURCE_MEM_64 0x00100000u
/** Longest single line the `resource` file can produce, with margin. */
#define RESOURCE_LINE_MAX 128u
/** Read a whole small sysfs attribute into `buffer`. */
static int read_attribute(const char *device_dir, const char *name, char *buffer,
size_t buffer_bytes)
{
char path[FT_TT_UDRV_PCI_PATH_MAX];
FILE *file;
size_t length;
int written;
written = snprintf(path, sizeof(path), "%s/%s", device_dir, name);
if (written < 0 || (size_t)written >= sizeof(path))
return FT_TT_UDRV_EINVAL;
file = fopen(path, "r");
if (!file)
return FT_TT_UDRV_ENODEV;
if (fgets(buffer, (int)buffer_bytes, file) == NULL) {
(void)fclose(file);
return FT_TT_UDRV_ENODEV;
}
(void)fclose(file);
length = strlen(buffer);
while (length > 0u && (buffer[length - 1u] == '\n' || buffer[length - 1u] == '\r'))
buffer[--length] = '\0';
return (length > 0u) ? FT_TT_UDRV_OK : FT_TT_UDRV_ENODEV;
}
/**
* Read an attribute holding one hexadecimal number ("0x1e52").
*
* strtoull with a checked end pointer, not sscanf: a file whose contents are
* "0x" or "banana" must be a refusal, and sscanf("%x") reports success on the
* first of those.
*/
static int read_hex_attribute(const char *device_dir, const char *name, uint64_t *out_value)
{
char buffer[64];
char *end = NULL;
unsigned long long value;
int rc;
rc = read_attribute(device_dir, name, buffer, sizeof(buffer));
if (rc != FT_TT_UDRV_OK)
return rc;
errno = 0;
value = strtoull(buffer, &end, 0);
if (errno != 0 || end == buffer || (end && *end != '\0'))
return FT_TT_UDRV_ENODEV;
*out_value = (uint64_t)value;
return FT_TT_UDRV_OK;
}
/** Decode the flag word into the booleans callers actually ask about. */
static void classify_bar(ft_tt_udrv_pci_bar *bar)
{
/* The kernel's own pci_resource_len(): zero when `end` is zero. A BAR the
* device does not implement prints "0x0 0x0 0x0", and a one-byte BAR would
* have end == start != 0, so this rule and "end > start" are NOT the same
* and only this one matches the kernel. */
bar->length = (bar->end == 0u) ? 0u : (bar->end - bar->start + 1u);
bar->present = (bar->length > 0u);
bar->memory = (bar->flags & IORESOURCE_MEM) != 0u;
bar->io = (bar->flags & IORESOURCE_IO) != 0u;
bar->prefetchable = (bar->flags & IORESOURCE_PREFETCH) != 0u;
bar->is_64bit = (bar->flags & IORESOURCE_MEM_64) != 0u;
}
/** Parse the first FT_TT_UDRV_PCI_BAR_COUNT lines of `resource`. */
static int read_resource_table(const char *device_dir, ft_tt_udrv_pci_device *out_device)
{
char path[FT_TT_UDRV_PCI_PATH_MAX];
char line[RESOURCE_LINE_MAX];
FILE *file;
unsigned index;
int written;
written = snprintf(path, sizeof(path), "%s/resource", device_dir);
if (written < 0 || (size_t)written >= sizeof(path))
return FT_TT_UDRV_EINVAL;
file = fopen(path, "r");
if (!file)
return FT_TT_UDRV_ENODEV;
for (index = 0u; index < FT_TT_UDRV_PCI_BAR_COUNT; index++) {
ft_tt_udrv_pci_bar *bar = &out_device->bar[index];
unsigned long long start = 0;
unsigned long long end = 0;
unsigned long long flags = 0;
if (fgets(line, (int)sizeof(line), file) == NULL) {
/* A `resource` file with fewer than six BAR lines is not a device
* we understand. Refuse rather than leave BARs zeroed, which would
* read as "absent" and silently lose a real aperture. */
(void)fclose(file);
return FT_TT_UDRV_ENODEV;
}
if (sscanf(line, "%llx %llx %llx", &start, &end, &flags) != 3) {
(void)fclose(file);
return FT_TT_UDRV_ENODEV;
}
bar->start = (uint64_t)start;
bar->end = (uint64_t)end;
bar->flags = (uint64_t)flags;
classify_bar(bar);
}
(void)fclose(file);
return FT_TT_UDRV_OK;
}
/** Copy the basename of a device directory into `out_address`. */
static int extract_address(const char *device_dir, char *out_address, size_t address_bytes)
{
const char *slash = strrchr(device_dir, '/');
const char *base = slash ? (slash + 1) : device_dir;
size_t length = strlen(base);
if (length == 0u || length >= address_bytes)
return FT_TT_UDRV_EINVAL;
memcpy(out_address, base, length + 1u);
return FT_TT_UDRV_OK;
}
int ft_tt_udrv_pci_read_device(const char *device_dir, ft_tt_udrv_pci_device *out_device)
{
uint64_t value = 0;
size_t length;
int rc;
if (!device_dir || !out_device)
return FT_TT_UDRV_EINVAL;
length = strlen(device_dir);
if (length == 0u || length >= sizeof(out_device->sysfs_path))
return FT_TT_UDRV_EINVAL;
memset(out_device, 0, sizeof(*out_device));
memcpy(out_device->sysfs_path, device_dir, length + 1u);
rc = extract_address(device_dir, out_device->address, sizeof(out_device->address));
if (rc != FT_TT_UDRV_OK)
return rc;
rc = read_hex_attribute(device_dir, "vendor", &value);
if (rc != FT_TT_UDRV_OK)
return rc;
out_device->vendor = (uint16_t)value;
rc = read_hex_attribute(device_dir, "device", &value);
if (rc != FT_TT_UDRV_OK)
return rc;
out_device->device = (uint16_t)value;
/* Subsystem ids, class and revision are informational — a device that does
* not publish them is still usable, so they default to 0 rather than
* failing the read. */
if (read_hex_attribute(device_dir, "subsystem_vendor", &value) == FT_TT_UDRV_OK)
out_device->subsystem_vendor = (uint16_t)value;
if (read_hex_attribute(device_dir, "subsystem_device", &value) == FT_TT_UDRV_OK)
out_device->subsystem_device = (uint16_t)value;
if (read_hex_attribute(device_dir, "class", &value) == FT_TT_UDRV_OK)
out_device->class_code = (uint32_t)value;
if (read_hex_attribute(device_dir, "revision", &value) == FT_TT_UDRV_OK)
out_device->revision = (uint8_t)value;
return read_resource_table(device_dir, out_device);
}
/** qsort comparator over device address, so enumeration order is stable. */
static int compare_by_address(const void *left, const void *right)
{
const ft_tt_udrv_pci_device *a = left;
const ft_tt_udrv_pci_device *b = right;
return strcmp(a->address, b->address);
}
int ft_tt_udrv_pci_find_devices(const char *devices_dir, uint16_t vendor, uint16_t device,
ft_tt_udrv_pci_device *out_devices, size_t capacity,
size_t *out_count)
{
const char *root = devices_dir ? devices_dir : FT_TT_UDRV_PCI_DEFAULT_DEVICES_DIR;
struct dirent *entry;
DIR *dir;
size_t found = 0u;
if (!out_count || (capacity > 0u && !out_devices))
return FT_TT_UDRV_EINVAL;
*out_count = 0u;
dir = opendir(root);
if (!dir)
return FT_TT_UDRV_ENODEV;
while ((entry = readdir(dir)) != NULL) {
char path[FT_TT_UDRV_PCI_PATH_MAX];
ft_tt_udrv_pci_device candidate;
int written;
if (entry->d_name[0] == '.')
continue;
written = snprintf(path, sizeof(path), "%s/%s", root, entry->d_name);
if (written < 0 || (size_t)written >= sizeof(path))
continue;
/* A directory that is not a PCI device, or one we cannot parse, is
* skipped rather than fatal: /sys/bus/pci/devices holds symlinks to
* every device on the machine and most are not ours. */
if (ft_tt_udrv_pci_read_device(path, &candidate) != FT_TT_UDRV_OK)
continue;
if (candidate.vendor != vendor)
continue;
if (device != 0xFFFFu && candidate.device != device)
continue;
if (found < capacity)
out_devices[found] = candidate;
found++;
}
(void)closedir(dir);
if (found > 0u && capacity > 0u) {
size_t sorted = (found < capacity) ? found : capacity;
qsort(out_devices, sorted, sizeof(*out_devices), compare_by_address);
}
*out_count = found;
return FT_TT_UDRV_OK;
}
int ft_tt_udrv_pci_resource_path(const ft_tt_udrv_pci_device *device, unsigned bar,
bool write_combine, char *out_path, size_t path_bytes)
{
int written;
if (!device || !out_path || bar >= FT_TT_UDRV_PCI_BAR_COUNT)
return FT_TT_UDRV_EINVAL;
if (!device->bar[bar].present)
return FT_TT_UDRV_EINVAL;
/* The PCI core only creates `resourceN_wc` for a prefetchable BAR, so
* asking for one on a non-prefetchable BAR would hand back a path that
* does not exist. Refuse here, where the reason is visible, rather than at
* an ENOENT three layers up. */
if (write_combine && !device->bar[bar].prefetchable)
return FT_TT_UDRV_EINVAL;
written = snprintf(out_path, path_bytes, "%s/resource%u%s", device->sysfs_path, bar,
write_combine ? "_wc" : "");
if (written < 0 || (size_t)written >= path_bytes)
return FT_TT_UDRV_EINVAL;
return FT_TT_UDRV_OK;
}
int ft_tt_udrv_pci_map_bar(const ft_tt_udrv_pci_device *device, unsigned bar, bool write_combine,
bool writable, uint64_t offset, size_t length, void **out_mapping)
{
char path[FT_TT_UDRV_PCI_PATH_MAX];
const ft_tt_udrv_pci_bar *description;
void *mapping;
long page_size;
int file;
int rc;
if (out_mapping)
*out_mapping = NULL;
if (!device || !out_mapping || bar >= FT_TT_UDRV_PCI_BAR_COUNT || length == 0u)
return FT_TT_UDRV_EINVAL;
/* Range-check against the BAR the KERNEL published, not against a length
* the caller believes. A window aimed one register past the end of the
* register file is not an error anywhere — it is a different register. */
description = &device->bar[bar];
if (!description->present)
return FT_TT_UDRV_EINVAL;
if (offset > description->length || (uint64_t)length > description->length - offset)
return FT_TT_UDRV_EINVAL;
page_size = sysconf(_SC_PAGESIZE);
if (page_size <= 0)
return FT_TT_UDRV_EIO;
/* mmap(2) requires a page-aligned offset, and the PCI core's own
* pci_mmap_resource() computes a pfn from it — an unaligned offset there
* would silently map the wrong page rather than fail. */
if ((offset % (uint64_t)page_size) != 0u)
return FT_TT_UDRV_EINVAL;
rc = ft_tt_udrv_pci_resource_path(device, bar, write_combine, path, sizeof(path));
if (rc != FT_TT_UDRV_OK)
return rc;
file = open(path, writable ? (O_RDWR | O_CLOEXEC) : (O_RDONLY | O_CLOEXEC));
if (file < 0)
return FT_TT_UDRV_EIO;
mapping = mmap(NULL, length, writable ? (PROT_READ | PROT_WRITE) : PROT_READ, MAP_SHARED,
file, (off_t)offset);
/* The mapping outlives the descriptor, so there is nothing to keep open —
* and an fd we do not hold is an fd a fork cannot leak. */
(void)close(file);
if (mapping == MAP_FAILED)
return FT_TT_UDRV_EIO;
*out_mapping = mapping;
return FT_TT_UDRV_OK;
}
int ft_tt_udrv_pci_unmap_bar(void *mapping, size_t length)
{
if (!mapping)
return FT_TT_UDRV_OK;
if (length == 0u)
return FT_TT_UDRV_EINVAL;
return (munmap(mapping, length) == 0) ? FT_TT_UDRV_OK : FT_TT_UDRV_EIO;
}