b04079837b
USB4 is the public specification based on Thunderbolt 3 protocol. There are some differences in register layouts and flows. In addition to PCIe and DP tunneling, USB4 supports tunneling of USB 3.x. USB4 is also backward compatible with Thunderbolt 3 (and older generations but the spec only talks about 3rd generation). USB4 compliant devices can be identified by checking USB4 version field in router configuration space. This patch adds initial support for USB4 compliant hosts and devices which enables following features provided by the existing functionality in the driver: - PCIe tunneling - Display Port tunneling - Host and device NVM firmware upgrade - P2P networking This brings the USB4 support to the same level that we already have for Thunderbolt 1, 2 and 3 devices. Note the spec talks about host and device "routers" but in the driver we still use term "switch" in most places. Both can be used interchangeably. Co-developed-by: Rajmohan Mani <rajmohan.mani@intel.com> Signed-off-by: Rajmohan Mani <rajmohan.mani@intel.com> Signed-off-by: Mika Westerberg <mika.westerberg@linux.intel.com> Link: https://lore.kernel.org/r/20191217123345.31850-5-mika.westerberg@linux.intel.com Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
1738 lines
43 KiB
C
1738 lines
43 KiB
C
// SPDX-License-Identifier: GPL-2.0
|
|
/*
|
|
* Thunderbolt XDomain discovery protocol support
|
|
*
|
|
* Copyright (C) 2017, Intel Corporation
|
|
* Authors: Michael Jamet <michael.jamet@intel.com>
|
|
* Mika Westerberg <mika.westerberg@linux.intel.com>
|
|
*/
|
|
|
|
#include <linux/device.h>
|
|
#include <linux/kmod.h>
|
|
#include <linux/module.h>
|
|
#include <linux/pm_runtime.h>
|
|
#include <linux/utsname.h>
|
|
#include <linux/uuid.h>
|
|
#include <linux/workqueue.h>
|
|
|
|
#include "tb.h"
|
|
|
|
#define XDOMAIN_DEFAULT_TIMEOUT 5000 /* ms */
|
|
#define XDOMAIN_UUID_RETRIES 10
|
|
#define XDOMAIN_PROPERTIES_RETRIES 60
|
|
#define XDOMAIN_PROPERTIES_CHANGED_RETRIES 10
|
|
|
|
struct xdomain_request_work {
|
|
struct work_struct work;
|
|
struct tb_xdp_header *pkg;
|
|
struct tb *tb;
|
|
};
|
|
|
|
/* Serializes access to the properties and protocol handlers below */
|
|
static DEFINE_MUTEX(xdomain_lock);
|
|
|
|
/* Properties exposed to the remote domains */
|
|
static struct tb_property_dir *xdomain_property_dir;
|
|
static u32 *xdomain_property_block;
|
|
static u32 xdomain_property_block_len;
|
|
static u32 xdomain_property_block_gen;
|
|
|
|
/* Additional protocol handlers */
|
|
static LIST_HEAD(protocol_handlers);
|
|
|
|
/* UUID for XDomain discovery protocol: b638d70e-42ff-40bb-97c2-90e2c0b2ff07 */
|
|
static const uuid_t tb_xdp_uuid =
|
|
UUID_INIT(0xb638d70e, 0x42ff, 0x40bb,
|
|
0x97, 0xc2, 0x90, 0xe2, 0xc0, 0xb2, 0xff, 0x07);
|
|
|
|
static bool tb_xdomain_match(const struct tb_cfg_request *req,
|
|
const struct ctl_pkg *pkg)
|
|
{
|
|
switch (pkg->frame.eof) {
|
|
case TB_CFG_PKG_ERROR:
|
|
return true;
|
|
|
|
case TB_CFG_PKG_XDOMAIN_RESP: {
|
|
const struct tb_xdp_header *res_hdr = pkg->buffer;
|
|
const struct tb_xdp_header *req_hdr = req->request;
|
|
|
|
if (pkg->frame.size < req->response_size / 4)
|
|
return false;
|
|
|
|
/* Make sure route matches */
|
|
if ((res_hdr->xd_hdr.route_hi & ~BIT(31)) !=
|
|
req_hdr->xd_hdr.route_hi)
|
|
return false;
|
|
if ((res_hdr->xd_hdr.route_lo) != req_hdr->xd_hdr.route_lo)
|
|
return false;
|
|
|
|
/* Check that the XDomain protocol matches */
|
|
if (!uuid_equal(&res_hdr->uuid, &req_hdr->uuid))
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static bool tb_xdomain_copy(struct tb_cfg_request *req,
|
|
const struct ctl_pkg *pkg)
|
|
{
|
|
memcpy(req->response, pkg->buffer, req->response_size);
|
|
req->result.err = 0;
|
|
return true;
|
|
}
|
|
|
|
static void response_ready(void *data)
|
|
{
|
|
tb_cfg_request_put(data);
|
|
}
|
|
|
|
static int __tb_xdomain_response(struct tb_ctl *ctl, const void *response,
|
|
size_t size, enum tb_cfg_pkg_type type)
|
|
{
|
|
struct tb_cfg_request *req;
|
|
|
|
req = tb_cfg_request_alloc();
|
|
if (!req)
|
|
return -ENOMEM;
|
|
|
|
req->match = tb_xdomain_match;
|
|
req->copy = tb_xdomain_copy;
|
|
req->request = response;
|
|
req->request_size = size;
|
|
req->request_type = type;
|
|
|
|
return tb_cfg_request(ctl, req, response_ready, req);
|
|
}
|
|
|
|
/**
|
|
* tb_xdomain_response() - Send a XDomain response message
|
|
* @xd: XDomain to send the message
|
|
* @response: Response to send
|
|
* @size: Size of the response
|
|
* @type: PDF type of the response
|
|
*
|
|
* This can be used to send a XDomain response message to the other
|
|
* domain. No response for the message is expected.
|
|
*
|
|
* Return: %0 in case of success and negative errno in case of failure
|
|
*/
|
|
int tb_xdomain_response(struct tb_xdomain *xd, const void *response,
|
|
size_t size, enum tb_cfg_pkg_type type)
|
|
{
|
|
return __tb_xdomain_response(xd->tb->ctl, response, size, type);
|
|
}
|
|
EXPORT_SYMBOL_GPL(tb_xdomain_response);
|
|
|
|
static int __tb_xdomain_request(struct tb_ctl *ctl, const void *request,
|
|
size_t request_size, enum tb_cfg_pkg_type request_type, void *response,
|
|
size_t response_size, enum tb_cfg_pkg_type response_type,
|
|
unsigned int timeout_msec)
|
|
{
|
|
struct tb_cfg_request *req;
|
|
struct tb_cfg_result res;
|
|
|
|
req = tb_cfg_request_alloc();
|
|
if (!req)
|
|
return -ENOMEM;
|
|
|
|
req->match = tb_xdomain_match;
|
|
req->copy = tb_xdomain_copy;
|
|
req->request = request;
|
|
req->request_size = request_size;
|
|
req->request_type = request_type;
|
|
req->response = response;
|
|
req->response_size = response_size;
|
|
req->response_type = response_type;
|
|
|
|
res = tb_cfg_request_sync(ctl, req, timeout_msec);
|
|
|
|
tb_cfg_request_put(req);
|
|
|
|
return res.err == 1 ? -EIO : res.err;
|
|
}
|
|
|
|
/**
|
|
* tb_xdomain_request() - Send a XDomain request
|
|
* @xd: XDomain to send the request
|
|
* @request: Request to send
|
|
* @request_size: Size of the request in bytes
|
|
* @request_type: PDF type of the request
|
|
* @response: Response is copied here
|
|
* @response_size: Expected size of the response in bytes
|
|
* @response_type: Expected PDF type of the response
|
|
* @timeout_msec: Timeout in milliseconds to wait for the response
|
|
*
|
|
* This function can be used to send XDomain control channel messages to
|
|
* the other domain. The function waits until the response is received
|
|
* or when timeout triggers. Whichever comes first.
|
|
*
|
|
* Return: %0 in case of success and negative errno in case of failure
|
|
*/
|
|
int tb_xdomain_request(struct tb_xdomain *xd, const void *request,
|
|
size_t request_size, enum tb_cfg_pkg_type request_type,
|
|
void *response, size_t response_size,
|
|
enum tb_cfg_pkg_type response_type, unsigned int timeout_msec)
|
|
{
|
|
return __tb_xdomain_request(xd->tb->ctl, request, request_size,
|
|
request_type, response, response_size,
|
|
response_type, timeout_msec);
|
|
}
|
|
EXPORT_SYMBOL_GPL(tb_xdomain_request);
|
|
|
|
static inline void tb_xdp_fill_header(struct tb_xdp_header *hdr, u64 route,
|
|
u8 sequence, enum tb_xdp_type type, size_t size)
|
|
{
|
|
u32 length_sn;
|
|
|
|
length_sn = (size - sizeof(hdr->xd_hdr)) / 4;
|
|
length_sn |= (sequence << TB_XDOMAIN_SN_SHIFT) & TB_XDOMAIN_SN_MASK;
|
|
|
|
hdr->xd_hdr.route_hi = upper_32_bits(route);
|
|
hdr->xd_hdr.route_lo = lower_32_bits(route);
|
|
hdr->xd_hdr.length_sn = length_sn;
|
|
hdr->type = type;
|
|
memcpy(&hdr->uuid, &tb_xdp_uuid, sizeof(tb_xdp_uuid));
|
|
}
|
|
|
|
static int tb_xdp_handle_error(const struct tb_xdp_header *hdr)
|
|
{
|
|
const struct tb_xdp_error_response *error;
|
|
|
|
if (hdr->type != ERROR_RESPONSE)
|
|
return 0;
|
|
|
|
error = (const struct tb_xdp_error_response *)hdr;
|
|
|
|
switch (error->error) {
|
|
case ERROR_UNKNOWN_PACKET:
|
|
case ERROR_UNKNOWN_DOMAIN:
|
|
return -EIO;
|
|
case ERROR_NOT_SUPPORTED:
|
|
return -ENOTSUPP;
|
|
case ERROR_NOT_READY:
|
|
return -EAGAIN;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int tb_xdp_uuid_request(struct tb_ctl *ctl, u64 route, int retry,
|
|
uuid_t *uuid)
|
|
{
|
|
struct tb_xdp_uuid_response res;
|
|
struct tb_xdp_uuid req;
|
|
int ret;
|
|
|
|
memset(&req, 0, sizeof(req));
|
|
tb_xdp_fill_header(&req.hdr, route, retry % 4, UUID_REQUEST,
|
|
sizeof(req));
|
|
|
|
memset(&res, 0, sizeof(res));
|
|
ret = __tb_xdomain_request(ctl, &req, sizeof(req),
|
|
TB_CFG_PKG_XDOMAIN_REQ, &res, sizeof(res),
|
|
TB_CFG_PKG_XDOMAIN_RESP,
|
|
XDOMAIN_DEFAULT_TIMEOUT);
|
|
if (ret)
|
|
return ret;
|
|
|
|
ret = tb_xdp_handle_error(&res.hdr);
|
|
if (ret)
|
|
return ret;
|
|
|
|
uuid_copy(uuid, &res.src_uuid);
|
|
return 0;
|
|
}
|
|
|
|
static int tb_xdp_uuid_response(struct tb_ctl *ctl, u64 route, u8 sequence,
|
|
const uuid_t *uuid)
|
|
{
|
|
struct tb_xdp_uuid_response res;
|
|
|
|
memset(&res, 0, sizeof(res));
|
|
tb_xdp_fill_header(&res.hdr, route, sequence, UUID_RESPONSE,
|
|
sizeof(res));
|
|
|
|
uuid_copy(&res.src_uuid, uuid);
|
|
res.src_route_hi = upper_32_bits(route);
|
|
res.src_route_lo = lower_32_bits(route);
|
|
|
|
return __tb_xdomain_response(ctl, &res, sizeof(res),
|
|
TB_CFG_PKG_XDOMAIN_RESP);
|
|
}
|
|
|
|
static int tb_xdp_error_response(struct tb_ctl *ctl, u64 route, u8 sequence,
|
|
enum tb_xdp_error error)
|
|
{
|
|
struct tb_xdp_error_response res;
|
|
|
|
memset(&res, 0, sizeof(res));
|
|
tb_xdp_fill_header(&res.hdr, route, sequence, ERROR_RESPONSE,
|
|
sizeof(res));
|
|
res.error = error;
|
|
|
|
return __tb_xdomain_response(ctl, &res, sizeof(res),
|
|
TB_CFG_PKG_XDOMAIN_RESP);
|
|
}
|
|
|
|
static int tb_xdp_properties_request(struct tb_ctl *ctl, u64 route,
|
|
const uuid_t *src_uuid, const uuid_t *dst_uuid, int retry,
|
|
u32 **block, u32 *generation)
|
|
{
|
|
struct tb_xdp_properties_response *res;
|
|
struct tb_xdp_properties req;
|
|
u16 data_len, len;
|
|
size_t total_size;
|
|
u32 *data = NULL;
|
|
int ret;
|
|
|
|
total_size = sizeof(*res) + TB_XDP_PROPERTIES_MAX_DATA_LENGTH * 4;
|
|
res = kzalloc(total_size, GFP_KERNEL);
|
|
if (!res)
|
|
return -ENOMEM;
|
|
|
|
memset(&req, 0, sizeof(req));
|
|
tb_xdp_fill_header(&req.hdr, route, retry % 4, PROPERTIES_REQUEST,
|
|
sizeof(req));
|
|
memcpy(&req.src_uuid, src_uuid, sizeof(*src_uuid));
|
|
memcpy(&req.dst_uuid, dst_uuid, sizeof(*dst_uuid));
|
|
|
|
len = 0;
|
|
data_len = 0;
|
|
|
|
do {
|
|
ret = __tb_xdomain_request(ctl, &req, sizeof(req),
|
|
TB_CFG_PKG_XDOMAIN_REQ, res,
|
|
total_size, TB_CFG_PKG_XDOMAIN_RESP,
|
|
XDOMAIN_DEFAULT_TIMEOUT);
|
|
if (ret)
|
|
goto err;
|
|
|
|
ret = tb_xdp_handle_error(&res->hdr);
|
|
if (ret)
|
|
goto err;
|
|
|
|
/*
|
|
* Package length includes the whole payload without the
|
|
* XDomain header. Validate first that the package is at
|
|
* least size of the response structure.
|
|
*/
|
|
len = res->hdr.xd_hdr.length_sn & TB_XDOMAIN_LENGTH_MASK;
|
|
if (len < sizeof(*res) / 4) {
|
|
ret = -EINVAL;
|
|
goto err;
|
|
}
|
|
|
|
len += sizeof(res->hdr.xd_hdr) / 4;
|
|
len -= sizeof(*res) / 4;
|
|
|
|
if (res->offset != req.offset) {
|
|
ret = -EINVAL;
|
|
goto err;
|
|
}
|
|
|
|
/*
|
|
* First time allocate block that has enough space for
|
|
* the whole properties block.
|
|
*/
|
|
if (!data) {
|
|
data_len = res->data_length;
|
|
if (data_len > TB_XDP_PROPERTIES_MAX_LENGTH) {
|
|
ret = -E2BIG;
|
|
goto err;
|
|
}
|
|
|
|
data = kcalloc(data_len, sizeof(u32), GFP_KERNEL);
|
|
if (!data) {
|
|
ret = -ENOMEM;
|
|
goto err;
|
|
}
|
|
}
|
|
|
|
memcpy(data + req.offset, res->data, len * 4);
|
|
req.offset += len;
|
|
} while (!data_len || req.offset < data_len);
|
|
|
|
*block = data;
|
|
*generation = res->generation;
|
|
|
|
kfree(res);
|
|
|
|
return data_len;
|
|
|
|
err:
|
|
kfree(data);
|
|
kfree(res);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int tb_xdp_properties_response(struct tb *tb, struct tb_ctl *ctl,
|
|
u64 route, u8 sequence, const uuid_t *src_uuid,
|
|
const struct tb_xdp_properties *req)
|
|
{
|
|
struct tb_xdp_properties_response *res;
|
|
size_t total_size;
|
|
u16 len;
|
|
int ret;
|
|
|
|
/*
|
|
* Currently we expect all requests to be directed to us. The
|
|
* protocol supports forwarding, though which we might add
|
|
* support later on.
|
|
*/
|
|
if (!uuid_equal(src_uuid, &req->dst_uuid)) {
|
|
tb_xdp_error_response(ctl, route, sequence,
|
|
ERROR_UNKNOWN_DOMAIN);
|
|
return 0;
|
|
}
|
|
|
|
mutex_lock(&xdomain_lock);
|
|
|
|
if (req->offset >= xdomain_property_block_len) {
|
|
mutex_unlock(&xdomain_lock);
|
|
return -EINVAL;
|
|
}
|
|
|
|
len = xdomain_property_block_len - req->offset;
|
|
len = min_t(u16, len, TB_XDP_PROPERTIES_MAX_DATA_LENGTH);
|
|
total_size = sizeof(*res) + len * 4;
|
|
|
|
res = kzalloc(total_size, GFP_KERNEL);
|
|
if (!res) {
|
|
mutex_unlock(&xdomain_lock);
|
|
return -ENOMEM;
|
|
}
|
|
|
|
tb_xdp_fill_header(&res->hdr, route, sequence, PROPERTIES_RESPONSE,
|
|
total_size);
|
|
res->generation = xdomain_property_block_gen;
|
|
res->data_length = xdomain_property_block_len;
|
|
res->offset = req->offset;
|
|
uuid_copy(&res->src_uuid, src_uuid);
|
|
uuid_copy(&res->dst_uuid, &req->src_uuid);
|
|
memcpy(res->data, &xdomain_property_block[req->offset], len * 4);
|
|
|
|
mutex_unlock(&xdomain_lock);
|
|
|
|
ret = __tb_xdomain_response(ctl, res, total_size,
|
|
TB_CFG_PKG_XDOMAIN_RESP);
|
|
|
|
kfree(res);
|
|
return ret;
|
|
}
|
|
|
|
static int tb_xdp_properties_changed_request(struct tb_ctl *ctl, u64 route,
|
|
int retry, const uuid_t *uuid)
|
|
{
|
|
struct tb_xdp_properties_changed_response res;
|
|
struct tb_xdp_properties_changed req;
|
|
int ret;
|
|
|
|
memset(&req, 0, sizeof(req));
|
|
tb_xdp_fill_header(&req.hdr, route, retry % 4,
|
|
PROPERTIES_CHANGED_REQUEST, sizeof(req));
|
|
uuid_copy(&req.src_uuid, uuid);
|
|
|
|
memset(&res, 0, sizeof(res));
|
|
ret = __tb_xdomain_request(ctl, &req, sizeof(req),
|
|
TB_CFG_PKG_XDOMAIN_REQ, &res, sizeof(res),
|
|
TB_CFG_PKG_XDOMAIN_RESP,
|
|
XDOMAIN_DEFAULT_TIMEOUT);
|
|
if (ret)
|
|
return ret;
|
|
|
|
return tb_xdp_handle_error(&res.hdr);
|
|
}
|
|
|
|
static int
|
|
tb_xdp_properties_changed_response(struct tb_ctl *ctl, u64 route, u8 sequence)
|
|
{
|
|
struct tb_xdp_properties_changed_response res;
|
|
|
|
memset(&res, 0, sizeof(res));
|
|
tb_xdp_fill_header(&res.hdr, route, sequence,
|
|
PROPERTIES_CHANGED_RESPONSE, sizeof(res));
|
|
return __tb_xdomain_response(ctl, &res, sizeof(res),
|
|
TB_CFG_PKG_XDOMAIN_RESP);
|
|
}
|
|
|
|
/**
|
|
* tb_register_protocol_handler() - Register protocol handler
|
|
* @handler: Handler to register
|
|
*
|
|
* This allows XDomain service drivers to hook into incoming XDomain
|
|
* messages. After this function is called the service driver needs to
|
|
* be able to handle calls to callback whenever a package with the
|
|
* registered protocol is received.
|
|
*/
|
|
int tb_register_protocol_handler(struct tb_protocol_handler *handler)
|
|
{
|
|
if (!handler->uuid || !handler->callback)
|
|
return -EINVAL;
|
|
if (uuid_equal(handler->uuid, &tb_xdp_uuid))
|
|
return -EINVAL;
|
|
|
|
mutex_lock(&xdomain_lock);
|
|
list_add_tail(&handler->list, &protocol_handlers);
|
|
mutex_unlock(&xdomain_lock);
|
|
|
|
return 0;
|
|
}
|
|
EXPORT_SYMBOL_GPL(tb_register_protocol_handler);
|
|
|
|
/**
|
|
* tb_unregister_protocol_handler() - Unregister protocol handler
|
|
* @handler: Handler to unregister
|
|
*
|
|
* Removes the previously registered protocol handler.
|
|
*/
|
|
void tb_unregister_protocol_handler(struct tb_protocol_handler *handler)
|
|
{
|
|
mutex_lock(&xdomain_lock);
|
|
list_del_init(&handler->list);
|
|
mutex_unlock(&xdomain_lock);
|
|
}
|
|
EXPORT_SYMBOL_GPL(tb_unregister_protocol_handler);
|
|
|
|
static void tb_xdp_handle_request(struct work_struct *work)
|
|
{
|
|
struct xdomain_request_work *xw = container_of(work, typeof(*xw), work);
|
|
const struct tb_xdp_header *pkg = xw->pkg;
|
|
const struct tb_xdomain_header *xhdr = &pkg->xd_hdr;
|
|
struct tb *tb = xw->tb;
|
|
struct tb_ctl *ctl = tb->ctl;
|
|
const uuid_t *uuid;
|
|
int ret = 0;
|
|
u32 sequence;
|
|
u64 route;
|
|
|
|
route = ((u64)xhdr->route_hi << 32 | xhdr->route_lo) & ~BIT_ULL(63);
|
|
sequence = xhdr->length_sn & TB_XDOMAIN_SN_MASK;
|
|
sequence >>= TB_XDOMAIN_SN_SHIFT;
|
|
|
|
mutex_lock(&tb->lock);
|
|
if (tb->root_switch)
|
|
uuid = tb->root_switch->uuid;
|
|
else
|
|
uuid = NULL;
|
|
mutex_unlock(&tb->lock);
|
|
|
|
if (!uuid) {
|
|
tb_xdp_error_response(ctl, route, sequence, ERROR_NOT_READY);
|
|
goto out;
|
|
}
|
|
|
|
switch (pkg->type) {
|
|
case PROPERTIES_REQUEST:
|
|
ret = tb_xdp_properties_response(tb, ctl, route, sequence, uuid,
|
|
(const struct tb_xdp_properties *)pkg);
|
|
break;
|
|
|
|
case PROPERTIES_CHANGED_REQUEST: {
|
|
const struct tb_xdp_properties_changed *xchg =
|
|
(const struct tb_xdp_properties_changed *)pkg;
|
|
struct tb_xdomain *xd;
|
|
|
|
ret = tb_xdp_properties_changed_response(ctl, route, sequence);
|
|
|
|
/*
|
|
* Since the properties have been changed, let's update
|
|
* the xdomain related to this connection as well in
|
|
* case there is a change in services it offers.
|
|
*/
|
|
xd = tb_xdomain_find_by_uuid_locked(tb, &xchg->src_uuid);
|
|
if (xd) {
|
|
queue_delayed_work(tb->wq, &xd->get_properties_work,
|
|
msecs_to_jiffies(50));
|
|
tb_xdomain_put(xd);
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case UUID_REQUEST_OLD:
|
|
case UUID_REQUEST:
|
|
ret = tb_xdp_uuid_response(ctl, route, sequence, uuid);
|
|
break;
|
|
|
|
default:
|
|
tb_xdp_error_response(ctl, route, sequence,
|
|
ERROR_NOT_SUPPORTED);
|
|
break;
|
|
}
|
|
|
|
if (ret) {
|
|
tb_warn(tb, "failed to send XDomain response for %#x\n",
|
|
pkg->type);
|
|
}
|
|
|
|
out:
|
|
kfree(xw->pkg);
|
|
kfree(xw);
|
|
|
|
tb_domain_put(tb);
|
|
}
|
|
|
|
static bool
|
|
tb_xdp_schedule_request(struct tb *tb, const struct tb_xdp_header *hdr,
|
|
size_t size)
|
|
{
|
|
struct xdomain_request_work *xw;
|
|
|
|
xw = kmalloc(sizeof(*xw), GFP_KERNEL);
|
|
if (!xw)
|
|
return false;
|
|
|
|
INIT_WORK(&xw->work, tb_xdp_handle_request);
|
|
xw->pkg = kmemdup(hdr, size, GFP_KERNEL);
|
|
if (!xw->pkg) {
|
|
kfree(xw);
|
|
return false;
|
|
}
|
|
xw->tb = tb_domain_get(tb);
|
|
|
|
schedule_work(&xw->work);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* tb_register_service_driver() - Register XDomain service driver
|
|
* @drv: Driver to register
|
|
*
|
|
* Registers new service driver from @drv to the bus.
|
|
*/
|
|
int tb_register_service_driver(struct tb_service_driver *drv)
|
|
{
|
|
drv->driver.bus = &tb_bus_type;
|
|
return driver_register(&drv->driver);
|
|
}
|
|
EXPORT_SYMBOL_GPL(tb_register_service_driver);
|
|
|
|
/**
|
|
* tb_unregister_service_driver() - Unregister XDomain service driver
|
|
* @xdrv: Driver to unregister
|
|
*
|
|
* Unregisters XDomain service driver from the bus.
|
|
*/
|
|
void tb_unregister_service_driver(struct tb_service_driver *drv)
|
|
{
|
|
driver_unregister(&drv->driver);
|
|
}
|
|
EXPORT_SYMBOL_GPL(tb_unregister_service_driver);
|
|
|
|
static ssize_t key_show(struct device *dev, struct device_attribute *attr,
|
|
char *buf)
|
|
{
|
|
struct tb_service *svc = container_of(dev, struct tb_service, dev);
|
|
|
|
/*
|
|
* It should be null terminated but anything else is pretty much
|
|
* allowed.
|
|
*/
|
|
return sprintf(buf, "%*pE\n", (int)strlen(svc->key), svc->key);
|
|
}
|
|
static DEVICE_ATTR_RO(key);
|
|
|
|
static int get_modalias(struct tb_service *svc, char *buf, size_t size)
|
|
{
|
|
return snprintf(buf, size, "tbsvc:k%sp%08Xv%08Xr%08X", svc->key,
|
|
svc->prtcid, svc->prtcvers, svc->prtcrevs);
|
|
}
|
|
|
|
static ssize_t modalias_show(struct device *dev, struct device_attribute *attr,
|
|
char *buf)
|
|
{
|
|
struct tb_service *svc = container_of(dev, struct tb_service, dev);
|
|
|
|
/* Full buffer size except new line and null termination */
|
|
get_modalias(svc, buf, PAGE_SIZE - 2);
|
|
return sprintf(buf, "%s\n", buf);
|
|
}
|
|
static DEVICE_ATTR_RO(modalias);
|
|
|
|
static ssize_t prtcid_show(struct device *dev, struct device_attribute *attr,
|
|
char *buf)
|
|
{
|
|
struct tb_service *svc = container_of(dev, struct tb_service, dev);
|
|
|
|
return sprintf(buf, "%u\n", svc->prtcid);
|
|
}
|
|
static DEVICE_ATTR_RO(prtcid);
|
|
|
|
static ssize_t prtcvers_show(struct device *dev, struct device_attribute *attr,
|
|
char *buf)
|
|
{
|
|
struct tb_service *svc = container_of(dev, struct tb_service, dev);
|
|
|
|
return sprintf(buf, "%u\n", svc->prtcvers);
|
|
}
|
|
static DEVICE_ATTR_RO(prtcvers);
|
|
|
|
static ssize_t prtcrevs_show(struct device *dev, struct device_attribute *attr,
|
|
char *buf)
|
|
{
|
|
struct tb_service *svc = container_of(dev, struct tb_service, dev);
|
|
|
|
return sprintf(buf, "%u\n", svc->prtcrevs);
|
|
}
|
|
static DEVICE_ATTR_RO(prtcrevs);
|
|
|
|
static ssize_t prtcstns_show(struct device *dev, struct device_attribute *attr,
|
|
char *buf)
|
|
{
|
|
struct tb_service *svc = container_of(dev, struct tb_service, dev);
|
|
|
|
return sprintf(buf, "0x%08x\n", svc->prtcstns);
|
|
}
|
|
static DEVICE_ATTR_RO(prtcstns);
|
|
|
|
static struct attribute *tb_service_attrs[] = {
|
|
&dev_attr_key.attr,
|
|
&dev_attr_modalias.attr,
|
|
&dev_attr_prtcid.attr,
|
|
&dev_attr_prtcvers.attr,
|
|
&dev_attr_prtcrevs.attr,
|
|
&dev_attr_prtcstns.attr,
|
|
NULL,
|
|
};
|
|
|
|
static struct attribute_group tb_service_attr_group = {
|
|
.attrs = tb_service_attrs,
|
|
};
|
|
|
|
static const struct attribute_group *tb_service_attr_groups[] = {
|
|
&tb_service_attr_group,
|
|
NULL,
|
|
};
|
|
|
|
static int tb_service_uevent(struct device *dev, struct kobj_uevent_env *env)
|
|
{
|
|
struct tb_service *svc = container_of(dev, struct tb_service, dev);
|
|
char modalias[64];
|
|
|
|
get_modalias(svc, modalias, sizeof(modalias));
|
|
return add_uevent_var(env, "MODALIAS=%s", modalias);
|
|
}
|
|
|
|
static void tb_service_release(struct device *dev)
|
|
{
|
|
struct tb_service *svc = container_of(dev, struct tb_service, dev);
|
|
struct tb_xdomain *xd = tb_service_parent(svc);
|
|
|
|
ida_simple_remove(&xd->service_ids, svc->id);
|
|
kfree(svc->key);
|
|
kfree(svc);
|
|
}
|
|
|
|
struct device_type tb_service_type = {
|
|
.name = "thunderbolt_service",
|
|
.groups = tb_service_attr_groups,
|
|
.uevent = tb_service_uevent,
|
|
.release = tb_service_release,
|
|
};
|
|
EXPORT_SYMBOL_GPL(tb_service_type);
|
|
|
|
static int remove_missing_service(struct device *dev, void *data)
|
|
{
|
|
struct tb_xdomain *xd = data;
|
|
struct tb_service *svc;
|
|
|
|
svc = tb_to_service(dev);
|
|
if (!svc)
|
|
return 0;
|
|
|
|
if (!tb_property_find(xd->properties, svc->key,
|
|
TB_PROPERTY_TYPE_DIRECTORY))
|
|
device_unregister(dev);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int find_service(struct device *dev, void *data)
|
|
{
|
|
const struct tb_property *p = data;
|
|
struct tb_service *svc;
|
|
|
|
svc = tb_to_service(dev);
|
|
if (!svc)
|
|
return 0;
|
|
|
|
return !strcmp(svc->key, p->key);
|
|
}
|
|
|
|
static int populate_service(struct tb_service *svc,
|
|
struct tb_property *property)
|
|
{
|
|
struct tb_property_dir *dir = property->value.dir;
|
|
struct tb_property *p;
|
|
|
|
/* Fill in standard properties */
|
|
p = tb_property_find(dir, "prtcid", TB_PROPERTY_TYPE_VALUE);
|
|
if (p)
|
|
svc->prtcid = p->value.immediate;
|
|
p = tb_property_find(dir, "prtcvers", TB_PROPERTY_TYPE_VALUE);
|
|
if (p)
|
|
svc->prtcvers = p->value.immediate;
|
|
p = tb_property_find(dir, "prtcrevs", TB_PROPERTY_TYPE_VALUE);
|
|
if (p)
|
|
svc->prtcrevs = p->value.immediate;
|
|
p = tb_property_find(dir, "prtcstns", TB_PROPERTY_TYPE_VALUE);
|
|
if (p)
|
|
svc->prtcstns = p->value.immediate;
|
|
|
|
svc->key = kstrdup(property->key, GFP_KERNEL);
|
|
if (!svc->key)
|
|
return -ENOMEM;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void enumerate_services(struct tb_xdomain *xd)
|
|
{
|
|
struct tb_service *svc;
|
|
struct tb_property *p;
|
|
struct device *dev;
|
|
int id;
|
|
|
|
/*
|
|
* First remove all services that are not available anymore in
|
|
* the updated property block.
|
|
*/
|
|
device_for_each_child_reverse(&xd->dev, xd, remove_missing_service);
|
|
|
|
/* Then re-enumerate properties creating new services as we go */
|
|
tb_property_for_each(xd->properties, p) {
|
|
if (p->type != TB_PROPERTY_TYPE_DIRECTORY)
|
|
continue;
|
|
|
|
/* If the service exists already we are fine */
|
|
dev = device_find_child(&xd->dev, p, find_service);
|
|
if (dev) {
|
|
put_device(dev);
|
|
continue;
|
|
}
|
|
|
|
svc = kzalloc(sizeof(*svc), GFP_KERNEL);
|
|
if (!svc)
|
|
break;
|
|
|
|
if (populate_service(svc, p)) {
|
|
kfree(svc);
|
|
break;
|
|
}
|
|
|
|
id = ida_simple_get(&xd->service_ids, 0, 0, GFP_KERNEL);
|
|
if (id < 0) {
|
|
kfree(svc);
|
|
break;
|
|
}
|
|
svc->id = id;
|
|
svc->dev.bus = &tb_bus_type;
|
|
svc->dev.type = &tb_service_type;
|
|
svc->dev.parent = &xd->dev;
|
|
dev_set_name(&svc->dev, "%s.%d", dev_name(&xd->dev), svc->id);
|
|
|
|
if (device_register(&svc->dev)) {
|
|
put_device(&svc->dev);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
static int populate_properties(struct tb_xdomain *xd,
|
|
struct tb_property_dir *dir)
|
|
{
|
|
const struct tb_property *p;
|
|
|
|
/* Required properties */
|
|
p = tb_property_find(dir, "deviceid", TB_PROPERTY_TYPE_VALUE);
|
|
if (!p)
|
|
return -EINVAL;
|
|
xd->device = p->value.immediate;
|
|
|
|
p = tb_property_find(dir, "vendorid", TB_PROPERTY_TYPE_VALUE);
|
|
if (!p)
|
|
return -EINVAL;
|
|
xd->vendor = p->value.immediate;
|
|
|
|
kfree(xd->device_name);
|
|
xd->device_name = NULL;
|
|
kfree(xd->vendor_name);
|
|
xd->vendor_name = NULL;
|
|
|
|
/* Optional properties */
|
|
p = tb_property_find(dir, "deviceid", TB_PROPERTY_TYPE_TEXT);
|
|
if (p)
|
|
xd->device_name = kstrdup(p->value.text, GFP_KERNEL);
|
|
p = tb_property_find(dir, "vendorid", TB_PROPERTY_TYPE_TEXT);
|
|
if (p)
|
|
xd->vendor_name = kstrdup(p->value.text, GFP_KERNEL);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* Called with @xd->lock held */
|
|
static void tb_xdomain_restore_paths(struct tb_xdomain *xd)
|
|
{
|
|
if (!xd->resume)
|
|
return;
|
|
|
|
xd->resume = false;
|
|
if (xd->transmit_path) {
|
|
dev_dbg(&xd->dev, "re-establishing DMA path\n");
|
|
tb_domain_approve_xdomain_paths(xd->tb, xd);
|
|
}
|
|
}
|
|
|
|
static void tb_xdomain_get_uuid(struct work_struct *work)
|
|
{
|
|
struct tb_xdomain *xd = container_of(work, typeof(*xd),
|
|
get_uuid_work.work);
|
|
struct tb *tb = xd->tb;
|
|
uuid_t uuid;
|
|
int ret;
|
|
|
|
ret = tb_xdp_uuid_request(tb->ctl, xd->route, xd->uuid_retries, &uuid);
|
|
if (ret < 0) {
|
|
if (xd->uuid_retries-- > 0) {
|
|
queue_delayed_work(xd->tb->wq, &xd->get_uuid_work,
|
|
msecs_to_jiffies(100));
|
|
} else {
|
|
dev_dbg(&xd->dev, "failed to read remote UUID\n");
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (uuid_equal(&uuid, xd->local_uuid)) {
|
|
dev_dbg(&xd->dev, "intra-domain loop detected\n");
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* If the UUID is different, there is another domain connected
|
|
* so mark this one unplugged and wait for the connection
|
|
* manager to replace it.
|
|
*/
|
|
if (xd->remote_uuid && !uuid_equal(&uuid, xd->remote_uuid)) {
|
|
dev_dbg(&xd->dev, "remote UUID is different, unplugging\n");
|
|
xd->is_unplugged = true;
|
|
return;
|
|
}
|
|
|
|
/* First time fill in the missing UUID */
|
|
if (!xd->remote_uuid) {
|
|
xd->remote_uuid = kmemdup(&uuid, sizeof(uuid_t), GFP_KERNEL);
|
|
if (!xd->remote_uuid)
|
|
return;
|
|
}
|
|
|
|
/* Now we can start the normal properties exchange */
|
|
queue_delayed_work(xd->tb->wq, &xd->properties_changed_work,
|
|
msecs_to_jiffies(100));
|
|
queue_delayed_work(xd->tb->wq, &xd->get_properties_work,
|
|
msecs_to_jiffies(1000));
|
|
}
|
|
|
|
static void tb_xdomain_get_properties(struct work_struct *work)
|
|
{
|
|
struct tb_xdomain *xd = container_of(work, typeof(*xd),
|
|
get_properties_work.work);
|
|
struct tb_property_dir *dir;
|
|
struct tb *tb = xd->tb;
|
|
bool update = false;
|
|
u32 *block = NULL;
|
|
u32 gen = 0;
|
|
int ret;
|
|
|
|
ret = tb_xdp_properties_request(tb->ctl, xd->route, xd->local_uuid,
|
|
xd->remote_uuid, xd->properties_retries,
|
|
&block, &gen);
|
|
if (ret < 0) {
|
|
if (xd->properties_retries-- > 0) {
|
|
queue_delayed_work(xd->tb->wq, &xd->get_properties_work,
|
|
msecs_to_jiffies(1000));
|
|
} else {
|
|
/* Give up now */
|
|
dev_err(&xd->dev,
|
|
"failed read XDomain properties from %pUb\n",
|
|
xd->remote_uuid);
|
|
}
|
|
return;
|
|
}
|
|
|
|
xd->properties_retries = XDOMAIN_PROPERTIES_RETRIES;
|
|
|
|
mutex_lock(&xd->lock);
|
|
|
|
/* Only accept newer generation properties */
|
|
if (xd->properties && gen <= xd->property_block_gen) {
|
|
/*
|
|
* On resume it is likely that the properties block is
|
|
* not changed (unless the other end added or removed
|
|
* services). However, we need to make sure the existing
|
|
* DMA paths are restored properly.
|
|
*/
|
|
tb_xdomain_restore_paths(xd);
|
|
goto err_free_block;
|
|
}
|
|
|
|
dir = tb_property_parse_dir(block, ret);
|
|
if (!dir) {
|
|
dev_err(&xd->dev, "failed to parse XDomain properties\n");
|
|
goto err_free_block;
|
|
}
|
|
|
|
ret = populate_properties(xd, dir);
|
|
if (ret) {
|
|
dev_err(&xd->dev, "missing XDomain properties in response\n");
|
|
goto err_free_dir;
|
|
}
|
|
|
|
/* Release the existing one */
|
|
if (xd->properties) {
|
|
tb_property_free_dir(xd->properties);
|
|
update = true;
|
|
}
|
|
|
|
xd->properties = dir;
|
|
xd->property_block_gen = gen;
|
|
|
|
tb_xdomain_restore_paths(xd);
|
|
|
|
mutex_unlock(&xd->lock);
|
|
|
|
kfree(block);
|
|
|
|
/*
|
|
* Now the device should be ready enough so we can add it to the
|
|
* bus and let userspace know about it. If the device is already
|
|
* registered, we notify the userspace that it has changed.
|
|
*/
|
|
if (!update) {
|
|
if (device_add(&xd->dev)) {
|
|
dev_err(&xd->dev, "failed to add XDomain device\n");
|
|
return;
|
|
}
|
|
} else {
|
|
kobject_uevent(&xd->dev.kobj, KOBJ_CHANGE);
|
|
}
|
|
|
|
enumerate_services(xd);
|
|
return;
|
|
|
|
err_free_dir:
|
|
tb_property_free_dir(dir);
|
|
err_free_block:
|
|
kfree(block);
|
|
mutex_unlock(&xd->lock);
|
|
}
|
|
|
|
static void tb_xdomain_properties_changed(struct work_struct *work)
|
|
{
|
|
struct tb_xdomain *xd = container_of(work, typeof(*xd),
|
|
properties_changed_work.work);
|
|
int ret;
|
|
|
|
ret = tb_xdp_properties_changed_request(xd->tb->ctl, xd->route,
|
|
xd->properties_changed_retries, xd->local_uuid);
|
|
if (ret) {
|
|
if (xd->properties_changed_retries-- > 0)
|
|
queue_delayed_work(xd->tb->wq,
|
|
&xd->properties_changed_work,
|
|
msecs_to_jiffies(1000));
|
|
return;
|
|
}
|
|
|
|
xd->properties_changed_retries = XDOMAIN_PROPERTIES_CHANGED_RETRIES;
|
|
}
|
|
|
|
static ssize_t device_show(struct device *dev, struct device_attribute *attr,
|
|
char *buf)
|
|
{
|
|
struct tb_xdomain *xd = container_of(dev, struct tb_xdomain, dev);
|
|
|
|
return sprintf(buf, "%#x\n", xd->device);
|
|
}
|
|
static DEVICE_ATTR_RO(device);
|
|
|
|
static ssize_t
|
|
device_name_show(struct device *dev, struct device_attribute *attr, char *buf)
|
|
{
|
|
struct tb_xdomain *xd = container_of(dev, struct tb_xdomain, dev);
|
|
int ret;
|
|
|
|
if (mutex_lock_interruptible(&xd->lock))
|
|
return -ERESTARTSYS;
|
|
ret = sprintf(buf, "%s\n", xd->device_name ? xd->device_name : "");
|
|
mutex_unlock(&xd->lock);
|
|
|
|
return ret;
|
|
}
|
|
static DEVICE_ATTR_RO(device_name);
|
|
|
|
static ssize_t vendor_show(struct device *dev, struct device_attribute *attr,
|
|
char *buf)
|
|
{
|
|
struct tb_xdomain *xd = container_of(dev, struct tb_xdomain, dev);
|
|
|
|
return sprintf(buf, "%#x\n", xd->vendor);
|
|
}
|
|
static DEVICE_ATTR_RO(vendor);
|
|
|
|
static ssize_t
|
|
vendor_name_show(struct device *dev, struct device_attribute *attr, char *buf)
|
|
{
|
|
struct tb_xdomain *xd = container_of(dev, struct tb_xdomain, dev);
|
|
int ret;
|
|
|
|
if (mutex_lock_interruptible(&xd->lock))
|
|
return -ERESTARTSYS;
|
|
ret = sprintf(buf, "%s\n", xd->vendor_name ? xd->vendor_name : "");
|
|
mutex_unlock(&xd->lock);
|
|
|
|
return ret;
|
|
}
|
|
static DEVICE_ATTR_RO(vendor_name);
|
|
|
|
static ssize_t unique_id_show(struct device *dev, struct device_attribute *attr,
|
|
char *buf)
|
|
{
|
|
struct tb_xdomain *xd = container_of(dev, struct tb_xdomain, dev);
|
|
|
|
return sprintf(buf, "%pUb\n", xd->remote_uuid);
|
|
}
|
|
static DEVICE_ATTR_RO(unique_id);
|
|
|
|
static struct attribute *xdomain_attrs[] = {
|
|
&dev_attr_device.attr,
|
|
&dev_attr_device_name.attr,
|
|
&dev_attr_unique_id.attr,
|
|
&dev_attr_vendor.attr,
|
|
&dev_attr_vendor_name.attr,
|
|
NULL,
|
|
};
|
|
|
|
static struct attribute_group xdomain_attr_group = {
|
|
.attrs = xdomain_attrs,
|
|
};
|
|
|
|
static const struct attribute_group *xdomain_attr_groups[] = {
|
|
&xdomain_attr_group,
|
|
NULL,
|
|
};
|
|
|
|
static void tb_xdomain_release(struct device *dev)
|
|
{
|
|
struct tb_xdomain *xd = container_of(dev, struct tb_xdomain, dev);
|
|
|
|
put_device(xd->dev.parent);
|
|
|
|
tb_property_free_dir(xd->properties);
|
|
ida_destroy(&xd->service_ids);
|
|
|
|
kfree(xd->local_uuid);
|
|
kfree(xd->remote_uuid);
|
|
kfree(xd->device_name);
|
|
kfree(xd->vendor_name);
|
|
kfree(xd);
|
|
}
|
|
|
|
static void start_handshake(struct tb_xdomain *xd)
|
|
{
|
|
xd->uuid_retries = XDOMAIN_UUID_RETRIES;
|
|
xd->properties_retries = XDOMAIN_PROPERTIES_RETRIES;
|
|
xd->properties_changed_retries = XDOMAIN_PROPERTIES_CHANGED_RETRIES;
|
|
|
|
if (xd->needs_uuid) {
|
|
queue_delayed_work(xd->tb->wq, &xd->get_uuid_work,
|
|
msecs_to_jiffies(100));
|
|
} else {
|
|
/* Start exchanging properties with the other host */
|
|
queue_delayed_work(xd->tb->wq, &xd->properties_changed_work,
|
|
msecs_to_jiffies(100));
|
|
queue_delayed_work(xd->tb->wq, &xd->get_properties_work,
|
|
msecs_to_jiffies(1000));
|
|
}
|
|
}
|
|
|
|
static void stop_handshake(struct tb_xdomain *xd)
|
|
{
|
|
xd->uuid_retries = 0;
|
|
xd->properties_retries = 0;
|
|
xd->properties_changed_retries = 0;
|
|
|
|
cancel_delayed_work_sync(&xd->get_uuid_work);
|
|
cancel_delayed_work_sync(&xd->get_properties_work);
|
|
cancel_delayed_work_sync(&xd->properties_changed_work);
|
|
}
|
|
|
|
static int __maybe_unused tb_xdomain_suspend(struct device *dev)
|
|
{
|
|
stop_handshake(tb_to_xdomain(dev));
|
|
return 0;
|
|
}
|
|
|
|
static int __maybe_unused tb_xdomain_resume(struct device *dev)
|
|
{
|
|
struct tb_xdomain *xd = tb_to_xdomain(dev);
|
|
|
|
/*
|
|
* Ask tb_xdomain_get_properties() restore any existing DMA
|
|
* paths after properties are re-read.
|
|
*/
|
|
xd->resume = true;
|
|
start_handshake(xd);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static const struct dev_pm_ops tb_xdomain_pm_ops = {
|
|
SET_SYSTEM_SLEEP_PM_OPS(tb_xdomain_suspend, tb_xdomain_resume)
|
|
};
|
|
|
|
struct device_type tb_xdomain_type = {
|
|
.name = "thunderbolt_xdomain",
|
|
.release = tb_xdomain_release,
|
|
.pm = &tb_xdomain_pm_ops,
|
|
};
|
|
EXPORT_SYMBOL_GPL(tb_xdomain_type);
|
|
|
|
/**
|
|
* tb_xdomain_alloc() - Allocate new XDomain object
|
|
* @tb: Domain where the XDomain belongs
|
|
* @parent: Parent device (the switch through the connection to the
|
|
* other domain is reached).
|
|
* @route: Route string used to reach the other domain
|
|
* @local_uuid: Our local domain UUID
|
|
* @remote_uuid: UUID of the other domain (optional)
|
|
*
|
|
* Allocates new XDomain structure and returns pointer to that. The
|
|
* object must be released by calling tb_xdomain_put().
|
|
*/
|
|
struct tb_xdomain *tb_xdomain_alloc(struct tb *tb, struct device *parent,
|
|
u64 route, const uuid_t *local_uuid,
|
|
const uuid_t *remote_uuid)
|
|
{
|
|
struct tb_switch *parent_sw = tb_to_switch(parent);
|
|
struct tb_xdomain *xd;
|
|
struct tb_port *down;
|
|
|
|
/* Make sure the downstream domain is accessible */
|
|
down = tb_port_at(route, parent_sw);
|
|
tb_port_unlock(down);
|
|
|
|
xd = kzalloc(sizeof(*xd), GFP_KERNEL);
|
|
if (!xd)
|
|
return NULL;
|
|
|
|
xd->tb = tb;
|
|
xd->route = route;
|
|
ida_init(&xd->service_ids);
|
|
mutex_init(&xd->lock);
|
|
INIT_DELAYED_WORK(&xd->get_uuid_work, tb_xdomain_get_uuid);
|
|
INIT_DELAYED_WORK(&xd->get_properties_work, tb_xdomain_get_properties);
|
|
INIT_DELAYED_WORK(&xd->properties_changed_work,
|
|
tb_xdomain_properties_changed);
|
|
|
|
xd->local_uuid = kmemdup(local_uuid, sizeof(uuid_t), GFP_KERNEL);
|
|
if (!xd->local_uuid)
|
|
goto err_free;
|
|
|
|
if (remote_uuid) {
|
|
xd->remote_uuid = kmemdup(remote_uuid, sizeof(uuid_t),
|
|
GFP_KERNEL);
|
|
if (!xd->remote_uuid)
|
|
goto err_free_local_uuid;
|
|
} else {
|
|
xd->needs_uuid = true;
|
|
}
|
|
|
|
device_initialize(&xd->dev);
|
|
xd->dev.parent = get_device(parent);
|
|
xd->dev.bus = &tb_bus_type;
|
|
xd->dev.type = &tb_xdomain_type;
|
|
xd->dev.groups = xdomain_attr_groups;
|
|
dev_set_name(&xd->dev, "%u-%llx", tb->index, route);
|
|
|
|
/*
|
|
* This keeps the DMA powered on as long as we have active
|
|
* connection to another host.
|
|
*/
|
|
pm_runtime_set_active(&xd->dev);
|
|
pm_runtime_get_noresume(&xd->dev);
|
|
pm_runtime_enable(&xd->dev);
|
|
|
|
return xd;
|
|
|
|
err_free_local_uuid:
|
|
kfree(xd->local_uuid);
|
|
err_free:
|
|
kfree(xd);
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/**
|
|
* tb_xdomain_add() - Add XDomain to the bus
|
|
* @xd: XDomain to add
|
|
*
|
|
* This function starts XDomain discovery protocol handshake and
|
|
* eventually adds the XDomain to the bus. After calling this function
|
|
* the caller needs to call tb_xdomain_remove() in order to remove and
|
|
* release the object regardless whether the handshake succeeded or not.
|
|
*/
|
|
void tb_xdomain_add(struct tb_xdomain *xd)
|
|
{
|
|
/* Start exchanging properties with the other host */
|
|
start_handshake(xd);
|
|
}
|
|
|
|
static int unregister_service(struct device *dev, void *data)
|
|
{
|
|
device_unregister(dev);
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* tb_xdomain_remove() - Remove XDomain from the bus
|
|
* @xd: XDomain to remove
|
|
*
|
|
* This will stop all ongoing configuration work and remove the XDomain
|
|
* along with any services from the bus. When the last reference to @xd
|
|
* is released the object will be released as well.
|
|
*/
|
|
void tb_xdomain_remove(struct tb_xdomain *xd)
|
|
{
|
|
stop_handshake(xd);
|
|
|
|
device_for_each_child_reverse(&xd->dev, xd, unregister_service);
|
|
|
|
/*
|
|
* Undo runtime PM here explicitly because it is possible that
|
|
* the XDomain was never added to the bus and thus device_del()
|
|
* is not called for it (device_del() would handle this otherwise).
|
|
*/
|
|
pm_runtime_disable(&xd->dev);
|
|
pm_runtime_put_noidle(&xd->dev);
|
|
pm_runtime_set_suspended(&xd->dev);
|
|
|
|
if (!device_is_registered(&xd->dev))
|
|
put_device(&xd->dev);
|
|
else
|
|
device_unregister(&xd->dev);
|
|
}
|
|
|
|
/**
|
|
* tb_xdomain_enable_paths() - Enable DMA paths for XDomain connection
|
|
* @xd: XDomain connection
|
|
* @transmit_path: HopID of the transmit path the other end is using to
|
|
* send packets
|
|
* @transmit_ring: DMA ring used to receive packets from the other end
|
|
* @receive_path: HopID of the receive path the other end is using to
|
|
* receive packets
|
|
* @receive_ring: DMA ring used to send packets to the other end
|
|
*
|
|
* The function enables DMA paths accordingly so that after successful
|
|
* return the caller can send and receive packets using high-speed DMA
|
|
* path.
|
|
*
|
|
* Return: %0 in case of success and negative errno in case of error
|
|
*/
|
|
int tb_xdomain_enable_paths(struct tb_xdomain *xd, u16 transmit_path,
|
|
u16 transmit_ring, u16 receive_path,
|
|
u16 receive_ring)
|
|
{
|
|
int ret;
|
|
|
|
mutex_lock(&xd->lock);
|
|
|
|
if (xd->transmit_path) {
|
|
ret = xd->transmit_path == transmit_path ? 0 : -EBUSY;
|
|
goto exit_unlock;
|
|
}
|
|
|
|
xd->transmit_path = transmit_path;
|
|
xd->transmit_ring = transmit_ring;
|
|
xd->receive_path = receive_path;
|
|
xd->receive_ring = receive_ring;
|
|
|
|
ret = tb_domain_approve_xdomain_paths(xd->tb, xd);
|
|
|
|
exit_unlock:
|
|
mutex_unlock(&xd->lock);
|
|
|
|
return ret;
|
|
}
|
|
EXPORT_SYMBOL_GPL(tb_xdomain_enable_paths);
|
|
|
|
/**
|
|
* tb_xdomain_disable_paths() - Disable DMA paths for XDomain connection
|
|
* @xd: XDomain connection
|
|
*
|
|
* This does the opposite of tb_xdomain_enable_paths(). After call to
|
|
* this the caller is not expected to use the rings anymore.
|
|
*
|
|
* Return: %0 in case of success and negative errno in case of error
|
|
*/
|
|
int tb_xdomain_disable_paths(struct tb_xdomain *xd)
|
|
{
|
|
int ret = 0;
|
|
|
|
mutex_lock(&xd->lock);
|
|
if (xd->transmit_path) {
|
|
xd->transmit_path = 0;
|
|
xd->transmit_ring = 0;
|
|
xd->receive_path = 0;
|
|
xd->receive_ring = 0;
|
|
|
|
ret = tb_domain_disconnect_xdomain_paths(xd->tb, xd);
|
|
}
|
|
mutex_unlock(&xd->lock);
|
|
|
|
return ret;
|
|
}
|
|
EXPORT_SYMBOL_GPL(tb_xdomain_disable_paths);
|
|
|
|
struct tb_xdomain_lookup {
|
|
const uuid_t *uuid;
|
|
u8 link;
|
|
u8 depth;
|
|
u64 route;
|
|
};
|
|
|
|
static struct tb_xdomain *switch_find_xdomain(struct tb_switch *sw,
|
|
const struct tb_xdomain_lookup *lookup)
|
|
{
|
|
struct tb_port *port;
|
|
|
|
tb_switch_for_each_port(sw, port) {
|
|
struct tb_xdomain *xd;
|
|
|
|
if (port->xdomain) {
|
|
xd = port->xdomain;
|
|
|
|
if (lookup->uuid) {
|
|
if (xd->remote_uuid &&
|
|
uuid_equal(xd->remote_uuid, lookup->uuid))
|
|
return xd;
|
|
} else if (lookup->link &&
|
|
lookup->link == xd->link &&
|
|
lookup->depth == xd->depth) {
|
|
return xd;
|
|
} else if (lookup->route &&
|
|
lookup->route == xd->route) {
|
|
return xd;
|
|
}
|
|
} else if (tb_port_has_remote(port)) {
|
|
xd = switch_find_xdomain(port->remote->sw, lookup);
|
|
if (xd)
|
|
return xd;
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/**
|
|
* tb_xdomain_find_by_uuid() - Find an XDomain by UUID
|
|
* @tb: Domain where the XDomain belongs to
|
|
* @uuid: UUID to look for
|
|
*
|
|
* Finds XDomain by walking through the Thunderbolt topology below @tb.
|
|
* The returned XDomain will have its reference count increased so the
|
|
* caller needs to call tb_xdomain_put() when it is done with the
|
|
* object.
|
|
*
|
|
* This will find all XDomains including the ones that are not yet added
|
|
* to the bus (handshake is still in progress).
|
|
*
|
|
* The caller needs to hold @tb->lock.
|
|
*/
|
|
struct tb_xdomain *tb_xdomain_find_by_uuid(struct tb *tb, const uuid_t *uuid)
|
|
{
|
|
struct tb_xdomain_lookup lookup;
|
|
struct tb_xdomain *xd;
|
|
|
|
memset(&lookup, 0, sizeof(lookup));
|
|
lookup.uuid = uuid;
|
|
|
|
xd = switch_find_xdomain(tb->root_switch, &lookup);
|
|
return tb_xdomain_get(xd);
|
|
}
|
|
EXPORT_SYMBOL_GPL(tb_xdomain_find_by_uuid);
|
|
|
|
/**
|
|
* tb_xdomain_find_by_link_depth() - Find an XDomain by link and depth
|
|
* @tb: Domain where the XDomain belongs to
|
|
* @link: Root switch link number
|
|
* @depth: Depth in the link
|
|
*
|
|
* Finds XDomain by walking through the Thunderbolt topology below @tb.
|
|
* The returned XDomain will have its reference count increased so the
|
|
* caller needs to call tb_xdomain_put() when it is done with the
|
|
* object.
|
|
*
|
|
* This will find all XDomains including the ones that are not yet added
|
|
* to the bus (handshake is still in progress).
|
|
*
|
|
* The caller needs to hold @tb->lock.
|
|
*/
|
|
struct tb_xdomain *tb_xdomain_find_by_link_depth(struct tb *tb, u8 link,
|
|
u8 depth)
|
|
{
|
|
struct tb_xdomain_lookup lookup;
|
|
struct tb_xdomain *xd;
|
|
|
|
memset(&lookup, 0, sizeof(lookup));
|
|
lookup.link = link;
|
|
lookup.depth = depth;
|
|
|
|
xd = switch_find_xdomain(tb->root_switch, &lookup);
|
|
return tb_xdomain_get(xd);
|
|
}
|
|
|
|
/**
|
|
* tb_xdomain_find_by_route() - Find an XDomain by route string
|
|
* @tb: Domain where the XDomain belongs to
|
|
* @route: XDomain route string
|
|
*
|
|
* Finds XDomain by walking through the Thunderbolt topology below @tb.
|
|
* The returned XDomain will have its reference count increased so the
|
|
* caller needs to call tb_xdomain_put() when it is done with the
|
|
* object.
|
|
*
|
|
* This will find all XDomains including the ones that are not yet added
|
|
* to the bus (handshake is still in progress).
|
|
*
|
|
* The caller needs to hold @tb->lock.
|
|
*/
|
|
struct tb_xdomain *tb_xdomain_find_by_route(struct tb *tb, u64 route)
|
|
{
|
|
struct tb_xdomain_lookup lookup;
|
|
struct tb_xdomain *xd;
|
|
|
|
memset(&lookup, 0, sizeof(lookup));
|
|
lookup.route = route;
|
|
|
|
xd = switch_find_xdomain(tb->root_switch, &lookup);
|
|
return tb_xdomain_get(xd);
|
|
}
|
|
EXPORT_SYMBOL_GPL(tb_xdomain_find_by_route);
|
|
|
|
bool tb_xdomain_handle_request(struct tb *tb, enum tb_cfg_pkg_type type,
|
|
const void *buf, size_t size)
|
|
{
|
|
const struct tb_protocol_handler *handler, *tmp;
|
|
const struct tb_xdp_header *hdr = buf;
|
|
unsigned int length;
|
|
int ret = 0;
|
|
|
|
/* We expect the packet is at least size of the header */
|
|
length = hdr->xd_hdr.length_sn & TB_XDOMAIN_LENGTH_MASK;
|
|
if (length != size / 4 - sizeof(hdr->xd_hdr) / 4)
|
|
return true;
|
|
if (length < sizeof(*hdr) / 4 - sizeof(hdr->xd_hdr) / 4)
|
|
return true;
|
|
|
|
/*
|
|
* Handle XDomain discovery protocol packets directly here. For
|
|
* other protocols (based on their UUID) we call registered
|
|
* handlers in turn.
|
|
*/
|
|
if (uuid_equal(&hdr->uuid, &tb_xdp_uuid)) {
|
|
if (type == TB_CFG_PKG_XDOMAIN_REQ)
|
|
return tb_xdp_schedule_request(tb, hdr, size);
|
|
return false;
|
|
}
|
|
|
|
mutex_lock(&xdomain_lock);
|
|
list_for_each_entry_safe(handler, tmp, &protocol_handlers, list) {
|
|
if (!uuid_equal(&hdr->uuid, handler->uuid))
|
|
continue;
|
|
|
|
mutex_unlock(&xdomain_lock);
|
|
ret = handler->callback(buf, size, handler->data);
|
|
mutex_lock(&xdomain_lock);
|
|
|
|
if (ret)
|
|
break;
|
|
}
|
|
mutex_unlock(&xdomain_lock);
|
|
|
|
return ret > 0;
|
|
}
|
|
|
|
static int rebuild_property_block(void)
|
|
{
|
|
u32 *block, len;
|
|
int ret;
|
|
|
|
ret = tb_property_format_dir(xdomain_property_dir, NULL, 0);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
len = ret;
|
|
|
|
block = kcalloc(len, sizeof(u32), GFP_KERNEL);
|
|
if (!block)
|
|
return -ENOMEM;
|
|
|
|
ret = tb_property_format_dir(xdomain_property_dir, block, len);
|
|
if (ret) {
|
|
kfree(block);
|
|
return ret;
|
|
}
|
|
|
|
kfree(xdomain_property_block);
|
|
xdomain_property_block = block;
|
|
xdomain_property_block_len = len;
|
|
xdomain_property_block_gen++;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int update_xdomain(struct device *dev, void *data)
|
|
{
|
|
struct tb_xdomain *xd;
|
|
|
|
xd = tb_to_xdomain(dev);
|
|
if (xd) {
|
|
queue_delayed_work(xd->tb->wq, &xd->properties_changed_work,
|
|
msecs_to_jiffies(50));
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void update_all_xdomains(void)
|
|
{
|
|
bus_for_each_dev(&tb_bus_type, NULL, NULL, update_xdomain);
|
|
}
|
|
|
|
static bool remove_directory(const char *key, const struct tb_property_dir *dir)
|
|
{
|
|
struct tb_property *p;
|
|
|
|
p = tb_property_find(xdomain_property_dir, key,
|
|
TB_PROPERTY_TYPE_DIRECTORY);
|
|
if (p && p->value.dir == dir) {
|
|
tb_property_remove(p);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* tb_register_property_dir() - Register property directory to the host
|
|
* @key: Key (name) of the directory to add
|
|
* @dir: Directory to add
|
|
*
|
|
* Service drivers can use this function to add new property directory
|
|
* to the host available properties. The other connected hosts are
|
|
* notified so they can re-read properties of this host if they are
|
|
* interested.
|
|
*
|
|
* Return: %0 on success and negative errno on failure
|
|
*/
|
|
int tb_register_property_dir(const char *key, struct tb_property_dir *dir)
|
|
{
|
|
int ret;
|
|
|
|
if (WARN_ON(!xdomain_property_dir))
|
|
return -EAGAIN;
|
|
|
|
if (!key || strlen(key) > 8)
|
|
return -EINVAL;
|
|
|
|
mutex_lock(&xdomain_lock);
|
|
if (tb_property_find(xdomain_property_dir, key,
|
|
TB_PROPERTY_TYPE_DIRECTORY)) {
|
|
ret = -EEXIST;
|
|
goto err_unlock;
|
|
}
|
|
|
|
ret = tb_property_add_dir(xdomain_property_dir, key, dir);
|
|
if (ret)
|
|
goto err_unlock;
|
|
|
|
ret = rebuild_property_block();
|
|
if (ret) {
|
|
remove_directory(key, dir);
|
|
goto err_unlock;
|
|
}
|
|
|
|
mutex_unlock(&xdomain_lock);
|
|
update_all_xdomains();
|
|
return 0;
|
|
|
|
err_unlock:
|
|
mutex_unlock(&xdomain_lock);
|
|
return ret;
|
|
}
|
|
EXPORT_SYMBOL_GPL(tb_register_property_dir);
|
|
|
|
/**
|
|
* tb_unregister_property_dir() - Removes property directory from host
|
|
* @key: Key (name) of the directory
|
|
* @dir: Directory to remove
|
|
*
|
|
* This will remove the existing directory from this host and notify the
|
|
* connected hosts about the change.
|
|
*/
|
|
void tb_unregister_property_dir(const char *key, struct tb_property_dir *dir)
|
|
{
|
|
int ret = 0;
|
|
|
|
mutex_lock(&xdomain_lock);
|
|
if (remove_directory(key, dir))
|
|
ret = rebuild_property_block();
|
|
mutex_unlock(&xdomain_lock);
|
|
|
|
if (!ret)
|
|
update_all_xdomains();
|
|
}
|
|
EXPORT_SYMBOL_GPL(tb_unregister_property_dir);
|
|
|
|
int tb_xdomain_init(void)
|
|
{
|
|
int ret;
|
|
|
|
xdomain_property_dir = tb_property_create_dir(NULL);
|
|
if (!xdomain_property_dir)
|
|
return -ENOMEM;
|
|
|
|
/*
|
|
* Initialize standard set of properties without any service
|
|
* directories. Those will be added by service drivers
|
|
* themselves when they are loaded.
|
|
*/
|
|
tb_property_add_immediate(xdomain_property_dir, "vendorid",
|
|
PCI_VENDOR_ID_INTEL);
|
|
tb_property_add_text(xdomain_property_dir, "vendorid", "Intel Corp.");
|
|
tb_property_add_immediate(xdomain_property_dir, "deviceid", 0x1);
|
|
tb_property_add_text(xdomain_property_dir, "deviceid",
|
|
utsname()->nodename);
|
|
tb_property_add_immediate(xdomain_property_dir, "devicerv", 0x80000100);
|
|
|
|
ret = rebuild_property_block();
|
|
if (ret) {
|
|
tb_property_free_dir(xdomain_property_dir);
|
|
xdomain_property_dir = NULL;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
void tb_xdomain_exit(void)
|
|
{
|
|
kfree(xdomain_property_block);
|
|
tb_property_free_dir(xdomain_property_dir);
|
|
}
|