Thread (36 messages) 36 messages, 3 authors, 13d ago

Re: [PATCH net-next v9 07/15] dpll: sit9531x: add support to get and set priority on input pins

From: netdev-bot+sashiko@kernel.org
Date: 2026-09-17 15:01:55
Also in: linux-devicetree, lkml

Thank you for your contribution! Sashiko AI review found 7 potential
issue(s) to consider:

Critical: 0 · High: 2 · Medium: 2 · Low: 3

- [High] Self-deadlock: sit9531x_dpll_input_pin_prio_set()
  (drivers/dpll/sit9531x/dpll.c) ends with `while (changed_cnt--)…
- [High] Use-after-free / NULL dereference window:
  sit9531x_dpll_input_pin_prio_set() is the first pin op that walks the…
- [Medium] sit9531x_input_prio_get() (drivers/dpll/sit9531x/core.c)
  collapses the documented 'absent' encoding (chan->prio_last[src] == 0,…
- [Medium] Silent no-op priority set: sit9531x_input_prio_set()
  (drivers/dpll/sit9531x/core.c:671) locates only the FIRST slot holding…
- [Low] The new struct member sit9531x_chan.prio_srcs[] (core.h:152) is
  written by sit9531x_prio_mask_build() ('chan->prio_srcs[slot] = src;')…
- [Low] sit9531x_dpll_input_pin_prio_set() notifies the pin the request
  was made for, in addition to the shifted siblings: the traversal…
- [Low] sit9531x_prio_table_commit() rebuilds the newly extended cache
  (chan->prio_srcs / chan->prio_last, alongside prio_mask) from the…

--

dpll: sit9531x: add support to get and set priority on input pins

This adds .prio_get and .prio_set to the sit9531x input pin ops, backed by a
new cached view of the hardware priority table (prio_srcs[] and prio_last[])
that is refreshed whenever the table is written or polled.  Getting a
priority looks up the cached slot for the pin's source code; setting one
moves the source to the requested slot through sit9531x_input_prio_set().

A few questions below.
quoted hunk ↗ jump to hunk
diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
index 25418485e9757..484c94def578c 100644
--- a/drivers/dpll/sit9531x/core.c
+++ b/drivers/dpll/sit9531x/core.c
@@ -521,21 +521,81 @@ bool sit9531x_input_prio_present(struct sit9531x_dev *sitdev, u8 pll_idx,
 }
 
 /*
- * Rebuild a PLL's membership mask from the source codes of its priority
- * table.  The mask is what the pin state getters test, so it is refreshed
- * from exactly the values the table holds -- here after a write, and once
- * per poll from the read-back in sit9531x_chan_state_fetch().
+ * sit9531x_input_prio_get - read an input's priority slot for a PLL
+ * @input_idx:	input source in hardware encoding (see
+ *		sit9531x_input_hw_src())
+ * @prio:	output slot position (0 = highest)
+ *
+ * Reports the last slot this source occupied on this PLL.  The value is
+ * cached from the hardware table read at startup and refreshed after every
+ * table write and poll read-back, so pin-get reflects hardware state without
+ * issuing synchronous register reads per pin.  A source with no known slot
+ * falls back to the lowest-priority valid slot.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+int sit9531x_input_prio_get(struct sit9531x_dev *sitdev, u8 pll_idx,
+			    u8 input_idx, u8 *prio)
+{
+	const struct sit9531x_chan *chan;
+	u8 slot;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	if (pll_idx >= SIT9531X_NUM_PLLS)
+		return -EINVAL;
+	input_idx = sit9531x_prio_src_canon(sitdev, input_idx);
+	if (input_idx >= SIT9531X_PRIO_NUM_SRC)
+		return -EINVAL;
+
+	chan = &sitdev->chan[pll_idx];
+	slot = chan->prio_last[input_idx];
+	if (!slot)
+		slot = SIT9531X_PRIO_MAX_SLOTS;
+
+	*prio = slot - 1;
+
+	return 0;
+}
[Severity: Medium]
The commit message says:

  "An input absent from the table has no priority to report, and the core is
   told so rather than handed a made-up number."

Does sit9531x_input_prio_get() actually tell the core that?  The absent
encoding documented for prio_last[] in core.h (0 = the source is not in the
table) is turned into SIT9531X_PRIO_MAX_SLOTS and the function returns 0:

	slot = chan->prio_last[input_idx];
	if (!slot)
		slot = SIT9531X_PRIO_MAX_SLOTS;

	*prio = slot - 1;

Since SIT9531X_PRIO_MAX_SLOTS is 11, *prio becomes 10, which is a valid
slot.  dpll_msg_add_pin_prio() puts whatever prio_get returns into
DPLL_A_PIN_PRIO, and dpll_msg_add_pin_dplls() calls it for every pin/dpll
reference regardless of connection state.

Can userspace then tell "input not in the priority table" apart from "input
programmed at slot 10"?

The value also does not round-trip: writing 10 back reaches
sit9531x_input_prio_set(), which returns -EINVAL when the source is not
found in the table, so
sit9531x_dpll_input_pin_prio_set() answers with "Pin is not a reference of
this DPLL; connect it first".

Would it be clearer either to return an error from prio_get for an absent
source, or to reword the changelog to match the fallback the code
implements?
quoted hunk ↗ jump to hunk
+
+/*
+ * Refresh a PLL's cached view of its priority table from the source codes
+ * the table holds -- here after a write, and once per poll from the
+ * read-back in sit9531x_chan_state_fetch().
[Severity: Low]
sit9531x_prio_table_commit() calls sit9531x_prio_mask_build() with the
intended srcs[] array even when a slot read or write failed mid-sequence
(the "goto commit" path).  With this patch prio_last[] becomes the source of
truth for the priority reported by the new .prio_get op, so a get issued
after a failed set can report a slot the device never accepted, and after a
partial shift-write the device may hold the source in two slots and select
it at a higher priority than the driver reports.

The existing comment at the rebuild site documents this choice, and
sit9531x_chan_state_fetch() calls sit9531x_prio_table_read() plus
sit9531x_prio_mask_build() on every poll, so the window is bounded by the
poll period.  Is it worth noting in the kernel-doc for
sit9531x_input_prio_get() that the cached slot may be an intended rather
than a confirmed one until the next poll?
quoted hunk ↗ jump to hunk
+ *
+ * The membership mask is what the pin state getters test; the per-slot
+ * copy and the last-slot-seen array are what priority get answers from,
+ * so neither costs a register read per pin.
  */
 static void sit9531x_prio_mask_build(struct sit9531x_dev *sitdev, u8 pll_idx,
 				     const u8 *srcs)
 {
+	struct sit9531x_chan *chan = &sitdev->chan[pll_idx];
+	u8 first[SIT9531X_PRIO_NUM_SRC] = { 0 };
 	u16 mask = 0;
-	u8 slot;
+	u8 slot, src, src_canon;
 
-	for (slot = 0; slot < SIT9531X_PRIO_MAX_SLOTS; slot++)
-		mask |= BIT(srcs[slot] & SIT9531X_PRIO_NIBBLE_MASK);
+	for (slot = 0; slot < SIT9531X_PRIO_MAX_SLOTS; slot++) {
+		src = srcs[slot] & SIT9531X_PRIO_NIBBLE_MASK;
+		chan->prio_srcs[slot] = src;
[ ... ]
quoted hunk ↗ jump to hunk
diff --git a/drivers/dpll/sit9531x/core.h b/drivers/dpll/sit9531x/core.h
index f628bd5b7a975..1ab8ebfd78da1 100644
--- a/drivers/dpll/sit9531x/core.h
+++ b/drivers/dpll/sit9531x/core.h
@@ -129,6 +129,12 @@ struct sit9531x_out {
  * @ho_freeze:		holdover freeze active
  * @ho_valid:		holdover memory acquired, i.e. the holdover window
  *			holds a valid estimate to fall back on
+ * @prio_srcs:		cached copy of the priority table, one source code
+ *			per slot; refreshed together with @prio_mask, so
+ *			priority reads generate no register traffic
+ * @prio_last:		slot each source occupies, plus one (0 = the source
+ *			is not in the table); refreshed from the same scan
+ *			as @prio_mask, so the two never disagree
[ ... ]
quoted hunk ↗ jump to hunk
@@ -143,6 +149,8 @@ struct sit9531x_chan {
 	bool		inner_lol;
 	bool		ho_freeze;
 	bool		ho_valid;
+	u8		prio_srcs[SIT9531X_PRIO_MAX_SLOTS];
[Severity: Low]
Is prio_srcs[] read anywhere?  Grepping drivers/dpll/sit9531x/ turns up only
this declaration and the single store in sit9531x_prio_mask_build()
("chan->prio_srcs[slot] = src;"), both at this commit and at the end of the
series.  sit9531x_input_prio_get() reads prio_last[] only.

The kernel-doc above ("so priority reads generate no register traffic") and
the comment on sit9531x_prio_mask_build() ("the per-slot copy and the
last-slot-seen array are what priority get answers from") describe a
consumer that does not exist.  Was a user of this array dropped, or can the
member and those comment lines go?
quoted hunk ↗ jump to hunk
+	u8		prio_last[SIT9531X_PRIO_NUM_SRC];
 	u16		prio_mask;
 };
 
diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
index ab9aa7db1839c..e2ff21adfdb23 100644
--- a/drivers/dpll/sit9531x/dpll.c
+++ b/drivers/dpll/sit9531x/dpll.c
@@ -469,10 +469,112 @@ sit9531x_dpll_input_pin_state_on_dpll_set(const struct dpll_pin *pin,
 	return rc;
 }
 
[ ... ]
quoted hunk ↗ jump to hunk
+/*
+ * sit9531x_dpll_input_pin_prio_set - set input pin priority
+ *
+ * writes input priority table on Page 1 via
+ * core.c sit9531x_input_prio_set().  Forces holdover during update.
+ */
+static int
+sit9531x_dpll_input_pin_prio_set(const struct dpll_pin *pin, void *pin_priv,
+				 const struct dpll_device *dpll, void *dpll_priv,
+				 u32 prio, struct netlink_ext_ack *extack)
+{
+	struct dpll_pin *changed[SIT9531X_MAX_INPUTS + 1];
+	struct sit9531x_dpll_pin *sibling;
+	struct sit9531x_dpll_pin *dpin = pin_priv;
+	struct sit9531x_dpll *sitdpll = dpll_priv;
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+	u8 changed_cnt = 0, hw_src, slot;
+	int get_rc, rc;
[ ... ]
quoted hunk ↗ jump to hunk
+	mutex_lock(&sitdev->multiop_lock);
+	rc = sit9531x_input_prio_set(sitdev, sitdpll->id,
+				     sit9531x_input_hw_src(dpin->id),
+				     (u8)prio);
[Severity: Medium]
This is the first caller of sit9531x_input_prio_set(); in the parent commit
the helper has none.  Can a set through it report success without changing
the effective priority?

sit9531x_input_prio_set() locates only the first slot holding the source:

	for (from = 0; from < SIT9531X_PRIO_MAX_SLOTS; from++)
		if (sit9531x_prio_src_canon(sitdev, srcs[from]) == input_idx)
			break;
	...
	srcs[prio] = input_idx;
	if (!memcmp(srcs, orig_srcs, sizeof(srcs)))
		return 0;

Duplicated source codes are the driver's own representation of an unused
slot -- sit9531x_input_prio_remove() backfills the freed tail:

	/* Backfill freed tail slots with the lowest-priority remaining src */
	while (count < SIT9531X_PRIO_MAX_SLOTS) {
		kept[count] = kept[count - 1];
		count++;
	}

So for a table like [A,B,B,B,...,B], moving B down to slot 5 shifts
duplicates onto each other, the array comes out bit-identical, the memcmp
shortcut returns 0 and no register is written.  The device keeps selecting B
at its old, higher slot, while sit9531x_prio_mask_build() records the first
occurrence, so a following get reports the old slot and not the one just
written.

The changelog says "setting it moves the input to the requested slot" --
should sit9531x_input_prio_set() clear or account for the other slots
holding the same source code?
quoted hunk ↗ jump to hunk
+	if (!rc) {
+		list_for_each_entry(sibling, &sitdpll->pins, list) {
+			if (!sit9531x_dpll_is_input_pin(sibling) ||
+			    sit9531x_dpll_is_xo_pin(sibling))
+				continue;
[Severity: High]
This is the first pin op that walks the other pins of the DPLL, and
sibling->dpll_pin is collected into changed[] without a NULL check.  Can it
race with teardown?

sit9531x_dpll_pins_unregister() in core.c does, per pin:

	list_for_each_entry_safe(pin, next, &sitdpll->pins, list) {
		sit9531x_dpll_pin_unregister(pin);
		list_del(&pin->list);
		kfree(pin);
	}

sit9531x_dpll_pin_unregister() calls dpll_pin_unregister(), which takes and
then drops dpll_lock:

drivers/dpll/dpll_core.c:dpll_pin_unregister() {
	mutex_lock(&dpll_lock);
	__dpll_pin_unregister(dpll, pin, ops, priv, NULL);
	mutex_unlock(&dpll_lock);
}

and then sets pin->dpll_pin = NULL, while list_del() and kfree() run with
dpll_lock dropped.  In that gap a netlink pin-set on a still-registered
sibling holds dpll_lock and walks &sitdpll->pins, so it can store a NULL
dpll_pin into changed[] and later hand it to dpll_pin_change_ntf() ->
__dpll_pin_change_ntf() -> dpll_pin_notify(pin, pin->clock_id, ...), or
follow a list node that is being list_del()'d and kfree()'d.

The pre-existing poll worker does a similar traversal, but
kthread_cancel_delayed_work_sync() in sit9531x_dev_stop() runs before any
unregister.  Is anything serializing this new traversal against
sit9531x_dpll_pins_unregister()?
quoted hunk ↗ jump to hunk
+
+			hw_src = sit9531x_input_hw_src(sibling->id);
+			get_rc = sit9531x_input_prio_get(sitdev, sitdpll->id, hw_src, &slot);
+			if (get_rc)
+				continue;
+
+			if (sibling->prio == slot)
+				continue;
+
+			sibling->prio = slot;
+			if (changed_cnt < ARRAY_SIZE(changed))
+				changed[changed_cnt++] = sibling->dpll_pin;
[Severity: Low]
The traversal filter skips only non-input and XO pins, so dpin itself is
collected here too.  The core already emits a notification for that pin
after a successful callback:

drivers/dpll/dpll_netlink.c:dpll_pin_prio_set() {
	ret = ops->prio_set(pin, dpll_pin_on_dpll_priv(dpll, pin), dpll,
			    dpll_priv(dpll), prio, extack);
	if (ret)
		return ret;
	__dpll_pin_change_ntf(pin);
}

Does one priority change end up sending two DPLL_CMD_PIN_CHANGE_NTF
messages for the requested pin?  Skipping sibling == dpin in the loop would
leave that notification to the core.
quoted hunk ↗ jump to hunk
+		}
+	}
+	mutex_unlock(&sitdev->multiop_lock);
[ ... ]
+	while (changed_cnt--)
+		dpll_pin_change_ntf(changed[changed_cnt]);
[Severity: High]
Can this deadlock on dpll_lock?  The .prio_set callback is invoked with
dpll_lock already held:

dpll_pin_pre_doit()
	mutex_lock(&dpll_lock);
	...
dpll_pin_set_from_nlattr()
  dpll_pin_prio_set()
	ret = ops->prio_set(...);
	if (ret)
		return ret;
	__dpll_pin_change_ntf(pin);	/* lockdep_assert_held(&dpll_lock) */

and dpll_pin_change_ntf() is the wrapper that takes the same non-recursive
mutex again:

drivers/dpll/dpll_netlink.c:dpll_pin_change_ntf() {
	mutex_lock(&dpll_lock);
	ret = __dpll_pin_change_ntf(pin);
	mutex_unlock(&dpll_lock);
}

changed_cnt is non-zero in the ordinary case: sibling->prio starts at 0 from
the kzalloc_obj() in sit9531x_dpll_pin_alloc() and is never seeded, and the
loop does not exclude the pin being set, so the first "pin-set ... prio N"
with N different from the cached value queues at least one entry.

Should this use __dpll_pin_change_ntf(), the way the core does from this
context, instead of dpll_pin_change_ntf()?
 static const struct dpll_pin_ops sit9531x_dpll_input_pin_ops = {
[ ... ]

-- 
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260915000015.80480-1-arouhi%40sitime.com
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help