--- /dev/null
+From 2a7618ba8698874e9871a8ec5453e0068e94d9e5 Mon Sep 17 00:00:00 2001
+From: Jonas Jelonek <jelonek.jonas@gmail.com>
+Date: Sat, 27 Dec 2025 18:01:33 +0000
+Subject: [PATCH] dt-bindings: gpio: add gpio-line-mux controller
+
+Add dt-schema for a gpio-line-mux controller which exposes virtual
+GPIOs for a shared GPIO controlled by a multiplexer, e.g. a gpio-mux.
+
+The gpio-line-mux controller is a gpio-controller, thus has mostly the
+same semantics. However, it requires a mux-control to be specified upon
+which it will operate.
+
+Signed-off-by: Jonas Jelonek <jelonek.jonas@gmail.com>
+Reviewed-by: Conor Dooley <conor.dooley@microchip.com>
+Reviewed-by: Linus Walleij <linus.walleij@linaro.org>
+Link: https://lore.kernel.org/r/20251227180134.1262138-2-jelonek.jonas@gmail.com
+Signed-off-by: Bartosz Golaszewski <bartosz.golaszewski@oss.qualcomm.com>
+
+--- /dev/null
++++ b/Documentation/devicetree/bindings/gpio/gpio-line-mux.yaml
+@@ -0,0 +1,107 @@
++# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
++%YAML 1.2
++---
++$id: http://devicetree.org/schemas/gpio/gpio-line-mux.yaml#
++$schema: http://devicetree.org/meta-schemas/core.yaml#
++
++title: GPIO line mux
++
++maintainers:
++ - Jonas Jelonek <jelonek.jonas@gmail.com>
++
++description: |
++ A GPIO controller to provide virtual GPIOs for a 1-to-many input-only mapping
++ backed by a single shared GPIO and a multiplexer. A simple illustrated
++ example is:
++
++ +----- A
++ IN /
++ <-----o------- B
++ / |\
++ | | +----- C
++ | | \
++ | | +--- D
++ | |
++ M1 M0
++
++ MUX CONTROL
++
++ M1 M0 IN
++ 0 0 A
++ 0 1 B
++ 1 0 C
++ 1 1 D
++
++ This can be used in case a real GPIO is connected to multiple inputs and
++ controlled by a multiplexer, and another subsystem/driver does not work
++ directly with the multiplexer subsystem.
++
++properties:
++ compatible:
++ const: gpio-line-mux
++
++ gpio-controller: true
++
++ "#gpio-cells":
++ const: 2
++
++ gpio-line-mux-states:
++ description: Mux states corresponding to the virtual GPIOs.
++ $ref: /schemas/types.yaml#/definitions/uint32-array
++
++ gpio-line-names: true
++
++ mux-controls:
++ maxItems: 1
++ description:
++ Phandle to the multiplexer to control access to the GPIOs.
++
++ ngpios: false
++
++ muxed-gpios:
++ maxItems: 1
++ description:
++ GPIO which is the '1' in 1-to-many and is shared by the virtual GPIOs
++ and controlled via the mux.
++
++required:
++ - compatible
++ - gpio-controller
++ - gpio-line-mux-states
++ - mux-controls
++ - muxed-gpios
++
++additionalProperties: false
++
++examples:
++ - |
++ #include <dt-bindings/gpio/gpio.h>
++ #include <dt-bindings/mux/mux.h>
++
++ sfp_gpio_mux: mux-controller-1 {
++ compatible = "gpio-mux";
++ mux-gpios = <&gpio0 0 GPIO_ACTIVE_HIGH>,
++ <&gpio0 1 GPIO_ACTIVE_HIGH>;
++ #mux-control-cells = <0>;
++ idle-state = <MUX_IDLE_AS_IS>;
++ };
++
++ sfp1_gpio: sfp-gpio-1 {
++ compatible = "gpio-line-mux";
++ gpio-controller;
++ #gpio-cells = <2>;
++
++ mux-controls = <&sfp_gpio_mux>;
++ muxed-gpios = <&gpio0 2 GPIO_ACTIVE_HIGH>;
++
++ gpio-line-mux-states = <0>, <1>, <3>;
++ };
++
++ sfp1: sfp-p1 {
++ compatible = "sff,sfp";
++
++ i2c-bus = <&sfp1_i2c>;
++ los-gpios = <&sfp1_gpio 0 GPIO_ACTIVE_HIGH>;
++ mod-def0-gpios = <&sfp1_gpio 1 GPIO_ACTIVE_LOW>;
++ tx-fault-gpios = <&sfp1_gpio 2 GPIO_ACTIVE_HIGH>;
++ };
--- /dev/null
+From 2b03d9a40cd1fea42fd65d2b66df80edc0f374c8 Mon Sep 17 00:00:00 2001
+From: Jonas Jelonek <jelonek.jonas@gmail.com>
+Date: Sat, 27 Dec 2025 18:01:34 +0000
+Subject: [PATCH] gpio: add gpio-line-mux driver
+
+Add a new driver which provides a 1-to-many mapping for a single real
+GPIO using a multiplexer. Each virtual GPIO corresponds to a multiplexer
+state which, if set for the multiplexer, connects the real GPIO to the
+corresponding virtual GPIO.
+
+This can help in various usecases. One practical case is the special
+hardware design of the Realtek-based XS1930-10 switch from Zyxel. It
+features two SFP+ ports/cages whose signals are wired directly to the
+switch SoC. Although Realtek SoCs are short on GPIOs, there are usually
+enough the fit the SFP signals without any hacks.
+
+However, Zyxel did some weird design and connected RX_LOS, MOD_ABS and
+TX_FAULT of one SFP cage onto a single GPIO line controlled by a
+multiplexer (the same for the other SFP cage). The single multiplexer
+controls the lines for both SFP and depending on the state, the
+designated 'signal GPIO lines' are connected to one of the three SFP
+signals.
+
+Because the SFP core/driver doesn't support multiplexer but needs single
+GPIOs for each of the signals, this driver fills the gap between both.
+It registers a gpio_chip, provides multiple virtual GPIOs and sets the
+backing multiplexer accordingly.
+
+Due to several practical issues, this is input-only and doesn't support
+IRQs.
+
+Signed-off-by: Jonas Jelonek <jelonek.jonas@gmail.com>
+Reviewed-by: Thomas Richard <thomas.richard@bootlin.com>
+Reviewed-by: Linus Walleij <linus.walleij@linaro.org>
+Link: https://lore.kernel.org/r/20251227180134.1262138-3-jelonek.jonas@gmail.com
+Signed-off-by: Bartosz Golaszewski <bartosz.golaszewski@oss.qualcomm.com>
+
+--- a/MAINTAINERS
++++ b/MAINTAINERS
+@@ -9704,6 +9704,12 @@ S: Maintained
+ F: Documentation/devicetree/bindings/leds/irled/gpio-ir-tx.yaml
+ F: drivers/media/rc/gpio-ir-tx.c
+
++GPIO LINE MUX
++M: Jonas Jelonek <jelonek.jonas@gmail.com>
++S: Maintained
++F: Documentation/devicetree/bindings/gpio/gpio-line-mux.yaml
++F: drivers/gpio/gpio-line-mux.c
++
+ GPIO MOCKUP DRIVER
+ M: Bamvor Jian Zhang <bamv2005@gmail.com>
+ L: linux-gpio@vger.kernel.org
+--- a/drivers/gpio/Kconfig
++++ b/drivers/gpio/Kconfig
+@@ -1866,6 +1866,15 @@ config GPIO_LATCH
+ Say yes here to enable a driver for GPIO multiplexers based on latches
+ connected to other GPIOs.
+
++config GPIO_LINE_MUX
++ tristate "GPIO line mux driver"
++ depends on OF_GPIO
++ select MULTIPLEXER
++ help
++ Say Y here to support the GPIO line mux, which can provide virtual
++ GPIOs backed by a shared real GPIO and a multiplexer in a 1-to-many
++ fashion.
++
+ config GPIO_MOCKUP
+ tristate "GPIO Testing Driver (DEPRECATED)"
+ select IRQ_SIM
+--- a/drivers/gpio/Makefile
++++ b/drivers/gpio/Makefile
+@@ -84,6 +84,7 @@ obj-$(CONFIG_GPIO_IXP4XX) += gpio-ixp4x
+ obj-$(CONFIG_GPIO_JANZ_TTL) += gpio-janz-ttl.o
+ obj-$(CONFIG_GPIO_KEMPLD) += gpio-kempld.o
+ obj-$(CONFIG_GPIO_LATCH) += gpio-latch.o
++obj-$(CONFIG_GPIO_LINE_MUX) += gpio-line-mux.o
+ obj-$(CONFIG_GPIO_LJCA) += gpio-ljca.o
+ obj-$(CONFIG_GPIO_LOGICVC) += gpio-logicvc.o
+ obj-$(CONFIG_GPIO_LOONGSON1) += gpio-loongson1.o
+--- /dev/null
++++ b/drivers/gpio/gpio-line-mux.c
+@@ -0,0 +1,126 @@
++// SPDX-License-Identifier: GPL-2.0
++/*
++ * GPIO line mux which acts as virtual gpiochip and provides a 1-to-many
++ * mapping between virtual GPIOs and a real GPIO + multiplexer.
++ *
++ * Copyright (c) 2025 Jonas Jelonek <jelonek.jonas@gmail.com>
++ */
++
++#include <linux/gpio/consumer.h>
++#include <linux/gpio/driver.h>
++#include <linux/mod_devicetable.h>
++#include <linux/mutex.h>
++#include <linux/mux/consumer.h>
++#include <linux/platform_device.h>
++
++#define MUX_SELECT_DELAY_US 100
++
++struct gpio_lmux {
++ struct gpio_chip gc;
++ struct mux_control *mux;
++ struct gpio_desc *muxed_gpio;
++
++ u32 num_gpio_mux_states;
++ unsigned int gpio_mux_states[] __counted_by(num_gpio_mux_states);
++};
++
++static int gpio_lmux_gpio_get(struct gpio_chip *gc, unsigned int offset)
++{
++ struct gpio_lmux *glm = gpiochip_get_data(gc);
++ int ret;
++
++ if (offset > gc->ngpio)
++ return -EINVAL;
++
++ ret = mux_control_select_delay(glm->mux, glm->gpio_mux_states[offset],
++ MUX_SELECT_DELAY_US);
++ if (ret < 0)
++ return ret;
++
++ ret = gpiod_get_raw_value_cansleep(glm->muxed_gpio);
++ mux_control_deselect(glm->mux);
++ return ret;
++}
++
++static int gpio_lmux_gpio_set(struct gpio_chip *gc, unsigned int offset,
++ int value)
++{
++ return -EOPNOTSUPP;
++}
++
++static int gpio_lmux_gpio_get_direction(struct gpio_chip *gc,
++ unsigned int offset)
++{
++ return GPIO_LINE_DIRECTION_IN;
++}
++
++static int gpio_lmux_probe(struct platform_device *pdev)
++{
++ struct device *dev = &pdev->dev;
++ struct gpio_lmux *glm;
++ unsigned int ngpio;
++ size_t size;
++ int ret;
++
++ ngpio = device_property_count_u32(dev, "gpio-line-mux-states");
++ if (!ngpio)
++ return -EINVAL;
++
++ size = struct_size(glm, gpio_mux_states, ngpio);
++ glm = devm_kzalloc(dev, size, GFP_KERNEL);
++ if (!glm)
++ return -ENOMEM;
++
++ glm->gc.base = -1;
++ glm->gc.can_sleep = true;
++ glm->gc.fwnode = dev_fwnode(dev);
++ glm->gc.label = dev_name(dev);
++ glm->gc.ngpio = ngpio;
++ glm->gc.owner = THIS_MODULE;
++ glm->gc.parent = dev;
++
++ glm->gc.get = gpio_lmux_gpio_get;
++ glm->gc.set = gpio_lmux_gpio_set;
++ glm->gc.get_direction = gpio_lmux_gpio_get_direction;
++
++ glm->mux = devm_mux_control_get(dev, NULL);
++ if (IS_ERR(glm->mux))
++ return dev_err_probe(dev, PTR_ERR(glm->mux),
++ "could not get mux controller\n");
++
++ glm->muxed_gpio = devm_gpiod_get(dev, "muxed", GPIOD_IN);
++ if (IS_ERR(glm->muxed_gpio))
++ return dev_err_probe(dev, PTR_ERR(glm->muxed_gpio),
++ "could not get muxed-gpio\n");
++
++ glm->num_gpio_mux_states = ngpio;
++ ret = device_property_read_u32_array(dev, "gpio-line-mux-states",
++ &glm->gpio_mux_states[0], ngpio);
++ if (ret)
++ return dev_err_probe(dev, ret, "could not get mux states\n");
++
++ ret = devm_gpiochip_add_data(dev, &glm->gc, glm);
++ if (ret)
++ return dev_err_probe(dev, ret, "failed to add gpiochip\n");
++
++ return 0;
++}
++
++static const struct of_device_id gpio_lmux_of_match[] = {
++ { .compatible = "gpio-line-mux" },
++ { }
++};
++MODULE_DEVICE_TABLE(of, gpio_lmux_of_match);
++
++static struct platform_driver gpio_lmux_driver = {
++ .driver = {
++ .name = "gpio-line-mux",
++ .of_match_table = gpio_lmux_of_match,
++ },
++ .probe = gpio_lmux_probe,
++};
++module_platform_driver(gpio_lmux_driver);
++
++MODULE_AUTHOR("Jonas Jelonek <jelonek.jonas@gmail.com>");
++MODULE_DESCRIPTION("GPIO line mux driver");
++MODULE_LICENSE("GPL");
--- /dev/null
+From e034e058897a12bc856f8b22d1796964c742f732 Mon Sep 17 00:00:00 2001
+From: Bartosz Golaszewski <bartosz.golaszewski@oss.qualcomm.com>
+Date: Wed, 7 Jan 2026 09:58:33 +0100
+Subject: [PATCH] gpio: line-mux: remove bits already handled by GPIO core
+
+GPIO core already handles checking the offset against the number of
+GPIOs as well as missing any of the GPIO chip callbacks. Remove the
+unnecessary bits.
+
+Also, the offset check was off-by-one as reported by Dan.
+
+Fixes: 2b03d9a40cd1 ("gpio: add gpio-line-mux driver")
+Reported-by: Dan Carpenter <dan.carpenter@linaro.org>
+Closes: https://lore.kernel.org/all/aV4b6GAGz1zyf8Xy@stanley.mountain/
+Tested-by: Jonas Jelonek <jelonek.jonas@gmail.com>
+Reviewed-by: Jonas Jelonek <jelonek.jonas@gmail.com>
+Link: https://lore.kernel.org/r/20260107085833.17338-1-bartosz.golaszewski@oss.qualcomm.com
+Signed-off-by: Bartosz Golaszewski <bartosz.golaszewski@oss.qualcomm.com>
+
+--- a/drivers/gpio/gpio-line-mux.c
++++ b/drivers/gpio/gpio-line-mux.c
+@@ -29,9 +29,6 @@ static int gpio_lmux_gpio_get(struct gpi
+ struct gpio_lmux *glm = gpiochip_get_data(gc);
+ int ret;
+
+- if (offset > gc->ngpio)
+- return -EINVAL;
+-
+ ret = mux_control_select_delay(glm->mux, glm->gpio_mux_states[offset],
+ MUX_SELECT_DELAY_US);
+ if (ret < 0)
+@@ -42,12 +39,6 @@ static int gpio_lmux_gpio_get(struct gpi
+ return ret;
+ }
+
+-static int gpio_lmux_gpio_set(struct gpio_chip *gc, unsigned int offset,
+- int value)
+-{
+- return -EOPNOTSUPP;
+-}
+-
+ static int gpio_lmux_gpio_get_direction(struct gpio_chip *gc,
+ unsigned int offset)
+ {
+@@ -80,7 +71,6 @@ static int gpio_lmux_probe(struct platfo
+ glm->gc.parent = dev;
+
+ glm->gc.get = gpio_lmux_gpio_get;
+- glm->gc.set = gpio_lmux_gpio_set;
+ glm->gc.get_direction = gpio_lmux_gpio_get_direction;
+
+ glm->mux = devm_mux_control_get(dev, NULL);