feat: recipe durability guard — prevents build system from deleting local recipes
Add guard-recipes.sh with four modes: - --verify: check all local/recipes have correct symlinks into recipes/ - --fix: repair broken symlinks (run before builds) - --save-all: snapshot all recipe.toml into local/recipes/ - --restore: recreate all symlinks from local/recipes/ (run after sync-upstream) Wired into apply-patches.sh (post-patch) and sync-upstream.sh (post-sync). This prevents the build system from deleting recipe files during cargo cook, make distclean, or upstream source refresh.
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
use alloc::boxed::Box;
|
||||
use alloc::vec::Vec;
|
||||
|
||||
/// DMA buffer for USB transfers.
|
||||
///
|
||||
/// This crate intentionally avoids Redox-specific allocation APIs, so `allocate`
|
||||
/// produces an owned staging buffer and leaves `physical_addr` unset (`0`).
|
||||
/// Controller-specific code must install a real physical address before using
|
||||
/// the buffer for hardware DMA.
|
||||
pub struct DmaBuffer {
|
||||
pub virtual_addr: usize,
|
||||
pub physical_addr: u64,
|
||||
pub size: usize,
|
||||
data: Box<[u8]>,
|
||||
mapped: bool,
|
||||
}
|
||||
|
||||
impl DmaBuffer {
|
||||
/// Allocate an owned staging buffer for a future DMA mapping.
|
||||
///
|
||||
/// The returned buffer is not DMA-mapped yet. `physical_addr` remains `0`
|
||||
/// until the caller supplies a real mapping with `set_physical_addr`.
|
||||
pub fn allocate(size: usize) -> Result<Self, DmaError> {
|
||||
if size > isize::MAX as usize {
|
||||
return Err(DmaError::TooLarge);
|
||||
}
|
||||
|
||||
if size == 0 {
|
||||
return Ok(Self {
|
||||
virtual_addr: 0,
|
||||
physical_addr: 0,
|
||||
size: 0,
|
||||
data: Vec::new().into_boxed_slice(),
|
||||
mapped: true,
|
||||
});
|
||||
}
|
||||
|
||||
let mut data = Vec::new();
|
||||
if data.try_reserve_exact(size).is_err() {
|
||||
return Err(DmaError::NoMemory);
|
||||
}
|
||||
|
||||
data.resize(size, 0);
|
||||
let mut data = data.into_boxed_slice();
|
||||
let virtual_addr = data.as_mut_ptr() as usize;
|
||||
|
||||
Ok(Self {
|
||||
virtual_addr,
|
||||
physical_addr: 0,
|
||||
size,
|
||||
data,
|
||||
mapped: false,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn as_slice(&self) -> &[u8] {
|
||||
&self.data
|
||||
}
|
||||
|
||||
pub fn as_mut_slice(&mut self) -> &mut [u8] {
|
||||
&mut self.data
|
||||
}
|
||||
|
||||
/// Returns `true` once the controller-specific layer has installed a
|
||||
/// hardware-usable physical address for this buffer.
|
||||
pub fn is_dma_mapped(&self) -> bool {
|
||||
self.mapped
|
||||
}
|
||||
|
||||
/// Attach a controller/OS-specific physical address to the staging buffer.
|
||||
pub fn set_physical_addr(&mut self, physical_addr: u64) -> Result<(), DmaError> {
|
||||
if self.size != 0 && physical_addr == 0 {
|
||||
return Err(DmaError::AllocFailed);
|
||||
}
|
||||
|
||||
self.physical_addr = physical_addr;
|
||||
self.mapped = true;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum DmaError {
|
||||
AllocFailed,
|
||||
TooLarge,
|
||||
NoMemory,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{DmaBuffer, DmaError};
|
||||
|
||||
#[test]
|
||||
fn allocated_buffers_start_unmapped() {
|
||||
let buffer = match DmaBuffer::allocate(16) {
|
||||
Ok(buffer) => buffer,
|
||||
Err(error) => panic!("expected allocation to succeed: {error:?}"),
|
||||
};
|
||||
|
||||
assert_eq!(buffer.physical_addr, 0);
|
||||
assert!(!buffer.is_dma_mapped());
|
||||
assert_eq!(buffer.as_slice().len(), 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attaching_a_physical_address_marks_the_buffer_mapped() {
|
||||
let mut buffer = match DmaBuffer::allocate(16) {
|
||||
Ok(buffer) => buffer,
|
||||
Err(error) => panic!("expected allocation to succeed: {error:?}"),
|
||||
};
|
||||
|
||||
let result = buffer.set_physical_addr(0x1000);
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(buffer.physical_addr, 0x1000);
|
||||
assert!(buffer.is_dma_mapped());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nonempty_buffers_reject_a_zero_physical_address() {
|
||||
let mut buffer = match DmaBuffer::allocate(8) {
|
||||
Ok(buffer) => buffer,
|
||||
Err(error) => panic!("expected allocation to succeed: {error:?}"),
|
||||
};
|
||||
|
||||
let result = buffer.set_physical_addr(0);
|
||||
assert!(matches!(result, Err(DmaError::AllocFailed)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direct_field_mutation_does_not_forge_mapping_state() {
|
||||
let mut buffer = match DmaBuffer::allocate(8) {
|
||||
Ok(buffer) => buffer,
|
||||
Err(error) => panic!("expected allocation to succeed: {error:?}"),
|
||||
};
|
||||
|
||||
buffer.physical_addr = 0x2000;
|
||||
|
||||
assert_eq!(buffer.physical_addr, 0x2000);
|
||||
assert!(!buffer.is_dma_mapped());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
#![doc = "Shared USB types and primitives for Red Bear host controller drivers."]
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
pub mod dma;
|
||||
pub mod scheme;
|
||||
pub mod spawn;
|
||||
pub mod transfer;
|
||||
pub mod types;
|
||||
|
||||
pub use dma::{DmaBuffer, DmaError};
|
||||
pub use scheme::{UsbError, UsbHostController};
|
||||
pub use spawn::spawn_usb_driver;
|
||||
pub use transfer::{
|
||||
control_transfer, parse_config_descriptor, parse_device_descriptor, parse_endpoint_descriptor,
|
||||
};
|
||||
pub use types::{
|
||||
ConfigDescriptor, DeviceDescriptor, EndpointDescriptor, PortStatus, SetupPacket,
|
||||
TransferDirection, TransferType, Urb, UrbStatus,
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
use crate::types::{PortStatus, SetupPacket, TransferDirection};
|
||||
|
||||
/// Trait that all USB host controller drivers must implement.
|
||||
/// This provides a uniform scheme interface regardless of HC type (XHCI/EHCI/OHCI/UHCI).
|
||||
pub trait UsbHostController {
|
||||
/// Get the number of ports on this controller
|
||||
fn port_count(&self) -> usize;
|
||||
|
||||
/// Get status of a specific port
|
||||
fn port_status(&self, port: usize) -> Option<PortStatus>;
|
||||
|
||||
/// Reset a port (USB enumeration step 1)
|
||||
fn port_reset(&mut self, port: usize) -> bool;
|
||||
|
||||
/// Submit a control transfer to endpoint 0
|
||||
fn control_transfer(
|
||||
&mut self,
|
||||
device_address: u8,
|
||||
setup: &SetupPacket,
|
||||
data: &mut [u8],
|
||||
) -> Result<usize, UsbError>;
|
||||
|
||||
/// Submit a bulk transfer
|
||||
fn bulk_transfer(
|
||||
&mut self,
|
||||
device_address: u8,
|
||||
endpoint: u8,
|
||||
data: &mut [u8],
|
||||
direction: TransferDirection,
|
||||
) -> Result<usize, UsbError>;
|
||||
|
||||
/// Submit an interrupt transfer
|
||||
fn interrupt_transfer(
|
||||
&mut self,
|
||||
device_address: u8,
|
||||
endpoint: u8,
|
||||
data: &mut [u8],
|
||||
) -> Result<usize, UsbError>;
|
||||
|
||||
/// Set device address (after reset, before config)
|
||||
fn set_address(&mut self, device_address: u8) -> bool;
|
||||
|
||||
/// Get the controller name for logging
|
||||
fn name(&self) -> &str;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum UsbError {
|
||||
Timeout,
|
||||
Stall,
|
||||
DataError,
|
||||
Babble,
|
||||
NoDevice,
|
||||
NotConfigured,
|
||||
IoError,
|
||||
Unsupported,
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/// Spawn a child USB class driver (hub, HID, storage).
|
||||
/// On Redox, this forks and execs the driver binary with the USB device path.
|
||||
#[cfg(feature = "std")]
|
||||
pub fn spawn_usb_driver(driver_binary: &str, device_path: &str) {
|
||||
if driver_binary.is_empty()
|
||||
|| device_path.is_empty()
|
||||
|| !driver_binary.starts_with('/')
|
||||
|| !device_path.starts_with('/')
|
||||
|| !is_trusted_usb_driver(driver_binary)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let mut command = std::process::Command::new(driver_binary);
|
||||
command.env_clear();
|
||||
command.stdin(std::process::Stdio::null());
|
||||
command.arg(device_path);
|
||||
let _ = command.spawn();
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
fn is_trusted_usb_driver(driver_binary: &str) -> bool {
|
||||
matches!(
|
||||
driver_binary,
|
||||
"/usr/bin/usbhubd" | "/usr/bin/usbhidd" | "/usr/bin/usbscsid"
|
||||
)
|
||||
}
|
||||
|
||||
/// Spawn a child USB class driver (hub, HID, storage).
|
||||
/// On no_std builds, class-driver spawning is not available.
|
||||
#[cfg(not(feature = "std"))]
|
||||
pub fn spawn_usb_driver(_driver_binary: &str, _device_path: &str) {}
|
||||
|
||||
#[cfg(all(test, feature = "std"))]
|
||||
mod tests {
|
||||
use super::is_trusted_usb_driver;
|
||||
|
||||
#[test]
|
||||
fn trusted_driver_whitelist_allows_expected_binaries() {
|
||||
assert!(is_trusted_usb_driver("/usr/bin/usbhubd"));
|
||||
assert!(is_trusted_usb_driver("/usr/bin/usbhidd"));
|
||||
assert!(is_trusted_usb_driver("/usr/bin/usbscsid"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trusted_driver_whitelist_rejects_other_binaries() {
|
||||
assert!(!is_trusted_usb_driver("/usr/bin/sh"));
|
||||
assert!(!is_trusted_usb_driver("/tmp/usbhubd"));
|
||||
assert!(!is_trusted_usb_driver("relative/usbhubd"));
|
||||
assert!(!is_trusted_usb_driver(""));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use crate::types::{
|
||||
ConfigDescriptor, DeviceDescriptor, EndpointDescriptor, SetupPacket, TransferDirection,
|
||||
TransferType, Urb, UrbStatus,
|
||||
};
|
||||
|
||||
/// Build a standard control transfer setup packet + data stage
|
||||
pub fn control_transfer(
|
||||
device_address: u8,
|
||||
request_type: u8,
|
||||
request: u8,
|
||||
value: u16,
|
||||
index: u16,
|
||||
data: &[u8],
|
||||
direction: TransferDirection,
|
||||
) -> Urb {
|
||||
let data = if data.len() > u16::MAX as usize {
|
||||
&data[..u16::MAX as usize]
|
||||
} else {
|
||||
data
|
||||
};
|
||||
|
||||
let setup = SetupPacket {
|
||||
request_type,
|
||||
request,
|
||||
value,
|
||||
index,
|
||||
length: data.len() as u16,
|
||||
};
|
||||
|
||||
let mut buffer = Vec::with_capacity(data.len().saturating_add(8));
|
||||
buffer.extend_from_slice(&setup_packet_bytes(&setup));
|
||||
buffer.extend_from_slice(data);
|
||||
|
||||
Urb {
|
||||
device_address,
|
||||
endpoint: 0,
|
||||
transfer_type: TransferType::Control,
|
||||
direction,
|
||||
buffer,
|
||||
actual_length: 0,
|
||||
status: UrbStatus::Pending,
|
||||
timeout_ms: 1000,
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a Device Descriptor from raw bytes
|
||||
pub fn parse_device_descriptor(data: &[u8]) -> Option<DeviceDescriptor> {
|
||||
if data.len() < 18 || data[0] != 18 || data[1] != 0x01 {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(DeviceDescriptor {
|
||||
length: data[0],
|
||||
descriptor_type: data[1],
|
||||
usb_version: u16::from_le_bytes([data[2], data[3]]),
|
||||
device_class: data[4],
|
||||
device_subclass: data[5],
|
||||
device_protocol: data[6],
|
||||
max_packet_size0: data[7],
|
||||
vendor_id: u16::from_le_bytes([data[8], data[9]]),
|
||||
product_id: u16::from_le_bytes([data[10], data[11]]),
|
||||
device_version: u16::from_le_bytes([data[12], data[13]]),
|
||||
manufacturer_idx: data[14],
|
||||
product_idx: data[15],
|
||||
serial_idx: data[16],
|
||||
num_configurations: data[17],
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse a Configuration Descriptor from raw bytes
|
||||
pub fn parse_config_descriptor(data: &[u8]) -> Option<ConfigDescriptor> {
|
||||
if data.len() < 9 || data[0] != 9 || data[1] != 0x02 {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(ConfigDescriptor {
|
||||
length: data[0],
|
||||
descriptor_type: data[1],
|
||||
total_length: u16::from_le_bytes([data[2], data[3]]),
|
||||
num_interfaces: data[4],
|
||||
config_value: data[5],
|
||||
config_idx: data[6],
|
||||
attributes: data[7],
|
||||
max_power: data[8],
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse an Endpoint Descriptor from raw bytes
|
||||
pub fn parse_endpoint_descriptor(data: &[u8]) -> Option<EndpointDescriptor> {
|
||||
if data.len() < 7 || data[0] != 7 || data[1] != 0x05 {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(EndpointDescriptor {
|
||||
length: data[0],
|
||||
descriptor_type: data[1],
|
||||
endpoint_address: data[2],
|
||||
attributes: data[3],
|
||||
max_packet_size: u16::from_le_bytes([data[4], data[5]]),
|
||||
interval: data[6],
|
||||
})
|
||||
}
|
||||
|
||||
fn setup_packet_bytes(setup: &SetupPacket) -> [u8; 8] {
|
||||
let value = setup.value.to_le_bytes();
|
||||
let index = setup.index.to_le_bytes();
|
||||
let length = setup.length.to_le_bytes();
|
||||
|
||||
[
|
||||
setup.request_type,
|
||||
setup.request,
|
||||
value[0],
|
||||
value[1],
|
||||
index[0],
|
||||
index[1],
|
||||
length[0],
|
||||
length[1],
|
||||
]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
parse_config_descriptor, parse_device_descriptor, parse_endpoint_descriptor,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn parses_valid_device_descriptor() {
|
||||
let raw = [
|
||||
18, 0x01, 0x00, 0x02, 0xff, 0x00, 0x00, 64, 0x34, 0x12, 0x78, 0x56, 0x00, 0x01,
|
||||
1, 2, 3, 1,
|
||||
];
|
||||
|
||||
match parse_device_descriptor(&raw) {
|
||||
Some(descriptor) => {
|
||||
assert_eq!(descriptor.length, 18);
|
||||
assert_eq!(descriptor.descriptor_type, 0x01);
|
||||
assert_eq!(descriptor.usb_version, 0x0200);
|
||||
assert_eq!(descriptor.vendor_id, 0x1234);
|
||||
assert_eq!(descriptor.product_id, 0x5678);
|
||||
assert_eq!(descriptor.num_configurations, 1);
|
||||
}
|
||||
None => panic!("valid device descriptor should parse"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_valid_config_descriptor() {
|
||||
let raw = [9, 0x02, 32, 0, 1, 1, 0, 0x80, 50];
|
||||
|
||||
match parse_config_descriptor(&raw) {
|
||||
Some(descriptor) => {
|
||||
assert_eq!(descriptor.length, 9);
|
||||
assert_eq!(descriptor.descriptor_type, 0x02);
|
||||
assert_eq!(descriptor.total_length, 32);
|
||||
assert_eq!(descriptor.num_interfaces, 1);
|
||||
assert_eq!(descriptor.max_power, 50);
|
||||
}
|
||||
None => panic!("valid config descriptor should parse"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_valid_endpoint_descriptor() {
|
||||
let raw = [7, 0x05, 0x81, 0x03, 8, 0, 10];
|
||||
|
||||
match parse_endpoint_descriptor(&raw) {
|
||||
Some(descriptor) => {
|
||||
assert_eq!(descriptor.length, 7);
|
||||
assert_eq!(descriptor.descriptor_type, 0x05);
|
||||
assert_eq!(descriptor.endpoint_address, 0x81);
|
||||
assert_eq!(descriptor.attributes, 0x03);
|
||||
assert_eq!(descriptor.max_packet_size, 8);
|
||||
assert_eq!(descriptor.interval, 10);
|
||||
}
|
||||
None => panic!("valid endpoint descriptor should parse"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
use alloc::vec::Vec;
|
||||
|
||||
// USB Standard Device Descriptor (USB 2.0 §9.6.1)
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DeviceDescriptor {
|
||||
pub length: u8,
|
||||
pub descriptor_type: u8,
|
||||
pub usb_version: u16,
|
||||
pub device_class: u8,
|
||||
pub device_subclass: u8,
|
||||
pub device_protocol: u8,
|
||||
pub max_packet_size0: u8,
|
||||
pub vendor_id: u16,
|
||||
pub product_id: u16,
|
||||
pub device_version: u16,
|
||||
pub manufacturer_idx: u8,
|
||||
pub product_idx: u8,
|
||||
pub serial_idx: u8,
|
||||
pub num_configurations: u8,
|
||||
}
|
||||
|
||||
// USB Configuration Descriptor (USB 2.0 §9.6.3)
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ConfigDescriptor {
|
||||
pub length: u8,
|
||||
pub descriptor_type: u8,
|
||||
pub total_length: u16,
|
||||
pub num_interfaces: u8,
|
||||
pub config_value: u8,
|
||||
pub config_idx: u8,
|
||||
pub attributes: u8,
|
||||
pub max_power: u8,
|
||||
}
|
||||
|
||||
// USB Endpoint Descriptor (USB 2.0 §9.6.6)
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct EndpointDescriptor {
|
||||
pub length: u8,
|
||||
pub descriptor_type: u8,
|
||||
pub endpoint_address: u8,
|
||||
pub attributes: u8,
|
||||
pub max_packet_size: u16,
|
||||
pub interval: u8,
|
||||
}
|
||||
|
||||
// Standard Setup Packet (USB 2.0 §9.3)
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SetupPacket {
|
||||
pub request_type: u8,
|
||||
pub request: u8,
|
||||
pub value: u16,
|
||||
pub index: u16,
|
||||
pub length: u16,
|
||||
}
|
||||
|
||||
// USB Request Block — a transfer to/from a device
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Urb {
|
||||
pub device_address: u8,
|
||||
pub endpoint: u8,
|
||||
pub transfer_type: TransferType,
|
||||
pub direction: TransferDirection,
|
||||
pub buffer: Vec<u8>,
|
||||
pub actual_length: usize,
|
||||
pub status: UrbStatus,
|
||||
pub timeout_ms: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum TransferType {
|
||||
Control,
|
||||
Bulk,
|
||||
Interrupt,
|
||||
Isochronous,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum TransferDirection {
|
||||
Out,
|
||||
In,
|
||||
Setup,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum UrbStatus {
|
||||
Pending,
|
||||
Complete,
|
||||
Error,
|
||||
Timeout,
|
||||
}
|
||||
|
||||
// USB port status
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PortStatus {
|
||||
pub connected: bool,
|
||||
pub enabled: bool,
|
||||
pub suspended: bool,
|
||||
pub over_current: bool,
|
||||
pub reset: bool,
|
||||
pub power: bool,
|
||||
pub low_speed: bool,
|
||||
pub high_speed: bool,
|
||||
pub test_mode: bool,
|
||||
pub indicator: bool,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use super::{SetupPacket, TransferDirection, TransferType, Urb, UrbStatus};
|
||||
|
||||
#[test]
|
||||
fn setup_packet_fields_are_accessible() {
|
||||
let packet = SetupPacket {
|
||||
request_type: 0x80,
|
||||
request: 0x06,
|
||||
value: 0x0100,
|
||||
index: 0x0000,
|
||||
length: 18,
|
||||
};
|
||||
|
||||
assert_eq!(packet.request_type, 0x80);
|
||||
assert_eq!(packet.request, 0x06);
|
||||
assert_eq!(packet.value, 0x0100);
|
||||
assert_eq!(packet.index, 0x0000);
|
||||
assert_eq!(packet.length, 18);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn urb_status_can_transition_between_states() {
|
||||
let mut urb = Urb {
|
||||
device_address: 1,
|
||||
endpoint: 1,
|
||||
transfer_type: TransferType::Bulk,
|
||||
direction: TransferDirection::In,
|
||||
buffer: Vec::new(),
|
||||
actual_length: 0,
|
||||
status: UrbStatus::Pending,
|
||||
timeout_ms: 1000,
|
||||
};
|
||||
|
||||
assert_eq!(urb.status, UrbStatus::Pending);
|
||||
|
||||
urb.status = UrbStatus::Complete;
|
||||
assert_eq!(urb.status, UrbStatus::Complete);
|
||||
|
||||
urb.status = UrbStatus::Timeout;
|
||||
assert_eq!(urb.status, UrbStatus::Timeout);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user