1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-05-15 17:54:57 +00:00
serenity/Kernel/Devices/USB/UHCIController.cpp
Brian Gianforcaro d76dedb381 Kernel: Fix UHCIController singleton startup null-deref race condition.
The following KUBSAN crash on startup was reported on discord:

```
UHCI: Started
KUBSAN: reference binding to null pointer of type struct UHCIController
KUBSAN: at ../../Kernel/Devices/USB/UHCIController.cpp, line 67
```

After inspecting the code, it became clear that there's a window of time
where the kernel task which monitors the UHCI port can startup and start
executing before the UHCIController constructor completes. This leaves
the singleton pointing to nullptr, thus in the duration of this race
window the "UHCI port proc" thread will go an and de-reference the null
pointer when trying to read for status changes on the UHCI root ports.

Reported-by: @stelar7
Reported-by: @bcoles

Fixes: #6154
2021-05-15 09:46:41 +02:00

441 lines
18 KiB
C++

/*
* Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
* Copyright (c) 2020, Jesse Buhagiar <jooster669@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Platform.h>
#include <Kernel/CommandLine.h>
#include <Kernel/Debug.h>
#include <Kernel/Devices/USB/UHCIController.h>
#include <Kernel/Process.h>
#include <Kernel/StdLib.h>
#include <Kernel/Time/TimeManagement.h>
#include <Kernel/VM/AnonymousVMObject.h>
#include <Kernel/VM/MemoryManager.h>
static constexpr u8 MAXIMUM_NUMBER_OF_TDS = 128; // Upper pool limit. This consumes the second page we have allocated
static constexpr u8 MAXIMUM_NUMBER_OF_QHS = 64;
namespace Kernel::USB {
static UHCIController* s_the;
static constexpr u16 UHCI_USBCMD_RUN = 0x0001;
static constexpr u16 UHCI_USBCMD_HOST_CONTROLLER_RESET = 0x0002;
static constexpr u16 UHCI_USBCMD_GLOBAL_RESET = 0x0004;
static constexpr u16 UHCI_USBCMD_ENTER_GLOBAL_SUSPEND_MODE = 0x0008;
static constexpr u16 UHCI_USBCMD_FORCE_GLOBAL_RESUME = 0x0010;
static constexpr u16 UHCI_USBCMD_SOFTWARE_DEBUG = 0x0020;
static constexpr u16 UHCI_USBCMD_CONFIGURE_FLAG = 0x0040;
static constexpr u16 UHCI_USBCMD_MAX_PACKET = 0x0080;
static constexpr u16 UHCI_USBSTS_HOST_CONTROLLER_HALTED = 0x0020;
static constexpr u16 UHCI_USBSTS_HOST_CONTROLLER_PROCESS_ERROR = 0x0010;
static constexpr u16 UHCI_USBSTS_PCI_BUS_ERROR = 0x0008;
static constexpr u16 UHCI_USBSTS_RESUME_RECEIVED = 0x0004;
static constexpr u16 UHCI_USBSTS_USB_ERROR_INTERRUPT = 0x0002;
static constexpr u16 UHCI_USBSTS_USB_INTERRUPT = 0x0001;
static constexpr u8 UHCI_USBINTR_TIMEOUT_CRC_ENABLE = 0x01;
static constexpr u8 UHCI_USBINTR_RESUME_INTR_ENABLE = 0x02;
static constexpr u8 UHCI_USBINTR_IOC_ENABLE = 0x04;
static constexpr u8 UHCI_USBINTR_SHORT_PACKET_INTR_ENABLE = 0x08;
static constexpr u16 UHCI_FRAMELIST_FRAME_COUNT = 1024; // Each entry is 4 bytes in our allocated page
static constexpr u16 UHCI_FRAMELIST_FRAME_INVALID = 0x0001;
// Port stuff
static constexpr u8 UHCI_ROOT_PORT_COUNT = 2;
static constexpr u16 UHCI_PORTSC_CURRRENT_CONNECT_STATUS = 0x0001;
static constexpr u16 UHCI_PORTSC_CONNECT_STATUS_CHANGED = 0x0002;
static constexpr u16 UHCI_PORTSC_PORT_ENABLED = 0x0004;
static constexpr u16 UHCI_PORTSC_PORT_ENABLE_CHANGED = 0x0008;
static constexpr u16 UHCI_PORTSC_LINE_STATUS = 0x0030;
static constexpr u16 UHCI_PORTSC_RESUME_DETECT = 0x40;
static constexpr u16 UHCI_PORTSC_LOW_SPEED_DEVICE = 0x0100;
static constexpr u16 UHCI_PORTSC_PORT_RESET = 0x0200;
static constexpr u16 UHCI_PORTSC_SUSPEND = 0x1000;
// *BSD and a few other drivers seem to use this number
static constexpr u8 UHCI_NUMBER_OF_ISOCHRONOUS_TDS = 128;
static constexpr u16 UHCI_NUMBER_OF_FRAMES = 1024;
UHCIController& UHCIController::the()
{
return *s_the;
}
UNMAP_AFTER_INIT void UHCIController::detect()
{
if (kernel_command_line().disable_uhci_controller())
return;
PCI::enumerate([&](const PCI::Address& address, PCI::ID id) {
if (address.is_null())
return;
if (PCI::get_class(address) == 0xc && PCI::get_subclass(address) == 0x03 && PCI::get_programming_interface(address) == 0) {
if (!s_the) {
s_the = new UHCIController(address, id);
s_the->spawn_port_proc();
}
}
});
}
UNMAP_AFTER_INIT UHCIController::UHCIController(PCI::Address address, PCI::ID id)
: PCI::Device(address)
, m_io_base(PCI::get_BAR4(pci_address()) & ~1)
{
dmesgln("UHCI: Controller found {} @ {}", id, address);
dmesgln("UHCI: I/O base {}", m_io_base);
dmesgln("UHCI: Interrupt line: {}", PCI::get_interrupt_line(pci_address()));
reset();
start();
}
UNMAP_AFTER_INIT UHCIController::~UHCIController()
{
}
void UHCIController::reset()
{
stop();
write_usbcmd(UHCI_USBCMD_HOST_CONTROLLER_RESET);
// FIXME: Timeout
for (;;) {
if (read_usbcmd() & UHCI_USBCMD_HOST_CONTROLLER_RESET)
continue;
break;
}
// Let's allocate the physical page for the Frame List (which is 4KiB aligned)
auto framelist_vmobj = ContiguousVMObject::create_with_size(PAGE_SIZE);
m_framelist = MemoryManager::the().allocate_kernel_region_with_vmobject(*framelist_vmobj, PAGE_SIZE, "UHCI Framelist", Region::Access::Write);
dbgln("UHCI: Allocated framelist at physical address {}", m_framelist->physical_page(0)->paddr());
dbgln("UHCI: Framelist is at virtual address {}", m_framelist->vaddr());
write_sofmod(64); // 1mS frame time
create_structures();
setup_schedule();
write_flbaseadd(m_framelist->physical_page(0)->paddr().get()); // Frame list (physical) address
write_frnum(0); // Set the initial frame number
// Enable all interrupt types
write_frnum(UHCI_USBINTR_TIMEOUT_CRC_ENABLE | UHCI_USBINTR_RESUME_INTR_ENABLE | UHCI_USBINTR_IOC_ENABLE | UHCI_USBINTR_SHORT_PACKET_INTR_ENABLE);
dbgln("UHCI: Reset completed");
}
UNMAP_AFTER_INIT void UHCIController::create_structures()
{
// Let's allocate memory for both the QH and TD pools
// First the QH pool and all of the Interrupt QH's
auto qh_pool_vmobject = ContiguousVMObject::create_with_size(2 * PAGE_SIZE);
m_qh_pool = MemoryManager::the().allocate_kernel_region_with_vmobject(*qh_pool_vmobject, 2 * PAGE_SIZE, "UHCI Queue Head Pool", Region::Access::Write);
memset(m_qh_pool->vaddr().as_ptr(), 0, 2 * PAGE_SIZE); // Zero out both pages
// Let's populate our free qh list (so we have some we can allocate later on)
m_free_qh_pool.resize(MAXIMUM_NUMBER_OF_TDS);
for (size_t i = 0; i < m_free_qh_pool.size(); i++) {
auto placement_addr = reinterpret_cast<void*>(m_qh_pool->vaddr().get() + (i * sizeof(QueueHead)));
auto paddr = static_cast<u32>(m_qh_pool->physical_page(0)->paddr().get() + (i * sizeof(QueueHead)));
m_free_qh_pool.at(i) = new (placement_addr) QueueHead(paddr);
}
// Create the Full Speed, Low Speed Control and Bulk Queue Heads
m_interrupt_transfer_queue = allocate_queue_head();
m_lowspeed_control_qh = allocate_queue_head();
m_fullspeed_control_qh = allocate_queue_head();
m_bulk_qh = allocate_queue_head();
m_dummy_qh = allocate_queue_head();
// Now the Transfer Descriptor pool
auto td_pool_vmobject = ContiguousVMObject::create_with_size(2 * PAGE_SIZE);
m_td_pool = MemoryManager::the().allocate_kernel_region_with_vmobject(*td_pool_vmobject, 2 * PAGE_SIZE, "UHCI Transfer Descriptor Pool", Region::Access::Write);
memset(m_td_pool->vaddr().as_ptr(), 0, 2 * PAGE_SIZE);
// Set up the Isochronous Transfer Descriptor list
m_iso_td_list.resize(UHCI_NUMBER_OF_ISOCHRONOUS_TDS);
for (size_t i = 0; i < m_iso_td_list.size(); i++) {
auto placement_addr = reinterpret_cast<void*>(m_td_pool->vaddr().get() + (i * sizeof(Kernel::USB::TransferDescriptor)));
auto paddr = static_cast<u32>(m_td_pool->physical_page(0)->paddr().get() + (i * sizeof(Kernel::USB::TransferDescriptor)));
// Place a new Transfer Descriptor with a 1:1 in our region
// The pointer returned by `new()` lines up exactly with the value
// that we store in `paddr`, meaning our member functions directly
// access the raw descriptor (that we later send to the controller)
m_iso_td_list.at(i) = new (placement_addr) Kernel::USB::TransferDescriptor(paddr);
auto transfer_descriptor = m_iso_td_list.at(i);
transfer_descriptor->set_in_use(true); // Isochronous transfers are ALWAYS marked as in use (in case we somehow get allocated one...)
transfer_descriptor->set_isochronous();
transfer_descriptor->link_queue_head(m_interrupt_transfer_queue->paddr());
if constexpr (UHCI_VERBOSE_DEBUG)
transfer_descriptor->print();
}
m_free_td_pool.resize(MAXIMUM_NUMBER_OF_TDS);
for (size_t i = 0; i < m_free_td_pool.size(); i++) {
auto placement_addr = reinterpret_cast<void*>(m_td_pool->vaddr().offset(PAGE_SIZE).get() + (i * sizeof(Kernel::USB::TransferDescriptor)));
auto paddr = static_cast<u32>(m_td_pool->physical_page(1)->paddr().get() + (i * sizeof(Kernel::USB::TransferDescriptor)));
// Place a new Transfer Descriptor with a 1:1 in our region
// The pointer returned by `new()` lines up exactly with the value
// that we store in `paddr`, meaning our member functions directly
// access the raw descriptor (that we later send to the controller)
m_free_td_pool.at(i) = new (placement_addr) Kernel::USB::TransferDescriptor(paddr);
if constexpr (UHCI_VERBOSE_DEBUG) {
auto transfer_descriptor = m_free_td_pool.at(i);
transfer_descriptor->print();
}
}
if constexpr (UHCI_DEBUG) {
dbgln("UHCI: Pool information:");
dbgln(" qh_pool: {}, length: {}", PhysicalAddress(m_qh_pool->physical_page(0)->paddr()), m_qh_pool->range().size());
dbgln(" td_pool: {}, length: {}", PhysicalAddress(m_td_pool->physical_page(0)->paddr()), m_td_pool->range().size());
}
}
UNMAP_AFTER_INIT void UHCIController::setup_schedule()
{
//
// https://github.com/alkber/minix3-usbsubsystem/blob/master/usb/uhci-hcd.c
//
// This lad probably has the best explanation as to how this is actually done. I'll try and
// explain it here to so that there's no need for anyone to go hunting for this shit again, because
// the USB spec and Intel explain next to nothing.
// According to the USB spec (and the UHCI datasheet), 90% of the bandwidth should be used for
// Isochronous and """Interrupt""" related transfers, with the rest being used for control and bulk
// transfers.
// That is, most of the time, the schedule is going to be executing either an Isochronous transfer
// in our framelist, or an Interrupt transfer. The allocation in `create_structures` reflects this.
//
// Each frame has it's own Isochronous transfer Transfer Descriptor(s) that point to each other
// horizontally in the list. The end of these transfers then point to the Interrupt Queue Headers,
// in which we can attach Transfer Descriptors (related to Interrupt Transfers). These are attached
// to the Queue Head _vertically_. We need to ensure that these are executed every 8ms, so they are inserted
// at different points in the schedule (TODO: How do we do this?!?!). After the Interrupt Transfer Queue Heads,
// we attach the Control Queue Heads. We need two in total, one for Low Speed devices, and one for Full Speed
// USB devices. Finally, we attach the Bulk Transfer Queue Head.
// Not specified in the datasheet, however, is another Queue Head with an "inactive" Transfer Descriptor. This
// is to circumvent a bug in the silicon of the PIIX4's UHCI controller.
// https://github.com/openbsd/src/blob/master/sys/dev/usb/uhci.c#L390
//
m_interrupt_transfer_queue->link_next_queue_head(m_lowspeed_control_qh);
m_interrupt_transfer_queue->terminate_element_link_ptr();
m_lowspeed_control_qh->link_next_queue_head(m_fullspeed_control_qh);
m_lowspeed_control_qh->terminate_element_link_ptr();
m_fullspeed_control_qh->link_next_queue_head(m_bulk_qh);
m_fullspeed_control_qh->terminate_element_link_ptr();
m_bulk_qh->link_next_queue_head(m_dummy_qh);
m_bulk_qh->terminate_element_link_ptr();
auto piix4_td_hack = allocate_transfer_descriptor();
piix4_td_hack->terminate();
piix4_td_hack->set_max_len(0x7ff); // Null data packet
piix4_td_hack->set_device_address(0x7f);
piix4_td_hack->set_packet_id(PacketID::IN);
m_dummy_qh->terminate_with_stray_descriptor(piix4_td_hack);
m_dummy_qh->terminate_element_link_ptr();
u32* framelist = reinterpret_cast<u32*>(m_framelist->vaddr().as_ptr());
for (int frame = 0; frame < UHCI_NUMBER_OF_FRAMES; frame++) {
// Each frame pointer points to iso_td % NUM_ISO_TDS
framelist[frame] = m_iso_td_list.at(frame % UHCI_NUMBER_OF_ISOCHRONOUS_TDS)->paddr();
}
m_interrupt_transfer_queue->print();
m_lowspeed_control_qh->print();
m_fullspeed_control_qh->print();
m_bulk_qh->print();
m_dummy_qh->print();
}
QueueHead* UHCIController::allocate_queue_head() const
{
for (QueueHead* queue_head : m_free_qh_pool) {
if (!queue_head->in_use()) {
queue_head->set_in_use(true);
dbgln_if(UHCI_DEBUG, "UHCI: Allocated a new Queue Head! Located @ {} ({})", VirtualAddress(queue_head), PhysicalAddress(queue_head->paddr()));
return queue_head;
}
}
VERIFY_NOT_REACHED(); // Let's just assert for now, this should never happen
return nullptr; // Huh!? We're outta queue heads!
}
TransferDescriptor* UHCIController::allocate_transfer_descriptor() const
{
for (TransferDescriptor* transfer_descriptor : m_free_td_pool) {
if (!transfer_descriptor->in_use()) {
transfer_descriptor->set_in_use(true);
dbgln_if(UHCI_DEBUG, "UHCI: Allocated a new Transfer Descriptor! Located @ {} ({})", VirtualAddress(transfer_descriptor), PhysicalAddress(transfer_descriptor->paddr()));
return transfer_descriptor;
}
}
VERIFY_NOT_REACHED(); // Let's just assert for now, this should never happen
return nullptr; // Huh?! We're outta TDs!!
}
void UHCIController::stop()
{
write_usbcmd(read_usbcmd() & ~UHCI_USBCMD_RUN);
// FIXME: Timeout
for (;;) {
if (read_usbsts() & UHCI_USBSTS_HOST_CONTROLLER_HALTED)
break;
}
}
void UHCIController::start()
{
write_usbcmd(read_usbcmd() | UHCI_USBCMD_RUN);
// FIXME: Timeout
for (;;) {
if (!(read_usbsts() & UHCI_USBSTS_HOST_CONTROLLER_HALTED))
break;
}
dbgln("UHCI: Started");
}
struct setup_packet {
u8 bmRequestType;
u8 bRequest;
u16 wValue;
u16 wIndex;
u16 wLength;
};
void UHCIController::do_debug_transfer()
{
dbgln("UHCI: Attempting a dummy transfer...");
// Okay, let's set up the buffer so we can write some data
auto vmobj = ContiguousVMObject::create_with_size(PAGE_SIZE);
m_td_buffer_region = MemoryManager::the().allocate_kernel_region_with_vmobject(*vmobj, PAGE_SIZE, "UHCI Debug Data Region", Region::Access::Write);
// We need to set up THREE Transfer descriptors here
// 1. The SETUP packet TD
// 2. The DATA packet
// 3. The ACK TD that will be filled by the device
// We can use the buffer pool provided above to do this, using nasty pointer offsets!
auto setup_td = allocate_transfer_descriptor();
auto data_td = allocate_transfer_descriptor();
auto response_td = allocate_transfer_descriptor();
dbgln("BUFFER PHYSICAL ADDRESS = {}", m_td_buffer_region->physical_page(0)->paddr());
setup_packet* packet = reinterpret_cast<setup_packet*>(m_td_buffer_region->vaddr().as_ptr());
packet->bmRequestType = 0x81;
packet->bRequest = 0x06;
packet->wValue = 0x2200;
packet->wIndex = 0x0;
packet->wLength = 8;
// Let's begin....
setup_td->set_status(0x18800000);
setup_td->set_token(0x00E0002D);
setup_td->set_buffer_address(m_td_buffer_region->physical_page(0)->paddr().get());
data_td->set_status(0x18800000);
data_td->set_token(0x00E80069);
data_td->set_buffer_address(m_td_buffer_region->physical_page(0)->paddr().get() + 16);
response_td->set_status(0x19800000);
response_td->set_token(0xFFE800E1);
setup_td->insert_next_transfer_descriptor(data_td);
data_td->insert_next_transfer_descriptor(response_td);
response_td->terminate();
setup_td->print();
data_td->print();
response_td->print();
// Now let's (attempt) to attach to one of the queue heads
m_lowspeed_control_qh->attach_transfer_descriptor_chain(setup_td);
}
void UHCIController::spawn_port_proc()
{
RefPtr<Thread> usb_hotplug_thread;
Process::create_kernel_process(usb_hotplug_thread, "UHCIHotplug", [&] {
for (;;) {
for (int port = 0; port < UHCI_ROOT_PORT_COUNT; port++) {
u16 port_data = 0;
if (port == 1) {
// Let's see what's happening on port 1
// Current status
port_data = read_portsc1();
if (port_data & UHCI_PORTSC_CONNECT_STATUS_CHANGED) {
if (port_data & UHCI_PORTSC_CURRRENT_CONNECT_STATUS) {
dmesgln("UHCI: Device attach detected on Root Port 1!");
// Reset the port
port_data = read_portsc1();
write_portsc1(port_data | UHCI_PORTSC_PORT_RESET);
for (size_t i = 0; i < 50000; ++i)
IO::in8(0x80);
write_portsc1(port_data & ~UHCI_PORTSC_PORT_RESET);
for (size_t i = 0; i < 100000; ++i)
IO::in8(0x80);
write_portsc1(port_data & (~UHCI_PORTSC_PORT_ENABLE_CHANGED | ~UHCI_PORTSC_CONNECT_STATUS_CHANGED));
} else {
dmesgln("UHCI: Device detach detected on Root Port 1!");
}
port_data = read_portsc1();
write_portsc1(port_data | UHCI_PORTSC_PORT_ENABLED);
dbgln("port should be enabled now: {:#04x}\n", read_portsc1());
do_debug_transfer();
}
} else {
port_data = UHCIController::the().read_portsc2();
if (port_data & UHCI_PORTSC_CONNECT_STATUS_CHANGED) {
if (port_data & UHCI_PORTSC_CURRRENT_CONNECT_STATUS) {
dmesgln("UHCI: Device attach detected on Root Port 2!");
} else {
dmesgln("UHCI: Device detach detected on Root Port 2!");
}
UHCIController::the().write_portsc2(
UHCI_PORTSC_CONNECT_STATUS_CHANGED);
}
}
}
(void)Thread::current()->sleep(Time::from_seconds(1));
}
});
}
void UHCIController::handle_irq(const RegisterState&)
{
// Shared IRQ. Not ours!
if (!read_usbsts())
return;
if constexpr (UHCI_DEBUG) {
dbgln("UHCI: Interrupt happened!");
dbgln("Value of USBSTS: {:#04x}", read_usbsts());
}
}
}