Skip to content

Commit

Permalink
usb: typec: ucsi: Retrieve all the PDOs instead of just the first 4
Browse files Browse the repository at this point in the history
commit 1f4642b upstream.

commit 4dbc6a4 ("usb: typec: ucsi: save power data objects
in PD mode") introduced retrieval of the PDOs when connected to a
PD-capable source. But only the first 4 PDOs are received since
that is the maximum number that can be fetched at a time given the
MESSAGE_IN length limitation (16 bytes). However, as per the PD spec
a connected source may advertise up to a maximum of 7 PDOs.

If such a source is connected it's possible the PPM could have
negotiated a power contract with one of the PDOs at index greater
than 4, and would be reflected in the request data object's (RDO)
object position field. This would result in an out-of-bounds access
when the rdo_index() is used to index into the src_pdos array in
ucsi_psy_get_voltage_now().

With the help of the UBSAN -fsanitize=array-bounds checker enabled
this exact issue is revealed when connecting to a PD source adapter
that advertise 5 PDOs and the PPM enters a contract having selected
the 5th one.

[  151.545106][   T70] Unexpected kernel BRK exception at EL1
[  151.545112][   T70] Internal error: BRK handler: f2005512 [#1] PREEMPT SMP
...
[  151.545499][   T70] pc : ucsi_psy_get_prop+0x208/0x20c
[  151.545507][   T70] lr : power_supply_show_property+0xc0/0x328
...
[  151.545542][   T70] Call trace:
[  151.545544][   T70]  ucsi_psy_get_prop+0x208/0x20c
[  151.545546][   T70]  power_supply_uevent+0x1a4/0x2f0
[  151.545550][   T70]  dev_uevent+0x200/0x384
[  151.545555][   T70]  kobject_uevent_env+0x1d4/0x7e8
[  151.545557][   T70]  power_supply_changed_work+0x174/0x31c
[  151.545562][   T70]  process_one_work+0x244/0x6f0
[  151.545564][   T70]  worker_thread+0x3e0/0xa64

We can resolve this by instead retrieving and storing up to the
maximum of 7 PDOs in the con->src_pdos array. This would involve
two calls to the GET_PDOS command.

Fixes: 992a60e ("usb: typec: ucsi: register with power_supply class")
Fixes: 4dbc6a4 ("usb: typec: ucsi: save power data objects in PD mode")
Cc: stable@vger.kernel.org
Reported-and-tested-by: Subbaraman Narayanamurthy <subbaram@codeaurora.org>
Reviewed-by: Heikki Krogerus <heikki.krogerus@linux.intel.com>
Signed-off-by: Jack Pham <jackp@codeaurora.org>
Link: https://lore.kernel.org/r/20210503074611.30973-1-jackp@codeaurora.org
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
  • Loading branch information
Jack Pham authored and gregkh committed May 19, 2021
1 parent 9bd96a2 commit e5366be
Show file tree
Hide file tree
Showing 2 changed files with 36 additions and 11 deletions.
41 changes: 32 additions & 9 deletions drivers/usb/typec/ucsi/ucsi.c
Original file line number Diff line number Diff line change
Expand Up @@ -495,25 +495,48 @@ static void ucsi_unregister_altmodes(struct ucsi_connector *con, u8 recipient)
}
}

static void ucsi_get_pdos(struct ucsi_connector *con, int is_partner)
static int ucsi_get_pdos(struct ucsi_connector *con, int is_partner,
u32 *pdos, int offset, int num_pdos)
{
struct ucsi *ucsi = con->ucsi;
u64 command;
int ret;

command = UCSI_COMMAND(UCSI_GET_PDOS) | UCSI_CONNECTOR_NUMBER(con->num);
command |= UCSI_GET_PDOS_PARTNER_PDO(is_partner);
command |= UCSI_GET_PDOS_NUM_PDOS(UCSI_MAX_PDOS - 1);
command |= UCSI_GET_PDOS_PDO_OFFSET(offset);
command |= UCSI_GET_PDOS_NUM_PDOS(num_pdos - 1);
command |= UCSI_GET_PDOS_SRC_PDOS;
ret = ucsi_send_command(ucsi, command, con->src_pdos,
sizeof(con->src_pdos));
if (ret < 0) {
ret = ucsi_send_command(ucsi, command, pdos + offset,
num_pdos * sizeof(u32));
if (ret < 0)
dev_err(ucsi->dev, "UCSI_GET_PDOS failed (%d)\n", ret);
if (ret == 0 && offset == 0)
dev_warn(ucsi->dev, "UCSI_GET_PDOS returned 0 bytes\n");

return ret;
}

static void ucsi_get_src_pdos(struct ucsi_connector *con, int is_partner)
{
int ret;

/* UCSI max payload means only getting at most 4 PDOs at a time */
ret = ucsi_get_pdos(con, 1, con->src_pdos, 0, UCSI_MAX_PDOS);
if (ret < 0)
return;
}

con->num_pdos = ret / sizeof(u32); /* number of bytes to 32-bit PDOs */
if (ret == 0)
dev_warn(ucsi->dev, "UCSI_GET_PDOS returned 0 bytes\n");
if (con->num_pdos < UCSI_MAX_PDOS)
return;

/* get the remaining PDOs, if any */
ret = ucsi_get_pdos(con, 1, con->src_pdos, UCSI_MAX_PDOS,
PDO_MAX_OBJECTS - UCSI_MAX_PDOS);
if (ret < 0)
return;

con->num_pdos += ret / sizeof(u32);
}

static void ucsi_pwr_opmode_change(struct ucsi_connector *con)
Expand All @@ -522,7 +545,7 @@ static void ucsi_pwr_opmode_change(struct ucsi_connector *con)
case UCSI_CONSTAT_PWR_OPMODE_PD:
con->rdo = con->status.request_data_obj;
typec_set_pwr_opmode(con->port, TYPEC_PWR_MODE_PD);
ucsi_get_pdos(con, 1);
ucsi_get_src_pdos(con, 1);
break;
case UCSI_CONSTAT_PWR_OPMODE_TYPEC1_5:
con->rdo = 0;
Expand Down
6 changes: 4 additions & 2 deletions drivers/usb/typec/ucsi/ucsi.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include <linux/power_supply.h>
#include <linux/types.h>
#include <linux/usb/typec.h>
#include <linux/usb/pd.h>

/* -------------------------------------------------------------------------- */

Expand Down Expand Up @@ -133,7 +134,9 @@ void ucsi_connector_change(struct ucsi *ucsi, u8 num);

/* GET_PDOS command bits */
#define UCSI_GET_PDOS_PARTNER_PDO(_r_) ((u64)(_r_) << 23)
#define UCSI_GET_PDOS_PDO_OFFSET(_r_) ((u64)(_r_) << 24)
#define UCSI_GET_PDOS_NUM_PDOS(_r_) ((u64)(_r_) << 32)
#define UCSI_MAX_PDOS (4)
#define UCSI_GET_PDOS_SRC_PDOS ((u64)1 << 34)

/* -------------------------------------------------------------------------- */
Expand Down Expand Up @@ -300,7 +303,6 @@ struct ucsi {

#define UCSI_MAX_SVID 5
#define UCSI_MAX_ALTMODES (UCSI_MAX_SVID * 6)
#define UCSI_MAX_PDOS (4)

#define UCSI_TYPEC_VSAFE5V 5000
#define UCSI_TYPEC_1_5_CURRENT 1500
Expand All @@ -327,7 +329,7 @@ struct ucsi_connector {
struct power_supply *psy;
struct power_supply_desc psy_desc;
u32 rdo;
u32 src_pdos[UCSI_MAX_PDOS];
u32 src_pdos[PDO_MAX_OBJECTS];
int num_pdos;
};

Expand Down

0 comments on commit e5366be

Please sign in to comment.