--- /dev/null
+From 8ac44d24c3a148c4177bd3ad790c377279f4674f Mon Sep 17 00:00:00 2001
+From: Greg Patrick <gregspatrick@hotmail.com>
+Date: Thu, 11 Jun 2026 17:53:41 +0000
+Subject: [PATCH] net: phy: sfp: detect presence via I2C when no MOD_DEF0 GPIO
+
+An SFP cage (compatible "sff,sfp") whose MOD_DEF0 signal is not wired to a
+GPIO currently falls back to sff_gpio_get_state(), which unconditionally
+reports the module as present. An empty cage therefore fails its probe and
+is parked in SFP_MOD_ERROR forever; because SFP_F_PRESENT never deasserts
+there is no REMOVE event to recover the state machine, so a module inserted
+after boot is never detected, and empty cages spam -EIO at boot.
+
+This affects boards that route none of the cage presence signal to a
+software-readable input. On the NicGiga S100-0800S-M (RTL9303, 8x SFP+) the
+cage I2C bus is the switch's SMBus master; TX_DISABLE is driven via a
+PCA9534 I/O expander, but no MOD_ABS/MOD_DEF0 line reaches a readable GPIO
+(the RTL9303 gpio0 lines read stuck-low, the single PCA9534 is fully
+consumed by TX_DISABLE, and there is no RTL8231). The Horaco ZX-SW82TS-L2P
+(RTL9302D, 2x SFP+) is independently affected in the same way.
+
+For such an SFP cage, derive presence from a throttled single-byte I2C read
+of the module EEPROM instead: a successful read asserts SFP_F_PRESENT,
+R_PROBE_ABSENT consecutive failures clear it (to ride out a transient error
+on a live module). The existing poll then emits SFP_E_INSERT / SFP_E_REMOVE
+normally, giving working hot-plug and silencing the boot-time -EIO spam on
+empty cages. Presence is re-probed every T_PROBE_PRESENT, so insertion is
+detected within that interval and removal within
+T_PROBE_PRESENT * R_PROBE_ABSENT.
+
+A soldered-down module (compatible "sff,sff") has no presence signal and is
+genuinely always present, so it continues to use sff_gpio_get_state(); the
+new path is gated on the cage type advertising SFP_F_PRESENT.
+
+Signed-off-by: Greg Patrick <gregspatrick@hotmail.com>
+Tested-by: Manuel Stocker <mensi@mensi.ch>
+Reviewed-by: Maxime Chevallier <maxime.chevallier@bootlin.com>
+Tested-by: Maxime Chevallier <maxime.chevallier@bootlin.com>
+Link: https://patch.msgid.link/20260611175341.2223184-1-gregspatrick@hotmail.com
+Signed-off-by: Jakub Kicinski <kuba@kernel.org>
+---
+ drivers/net/phy/sfp.c | 83 +++++++++++++++++++++++++++++++++++++++++--
+ 1 file changed, 80 insertions(+), 3 deletions(-)
+
+--- a/drivers/net/phy/sfp.c
++++ b/drivers/net/phy/sfp.c
+@@ -206,6 +206,16 @@ static const enum gpiod_flags gpio_flags
+ #define T_PROBE_RETRY_SLOW msecs_to_jiffies(5000)
+ #define R_PROBE_RETRY_SLOW 12
+
++/* Polling interval and consecutive-failure threshold for the I2C presence
++ * probe used on boards without a MOD_DEF0 GPIO (see sfp_i2c_get_state()).
++ * A single successful read asserts presence immediately; R_PROBE_ABSENT
++ * consecutive failures are required to declare a live module removed, to ride
++ * out a transient I2C error. Insertion is thus detected within
++ * T_PROBE_PRESENT and removal within T_PROBE_PRESENT * R_PROBE_ABSENT.
++ */
++#define T_PROBE_PRESENT msecs_to_jiffies(500)
++#define R_PROBE_ABSENT 3
++
+ /* SFP modules appear to always have their PHY configured for bus address
+ * 0x56 (which with mdio-i2c, translates to a PHY address of 22).
+ * RollBall SFPs access phy via SFP Enhanced Digital Diagnostic Interface
+@@ -249,6 +259,13 @@ struct sfp {
+
+ bool need_poll;
+
++ /* I2C-probed presence, for boards without a MOD_DEF0 GPIO.
++ * Access rules: st_mutex held (updated from the poll/state machine).
++ */
++ bool i2c_present;
++ u8 i2c_present_nak;
++ unsigned long i2c_present_next;
++
+ /* Access rules:
+ * state_hw_drive: st_mutex held
+ * state_hw_mask: st_mutex held
+@@ -860,6 +877,45 @@ static int sfp_read(struct sfp *sfp, boo
+ return sfp->read(sfp, a2, addr, buf, len);
+ }
+
++/* Probe whether a module is physically present by attempting a single-byte
++ * I2C read of the EEPROM identifier (an empty cage NAKs). Used as the presence
++ * source on boards that do not wire MOD_DEF0 to a GPIO.
++ */
++static bool sfp_module_present_i2c(struct sfp *sfp)
++{
++ u8 id;
++
++ return sfp_read(sfp, false, SFP_PHYS_ID, &id, sizeof(id)) == sizeof(id);
++}
++
++/* get_state variant for boards without a MOD_DEF0 GPIO. Instead of assuming
++ * the module is always present, derive SFP_F_PRESENT from a throttled I2C
++ * probe so that hot-insertion and removal are detected. A single ACK asserts
++ * presence; R_PROBE_ABSENT consecutive failures clear it, to ride out a
++ * transient I2C error on a live module.
++ */
++static unsigned int sfp_i2c_get_state(struct sfp *sfp)
++{
++ unsigned int state = sfp_gpio_get_state(sfp);
++
++ if (time_after_eq(jiffies, sfp->i2c_present_next)) {
++ if (sfp_module_present_i2c(sfp)) {
++ sfp->i2c_present = true;
++ sfp->i2c_present_nak = 0;
++ } else if (sfp->i2c_present &&
++ ++sfp->i2c_present_nak >= R_PROBE_ABSENT) {
++ sfp->i2c_present = false;
++ sfp->i2c_present_nak = 0;
++ }
++ sfp->i2c_present_next = jiffies + T_PROBE_PRESENT;
++ }
++
++ if (sfp->i2c_present)
++ state |= SFP_F_PRESENT;
++
++ return state;
++}
++
+ static int sfp_write(struct sfp *sfp, bool a2, u8 addr, void *buf, size_t len)
+ {
+ return sfp->write(sfp, a2, addr, buf, len);
+@@ -3158,9 +3214,30 @@ static int sfp_probe(struct platform_dev
+ sfp->get_state = sfp_gpio_get_state;
+ sfp->set_state = sfp_gpio_set_state;
+
+- /* Modules that have no detect signal are always present */
+- if (!(sfp->gpio[GPIO_MODDEF0]))
+- sfp->get_state = sff_gpio_get_state;
++ /* An SFP cage with no MOD_DEF0 GPIO has no hardware presence signal.
++ * Assuming the module is always present traps an empty cage in
++ * MOD_ERROR and never detects hot-insertion, so derive presence from a
++ * throttled I2C probe and poll for changes instead. sfp_i2c_configure()
++ * has already set i2c_max_block_size; seed i2c_block_size so the
++ * presence read does not issue a zero-length transfer before the first
++ * EEPROM read. Seed i2c_present_next to jiffies so the first probe
++ * happens immediately (a zero value would be in the past relative to
++ * the negative INITIAL_JIFFIES at boot and delay detection).
++ *
++ * A soldered-down module (sff,sff) has no presence signal and is
++ * genuinely always present, so it keeps the always-present behaviour;
++ * the I2C probe is gated on the cage type advertising SFP_F_PRESENT.
++ */
++ if (!sfp->gpio[GPIO_MODDEF0]) {
++ if (sff->gpios & SFP_F_PRESENT) {
++ sfp->get_state = sfp_i2c_get_state;
++ sfp->i2c_block_size = sfp->i2c_max_block_size;
++ sfp->i2c_present_next = jiffies;
++ sfp->need_poll = true;
++ } else {
++ sfp->get_state = sff_gpio_get_state;
++ }
++ }
+
+ device_property_read_u32(&pdev->dev, "maximum-power-milliwatt",
+ &sfp->max_power_mW);