usb: typec: ucsi: Detect and skip duplicate altmodes from buggy firmware

Some firmware implementations incorrectly return the same altmode
multiple times at different offsets when queried via
UCSI_GET_ALTERNATE_MODES.  This causes sysfs duplicate filename errors
and kernel call traces when the driver attempts to register the same
altmode twice:

  sysfs: cannot create duplicate filename '/devices/.../typec/port0/port0.0/partner'
  typec-thunderbolt port0-partner.1: failed to create symlinks
  typec-thunderbolt port0-partner.1: probe with driver typec-thunderbolt failed with error -17

The matching rules differ by recipient:

  - UCSI_RECIPIENT_CON (port) and UCSI_RECIPIENT_SOP_P (plug):
    Two altmodes with identical SVID and VDO are byte-for-byte
    duplicates and the second has no observable function, so drop it.

  - UCSI_RECIPIENT_SOP (partner):
    The typec class binds each partner altmode to a port altmode of
    the same SVID via altmode_match()/device_find_child(), which
    returns the first port altmode with a matching SVID.  If the
    partner advertises more altmodes for SVID X than the port
    advertises, the surplus partner altmode(s) collapse onto an
    already-paired port altmode and trigger the
    "duplicate filename .../partner" sysfs error during
    typec_altmode_create_links().  Use the port-side altmode count for
    SVID X as the authoritative cap and reject any partner altmode
    that would exceed it.  This preserves legitimate multi-Mode
    partner altmodes (vendor SVIDs that the port really does
    advertise more than once) while filtering the firmware-generated
    duplicates that have no port counterpart, and is therefore
    stricter than a plain SVID+VDO comparison (which still admits the
    Thunderbolt case where firmware reports the same SVID twice with
    different VDOs) without being over-broad like a plain SVID match
    (which would falsely drop legitimate vendor multi-Mode entries).

If a duplicate is detected, skip it and emit a clean warning instead
of generating a kernel call trace:

  ucsi_acpi USBC000:00: con2: Firmware bug: duplicate partner altmode SVID 0x8087 at offset 1, ignoring.
  ucsi_acpi USBC000:00: con2: VDO mismatch: 0x8087a043 vs 0x00000001

The duplicate detection logic lives in a reusable helper
ucsi_altmode_is_duplicate() and is invoked from
ucsi_register_altmodes().  It applies to all three recipient types:
partner (SOP), port (CON), and plug (SOP_P) altmodes.

Fixes: a79f16efcd ("usb: typec: ucsi: Add support for the partner USB Modes")
Cc: stable <stable@kernel.org>
Signed-off-by: Chia-Lin Kao (AceLan) <acelan.kao@canonical.com>
unchanged: still SVID+VDO exact-dup match.
Reviewed-by: Heikki Krogerus <heikki.krogerus@linux.intel.com>
Link: https://patch.msgid.link/20260713084323.287516-1-acelan.kao@canonical.com
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
This commit is contained in:
Chia-Lin Kao (AceLan) 2026-07-13 16:43:21 +08:00 committed by Greg Kroah-Hartman
parent d5e5cd3654
commit 67c92c6419

View File

@ -529,6 +529,129 @@ static int ucsi_register_altmode(struct ucsi_connector *con,
return ret;
}
static void ucsi_dump_duplicate_altmode(struct ucsi_connector *con,
u8 recipient, u16 svid,
u32 existing_vdo, u32 new_vdo,
int offset)
{
static const char * const recipient_names[] = {
[UCSI_RECIPIENT_CON] = "port",
[UCSI_RECIPIENT_SOP] = "partner",
[UCSI_RECIPIENT_SOP_P] = "plug",
[UCSI_RECIPIENT_SOP_PP] = "cable plug prime",
};
dev_warn(con->ucsi->dev,
"con%d: Firmware bug: duplicate %s altmode SVID 0x%04x at offset %d, ignoring but please contact the BIOS vendor to fix this issue.\n",
con->num, recipient_names[recipient], svid, offset);
if (existing_vdo != new_vdo)
dev_warn(con->ucsi->dev,
"con%d: VDO mismatch: 0x%08x vs 0x%08x\n",
con->num, existing_vdo, new_vdo);
}
/* Count altmodes in @altmodes that advertise @svid. */
static int ucsi_altmode_count_svid(struct typec_altmode **altmodes, u16 svid)
{
int count = 0;
int k;
for (k = 0; k < UCSI_MAX_ALTMODES; k++) {
if (!altmodes[k])
break;
if (altmodes[k]->svid == svid)
count++;
}
return count;
}
/*
* Check if an altmode is a duplicate. Some firmware implementations
* incorrectly return the same altmode multiple times, causing sysfs errors.
* Returns true if the altmode should be skipped.
*
* The matching rules differ by recipient:
*
* - UCSI_RECIPIENT_CON (port) and UCSI_RECIPIENT_SOP_P (plug):
* Two altmodes with identical SVID and VDO are byte-for-byte duplicates
* and the second has no observable function. Drop them.
*
* - UCSI_RECIPIENT_SOP (partner):
* The typec class binds each partner altmode to a port altmode of the
* same SVID via altmode_match()/device_find_child(), which returns the
* first port altmode with a matching SVID. If the partner advertises
* more altmodes for SVID X than the port advertises, the surplus
* partner altmode(s) collapse onto an already-paired port altmode and
* trigger a "duplicate filename .../partner" sysfs error during
* typec_altmode_create_links(). Use the port-side altmode count for
* SVID X as the authoritative cap and reject any partner altmode that
* would exceed it. This preserves legitimate multi-Mode partner
* altmodes (e.g. vendor SVIDs that the port really does advertise
* twice) while filtering the firmware-generated duplicates that have
* no port counterpart.
*/
static bool ucsi_altmode_is_duplicate(struct ucsi_connector *con, u8 recipient,
const struct ucsi_altmode *alt_batch, int batch_idx,
u16 svid, u32 vdo, int offset)
{
struct typec_altmode **altmodes;
int port_count, partner_count;
int k;
/* Check for duplicates within the current batch first */
for (k = 0; k < batch_idx; k++) {
if (alt_batch[k].svid == svid && alt_batch[k].mid == vdo) {
ucsi_dump_duplicate_altmode(con, recipient, svid,
vdo, vdo, offset);
return true;
}
}
switch (recipient) {
case UCSI_RECIPIENT_SOP:
/*
* Cap partner altmodes per SVID by the port-side count:
* any further partner altmode for that SVID would alias an
* already-paired port altmode and break typec sysfs.
*/
port_count = ucsi_altmode_count_svid(con->port_altmode, svid);
partner_count = ucsi_altmode_count_svid(con->partner_altmode,
svid);
if (port_count && partner_count >= port_count) {
ucsi_dump_duplicate_altmode(con, recipient, svid,
con->partner_altmode[partner_count - 1]->vdo,
vdo, offset);
return true;
}
return false;
case UCSI_RECIPIENT_CON:
altmodes = con->port_altmode;
break;
case UCSI_RECIPIENT_SOP_P:
altmodes = con->plug_altmode;
break;
default:
return false;
}
/* CON and SOP_P: drop only exact SVID+VDO duplicates. */
for (k = 0; k < UCSI_MAX_ALTMODES; k++) {
if (!altmodes[k])
break;
if (altmodes[k]->svid != svid || altmodes[k]->vdo != vdo)
continue;
ucsi_dump_duplicate_altmode(con, recipient, svid,
altmodes[k]->vdo, vdo, offset);
return true;
}
return false;
}
static int
ucsi_register_altmodes_nvidia(struct ucsi_connector *con, u8 recipient)
{
@ -653,6 +776,15 @@ static int ucsi_register_altmodes(struct ucsi_connector *con, u8 recipient)
if (!alt[j].svid)
return 0;
/*
* Check for duplicates in current batch and already
* registered altmodes. Skip if duplicate found.
*/
if (ucsi_altmode_is_duplicate(con, recipient, alt, j,
alt[j].svid, alt[j].mid,
i - num + j))
continue;
memset(&desc, 0, sizeof(desc));
desc.vdo = alt[j].mid;
desc.svid = alt[j].svid;