Resguardo inicial

This commit is contained in:
lastdragon
2026-09-06 12:53:21 -06:00
parent 662efe3c2a
commit ad2088d178
94853 changed files with 43200404 additions and 0 deletions
@@ -0,0 +1,284 @@
# SPDX-License-Identifier: GPL-2.0-only
#
# Dell X86 Platform Specific Drivers
#
menuconfig X86_PLATFORM_DRIVERS_DELL
bool "Dell X86 Platform Specific Device Drivers"
help
Say Y here to get to see options for device drivers for various
Dell x86 platforms, including vendor-specific laptop extension drivers.
This option alone does not add any kernel code.
If you say N, all options in this submenu will be skipped and disabled.
if X86_PLATFORM_DRIVERS_DELL
config ALIENWARE_WMI
tristate "Alienware Special feature control"
default m
depends on ACPI
depends on ACPI_WMI
depends on DMI
depends on LEDS_CLASS
depends on NEW_LEDS
depends on HWMON
help
This is a driver for controlling Alienware WMI driven features.
On legacy devices, it exposes an interface for controlling the AlienFX
zones on Alienware machines that don't contain a dedicated
AlienFX USB MCU such as the X51 and X51-R2.
On newer devices, it exposes the AWCC thermal control interface through
known Kernel APIs.
config ALIENWARE_WMI_LEGACY
bool "Alienware Legacy WMI device driver"
default y
depends on ALIENWARE_WMI
help
Legacy Alienware WMI driver with AlienFX LED control capabilities.
config ALIENWARE_WMI_WMAX
bool "Alienware WMAX WMI device driver"
default y
depends on ALIENWARE_WMI
select ACPI_PLATFORM_PROFILE
help
Alienware WMI driver with AlienFX LED, HDMI, amplifier, deep sleep and
AWCC thermal control capabilities.
config DCDBAS
tristate "Dell Systems Management Base Driver"
default m
depends on X86
help
The Dell Systems Management Base Driver provides a sysfs interface
for systems management software to perform System Management
Interrupts (SMIs) and Host Control Actions (system power cycle or
power off after OS shutdown) on certain Dell systems.
See <file:Documentation/userspace-api/dcdbas.rst> for more details on the driver
and the Dell systems on which Dell systems management software makes
use of this driver.
Say Y or M here to enable the driver for use by Dell systems
management software such as Dell OpenManage.
config DELL_LAPTOP
tristate "Dell Laptop Extras"
default m
depends on DMI
depends on BACKLIGHT_CLASS_DEVICE
depends on ACPI_BATTERY
depends on ACPI_VIDEO || ACPI_VIDEO = n
depends on RFKILL || RFKILL = n
depends on DELL_WMI || DELL_WMI = n
depends on SERIO_I8042
depends on DELL_SMBIOS
select POWER_SUPPLY
select LEDS_CLASS
select NEW_LEDS
help
This driver adds support for rfkill and backlight control to Dell
laptops (except for some models covered by the Compal driver).
config DELL_RBU
tristate "BIOS update support for DELL systems via sysfs"
default m
depends on X86
select FW_LOADER
select FW_LOADER_USER_HELPER
help
Say m if you want to have the option of updating the BIOS for your
DELL system. Note you need a Dell OpenManage or Dell Update package (DUP)
supporting application to communicate with the BIOS regarding the new
image for the image update to take effect.
See <file:Documentation/admin-guide/dell_rbu.rst> for more details on the driver.
config DELL_RBTN
tristate "Dell Airplane Mode Switch driver"
default m
depends on ACPI
depends on INPUT
depends on RFKILL
help
Say Y here if you want to support Dell Airplane Mode Switch ACPI
device on Dell laptops. Sometimes it has names: DELLABCE or DELRBTN.
This driver register rfkill device or input hotkey device depending
on hardware type (hw switch slider or keyboard toggle button). For
rfkill devices it receive HW switch events and set correct hard
rfkill state.
To compile this driver as a module, choose M here: the module will
be called dell-rbtn.
config DELL_PC
tristate "Dell PC Extras"
default m
depends on ACPI
depends on DMI
depends on DELL_SMBIOS
select ACPI_PLATFORM_PROFILE
help
This driver adds support for controlling the fan modes via platform_profile
on supported Dell systems regardless of formfactor.
Module will simply do nothing if thermal management commands are not
supported.
#
# The DELL_SMBIOS driver depends on ACPI_WMI and/or DCDBAS if those
# backends are selected. The "depends" line prevents a configuration
# where DELL_SMBIOS=y while either of those dependencies =m.
#
config DELL_SMBIOS
tristate "Dell SMBIOS driver"
default m
depends on DCDBAS || DCDBAS=n
depends on ACPI_WMI || ACPI_WMI=n
help
This provides support for the Dell SMBIOS calling interface.
If you have a Dell computer you should enable this option.
Be sure to select at least one backend for it to work properly.
config DELL_SMBIOS_WMI
bool "Dell SMBIOS driver WMI backend"
default y
depends on ACPI_WMI
select DELL_WMI_DESCRIPTOR
depends on DELL_SMBIOS
help
This provides an implementation for the Dell SMBIOS calling interface
communicated over ACPI-WMI.
If you have a Dell computer from >2007 you should say Y here.
If you aren't sure and this module doesn't work for your computer
it just won't load.
config DELL_SMBIOS_SMM
bool "Dell SMBIOS driver SMM backend"
default y
depends on DCDBAS
depends on DELL_SMBIOS
help
This provides an implementation for the Dell SMBIOS calling interface
communicated over SMI/SMM.
If you have a Dell computer from <=2017 you should say Y here.
If you aren't sure and this module doesn't work for your computer
it just won't load.
config DELL_SMO8800
tristate "Dell Latitude freefall driver (ACPI SMO88XX)"
default m if ACPI
depends on I2C
depends on ACPI || COMPILE_TEST
help
Say Y here if you want to support SMO88XX freefall devices
on Dell Latitude laptops.
To compile this driver as a module, choose M here: the module will
be called dell-smo8800.
config DELL_UART_BACKLIGHT
tristate "Dell AIO UART Backlight driver"
depends on ACPI
depends on ACPI_VIDEO
depends on BACKLIGHT_CLASS_DEVICE
depends on SERIAL_DEV_BUS
help
Say Y here if you want to support Dell AIO UART backlight interface.
The Dell AIO machines released after 2017 come with a UART interface
to communicate with the backlight scalar board. This driver creates
a standard backlight interface and talks to the scalar board through
UART to adjust the AIO screen brightness.
To compile this driver as a module, choose M here: the module will
be called dell_uart_backlight.
config DELL_WMI
tristate "Dell WMI notifications"
default m
depends on ACPI_WMI
depends on DMI
depends on INPUT
depends on ACPI_VIDEO || ACPI_VIDEO = n
depends on DELL_SMBIOS
select DELL_WMI_DESCRIPTOR
select INPUT_SPARSEKMAP
help
Say Y here if you want to support WMI-based hotkeys on Dell laptops.
To compile this driver as a module, choose M here: the module will
be called dell-wmi.
config DELL_WMI_PRIVACY
bool "Dell WMI Hardware Privacy Support"
depends on DELL_WMI
depends on ACPI_EC
help
This option adds integration with the "Dell Hardware Privacy"
feature of Dell laptops to the dell-wmi driver.
config DELL_WMI_AIO
tristate "WMI Hotkeys for Dell All-In-One series"
default m
depends on ACPI_WMI
depends on INPUT
select INPUT_SPARSEKMAP
help
Say Y here if you want to support WMI-based hotkeys on Dell
All-In-One machines.
To compile this driver as a module, choose M here: the module will
be called dell-wmi-aio.
config DELL_WMI_DESCRIPTOR
tristate
default n
depends on ACPI_WMI
config DELL_WMI_DDV
tristate "Dell WMI sensors Support"
default m
depends on ACPI_WMI
depends on ACPI_BATTERY || HWMON
help
This option adds support for WMI-based fan and thermal sensors
found on some Dell notebooks. It also supports various WMI-based battery
extras like reading of the battery temperature and ePPID.
To compile this drivers as a module, choose M here: the module will
be called dell-wmi-ddv.
config DELL_WMI_LED
tristate "External LED on Dell Business Netbooks"
default m
depends on LEDS_CLASS
depends on ACPI_WMI
help
This adds support for the Latitude 2100 and similar
notebooks that have an external LED.
config DELL_WMI_SYSMAN
tristate "Dell WMI-based Systems management driver"
default m
depends on ACPI_WMI
depends on DMI
select NLS
select FW_ATTR_CLASS
help
This driver allows changing BIOS settings on many Dell machines from
2018 and newer without the use of any additional software.
To compile this driver as a module, choose M here: the module will
be called dell-wmi-sysman.
config DELL_DW5826E_RESET
tristate "Dell DW5826e PLDR reset support"
depends on ACPI
help
This adds support for the Dell DW5826e PLDR reset via ACPI
endif # X86_PLATFORM_DRIVERS_DELL
@@ -0,0 +1,31 @@
# SPDX-License-Identifier: GPL-2.0
#
# Makefile for linux/drivers/platform/x86/dell
# Dell x86 Platform-Specific Drivers
#
obj-$(CONFIG_ALIENWARE_WMI) += alienware-wmi.o
alienware-wmi-y := alienware-wmi-base.o
alienware-wmi-$(CONFIG_ALIENWARE_WMI_LEGACY) += alienware-wmi-legacy.o
alienware-wmi-$(CONFIG_ALIENWARE_WMI_WMAX) += alienware-wmi-wmax.o
obj-$(CONFIG_DCDBAS) += dcdbas.o
obj-$(CONFIG_DELL_LAPTOP) += dell-laptop.o
obj-$(CONFIG_DELL_RBTN) += dell-rbtn.o
obj-$(CONFIG_DELL_RBU) += dell_rbu.o
obj-$(CONFIG_DELL_PC) += dell-pc.o
obj-$(CONFIG_DELL_SMBIOS) += dell-smbios.o
dell-smbios-y := dell-smbios-base.o
dell-smbios-$(CONFIG_DELL_SMBIOS_WMI) += dell-smbios-wmi.o
dell-smbios-$(CONFIG_DELL_SMBIOS_SMM) += dell-smbios-smm.o
obj-$(CONFIG_DELL_SMO8800) += dell-smo8800.o
obj-$(CONFIG_DELL_SMO8800) += dell-lis3lv02d.o
obj-$(CONFIG_DELL_UART_BACKLIGHT) += dell-uart-backlight.o
obj-$(CONFIG_DELL_WMI) += dell-wmi.o
dell-wmi-y := dell-wmi-base.o
dell-wmi-$(CONFIG_DELL_WMI_PRIVACY) += dell-wmi-privacy.o
obj-$(CONFIG_DELL_WMI_AIO) += dell-wmi-aio.o
obj-$(CONFIG_DELL_WMI_DESCRIPTOR) += dell-wmi-descriptor.o
obj-$(CONFIG_DELL_WMI_DDV) += dell-wmi-ddv.o
obj-$(CONFIG_DELL_WMI_LED) += dell-wmi-led.o
obj-$(CONFIG_DELL_WMI_SYSMAN) += dell-wmi-sysman/
obj-$(CONFIG_DELL_DW5826E_RESET) += dell-dw5826e-reset.o
@@ -0,0 +1,496 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Alienware special feature control
*
* Copyright (C) 2014 Dell Inc <Dell.Client.Kernel@dell.com>
* Copyright (C) 2025 Kurt Borja <kuurtb@gmail.com>
*/
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/acpi.h>
#include <linux/cleanup.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/slab.h>
#include <linux/types.h>
#include <linux/dmi.h>
#include <linux/leds.h>
#include <asm/byteorder.h>
#include "alienware-wmi.h"
MODULE_AUTHOR("Mario Limonciello <mario.limonciello@outlook.com>");
MODULE_AUTHOR("Kurt Borja <kuurtb@gmail.com>");
MODULE_DESCRIPTION("Alienware special feature control");
MODULE_LICENSE("GPL");
struct alienfx_quirks *alienfx;
static struct alienfx_quirks quirk_inspiron5675 = {
.num_zones = 2,
.hdmi_mux = false,
.amplifier = false,
.deepslp = false,
};
static struct alienfx_quirks quirk_unknown = {
.num_zones = 2,
.hdmi_mux = false,
.amplifier = false,
.deepslp = false,
};
static struct alienfx_quirks quirk_x51_r1_r2 = {
.num_zones = 3,
.hdmi_mux = false,
.amplifier = false,
.deepslp = false,
};
static struct alienfx_quirks quirk_x51_r3 = {
.num_zones = 4,
.hdmi_mux = false,
.amplifier = true,
.deepslp = false,
};
static struct alienfx_quirks quirk_asm100 = {
.num_zones = 2,
.hdmi_mux = true,
.amplifier = false,
.deepslp = false,
};
static struct alienfx_quirks quirk_asm200 = {
.num_zones = 2,
.hdmi_mux = true,
.amplifier = false,
.deepslp = true,
};
static struct alienfx_quirks quirk_asm201 = {
.num_zones = 2,
.hdmi_mux = true,
.amplifier = true,
.deepslp = true,
};
static int __init dmi_matched(const struct dmi_system_id *dmi)
{
alienfx = dmi->driver_data;
return 1;
}
static const struct dmi_system_id alienware_quirks[] __initconst = {
{
.callback = dmi_matched,
.ident = "Alienware ASM100",
.matches = {
DMI_MATCH(DMI_SYS_VENDOR, "Alienware"),
DMI_MATCH(DMI_PRODUCT_NAME, "ASM100"),
},
.driver_data = &quirk_asm100,
},
{
.callback = dmi_matched,
.ident = "Alienware ASM200",
.matches = {
DMI_MATCH(DMI_SYS_VENDOR, "Alienware"),
DMI_MATCH(DMI_PRODUCT_NAME, "ASM200"),
},
.driver_data = &quirk_asm200,
},
{
.callback = dmi_matched,
.ident = "Alienware ASM201",
.matches = {
DMI_MATCH(DMI_SYS_VENDOR, "Alienware"),
DMI_MATCH(DMI_PRODUCT_NAME, "ASM201"),
},
.driver_data = &quirk_asm201,
},
{
.callback = dmi_matched,
.ident = "Alienware X51 R1",
.matches = {
DMI_MATCH(DMI_SYS_VENDOR, "Alienware"),
DMI_MATCH(DMI_PRODUCT_NAME, "Alienware X51"),
},
.driver_data = &quirk_x51_r1_r2,
},
{
.callback = dmi_matched,
.ident = "Alienware X51 R2",
.matches = {
DMI_MATCH(DMI_SYS_VENDOR, "Alienware"),
DMI_MATCH(DMI_PRODUCT_NAME, "Alienware X51 R2"),
},
.driver_data = &quirk_x51_r1_r2,
},
{
.callback = dmi_matched,
.ident = "Alienware X51 R3",
.matches = {
DMI_MATCH(DMI_SYS_VENDOR, "Alienware"),
DMI_MATCH(DMI_PRODUCT_NAME, "Alienware X51 R3"),
},
.driver_data = &quirk_x51_r3,
},
{
.callback = dmi_matched,
.ident = "Dell Inc. Inspiron 5675",
.matches = {
DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."),
DMI_MATCH(DMI_PRODUCT_NAME, "Inspiron 5675"),
},
.driver_data = &quirk_inspiron5675,
},
{}
};
u8 alienware_interface;
int alienware_wmi_command(struct wmi_device *wdev, u32 method_id,
void *in_args, size_t in_size, u32 *out_data)
{
struct wmi_buffer out, in = {
.data = in_args,
.length = in_size,
};
int ret;
if (!out_data)
return wmidev_invoke_procedure(wdev, 0, method_id, &in);
ret = wmidev_invoke_method(wdev, 0, method_id, &in, &out,
sizeof(*out_data));
if (ret)
return ret;
__le32 *data __free(kfree) = out.data;
*out_data = le32_to_cpu(*data);
return 0;
}
/*
* Helpers used for zone control
*/
static int parse_rgb(const char *buf, struct color_platform *colors)
{
long unsigned int rgb;
int ret;
union color_union {
struct color_platform cp;
int package;
} repackager;
ret = kstrtoul(buf, 16, &rgb);
if (ret)
return ret;
/* RGB triplet notation is 24-bit hexadecimal */
if (rgb > 0xFFFFFF)
return -EINVAL;
repackager.package = rgb & 0x0f0f0f0f;
pr_debug("alienware-wmi: r: %d g:%d b: %d\n",
repackager.cp.red, repackager.cp.green, repackager.cp.blue);
*colors = repackager.cp;
return 0;
}
/*
* Individual RGB zone control
*/
static ssize_t zone_show(struct device *dev, struct device_attribute *attr,
char *buf, u8 location)
{
struct alienfx_priv *priv = dev_get_drvdata(dev);
struct color_platform *colors = &priv->colors[location];
return sprintf(buf, "red: %d, green: %d, blue: %d\n",
colors->red, colors->green, colors->blue);
}
static ssize_t zone_store(struct device *dev, struct device_attribute *attr,
const char *buf, size_t count, u8 location)
{
struct alienfx_priv *priv = dev_get_drvdata(dev);
struct color_platform *colors = &priv->colors[location];
struct alienfx_platdata *pdata = dev_get_platdata(dev);
int ret;
ret = parse_rgb(buf, colors);
if (ret)
return ret;
ret = pdata->ops.upd_led(priv, pdata->wdev, location);
return ret ? ret : count;
}
static ssize_t zone00_show(struct device *dev, struct device_attribute *attr,
char *buf)
{
return zone_show(dev, attr, buf, 0);
}
static ssize_t zone00_store(struct device *dev, struct device_attribute *attr,
const char *buf, size_t count)
{
return zone_store(dev, attr, buf, count, 0);
}
static DEVICE_ATTR_RW(zone00);
static ssize_t zone01_show(struct device *dev, struct device_attribute *attr,
char *buf)
{
return zone_show(dev, attr, buf, 1);
}
static ssize_t zone01_store(struct device *dev, struct device_attribute *attr,
const char *buf, size_t count)
{
return zone_store(dev, attr, buf, count, 1);
}
static DEVICE_ATTR_RW(zone01);
static ssize_t zone02_show(struct device *dev, struct device_attribute *attr,
char *buf)
{
return zone_show(dev, attr, buf, 2);
}
static ssize_t zone02_store(struct device *dev, struct device_attribute *attr,
const char *buf, size_t count)
{
return zone_store(dev, attr, buf, count, 2);
}
static DEVICE_ATTR_RW(zone02);
static ssize_t zone03_show(struct device *dev, struct device_attribute *attr,
char *buf)
{
return zone_show(dev, attr, buf, 3);
}
static ssize_t zone03_store(struct device *dev, struct device_attribute *attr,
const char *buf, size_t count)
{
return zone_store(dev, attr, buf, count, 3);
}
static DEVICE_ATTR_RW(zone03);
/*
* Lighting control state device attribute (Global)
*/
static ssize_t lighting_control_state_show(struct device *dev,
struct device_attribute *attr,
char *buf)
{
struct alienfx_priv *priv = dev_get_drvdata(dev);
if (priv->lighting_control_state == LEGACY_BOOTING)
return sysfs_emit(buf, "[booting] running suspend\n");
else if (priv->lighting_control_state == LEGACY_SUSPEND)
return sysfs_emit(buf, "booting running [suspend]\n");
return sysfs_emit(buf, "booting [running] suspend\n");
}
static ssize_t lighting_control_state_store(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t count)
{
struct alienfx_priv *priv = dev_get_drvdata(dev);
u8 val;
if (strcmp(buf, "booting\n") == 0)
val = LEGACY_BOOTING;
else if (strcmp(buf, "suspend\n") == 0)
val = LEGACY_SUSPEND;
else if (alienware_interface == LEGACY)
val = LEGACY_RUNNING;
else
val = WMAX_RUNNING;
priv->lighting_control_state = val;
pr_debug("alienware-wmi: updated control state to %d\n",
priv->lighting_control_state);
return count;
}
static DEVICE_ATTR_RW(lighting_control_state);
static umode_t zone_attr_visible(struct kobject *kobj,
struct attribute *attr, int n)
{
if (n < alienfx->num_zones + 1)
return attr->mode;
return 0;
}
static bool zone_group_visible(struct kobject *kobj)
{
return alienfx->num_zones > 0;
}
DEFINE_SYSFS_GROUP_VISIBLE(zone);
static struct attribute *zone_attrs[] = {
&dev_attr_lighting_control_state.attr,
&dev_attr_zone00.attr,
&dev_attr_zone01.attr,
&dev_attr_zone02.attr,
&dev_attr_zone03.attr,
NULL
};
static struct attribute_group zone_attribute_group = {
.name = "rgb_zones",
.is_visible = SYSFS_GROUP_VISIBLE(zone),
.attrs = zone_attrs,
};
/*
* LED Brightness (Global)
*/
static void global_led_set(struct led_classdev *led_cdev,
enum led_brightness brightness)
{
struct alienfx_priv *priv = container_of(led_cdev, struct alienfx_priv,
global_led);
struct alienfx_platdata *pdata = dev_get_platdata(&priv->pdev->dev);
int ret;
priv->global_brightness = brightness;
ret = pdata->ops.upd_brightness(priv, pdata->wdev, brightness);
if (ret)
pr_err("LED brightness update failed\n");
}
static enum led_brightness global_led_get(struct led_classdev *led_cdev)
{
struct alienfx_priv *priv = container_of(led_cdev, struct alienfx_priv,
global_led);
return priv->global_brightness;
}
/*
* Platform Driver
*/
static int alienfx_probe(struct platform_device *pdev)
{
struct alienfx_priv *priv;
priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
if (alienware_interface == WMAX)
priv->lighting_control_state = WMAX_RUNNING;
else
priv->lighting_control_state = LEGACY_RUNNING;
priv->pdev = pdev;
priv->global_led.name = "alienware::global_brightness";
priv->global_led.brightness_set = global_led_set;
priv->global_led.brightness_get = global_led_get;
priv->global_led.max_brightness = 0x0F;
priv->global_brightness = priv->global_led.max_brightness;
platform_set_drvdata(pdev, priv);
return devm_led_classdev_register(&pdev->dev, &priv->global_led);
}
static const struct attribute_group *alienfx_groups[] = {
&zone_attribute_group,
WMAX_DEV_GROUPS
NULL
};
static struct platform_driver platform_driver = {
.driver = {
.name = "alienware-wmi",
.dev_groups = alienfx_groups,
},
.probe = alienfx_probe,
};
static void alienware_alienfx_remove(void *data)
{
struct platform_device *pdev = data;
platform_device_unregister(pdev);
}
int alienware_alienfx_setup(struct alienfx_platdata *pdata)
{
struct device *dev = &pdata->wdev->dev;
struct platform_device *pdev;
int ret;
pdev = platform_device_register_data(NULL, "alienware-wmi",
PLATFORM_DEVID_NONE, pdata,
sizeof(*pdata));
if (IS_ERR(pdev))
return PTR_ERR(pdev);
dev_set_drvdata(dev, pdev);
ret = devm_add_action_or_reset(dev, alienware_alienfx_remove, pdev);
if (ret)
return ret;
return 0;
}
static int __init alienware_wmi_init(void)
{
int ret;
dmi_check_system(alienware_quirks);
if (!alienfx)
alienfx = &quirk_unknown;
ret = platform_driver_register(&platform_driver);
if (ret < 0)
return ret;
if (wmi_has_guid(WMAX_CONTROL_GUID)) {
alienware_interface = WMAX;
ret = alienware_wmax_wmi_init();
} else {
alienware_interface = LEGACY;
ret = alienware_legacy_wmi_init();
}
if (ret < 0)
platform_driver_unregister(&platform_driver);
return ret;
}
module_init(alienware_wmi_init);
static void __exit alienware_wmi_exit(void)
{
if (alienware_interface == WMAX)
alienware_wmax_wmi_exit();
else
alienware_legacy_wmi_exit();
platform_driver_unregister(&platform_driver);
}
module_exit(alienware_wmi_exit);
@@ -0,0 +1,95 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Alienware LEGACY WMI device driver
*
* Copyright (C) 2025 Kurt Borja <kuurtb@gmail.com>
*/
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/wmi.h>
#include "alienware-wmi.h"
struct legacy_led_args {
struct color_platform colors;
u8 brightness;
u8 state;
} __packed;
/*
* Legacy WMI driver
*/
static int legacy_wmi_update_led(struct alienfx_priv *priv,
struct wmi_device *wdev, u8 location)
{
struct legacy_led_args legacy_args = {
.colors = priv->colors[location],
.brightness = priv->global_brightness,
.state = 0,
};
struct acpi_buffer input;
acpi_status status;
if (legacy_args.state != LEGACY_RUNNING) {
legacy_args.state = priv->lighting_control_state;
input.length = sizeof(legacy_args);
input.pointer = &legacy_args;
status = wmi_evaluate_method(LEGACY_POWER_CONTROL_GUID, 0,
location + 1, &input, NULL);
if (ACPI_FAILURE(status))
return -EIO;
return 0;
}
return alienware_wmi_command(wdev, location + 1, &legacy_args,
sizeof(legacy_args), NULL);
}
static int legacy_wmi_update_brightness(struct alienfx_priv *priv,
struct wmi_device *wdev, u8 brightness)
{
return legacy_wmi_update_led(priv, wdev, 0);
}
static int legacy_wmi_probe(struct wmi_device *wdev, const void *context)
{
struct alienfx_platdata pdata = {
.wdev = wdev,
.ops = {
.upd_led = legacy_wmi_update_led,
.upd_brightness = legacy_wmi_update_brightness,
},
};
return alienware_alienfx_setup(&pdata);
}
static const struct wmi_device_id alienware_legacy_device_id_table[] = {
{ LEGACY_CONTROL_GUID, NULL },
{ },
};
MODULE_DEVICE_TABLE(wmi, alienware_legacy_device_id_table);
static struct wmi_driver alienware_legacy_wmi_driver = {
.driver = {
.name = "alienware-wmi-alienfx",
.probe_type = PROBE_PREFER_ASYNCHRONOUS,
},
.id_table = alienware_legacy_device_id_table,
.probe = legacy_wmi_probe,
.no_singleton = true,
};
int __init alienware_legacy_wmi_init(void)
{
return wmi_driver_register(&alienware_legacy_wmi_driver);
}
void __exit alienware_legacy_wmi_exit(void)
{
wmi_driver_unregister(&alienware_legacy_wmi_driver);
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,117 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* Alienware WMI special features driver
*
* Copyright (C) 2014 Dell Inc <Dell.Client.Kernel@dell.com>
* Copyright (C) 2024 Kurt Borja <kuurtb@gmail.com>
*/
#ifndef _ALIENWARE_WMI_H_
#define _ALIENWARE_WMI_H_
#include <linux/leds.h>
#include <linux/platform_device.h>
#include <linux/wmi.h>
#define LEGACY_CONTROL_GUID "A90597CE-A997-11DA-B012-B622A1EF5492"
#define LEGACY_POWER_CONTROL_GUID "A80593CE-A997-11DA-B012-B622A1EF5492"
#define WMAX_CONTROL_GUID "A70591CE-A997-11DA-B012-B622A1EF5492"
enum INTERFACE_FLAGS {
LEGACY,
WMAX,
};
enum LEGACY_CONTROL_STATES {
LEGACY_RUNNING = 1,
LEGACY_BOOTING = 0,
LEGACY_SUSPEND = 3,
};
enum WMAX_CONTROL_STATES {
WMAX_RUNNING = 0xFF,
WMAX_BOOTING = 0,
WMAX_SUSPEND = 3,
};
struct alienfx_quirks {
u8 num_zones;
bool hdmi_mux;
bool amplifier;
bool deepslp;
};
struct color_platform {
u8 blue;
u8 green;
u8 red;
} __packed;
struct alienfx_priv {
struct platform_device *pdev;
struct led_classdev global_led;
struct color_platform colors[4];
u8 global_brightness;
u8 lighting_control_state;
};
struct alienfx_ops {
int (*upd_led)(struct alienfx_priv *priv, struct wmi_device *wdev,
u8 location);
int (*upd_brightness)(struct alienfx_priv *priv, struct wmi_device *wdev,
u8 brightness);
};
struct alienfx_platdata {
struct wmi_device *wdev;
struct alienfx_ops ops;
};
extern u8 alienware_interface;
extern struct alienfx_quirks *alienfx;
int alienware_wmi_command(struct wmi_device *wdev, u32 method_id,
void *in_args, size_t in_size, u32 *out_data);
int alienware_alienfx_setup(struct alienfx_platdata *pdata);
#if IS_ENABLED(CONFIG_ALIENWARE_WMI_LEGACY)
int __init alienware_legacy_wmi_init(void);
void __exit alienware_legacy_wmi_exit(void);
#else
static inline int alienware_legacy_wmi_init(void)
{
return -ENODEV;
}
static inline void alienware_legacy_wmi_exit(void)
{
}
#endif
#if IS_ENABLED(CONFIG_ALIENWARE_WMI_WMAX)
extern const struct attribute_group wmax_hdmi_attribute_group;
extern const struct attribute_group wmax_amplifier_attribute_group;
extern const struct attribute_group wmax_deepsleep_attribute_group;
#define WMAX_DEV_GROUPS &wmax_hdmi_attribute_group, \
&wmax_amplifier_attribute_group, \
&wmax_deepsleep_attribute_group,
int __init alienware_wmax_wmi_init(void);
void __exit alienware_wmax_wmi_exit(void);
#else
#define WMAX_DEV_GROUPS
static inline int alienware_wmax_wmi_init(void)
{
return -ENODEV;
}
static inline void alienware_wmax_wmi_exit(void)
{
}
#endif
#endif
@@ -0,0 +1,781 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* dcdbas.c: Dell Systems Management Base Driver
*
* The Dell Systems Management Base Driver provides a sysfs interface for
* systems management software to perform System Management Interrupts (SMIs)
* and Host Control Actions (power cycle or power off after OS shutdown) on
* Dell systems.
*
* See Documentation/userspace-api/dcdbas.rst for more information.
*
* Copyright (C) 1995-2006 Dell Inc.
*/
#include <linux/platform_device.h>
#include <linux/acpi.h>
#include <linux/dma-mapping.h>
#include <linux/dmi.h>
#include <linux/errno.h>
#include <linux/cpu.h>
#include <linux/gfp.h>
#include <linux/init.h>
#include <linux/io.h>
#include <linux/kernel.h>
#include <linux/mc146818rtc.h>
#include <linux/module.h>
#include <linux/reboot.h>
#include <linux/sched.h>
#include <linux/smp.h>
#include <linux/spinlock.h>
#include <linux/string.h>
#include <linux/sysfs.h>
#include <linux/types.h>
#include <linux/mutex.h>
#include "dcdbas.h"
#define DRIVER_NAME "dcdbas"
#define DRIVER_VERSION "5.6.0-3.4"
#define DRIVER_DESCRIPTION "Dell Systems Management Base Driver"
static struct platform_device *dcdbas_pdev;
static unsigned long max_smi_data_buf_size = MAX_SMI_DATA_BUF_SIZE;
static DEFINE_MUTEX(smi_data_lock);
static u8 *bios_buffer;
static struct smi_buffer smi_buf;
static unsigned int host_control_action;
static unsigned int host_control_smi_type;
static unsigned int host_control_on_shutdown;
static bool wsmt_enabled;
int dcdbas_smi_alloc(struct smi_buffer *smi_buffer, unsigned long size)
{
smi_buffer->virt = dma_alloc_coherent(&dcdbas_pdev->dev, size,
&smi_buffer->dma, GFP_KERNEL);
if (!smi_buffer->virt) {
dev_dbg(&dcdbas_pdev->dev,
"%s: failed to allocate memory size %lu\n",
__func__, size);
return -ENOMEM;
}
smi_buffer->size = size;
dev_dbg(&dcdbas_pdev->dev, "%s: phys: %x size: %lu\n",
__func__, (u32)smi_buffer->dma, smi_buffer->size);
return 0;
}
EXPORT_SYMBOL_GPL(dcdbas_smi_alloc);
void dcdbas_smi_free(struct smi_buffer *smi_buffer)
{
if (!smi_buffer->virt)
return;
dev_dbg(&dcdbas_pdev->dev, "%s: phys: %x size: %lu\n",
__func__, (u32)smi_buffer->dma, smi_buffer->size);
dma_free_coherent(&dcdbas_pdev->dev, smi_buffer->size,
smi_buffer->virt, smi_buffer->dma);
smi_buffer->virt = NULL;
smi_buffer->dma = 0;
smi_buffer->size = 0;
}
EXPORT_SYMBOL_GPL(dcdbas_smi_free);
/**
* smi_data_buf_free: free SMI data buffer
*/
static void smi_data_buf_free(void)
{
if (!smi_buf.virt || wsmt_enabled)
return;
dcdbas_smi_free(&smi_buf);
}
/**
* smi_data_buf_realloc: grow SMI data buffer if needed
*/
static int smi_data_buf_realloc(unsigned long size)
{
struct smi_buffer tmp;
int ret;
if (smi_buf.size >= size)
return 0;
if (size > max_smi_data_buf_size)
return -EINVAL;
/* new buffer is needed */
ret = dcdbas_smi_alloc(&tmp, size);
if (ret)
return ret;
/* memory zeroed by dma_alloc_coherent */
if (smi_buf.virt)
memcpy(tmp.virt, smi_buf.virt, smi_buf.size);
/* free any existing buffer */
smi_data_buf_free();
/* set up new buffer for use */
smi_buf = tmp;
return 0;
}
static ssize_t smi_data_buf_phys_addr_show(struct device *dev,
struct device_attribute *attr,
char *buf)
{
return sysfs_emit(buf, "%x\n", (u32)smi_buf.dma);
}
static ssize_t smi_data_buf_size_show(struct device *dev,
struct device_attribute *attr,
char *buf)
{
return sysfs_emit(buf, "%lu\n", smi_buf.size);
}
static ssize_t smi_data_buf_size_store(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t count)
{
unsigned long buf_size;
ssize_t ret;
buf_size = simple_strtoul(buf, NULL, 10);
/* make sure SMI data buffer is at least buf_size */
mutex_lock(&smi_data_lock);
ret = smi_data_buf_realloc(buf_size);
mutex_unlock(&smi_data_lock);
if (ret)
return ret;
return count;
}
static ssize_t smi_data_read(struct file *filp, struct kobject *kobj,
const struct bin_attribute *bin_attr,
char *buf, loff_t pos, size_t count)
{
ssize_t ret;
mutex_lock(&smi_data_lock);
ret = memory_read_from_buffer(buf, count, &pos, smi_buf.virt,
smi_buf.size);
mutex_unlock(&smi_data_lock);
return ret;
}
static ssize_t smi_data_write(struct file *filp, struct kobject *kobj,
const struct bin_attribute *bin_attr,
char *buf, loff_t pos, size_t count)
{
ssize_t ret;
if ((pos + count) > max_smi_data_buf_size)
return -EINVAL;
mutex_lock(&smi_data_lock);
ret = smi_data_buf_realloc(pos + count);
if (ret)
goto out;
memcpy(smi_buf.virt + pos, buf, count);
ret = count;
out:
mutex_unlock(&smi_data_lock);
return ret;
}
static ssize_t host_control_action_show(struct device *dev,
struct device_attribute *attr,
char *buf)
{
return sysfs_emit(buf, "%u\n", host_control_action);
}
static ssize_t host_control_action_store(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t count)
{
ssize_t ret;
/* make sure buffer is available for host control command */
mutex_lock(&smi_data_lock);
ret = smi_data_buf_realloc(sizeof(struct apm_cmd));
mutex_unlock(&smi_data_lock);
if (ret)
return ret;
host_control_action = simple_strtoul(buf, NULL, 10);
return count;
}
static ssize_t host_control_smi_type_show(struct device *dev,
struct device_attribute *attr,
char *buf)
{
return sysfs_emit(buf, "%u\n", host_control_smi_type);
}
static ssize_t host_control_smi_type_store(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t count)
{
host_control_smi_type = simple_strtoul(buf, NULL, 10);
return count;
}
static ssize_t host_control_on_shutdown_show(struct device *dev,
struct device_attribute *attr,
char *buf)
{
return sysfs_emit(buf, "%u\n", host_control_on_shutdown);
}
static ssize_t host_control_on_shutdown_store(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t count)
{
host_control_on_shutdown = simple_strtoul(buf, NULL, 10);
return count;
}
static int raise_smi(void *par)
{
struct smi_cmd *smi_cmd = par;
if (smp_processor_id() != 0) {
dev_dbg(&dcdbas_pdev->dev, "%s: failed to get CPU 0\n",
__func__);
return -EBUSY;
}
/* generate SMI */
/* inb to force posted write through and make SMI happen now */
asm volatile (
"outb %b0,%w1\n"
"inb %w1"
: /* no output args */
: "a" (smi_cmd->command_code),
"d" (smi_cmd->command_address),
"b" (smi_cmd->ebx),
"c" (smi_cmd->ecx)
: "memory"
);
return 0;
}
/**
* dcdbas_smi_request: generate SMI request
*
* Called with smi_data_lock.
*/
int dcdbas_smi_request(struct smi_cmd *smi_cmd)
{
int ret;
if (smi_cmd->magic != SMI_CMD_MAGIC) {
dev_info(&dcdbas_pdev->dev, "%s: invalid magic value\n",
__func__);
return -EBADR;
}
/* SMI requires CPU 0 */
cpus_read_lock();
ret = smp_call_on_cpu(0, raise_smi, smi_cmd, true);
cpus_read_unlock();
return ret;
}
EXPORT_SYMBOL(dcdbas_smi_request);
/**
* smi_request_store:
*
* The valid values are:
* 0: zero SMI data buffer
* 1: generate calling interface SMI
* 2: generate raw SMI
*
* User application writes smi_cmd to smi_data before telling driver
* to generate SMI.
*/
static ssize_t smi_request_store(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t count)
{
struct smi_cmd *smi_cmd;
unsigned long val = simple_strtoul(buf, NULL, 10);
ssize_t ret;
mutex_lock(&smi_data_lock);
if (smi_buf.size < sizeof(struct smi_cmd)) {
ret = -ENODEV;
goto out;
}
smi_cmd = (struct smi_cmd *)smi_buf.virt;
switch (val) {
case 2:
/* Raw SMI */
ret = dcdbas_smi_request(smi_cmd);
if (!ret)
ret = count;
break;
case 1:
/*
* Calling Interface SMI
*
* Provide physical address of command buffer field within
* the struct smi_cmd to BIOS.
*
* Because the address that smi_cmd (smi_buf.virt) points to
* will be from memremap() of a non-memory address if WSMT
* is present, we can't use virt_to_phys() on smi_cmd, so
* we have to use the physical address that was saved when
* the virtual address for smi_cmd was received.
*/
smi_cmd->ebx = (u32)smi_buf.dma +
offsetof(struct smi_cmd, command_buffer);
ret = dcdbas_smi_request(smi_cmd);
if (!ret)
ret = count;
break;
case 0:
memset(smi_buf.virt, 0, smi_buf.size);
ret = count;
break;
default:
ret = -EINVAL;
break;
}
out:
mutex_unlock(&smi_data_lock);
return ret;
}
/**
* host_control_smi: generate host control SMI
*
* Caller must set up the host control command in smi_buf.virt.
*/
static int host_control_smi(void)
{
struct apm_cmd *apm_cmd;
u8 *data;
unsigned long flags;
u32 num_ticks;
s8 cmd_status;
u8 index;
apm_cmd = (struct apm_cmd *)smi_buf.virt;
apm_cmd->status = ESM_STATUS_CMD_UNSUCCESSFUL;
switch (host_control_smi_type) {
case HC_SMITYPE_TYPE1:
spin_lock_irqsave(&rtc_lock, flags);
/* write SMI data buffer physical address */
data = (u8 *)&smi_buf.dma;
for (index = PE1300_CMOS_CMD_STRUCT_PTR;
index < (PE1300_CMOS_CMD_STRUCT_PTR + 4);
index++, data++) {
outb(index,
(CMOS_BASE_PORT + CMOS_PAGE2_INDEX_PORT_PIIX4));
outb(*data,
(CMOS_BASE_PORT + CMOS_PAGE2_DATA_PORT_PIIX4));
}
/* first set status to -1 as called by spec */
cmd_status = ESM_STATUS_CMD_UNSUCCESSFUL;
outb((u8) cmd_status, PCAT_APM_STATUS_PORT);
/* generate SMM call */
outb(ESM_APM_CMD, PCAT_APM_CONTROL_PORT);
spin_unlock_irqrestore(&rtc_lock, flags);
/* wait a few to see if it executed */
num_ticks = TIMEOUT_USEC_SHORT_SEMA_BLOCKING;
while ((s8)inb(PCAT_APM_STATUS_PORT) == ESM_STATUS_CMD_UNSUCCESSFUL) {
num_ticks--;
if (num_ticks == EXPIRED_TIMER)
return -ETIME;
}
break;
case HC_SMITYPE_TYPE2:
case HC_SMITYPE_TYPE3:
spin_lock_irqsave(&rtc_lock, flags);
/* write SMI data buffer physical address */
data = (u8 *)&smi_buf.dma;
for (index = PE1400_CMOS_CMD_STRUCT_PTR;
index < (PE1400_CMOS_CMD_STRUCT_PTR + 4);
index++, data++) {
outb(index, (CMOS_BASE_PORT + CMOS_PAGE1_INDEX_PORT));
outb(*data, (CMOS_BASE_PORT + CMOS_PAGE1_DATA_PORT));
}
/* generate SMM call */
if (host_control_smi_type == HC_SMITYPE_TYPE3)
outb(ESM_APM_CMD, PCAT_APM_CONTROL_PORT);
else
outb(ESM_APM_CMD, PE1400_APM_CONTROL_PORT);
/* restore RTC index pointer since it was written to above */
CMOS_READ(RTC_REG_C);
spin_unlock_irqrestore(&rtc_lock, flags);
/* read control port back to serialize write */
cmd_status = inb(PE1400_APM_CONTROL_PORT);
/* wait a few to see if it executed */
num_ticks = TIMEOUT_USEC_SHORT_SEMA_BLOCKING;
while (apm_cmd->status == ESM_STATUS_CMD_UNSUCCESSFUL) {
num_ticks--;
if (num_ticks == EXPIRED_TIMER)
return -ETIME;
}
break;
default:
dev_dbg(&dcdbas_pdev->dev, "%s: invalid SMI type %u\n",
__func__, host_control_smi_type);
return -ENOSYS;
}
return 0;
}
/**
* dcdbas_host_control: initiate host control
*
* This function is called by the driver after the system has
* finished shutting down if the user application specified a
* host control action to perform on shutdown. It is safe to
* use smi_buf.virt at this point because the system has finished
* shutting down and no userspace apps are running.
*/
static void dcdbas_host_control(void)
{
struct apm_cmd *apm_cmd;
u8 action;
if (host_control_action == HC_ACTION_NONE)
return;
action = host_control_action;
host_control_action = HC_ACTION_NONE;
if (!smi_buf.virt) {
dev_dbg(&dcdbas_pdev->dev, "%s: no SMI buffer\n", __func__);
return;
}
if (smi_buf.size < sizeof(struct apm_cmd)) {
dev_dbg(&dcdbas_pdev->dev, "%s: SMI buffer too small\n",
__func__);
return;
}
apm_cmd = (struct apm_cmd *)smi_buf.virt;
/* power off takes precedence */
if (action & HC_ACTION_HOST_CONTROL_POWEROFF) {
apm_cmd->command = ESM_APM_POWER_CYCLE;
apm_cmd->reserved = 0;
*((s16 *)&apm_cmd->parameters.shortreq.parm[0]) = (s16) 0;
host_control_smi();
} else if (action & HC_ACTION_HOST_CONTROL_POWERCYCLE) {
apm_cmd->command = ESM_APM_POWER_CYCLE;
apm_cmd->reserved = 0;
*((s16 *)&apm_cmd->parameters.shortreq.parm[0]) = (s16) 20;
host_control_smi();
}
}
/* WSMT */
static u8 checksum(u8 *buffer, u8 length)
{
u8 sum = 0;
u8 *end = buffer + length;
while (buffer < end)
sum += *buffer++;
return sum;
}
static inline struct smm_eps_table *check_eps_table(u8 *addr)
{
struct smm_eps_table *eps = (struct smm_eps_table *)addr;
if (strncmp(eps->smm_comm_buff_anchor, SMM_EPS_SIG, 4) != 0)
return NULL;
if (checksum(addr, eps->length) != 0)
return NULL;
return eps;
}
static int dcdbas_check_wsmt(void)
{
const struct dmi_device *dev = NULL;
struct acpi_table_wsmt *wsmt = NULL;
struct smm_eps_table *eps = NULL;
u64 bios_buf_paddr;
u64 remap_size;
u8 *addr;
acpi_get_table(ACPI_SIG_WSMT, 0, (struct acpi_table_header **)&wsmt);
if (!wsmt)
return 0;
/* Check if WSMT ACPI table shows that protection is enabled */
if (!(wsmt->protection_flags & ACPI_WSMT_FIXED_COMM_BUFFERS) ||
!(wsmt->protection_flags & ACPI_WSMT_COMM_BUFFER_NESTED_PTR_PROTECTION))
return 0;
/*
* BIOS could provide the address/size of the protected buffer
* in an SMBIOS string or in an EPS structure in 0xFxxxx.
*/
/* Check SMBIOS for buffer address */
while ((dev = dmi_find_device(DMI_DEV_TYPE_OEM_STRING, NULL, dev)))
if (sscanf(dev->name, "30[%16llx;%8llx]", &bios_buf_paddr,
&remap_size) == 2)
goto remap;
/* Scan for EPS (entry point structure) */
for (addr = (u8 *)__va(0xf0000);
addr < (u8 *)__va(0x100000 - sizeof(struct smm_eps_table));
addr += 16) {
eps = check_eps_table(addr);
if (eps)
break;
}
if (!eps) {
dev_dbg(&dcdbas_pdev->dev, "found WSMT, but no firmware buffer found\n");
return -ENODEV;
}
bios_buf_paddr = eps->smm_comm_buff_addr;
remap_size = eps->num_of_4k_pages * PAGE_SIZE;
remap:
/*
* Get physical address of buffer and map to virtual address.
* Table gives size in 4K pages, regardless of actual system page size.
*/
if (upper_32_bits(bios_buf_paddr + 8)) {
dev_warn(&dcdbas_pdev->dev, "found WSMT, but buffer address is above 4GB\n");
return -EINVAL;
}
/*
* Limit remap size to MAX_SMI_DATA_BUF_SIZE + 8 (since the first 8
* bytes are used for a semaphore, not the data buffer itself).
*/
if (remap_size > MAX_SMI_DATA_BUF_SIZE + 8)
remap_size = MAX_SMI_DATA_BUF_SIZE + 8;
bios_buffer = memremap(bios_buf_paddr, remap_size, MEMREMAP_WB);
if (!bios_buffer) {
dev_warn(&dcdbas_pdev->dev, "found WSMT, but failed to map buffer\n");
return -ENOMEM;
}
/* First 8 bytes is for a semaphore, not part of the smi_buf.virt */
smi_buf.dma = bios_buf_paddr + 8;
smi_buf.virt = bios_buffer + 8;
smi_buf.size = remap_size - 8;
max_smi_data_buf_size = smi_buf.size;
wsmt_enabled = true;
dev_info(&dcdbas_pdev->dev,
"WSMT found, using firmware-provided SMI buffer.\n");
return 1;
}
/**
* dcdbas_reboot_notify: handle reboot notification for host control
*/
static int dcdbas_reboot_notify(struct notifier_block *nb, unsigned long code,
void *unused)
{
switch (code) {
case SYS_DOWN:
case SYS_HALT:
case SYS_POWER_OFF:
if (host_control_on_shutdown) {
/* firmware is going to perform host control action */
printk(KERN_WARNING "Please wait for shutdown "
"action to complete...\n");
dcdbas_host_control();
}
break;
}
return NOTIFY_DONE;
}
static struct notifier_block dcdbas_reboot_nb = {
.notifier_call = dcdbas_reboot_notify,
.next = NULL,
.priority = INT_MIN
};
static const BIN_ATTR_ADMIN_RW(smi_data, 0);
static const struct bin_attribute *const dcdbas_bin_attrs[] = {
&bin_attr_smi_data,
NULL
};
static DCDBAS_DEV_ATTR_RW(smi_data_buf_size);
static DCDBAS_DEV_ATTR_RO(smi_data_buf_phys_addr);
static DCDBAS_DEV_ATTR_WO(smi_request);
static DCDBAS_DEV_ATTR_RW(host_control_action);
static DCDBAS_DEV_ATTR_RW(host_control_smi_type);
static DCDBAS_DEV_ATTR_RW(host_control_on_shutdown);
static struct attribute *dcdbas_dev_attrs[] = {
&dev_attr_smi_data_buf_size.attr,
&dev_attr_smi_data_buf_phys_addr.attr,
&dev_attr_smi_request.attr,
&dev_attr_host_control_action.attr,
&dev_attr_host_control_smi_type.attr,
&dev_attr_host_control_on_shutdown.attr,
NULL
};
static const struct attribute_group dcdbas_attr_group = {
.attrs = dcdbas_dev_attrs,
.bin_attrs = dcdbas_bin_attrs,
};
static int dcdbas_probe(struct platform_device *dev)
{
int error;
host_control_action = HC_ACTION_NONE;
host_control_smi_type = HC_SMITYPE_NONE;
dcdbas_pdev = dev;
/* Check if ACPI WSMT table specifies protected SMI buffer address */
error = dcdbas_check_wsmt();
if (error < 0)
return error;
/*
* BIOS SMI calls require buffer addresses be in 32-bit address space.
* This is done by setting the DMA mask below.
*/
error = dma_set_coherent_mask(&dcdbas_pdev->dev, DMA_BIT_MASK(32));
if (error)
return error;
error = sysfs_create_group(&dev->dev.kobj, &dcdbas_attr_group);
if (error)
return error;
register_reboot_notifier(&dcdbas_reboot_nb);
dev_info(&dev->dev, "%s (version %s)\n",
DRIVER_DESCRIPTION, DRIVER_VERSION);
return 0;
}
static void dcdbas_remove(struct platform_device *dev)
{
unregister_reboot_notifier(&dcdbas_reboot_nb);
sysfs_remove_group(&dev->dev.kobj, &dcdbas_attr_group);
}
static struct platform_driver dcdbas_driver = {
.driver = {
.name = DRIVER_NAME,
},
.probe = dcdbas_probe,
.remove = dcdbas_remove,
};
static const struct platform_device_info dcdbas_dev_info __initconst = {
.name = DRIVER_NAME,
.id = PLATFORM_DEVID_NONE,
.dma_mask = DMA_BIT_MASK(32),
};
static struct platform_device *dcdbas_pdev_reg;
/**
* dcdbas_init: initialize driver
*/
static int __init dcdbas_init(void)
{
int error;
error = platform_driver_register(&dcdbas_driver);
if (error)
return error;
dcdbas_pdev_reg = platform_device_register_full(&dcdbas_dev_info);
if (IS_ERR(dcdbas_pdev_reg)) {
error = PTR_ERR(dcdbas_pdev_reg);
goto err_unregister_driver;
}
return 0;
err_unregister_driver:
platform_driver_unregister(&dcdbas_driver);
return error;
}
/**
* dcdbas_exit: perform driver cleanup
*/
static void __exit dcdbas_exit(void)
{
/*
* make sure functions that use dcdbas_pdev are called
* before platform_device_unregister
*/
unregister_reboot_notifier(&dcdbas_reboot_nb);
/*
* We have to free the buffer here instead of dcdbas_remove
* because only in module exit function we can be sure that
* all sysfs attributes belonging to this module have been
* released.
*/
if (dcdbas_pdev)
smi_data_buf_free();
if (bios_buffer)
memunmap(bios_buffer);
platform_device_unregister(dcdbas_pdev_reg);
platform_driver_unregister(&dcdbas_driver);
}
subsys_initcall_sync(dcdbas_init);
module_exit(dcdbas_exit);
MODULE_DESCRIPTION(DRIVER_DESCRIPTION " (version " DRIVER_VERSION ")");
MODULE_VERSION(DRIVER_VERSION);
MODULE_AUTHOR("Dell Inc.");
MODULE_LICENSE("GPL");
/* Any System or BIOS claiming to be by Dell */
MODULE_ALIAS("dmi:*:[bs]vnD[Ee][Ll][Ll]*:*");
@@ -0,0 +1,110 @@
/* SPDX-License-Identifier: GPL-2.0-only */
/*
* dcdbas.h: Definitions for Dell Systems Management Base driver
*
* Copyright (C) 1995-2005 Dell Inc.
*/
#ifndef _DCDBAS_H_
#define _DCDBAS_H_
#include <linux/device.h>
#include <linux/sysfs.h>
#include <linux/types.h>
#define MAX_SMI_DATA_BUF_SIZE (256 * 1024)
#define HC_ACTION_NONE (0)
#define HC_ACTION_HOST_CONTROL_POWEROFF BIT(1)
#define HC_ACTION_HOST_CONTROL_POWERCYCLE BIT(2)
#define HC_SMITYPE_NONE (0)
#define HC_SMITYPE_TYPE1 (1)
#define HC_SMITYPE_TYPE2 (2)
#define HC_SMITYPE_TYPE3 (3)
#define ESM_APM_CMD (0x0A0)
#define ESM_APM_POWER_CYCLE (0x10)
#define ESM_STATUS_CMD_UNSUCCESSFUL (-1)
#define CMOS_BASE_PORT (0x070)
#define CMOS_PAGE1_INDEX_PORT (0)
#define CMOS_PAGE1_DATA_PORT (1)
#define CMOS_PAGE2_INDEX_PORT_PIIX4 (2)
#define CMOS_PAGE2_DATA_PORT_PIIX4 (3)
#define PE1400_APM_CONTROL_PORT (0x0B0)
#define PCAT_APM_CONTROL_PORT (0x0B2)
#define PCAT_APM_STATUS_PORT (0x0B3)
#define PE1300_CMOS_CMD_STRUCT_PTR (0x38)
#define PE1400_CMOS_CMD_STRUCT_PTR (0x70)
#define MAX_SYSMGMT_SHORTCMD_PARMBUF_LEN (14)
#define MAX_SYSMGMT_LONGCMD_SGENTRY_NUM (16)
#define TIMEOUT_USEC_SHORT_SEMA_BLOCKING (10000)
#define EXPIRED_TIMER (0)
#define SMI_CMD_MAGIC (0x534D4931)
#define SMM_EPS_SIG "$SCB"
#define DCDBAS_DEV_ATTR_RW(_name) \
DEVICE_ATTR(_name,0600,_name##_show,_name##_store);
#define DCDBAS_DEV_ATTR_RO(_name) \
DEVICE_ATTR(_name,0400,_name##_show,NULL);
#define DCDBAS_DEV_ATTR_WO(_name) \
DEVICE_ATTR(_name,0200,NULL,_name##_store);
struct smi_cmd {
__u32 magic;
__u32 ebx;
__u32 ecx;
__u16 command_address;
__u8 command_code;
__u8 reserved;
__u8 command_buffer[1];
} __attribute__ ((packed));
struct apm_cmd {
__u8 command;
__s8 status;
__u16 reserved;
union {
struct {
__u8 parm[MAX_SYSMGMT_SHORTCMD_PARMBUF_LEN];
} __attribute__ ((packed)) shortreq;
struct {
__u16 num_sg_entries;
struct {
__u32 size;
__u64 addr;
} __attribute__ ((packed))
sglist[MAX_SYSMGMT_LONGCMD_SGENTRY_NUM];
} __attribute__ ((packed)) longreq;
} __attribute__ ((packed)) parameters;
} __attribute__ ((packed));
int dcdbas_smi_request(struct smi_cmd *smi_cmd);
struct smm_eps_table {
char smm_comm_buff_anchor[4];
u8 length;
u8 checksum;
u8 version;
u64 smm_comm_buff_addr;
u64 num_of_4k_pages;
} __packed;
struct smi_buffer {
u8 *virt;
unsigned long size;
dma_addr_t dma;
};
int dcdbas_smi_alloc(struct smi_buffer *smi_buffer, unsigned long size);
void dcdbas_smi_free(struct smi_buffer *smi_buffer);
#endif /* _DCDBAS_H_ */
@@ -0,0 +1,93 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* dell-dw5826e-reset.c - Dell DW5826e reset driver
*
* Copyright (C) 2026 Jackbb Wu <jackbb.wu@compal.com>
*/
#include <linux/acpi.h>
#include <linux/bits.h>
#include <linux/device.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/types.h>
#include <linux/uuid.h>
#define PALC_DSM_FN_TRIGGER_PLDR 1
static guid_t palc_dsm_guid =
GUID_INIT(0x5a1a4bba, 0x8006, 0x487e, 0xbe, 0x0a, 0xac, 0xf5, 0xd8, 0xfd, 0xfe, 0x59);
static int trigger_palc_pldr(struct device *dev, acpi_handle handle)
{
union acpi_object *obj;
int ret = 0;
obj = acpi_evaluate_dsm(handle, &palc_dsm_guid, 1, PALC_DSM_FN_TRIGGER_PLDR, NULL);
if (!obj) {
dev_err(dev, "Failed to evaluate _DSM\n");
return -EIO;
}
if (obj->type != ACPI_TYPE_BUFFER) {
dev_err(dev, "Unexpected _DSM return type: %d\n", obj->type);
ret = -EINVAL;
}
ACPI_FREE(obj);
return ret;
}
static ssize_t wwan_reset_store(struct device *dev, struct device_attribute *attr,
const char *buf, size_t count)
{
acpi_handle handle = ACPI_HANDLE(dev);
int ret;
ret = trigger_palc_pldr(dev, handle);
if (ret)
return ret;
return count;
}
static DEVICE_ATTR_WO(wwan_reset);
static struct attribute *palc_attrs[] = {
&dev_attr_wwan_reset.attr,
NULL
};
ATTRIBUTE_GROUPS(palc);
static int palc_probe(struct platform_device *pdev)
{
acpi_handle handle;
handle = ACPI_HANDLE(&pdev->dev);
if (!handle)
return -ENODEV;
if (!acpi_check_dsm(handle, &palc_dsm_guid, 1, BIT(PALC_DSM_FN_TRIGGER_PLDR)))
return -ENODEV;
return 0;
}
static const struct acpi_device_id palc_acpi_ids[] = {
{ "PALC0001", 0 },
{ }
};
MODULE_DEVICE_TABLE(acpi, palc_acpi_ids);
static struct platform_driver palc_driver = {
.driver = {
.name = "dell-dw5826e-reset",
.acpi_match_table = palc_acpi_ids,
.dev_groups = palc_groups,
},
.probe = palc_probe,
};
module_platform_driver(palc_driver);
MODULE_DESCRIPTION("Dell DW5826e reset driver");
MODULE_LICENSE("GPL");
MODULE_AUTHOR("JackBB Wu");
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,260 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* lis3lv02d i2c-client instantiation for ACPI SMO88xx devices without I2C resources.
*
* Copyright (C) 2024 Hans de Goede <hansg@kernel.org>
*/
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/device/bus.h>
#include <linux/dmi.h>
#include <linux/i2c.h>
#include <linux/module.h>
#include <linux/notifier.h>
#include <linux/platform_device.h>
#include <linux/workqueue.h>
#include "dell-smo8800-ids.h"
#define LIS3_WHO_AM_I 0x0f
#define DELL_LIS3LV02D_DMI_ENTRY(product_name, i2c_addr) \
{ \
.matches = { \
DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Dell Inc."), \
DMI_EXACT_MATCH(DMI_PRODUCT_NAME, product_name), \
}, \
.driver_data = (void *)(uintptr_t)(i2c_addr), \
}
/*
* Accelerometer's I2C address is not specified in DMI nor ACPI,
* so it is needed to define mapping table based on DMI product names.
*/
static const struct dmi_system_id lis3lv02d_devices[] __initconst = {
/*
* Dell platform team told us that these Latitude devices have
* ST microelectronics accelerometer at I2C address 0x29.
*/
DELL_LIS3LV02D_DMI_ENTRY("Latitude E5250", 0x29),
DELL_LIS3LV02D_DMI_ENTRY("Latitude E5450", 0x29),
DELL_LIS3LV02D_DMI_ENTRY("Latitude E5550", 0x29),
DELL_LIS3LV02D_DMI_ENTRY("Latitude E6440", 0x29),
DELL_LIS3LV02D_DMI_ENTRY("Latitude E6440 ATG", 0x29),
DELL_LIS3LV02D_DMI_ENTRY("Latitude E6540", 0x29),
/*
* Additional individual entries were added after verification.
*/
DELL_LIS3LV02D_DMI_ENTRY("Latitude 5400", 0x29),
DELL_LIS3LV02D_DMI_ENTRY("Latitude 5480", 0x29),
DELL_LIS3LV02D_DMI_ENTRY("Latitude 5500", 0x29),
DELL_LIS3LV02D_DMI_ENTRY("Latitude E6330", 0x29),
DELL_LIS3LV02D_DMI_ENTRY("Latitude E6430", 0x29),
DELL_LIS3LV02D_DMI_ENTRY("Latitude E6530", 0x29),
DELL_LIS3LV02D_DMI_ENTRY("Precision 3540", 0x29),
DELL_LIS3LV02D_DMI_ENTRY("Precision 3551", 0x29),
DELL_LIS3LV02D_DMI_ENTRY("Precision M6800", 0x29),
DELL_LIS3LV02D_DMI_ENTRY("Vostro V131", 0x1d),
DELL_LIS3LV02D_DMI_ENTRY("Vostro 5568", 0x29),
DELL_LIS3LV02D_DMI_ENTRY("XPS 15 7590", 0x29),
DELL_LIS3LV02D_DMI_ENTRY("XPS 15 9550", 0x29),
{ }
};
static u8 i2c_addr;
static struct i2c_client *i2c_dev;
static bool notifier_registered;
static bool probe_i2c_addr;
module_param(probe_i2c_addr, bool, 0444);
MODULE_PARM_DESC(probe_i2c_addr, "Probe the i801 I2C bus for the accelerometer on models where the address is unknown, this may be dangerous.");
static int detect_lis3lv02d(struct i2c_adapter *adap, unsigned short addr)
{
union i2c_smbus_data smbus_data;
int err;
dev_info(&adap->dev, "Probing for lis3lv02d on address 0x%02x\n", addr);
err = i2c_smbus_xfer(adap, addr, 0, I2C_SMBUS_READ, LIS3_WHO_AM_I,
I2C_SMBUS_BYTE_DATA, &smbus_data);
if (err < 0)
return 0; /* Not found */
/* valid who-am-i values are from drivers/misc/lis3lv02d/lis3lv02d.c */
switch (smbus_data.byte) {
case 0x32:
case 0x33:
case 0x3a:
case 0x3b:
break;
default:
dev_warn(&adap->dev, "Unknown who-am-i register value 0x%02x\n",
smbus_data.byte);
return 0; /* Not found */
}
dev_info(&adap->dev,
"Detected lis3lv02d on address 0x%02x, please report this upstream to platform-driver-x86@vger.kernel.org so that a quirk can be added\n",
addr);
return 1; /* Found */
}
static bool i2c_adapter_is_main_i801(struct i2c_adapter *adap)
{
/*
* Only match the main I801 adapter and reject secondary adapters
* which names start with "SMBus I801 IDF adapter".
*/
return strstarts(adap->name, "SMBus I801 adapter");
}
static int find_i801(struct device *dev, void *data)
{
struct i2c_adapter *adap, **adap_ret = data;
adap = i2c_verify_adapter(dev);
if (!adap)
return 0;
if (!i2c_adapter_is_main_i801(adap))
return 0;
*adap_ret = i2c_get_adapter(adap->nr);
return 1;
}
static void instantiate_i2c_client(struct work_struct *work)
{
struct i2c_board_info info = { };
struct i2c_adapter *adap = NULL;
if (i2c_dev)
return;
/*
* bus_for_each_dev() and not i2c_for_each_dev() to avoid
* a deadlock when find_i801() calls i2c_get_adapter().
*/
bus_for_each_dev(&i2c_bus_type, NULL, &adap, find_i801);
if (!adap)
return;
strscpy(info.type, "lis3lv02d", I2C_NAME_SIZE);
if (i2c_addr) {
info.addr = i2c_addr;
i2c_dev = i2c_new_client_device(adap, &info);
} else {
/* First try address 0x29 (most used) and then try 0x1d */
static const unsigned short addr_list[] = { 0x29, 0x1d, I2C_CLIENT_END };
i2c_dev = i2c_new_scanned_device(adap, &info, addr_list, detect_lis3lv02d);
}
if (IS_ERR(i2c_dev)) {
dev_err(&adap->dev, "error %ld registering i2c_client\n", PTR_ERR(i2c_dev));
i2c_dev = NULL;
} else {
dev_dbg(&adap->dev, "registered lis3lv02d on address 0x%02x\n", info.addr);
}
i2c_put_adapter(adap);
}
static DECLARE_WORK(i2c_work, instantiate_i2c_client);
static int i2c_bus_notify(struct notifier_block *nb, unsigned long action, void *data)
{
struct device *dev = data;
struct i2c_client *client;
struct i2c_adapter *adap;
switch (action) {
case BUS_NOTIFY_ADD_DEVICE:
adap = i2c_verify_adapter(dev);
if (!adap)
break;
if (i2c_adapter_is_main_i801(adap))
queue_work(system_long_wq, &i2c_work);
break;
case BUS_NOTIFY_REMOVED_DEVICE:
client = i2c_verify_client(dev);
if (!client)
break;
if (i2c_dev == client) {
dev_dbg(&client->adapter->dev, "lis3lv02d i2c_client removed\n");
i2c_dev = NULL;
}
break;
default:
break;
}
return 0;
}
static struct notifier_block i2c_nb = { .notifier_call = i2c_bus_notify };
static int __init match_acpi_device_ids(struct device *dev, const void *data)
{
return acpi_match_device(data, dev) ? 1 : 0;
}
static int __init dell_lis3lv02d_init(void)
{
const struct dmi_system_id *lis3lv02d_dmi_id;
struct device *dev;
int err;
/*
* First check for a matching platform_device. This protects against
* SMO88xx ACPI fwnodes which actually do have an I2C resource, which
* will already have an i2c_client instantiated (not a platform_device).
*/
dev = bus_find_device(&platform_bus_type, NULL, smo8800_ids, match_acpi_device_ids);
if (!dev) {
pr_debug("No SMO88xx platform-device found\n");
return 0;
}
put_device(dev);
lis3lv02d_dmi_id = dmi_first_match(lis3lv02d_devices);
if (!lis3lv02d_dmi_id && !probe_i2c_addr) {
pr_warn("accelerometer is present on SMBus but its address is unknown, skipping registration\n");
pr_info("Pass dell_lis3lv02d.probe_i2c_addr=1 on the kernel command line to probe, this may be dangerous!\n");
return 0;
}
if (lis3lv02d_dmi_id)
i2c_addr = (long)lis3lv02d_dmi_id->driver_data;
/*
* Register i2c-bus notifier + queue initial scan for lis3lv02d
* i2c_client instantiation.
*/
err = bus_register_notifier(&i2c_bus_type, &i2c_nb);
if (err)
return err;
notifier_registered = true;
queue_work(system_long_wq, &i2c_work);
return 0;
}
module_init(dell_lis3lv02d_init);
static void __exit dell_lis3lv02d_module_exit(void)
{
if (!notifier_registered)
return;
bus_unregister_notifier(&i2c_bus_type, &i2c_nb);
cancel_work_sync(&i2c_work);
i2c_unregister_device(i2c_dev);
}
module_exit(dell_lis3lv02d_module_exit);
MODULE_DESCRIPTION("lis3lv02d i2c-client instantiation for ACPI SMO88xx devices");
MODULE_AUTHOR("Hans de Goede <hansg@kernel.org>");
MODULE_LICENSE("GPL");
@@ -0,0 +1,302 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Driver for Dell laptop extras
*
* Copyright (c) Lyndon Sanche <lsanche@lyndeno.ca>
*
* Based on documentation in the libsmbios package:
* Copyright (C) 2005-2014 Dell Inc.
*/
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/bitfield.h>
#include <linux/bitops.h>
#include <linux/bits.h>
#include <linux/device/faux.h>
#include <linux/dmi.h>
#include <linux/err.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/platform_profile.h>
#include <linux/slab.h>
#include "dell-smbios.h"
static struct faux_device *dell_pc_fdev;
static int supported_modes;
static const struct dmi_system_id dell_device_table[] __initconst = {
{
.ident = "Dell Inc.",
.matches = {
DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."),
},
},
{
.ident = "Dell Computer Corporation",
.matches = {
DMI_MATCH(DMI_SYS_VENDOR, "Dell Computer Corporation"),
},
},
{ }
};
MODULE_DEVICE_TABLE(dmi, dell_device_table);
/* Derived from smbios-thermal-ctl
*
* cbClass 17
* cbSelect 19
* User Selectable Thermal Tables(USTT)
* cbArg1 determines the function to be performed
* cbArg1 0x0 = Get Thermal Information
* cbRES1 Standard return codes (0, -1, -2)
* cbRES2, byte 0 Bitmap of supported thermal modes. A mode is supported if
* its bit is set to 1
* Bit 0 Balanced
* Bit 1 Cool Bottom
* Bit 2 Quiet
* Bit 3 Performance
* cbRES2, byte 1 Bitmap of supported Active Acoustic Controller (AAC) modes.
* Each mode corresponds to the supported thermal modes in
* byte 0. A mode is supported if its bit is set to 1.
* Bit 0 AAC (Balanced)
* Bit 1 AAC (Cool Bottom
* Bit 2 AAC (Quiet)
* Bit 3 AAC (Performance)
* cbRes3, byte 0 Current Thermal Mode
* Bit 0 Balanced
* Bit 1 Cool Bottom
* Bit 2 Quiet
* Bit 3 Performanc
* cbRes3, byte 1 AAC Configuration type
* 0 Global (AAC enable/disable applies to all supported USTT modes)
* 1 USTT mode specific
* cbRes3, byte 2 Current Active Acoustic Controller (AAC) Mode
* If AAC Configuration Type is Global,
* 0 AAC mode disabled
* 1 AAC mode enabled
* If AAC Configuration Type is USTT mode specific (multiple bits may be set),
* Bit 0 AAC (Balanced)
* Bit 1 AAC (Cool Bottom
* Bit 2 AAC (Quiet)
* Bit 3 AAC (Performance)
* cbRes3, byte 3 Current Fan Failure Mode
* Bit 0 Minimal Fan Failure (at least one fan has failed, one fan working)
* Bit 1 Catastrophic Fan Failure (all fans have failed)
*
* cbArg1 0x1 (Set Thermal Information), both desired thermal mode and
* desired AAC mode shall be applied
* cbArg2, byte 0 Desired Thermal Mode to set
* (only one bit may be set for this parameter)
* Bit 0 Balanced
* Bit 1 Cool Bottom
* Bit 2 Quiet
* Bit 3 Performance
* cbArg2, byte 1 Desired Active Acoustic Controller (AAC) Mode to set
* If AAC Configuration Type is Global,
* 0 AAC mode disabled
* 1 AAC mode enabled
* If AAC Configuration Type is USTT mode specific
* (multiple bits may be set for this parameter),
* Bit 0 AAC (Balanced)
* Bit 1 AAC (Cool Bottom
* Bit 2 AAC (Quiet)
* Bit 3 AAC (Performance)
*/
#define DELL_ACC_GET_FIELD GENMASK(19, 16)
#define DELL_ACC_SET_FIELD GENMASK(11, 8)
#define DELL_THERMAL_SUPPORTED GENMASK(3, 0)
enum thermal_mode_bits {
DELL_BALANCED = BIT(0),
DELL_COOL_BOTTOM = BIT(1),
DELL_QUIET = BIT(2),
DELL_PERFORMANCE = BIT(3),
};
static int thermal_get_mode(void)
{
struct calling_interface_buffer buffer;
int state;
int ret;
dell_fill_request(&buffer, 0x0, 0, 0, 0);
ret = dell_send_request(&buffer, CLASS_INFO, SELECT_THERMAL_MANAGEMENT);
if (ret)
return ret;
state = buffer.output[2];
if (state & DELL_BALANCED)
return DELL_BALANCED;
else if (state & DELL_COOL_BOTTOM)
return DELL_COOL_BOTTOM;
else if (state & DELL_QUIET)
return DELL_QUIET;
else if (state & DELL_PERFORMANCE)
return DELL_PERFORMANCE;
else
return -ENXIO;
}
static int thermal_get_supported_modes(int *supported_bits)
{
struct calling_interface_buffer buffer;
int ret;
dell_fill_request(&buffer, 0x0, 0, 0, 0);
ret = dell_send_request(&buffer, CLASS_INFO, SELECT_THERMAL_MANAGEMENT);
if (ret)
return ret;
*supported_bits = FIELD_GET(DELL_THERMAL_SUPPORTED, buffer.output[1]);
return 0;
}
static int thermal_get_acc_mode(int *acc_mode)
{
struct calling_interface_buffer buffer;
int ret;
dell_fill_request(&buffer, 0x0, 0, 0, 0);
ret = dell_send_request(&buffer, CLASS_INFO, SELECT_THERMAL_MANAGEMENT);
if (ret)
return ret;
*acc_mode = FIELD_GET(DELL_ACC_GET_FIELD, buffer.output[3]);
return 0;
}
static int thermal_set_mode(enum thermal_mode_bits state)
{
struct calling_interface_buffer buffer;
int ret;
int acc_mode;
ret = thermal_get_acc_mode(&acc_mode);
if (ret)
return ret;
dell_fill_request(&buffer, 0x1, FIELD_PREP(DELL_ACC_SET_FIELD, acc_mode) | state, 0, 0);
return dell_send_request(&buffer, CLASS_INFO, SELECT_THERMAL_MANAGEMENT);
}
static int thermal_platform_profile_set(struct device *dev,
enum platform_profile_option profile)
{
switch (profile) {
case PLATFORM_PROFILE_BALANCED:
return thermal_set_mode(DELL_BALANCED);
case PLATFORM_PROFILE_PERFORMANCE:
return thermal_set_mode(DELL_PERFORMANCE);
case PLATFORM_PROFILE_QUIET:
return thermal_set_mode(DELL_QUIET);
case PLATFORM_PROFILE_COOL:
return thermal_set_mode(DELL_COOL_BOTTOM);
default:
return -EOPNOTSUPP;
}
}
static int thermal_platform_profile_get(struct device *dev,
enum platform_profile_option *profile)
{
int ret;
ret = thermal_get_mode();
if (ret < 0)
return ret;
switch (ret) {
case DELL_BALANCED:
*profile = PLATFORM_PROFILE_BALANCED;
break;
case DELL_PERFORMANCE:
*profile = PLATFORM_PROFILE_PERFORMANCE;
break;
case DELL_COOL_BOTTOM:
*profile = PLATFORM_PROFILE_COOL;
break;
case DELL_QUIET:
*profile = PLATFORM_PROFILE_QUIET;
break;
default:
return -EINVAL;
}
return 0;
}
static int thermal_platform_profile_probe(void *drvdata, unsigned long *choices)
{
int current_mode;
if (supported_modes & DELL_QUIET)
__set_bit(PLATFORM_PROFILE_QUIET, choices);
if (supported_modes & DELL_COOL_BOTTOM)
__set_bit(PLATFORM_PROFILE_COOL, choices);
if (supported_modes & DELL_BALANCED)
__set_bit(PLATFORM_PROFILE_BALANCED, choices);
if (supported_modes & DELL_PERFORMANCE)
__set_bit(PLATFORM_PROFILE_PERFORMANCE, choices);
/* Make sure that ACPI is in sync with the profile set by USTT */
current_mode = thermal_get_mode();
if (current_mode < 0)
return current_mode;
thermal_set_mode(current_mode);
return 0;
}
static const struct platform_profile_ops dell_pc_platform_profile_ops = {
.probe = thermal_platform_profile_probe,
.profile_get = thermal_platform_profile_get,
.profile_set = thermal_platform_profile_set,
};
static int dell_pc_faux_probe(struct faux_device *fdev)
{
struct device *ppdev;
int ret;
if (!dell_smbios_class_is_supported(CLASS_INFO))
return -ENODEV;
ret = thermal_get_supported_modes(&supported_modes);
if (ret < 0)
return ret;
ppdev = devm_platform_profile_register(&fdev->dev, "dell-pc", NULL,
&dell_pc_platform_profile_ops);
return PTR_ERR_OR_ZERO(ppdev);
}
static const struct faux_device_ops dell_pc_faux_ops = {
.probe = dell_pc_faux_probe,
};
static int __init dell_init(void)
{
if (!dmi_check_system(dell_device_table))
return -ENODEV;
dell_pc_fdev = faux_device_create("dell-pc", NULL, &dell_pc_faux_ops);
if (!dell_pc_fdev)
return -ENODEV;
return 0;
}
static void __exit dell_exit(void)
{
faux_device_destroy(dell_pc_fdev);
}
module_init(dell_init);
module_exit(dell_exit);
MODULE_AUTHOR("Lyndon Sanche <lsanche@lyndeno.ca>");
MODULE_DESCRIPTION("Dell PC driver");
MODULE_LICENSE("GPL");
@@ -0,0 +1,513 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
Dell Airplane Mode Switch driver
Copyright (C) 2014-2015 Pali Rohár <pali@kernel.org>
*/
#include <linux/module.h>
#include <linux/acpi.h>
#include <linux/rfkill.h>
#include <linux/input.h>
#include <linux/platform_device.h>
#include "dell-rbtn.h"
enum rbtn_type {
RBTN_UNKNOWN,
RBTN_TOGGLE,
RBTN_SLIDER,
};
struct rbtn_data {
enum rbtn_type type;
struct rfkill *rfkill;
struct input_dev *input_dev;
bool suspended;
};
/*
* acpi functions
*/
static enum rbtn_type rbtn_check(struct acpi_device *device)
{
unsigned long long output;
acpi_status status;
status = acpi_evaluate_integer(device->handle, "CRBT", NULL, &output);
if (ACPI_FAILURE(status))
return RBTN_UNKNOWN;
switch (output) {
case 0:
case 1:
return RBTN_TOGGLE;
case 2:
case 3:
return RBTN_SLIDER;
default:
return RBTN_UNKNOWN;
}
}
static int rbtn_get(struct acpi_device *device)
{
unsigned long long output;
acpi_status status;
status = acpi_evaluate_integer(device->handle, "GRBT", NULL, &output);
if (ACPI_FAILURE(status))
return -EINVAL;
return !output;
}
static int rbtn_acquire(struct acpi_device *device, bool enable)
{
struct acpi_object_list input;
union acpi_object param;
acpi_status status;
param.type = ACPI_TYPE_INTEGER;
param.integer.value = enable;
input.count = 1;
input.pointer = &param;
status = acpi_evaluate_object(device->handle, "ARBT", &input, NULL);
if (ACPI_FAILURE(status))
return -EINVAL;
return 0;
}
/*
* rfkill device
*/
static void rbtn_rfkill_query(struct rfkill *rfkill, void *data)
{
struct acpi_device *device = data;
int state;
state = rbtn_get(device);
if (state < 0)
return;
rfkill_set_states(rfkill, state, state);
}
static int rbtn_rfkill_set_block(void *data, bool blocked)
{
/* NOTE: setting soft rfkill state is not supported */
return -EINVAL;
}
static const struct rfkill_ops rbtn_ops = {
.query = rbtn_rfkill_query,
.set_block = rbtn_rfkill_set_block,
};
static int rbtn_rfkill_init(struct device *dev)
{
struct rbtn_data *rbtn_data = dev_get_drvdata(dev);
int ret;
if (rbtn_data->rfkill)
return 0;
/*
* NOTE: rbtn controls all radio devices, not only WLAN
* but rfkill interface does not support "ANY" type
* so "WLAN" type is used
*/
rbtn_data->rfkill = rfkill_alloc("dell-rbtn", dev, RFKILL_TYPE_WLAN,
&rbtn_ops, ACPI_COMPANION(dev));
if (!rbtn_data->rfkill)
return -ENOMEM;
ret = rfkill_register(rbtn_data->rfkill);
if (ret) {
rfkill_destroy(rbtn_data->rfkill);
rbtn_data->rfkill = NULL;
return ret;
}
return 0;
}
static void rbtn_rfkill_exit(struct device *dev)
{
struct rbtn_data *rbtn_data = dev_get_drvdata(dev);
if (!rbtn_data->rfkill)
return;
rfkill_unregister(rbtn_data->rfkill);
rfkill_destroy(rbtn_data->rfkill);
rbtn_data->rfkill = NULL;
}
static void rbtn_rfkill_event(struct device *dev)
{
struct rbtn_data *rbtn_data = dev_get_drvdata(dev);
if (rbtn_data->rfkill)
rbtn_rfkill_query(rbtn_data->rfkill, ACPI_COMPANION(dev));
}
/*
* input device
*/
static int rbtn_input_init(struct rbtn_data *rbtn_data)
{
int ret;
rbtn_data->input_dev = input_allocate_device();
if (!rbtn_data->input_dev)
return -ENOMEM;
rbtn_data->input_dev->name = "DELL Wireless hotkeys";
rbtn_data->input_dev->phys = "dellabce/input0";
rbtn_data->input_dev->id.bustype = BUS_HOST;
rbtn_data->input_dev->evbit[0] = BIT(EV_KEY);
set_bit(KEY_RFKILL, rbtn_data->input_dev->keybit);
ret = input_register_device(rbtn_data->input_dev);
if (ret) {
input_free_device(rbtn_data->input_dev);
rbtn_data->input_dev = NULL;
return ret;
}
return 0;
}
static void rbtn_input_exit(struct rbtn_data *rbtn_data)
{
input_unregister_device(rbtn_data->input_dev);
rbtn_data->input_dev = NULL;
}
static void rbtn_input_event(struct rbtn_data *rbtn_data)
{
input_report_key(rbtn_data->input_dev, KEY_RFKILL, 1);
input_sync(rbtn_data->input_dev);
input_report_key(rbtn_data->input_dev, KEY_RFKILL, 0);
input_sync(rbtn_data->input_dev);
}
/*
* acpi driver
*/
static int rbtn_probe(struct platform_device *pdev);
static void rbtn_remove(struct platform_device *pdev);
static void rbtn_notify(acpi_handle handle, u32 event, void *data);
static const struct acpi_device_id rbtn_ids[] = {
{ "DELRBTN", 0 },
{ "DELLABCE", 0 },
/*
* This driver can also handle the "DELLABC6" device that
* appears on the XPS 13 9350, but that device is disabled by
* the DSDT unless booted with acpi_osi="!Windows 2012"
* acpi_osi="!Windows 2013".
*
* According to Mario at Dell:
*
* DELLABC6 is a custom interface that was created solely to
* have airplane mode support for Windows 7. For Windows 10
* the proper interface is to use that which is handled by
* intel-hid. A OEM airplane mode driver is not used.
*
* Since the kernel doesn't identify as Windows 7 it would be
* incorrect to do attempt to use that interface.
*
* Even if we override _OSI and bind to DELLABC6, we end up with
* inconsistent behavior in which userspace can get out of sync
* with the rfkill state as it conflicts with events from
* intel-hid.
*
* The upshot is that it is better to just ignore DELLABC6
* devices.
*/
{ "", 0 },
};
#ifdef CONFIG_PM_SLEEP
static void ACPI_SYSTEM_XFACE rbtn_clear_suspended_flag(void *context)
{
struct rbtn_data *rbtn_data = context;
rbtn_data->suspended = false;
}
static int rbtn_suspend(struct device *dev)
{
struct rbtn_data *rbtn_data = dev_get_drvdata(dev);
rbtn_data->suspended = true;
return 0;
}
static int rbtn_resume(struct device *dev)
{
struct rbtn_data *rbtn_data = dev_get_drvdata(dev);
acpi_status status;
/*
* Upon resume, some BIOSes send an ACPI notification thet triggers
* an unwanted input event. In order to ignore it, we use a flag
* that we set at suspend and clear once we have received the extra
* ACPI notification. Since ACPI notifications are delivered
* asynchronously to drivers, we clear the flag from the workqueue
* used to deliver the notifications. This should be enough
* to have the flag cleared only after we received the extra
* notification, if any.
*/
status = acpi_os_execute(OSL_NOTIFY_HANDLER,
rbtn_clear_suspended_flag, rbtn_data);
if (ACPI_FAILURE(status))
rbtn_clear_suspended_flag(rbtn_data);
return 0;
}
#endif
static SIMPLE_DEV_PM_OPS(rbtn_pm_ops, rbtn_suspend, rbtn_resume);
static struct platform_driver rbtn_driver = {
.probe = rbtn_probe,
.remove = rbtn_remove,
.driver = {
.name = "dell-rbtn",
.acpi_match_table = rbtn_ids,
.pm = &rbtn_pm_ops,
},
};
/*
* notifier export functions
*/
static bool auto_remove_rfkill = true;
static ATOMIC_NOTIFIER_HEAD(rbtn_chain_head);
static int rbtn_inc_count(struct device *dev, void *data)
{
struct rbtn_data *rbtn_data = dev_get_drvdata(dev);
int *count = data;
if (rbtn_data->type == RBTN_SLIDER)
(*count)++;
return 0;
}
static int rbtn_switch_dev(struct device *dev, void *data)
{
struct rbtn_data *rbtn_data = dev_get_drvdata(dev);
bool enable = data;
if (rbtn_data->type != RBTN_SLIDER)
return 0;
if (enable)
rbtn_rfkill_init(dev);
else
rbtn_rfkill_exit(dev);
return 0;
}
int dell_rbtn_notifier_register(struct notifier_block *nb)
{
bool first;
int count;
int ret;
count = 0;
ret = driver_for_each_device(&rbtn_driver.driver, NULL, &count,
rbtn_inc_count);
if (ret || count == 0)
return -ENODEV;
first = !rbtn_chain_head.head;
ret = atomic_notifier_chain_register(&rbtn_chain_head, nb);
if (ret != 0)
return ret;
if (auto_remove_rfkill && first)
ret = driver_for_each_device(&rbtn_driver.driver, NULL,
(void *)false, rbtn_switch_dev);
return ret;
}
EXPORT_SYMBOL_GPL(dell_rbtn_notifier_register);
int dell_rbtn_notifier_unregister(struct notifier_block *nb)
{
int ret;
ret = atomic_notifier_chain_unregister(&rbtn_chain_head, nb);
if (ret != 0)
return ret;
if (auto_remove_rfkill && !rbtn_chain_head.head)
ret = driver_for_each_device(&rbtn_driver.driver, NULL,
(void *)true, rbtn_switch_dev);
return ret;
}
EXPORT_SYMBOL_GPL(dell_rbtn_notifier_unregister);
/*
* acpi driver functions
*/
static void rbtn_cleanup(struct device *dev)
{
struct rbtn_data *rbtn_data = dev_get_drvdata(dev);
switch (rbtn_data->type) {
case RBTN_TOGGLE:
rbtn_input_exit(rbtn_data);
break;
case RBTN_SLIDER:
rbtn_rfkill_exit(dev);
break;
default:
break;
}
}
static int rbtn_probe(struct platform_device *pdev)
{
struct rbtn_data *rbtn_data;
struct acpi_device *device;
enum rbtn_type type;
int ret = 0;
device = ACPI_COMPANION(&pdev->dev);
if (!device)
return -ENODEV;
type = rbtn_check(device);
if (type == RBTN_UNKNOWN) {
dev_info(&pdev->dev, "Unknown device type\n");
return -EINVAL;
}
rbtn_data = devm_kzalloc(&pdev->dev, sizeof(*rbtn_data), GFP_KERNEL);
if (!rbtn_data)
return -ENOMEM;
ret = rbtn_acquire(device, true);
if (ret < 0) {
dev_err(&pdev->dev, "Cannot enable device\n");
return ret;
}
platform_set_drvdata(pdev, rbtn_data);
rbtn_data->type = type;
switch (rbtn_data->type) {
case RBTN_TOGGLE:
ret = rbtn_input_init(rbtn_data);
break;
case RBTN_SLIDER:
if (auto_remove_rfkill && rbtn_chain_head.head)
ret = 0;
else
ret = rbtn_rfkill_init(&pdev->dev);
break;
default:
ret = -EINVAL;
break;
}
if (ret)
goto err;
ret = acpi_dev_install_notify_handler(device, ACPI_DEVICE_NOTIFY,
rbtn_notify, &pdev->dev);
if (ret)
goto err_cleanup;
return 0;
err_cleanup:
rbtn_cleanup(&pdev->dev);
err:
rbtn_acquire(device, false);
return ret;
}
static void rbtn_remove(struct platform_device *pdev)
{
struct acpi_device *device = ACPI_COMPANION(&pdev->dev);
acpi_dev_remove_notify_handler(device, ACPI_DEVICE_NOTIFY, rbtn_notify);
rbtn_cleanup(&pdev->dev);
rbtn_acquire(device, false);
}
static void rbtn_notify(acpi_handle handle, u32 event, void *data)
{
struct device *dev = data;
struct rbtn_data *rbtn_data = dev_get_drvdata(dev);
/*
* Some BIOSes send a notification at resume.
* Ignore it to prevent unwanted input events.
*/
if (rbtn_data->suspended) {
dev_dbg(dev, "ACPI notification ignored\n");
return;
}
if (event != 0x80) {
dev_info(dev, "Received unknown event (0x%x)\n",
event);
return;
}
switch (rbtn_data->type) {
case RBTN_TOGGLE:
rbtn_input_event(rbtn_data);
break;
case RBTN_SLIDER:
rbtn_rfkill_event(dev);
atomic_notifier_call_chain(&rbtn_chain_head, event, NULL);
break;
default:
break;
}
}
module_platform_driver(rbtn_driver);
module_param(auto_remove_rfkill, bool, 0444);
MODULE_PARM_DESC(auto_remove_rfkill, "Automatically remove rfkill devices when "
"other modules start receiving events "
"from this module and re-add them when "
"the last module stops receiving events "
"(default true)");
MODULE_DEVICE_TABLE(acpi, rbtn_ids);
MODULE_DESCRIPTION("Dell Airplane Mode Switch driver");
MODULE_AUTHOR("Pali Rohár <pali@kernel.org>");
MODULE_LICENSE("GPL");
@@ -0,0 +1,16 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
Dell Airplane Mode Switch driver
Copyright (C) 2014-2015 Pali Rohár <pali@kernel.org>
*/
#ifndef _DELL_RBTN_H_
#define _DELL_RBTN_H_
struct notifier_block;
int dell_rbtn_notifier_register(struct notifier_block *nb);
int dell_rbtn_notifier_unregister(struct notifier_block *nb);
#endif
@@ -0,0 +1,667 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Common functions for kernel modules using Dell SMBIOS
*
* Copyright (c) Red Hat <mjg@redhat.com>
* Copyright (c) 2014 Gabriele Mazzotta <gabriele.mzt@gmail.com>
* Copyright (c) 2014 Pali Rohár <pali@kernel.org>
*
* Based on documentation in the libsmbios package:
* Copyright (C) 2005-2014 Dell Inc.
*/
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/container_of.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/capability.h>
#include <linux/dmi.h>
#include <linux/err.h>
#include <linux/mutex.h>
#include <linux/platform_device.h>
#include <linux/slab.h>
#include "dell-smbios.h"
static u32 da_supported_commands;
static int da_num_tokens;
static struct platform_device *platform_device;
static struct calling_interface_token *da_tokens;
static struct token_sysfs_data *token_entries;
static struct attribute **token_attrs;
static DEFINE_MUTEX(smbios_mutex);
struct token_sysfs_data {
struct device_attribute location_attr;
struct device_attribute value_attr;
struct calling_interface_token *token;
};
struct smbios_device {
struct list_head list;
struct device *device;
int priority;
int (*call_fn)(struct calling_interface_buffer *arg);
};
struct smbios_call {
u32 need_capability;
int cmd_class;
int cmd_select;
};
/* calls that are whitelisted for given capabilities */
static struct smbios_call call_whitelist[] = {
/* generally tokens are allowed, but may be further filtered or
* restricted by token blacklist or whitelist
*/
{CAP_SYS_ADMIN, CLASS_TOKEN_READ, SELECT_TOKEN_STD},
{CAP_SYS_ADMIN, CLASS_TOKEN_READ, SELECT_TOKEN_AC},
{CAP_SYS_ADMIN, CLASS_TOKEN_READ, SELECT_TOKEN_BAT},
{CAP_SYS_ADMIN, CLASS_TOKEN_WRITE, SELECT_TOKEN_STD},
{CAP_SYS_ADMIN, CLASS_TOKEN_WRITE, SELECT_TOKEN_AC},
{CAP_SYS_ADMIN, CLASS_TOKEN_WRITE, SELECT_TOKEN_BAT},
/* used by userspace: fwupdate */
{CAP_SYS_ADMIN, CLASS_ADMIN_PROP, SELECT_ADMIN_PROP},
/* used by userspace: fwupd */
{CAP_SYS_ADMIN, CLASS_INFO, SELECT_DOCK},
{CAP_SYS_ADMIN, CLASS_FLASH_INTERFACE, SELECT_FLASH_INTERFACE},
};
/* calls that are explicitly blacklisted */
static struct smbios_call call_blacklist[] = {
{0x0000, 1, 7}, /* manufacturing use */
{0x0000, 6, 5}, /* manufacturing use */
{0x0000, 11, 3}, /* write once */
{0x0000, 11, 7}, /* write once */
{0x0000, 11, 11}, /* write once */
{0x0000, 19, -1}, /* diagnostics */
/* handled by kernel: dell-laptop */
{0x0000, CLASS_INFO, SELECT_RFKILL},
{0x0000, CLASS_KBD_BACKLIGHT, SELECT_KBD_BACKLIGHT},
{0x0000, CLASS_INFO, SELECT_THERMAL_MANAGEMENT},
};
struct token_range {
u32 need_capability;
u16 min;
u16 max;
};
/* tokens that are whitelisted for given capabilities */
static struct token_range token_whitelist[] = {
/* used by userspace: fwupdate */
{CAP_SYS_ADMIN, CAPSULE_EN_TOKEN, CAPSULE_DIS_TOKEN},
/* can indicate to userspace that WMI is needed */
{0x0000, WSMT_EN_TOKEN, WSMT_DIS_TOKEN}
};
/* tokens that are explicitly blacklisted */
static struct token_range token_blacklist[] = {
{0x0000, 0x0058, 0x0059}, /* ME use */
{0x0000, 0x00CD, 0x00D0}, /* raid shadow copy */
{0x0000, 0x013A, 0x01FF}, /* sata shadow copy */
{0x0000, 0x0175, 0x0176}, /* write once */
{0x0000, 0x0195, 0x0197}, /* diagnostics */
{0x0000, 0x01DC, 0x01DD}, /* manufacturing use */
{0x0000, 0x027D, 0x0284}, /* diagnostics */
{0x0000, 0x02E3, 0x02E3}, /* manufacturing use */
{0x0000, 0x02FF, 0x02FF}, /* manufacturing use */
{0x0000, 0x0300, 0x0302}, /* manufacturing use */
{0x0000, 0x0325, 0x0326}, /* manufacturing use */
{0x0000, 0x0332, 0x0335}, /* fan control */
{0x0000, 0x0350, 0x0350}, /* manufacturing use */
{0x0000, 0x0363, 0x0363}, /* manufacturing use */
{0x0000, 0x0368, 0x0368}, /* manufacturing use */
{0x0000, 0x03F6, 0x03F7}, /* manufacturing use */
{0x0000, 0x049E, 0x049F}, /* manufacturing use */
{0x0000, 0x04A0, 0x04A3}, /* disagnostics */
{0x0000, 0x04E6, 0x04E7}, /* manufacturing use */
{0x0000, 0x4000, 0x7FFF}, /* internal BIOS use */
{0x0000, 0x9000, 0x9001}, /* internal BIOS use */
{0x0000, 0xA000, 0xBFFF}, /* write only */
{0x0000, 0xEFF0, 0xEFFF}, /* internal BIOS use */
/* handled by kernel: dell-laptop */
{0x0000, BRIGHTNESS_TOKEN, BRIGHTNESS_TOKEN},
{0x0000, KBD_LED_OFF_TOKEN, KBD_LED_AUTO_TOKEN},
{0x0000, KBD_LED_AC_TOKEN, KBD_LED_AC_TOKEN},
{0x0000, KBD_LED_AUTO_25_TOKEN, KBD_LED_AUTO_75_TOKEN},
{0x0000, KBD_LED_AUTO_100_TOKEN, KBD_LED_AUTO_100_TOKEN},
{0x0000, GLOBAL_MIC_MUTE_ENABLE, GLOBAL_MIC_MUTE_DISABLE},
};
static LIST_HEAD(smbios_device_list);
int dell_smbios_error(int value)
{
switch (value) {
case 0: /* Completed successfully */
return 0;
case -1: /* Completed with error */
return -EIO;
case -2: /* Function not supported */
return -ENXIO;
default: /* Unknown error */
return -EINVAL;
}
}
EXPORT_SYMBOL_GPL(dell_smbios_error);
int dell_smbios_register_device(struct device *d, int priority, void *call_fn)
{
struct smbios_device *priv;
priv = devm_kzalloc(d, sizeof(struct smbios_device), GFP_KERNEL);
if (!priv)
return -ENOMEM;
get_device(d);
priv->device = d;
priv->priority = priority;
priv->call_fn = call_fn;
mutex_lock(&smbios_mutex);
list_add_tail(&priv->list, &smbios_device_list);
mutex_unlock(&smbios_mutex);
dev_dbg(d, "Added device: %s\n", d->driver->name);
return 0;
}
EXPORT_SYMBOL_GPL(dell_smbios_register_device);
void dell_smbios_unregister_device(struct device *d)
{
struct smbios_device *priv;
mutex_lock(&smbios_mutex);
list_for_each_entry(priv, &smbios_device_list, list) {
if (priv->device == d) {
list_del(&priv->list);
put_device(d);
break;
}
}
mutex_unlock(&smbios_mutex);
dev_dbg(d, "Remove device: %s\n", d->driver->name);
}
EXPORT_SYMBOL_GPL(dell_smbios_unregister_device);
int dell_smbios_call_filter(struct device *d,
struct calling_interface_buffer *buffer)
{
u16 t = 0;
int i;
/* can't make calls over 30 */
if (buffer->cmd_class > 30) {
dev_dbg(d, "class too big: %u\n", buffer->cmd_class);
return -EINVAL;
}
/* supported calls on the particular system */
if (!(da_supported_commands & (1 << buffer->cmd_class))) {
dev_dbg(d, "invalid command, supported commands: 0x%8x\n",
da_supported_commands);
return -EINVAL;
}
/* match against call blacklist */
for (i = 0; i < ARRAY_SIZE(call_blacklist); i++) {
if (buffer->cmd_class != call_blacklist[i].cmd_class)
continue;
if (buffer->cmd_select != call_blacklist[i].cmd_select &&
call_blacklist[i].cmd_select != -1)
continue;
dev_dbg(d, "blacklisted command: %u/%u\n",
buffer->cmd_class, buffer->cmd_select);
return -EINVAL;
}
/* if a token call, find token ID */
if ((buffer->cmd_class == CLASS_TOKEN_READ ||
buffer->cmd_class == CLASS_TOKEN_WRITE) &&
buffer->cmd_select < 3) {
/* tokens enabled ? */
if (!da_tokens) {
dev_dbg(d, "no token support on this system\n");
return -EINVAL;
}
/* find the matching token ID */
for (i = 0; i < da_num_tokens; i++) {
if (da_tokens[i].location != buffer->input[0])
continue;
t = da_tokens[i].tokenID;
break;
}
/* token call; but token didn't exist */
if (!t) {
dev_dbg(d, "token at location %04x doesn't exist\n",
buffer->input[0]);
return -EINVAL;
}
/* match against token blacklist */
for (i = 0; i < ARRAY_SIZE(token_blacklist); i++) {
if (!token_blacklist[i].min || !token_blacklist[i].max)
continue;
if (t >= token_blacklist[i].min &&
t <= token_blacklist[i].max)
return -EINVAL;
}
/* match against token whitelist */
for (i = 0; i < ARRAY_SIZE(token_whitelist); i++) {
if (!token_whitelist[i].min || !token_whitelist[i].max)
continue;
if (t < token_whitelist[i].min ||
t > token_whitelist[i].max)
continue;
if (!token_whitelist[i].need_capability ||
capable(token_whitelist[i].need_capability)) {
dev_dbg(d, "whitelisted token: %x\n", t);
return 0;
}
}
}
/* match against call whitelist */
for (i = 0; i < ARRAY_SIZE(call_whitelist); i++) {
if (buffer->cmd_class != call_whitelist[i].cmd_class)
continue;
if (buffer->cmd_select != call_whitelist[i].cmd_select)
continue;
if (!call_whitelist[i].need_capability ||
capable(call_whitelist[i].need_capability)) {
dev_dbg(d, "whitelisted capable command: %u/%u\n",
buffer->cmd_class, buffer->cmd_select);
return 0;
}
dev_dbg(d, "missing capability %d for %u/%u\n",
call_whitelist[i].need_capability,
buffer->cmd_class, buffer->cmd_select);
}
/* not in a whitelist, only allow processes with capabilities */
if (capable(CAP_SYS_RAWIO)) {
dev_dbg(d, "Allowing %u/%u due to CAP_SYS_RAWIO\n",
buffer->cmd_class, buffer->cmd_select);
return 0;
}
return -EACCES;
}
EXPORT_SYMBOL_GPL(dell_smbios_call_filter);
int dell_smbios_call(struct calling_interface_buffer *buffer)
{
struct smbios_device *selected = NULL;
struct smbios_device *priv;
int ret;
mutex_lock(&smbios_mutex);
list_for_each_entry(priv, &smbios_device_list, list) {
if (!selected || priv->priority >= selected->priority) {
dev_dbg(priv->device, "Trying device ID: %d\n", priv->priority);
selected = priv;
}
}
if (!selected) {
ret = -ENODEV;
pr_err("No dell-smbios drivers are loaded\n");
goto out_smbios_call;
}
ret = selected->call_fn(buffer);
out_smbios_call:
mutex_unlock(&smbios_mutex);
return ret;
}
EXPORT_SYMBOL_GPL(dell_smbios_call);
void dell_fill_request(struct calling_interface_buffer *buffer,
u32 arg0, u32 arg1, u32 arg2, u32 arg3)
{
memset(buffer, 0, sizeof(struct calling_interface_buffer));
buffer->input[0] = arg0;
buffer->input[1] = arg1;
buffer->input[2] = arg2;
buffer->input[3] = arg3;
}
EXPORT_SYMBOL_GPL(dell_fill_request);
int dell_send_request(struct calling_interface_buffer *buffer,
u16 class, u16 select)
{
int ret;
buffer->cmd_class = class;
buffer->cmd_select = select;
ret = dell_smbios_call(buffer);
if (ret != 0)
return ret;
return dell_smbios_error(buffer->output[0]);
}
EXPORT_SYMBOL_GPL(dell_send_request);
struct calling_interface_token *dell_smbios_find_token(int tokenid)
{
int i;
if (!da_tokens)
return NULL;
for (i = 0; i < da_num_tokens; i++) {
if (da_tokens[i].tokenID == tokenid)
return &da_tokens[i];
}
return NULL;
}
EXPORT_SYMBOL_GPL(dell_smbios_find_token);
static BLOCKING_NOTIFIER_HEAD(dell_laptop_chain_head);
int dell_laptop_register_notifier(struct notifier_block *nb)
{
return blocking_notifier_chain_register(&dell_laptop_chain_head, nb);
}
EXPORT_SYMBOL_GPL(dell_laptop_register_notifier);
int dell_laptop_unregister_notifier(struct notifier_block *nb)
{
return blocking_notifier_chain_unregister(&dell_laptop_chain_head, nb);
}
EXPORT_SYMBOL_GPL(dell_laptop_unregister_notifier);
void dell_laptop_call_notifier(unsigned long action, void *data)
{
blocking_notifier_call_chain(&dell_laptop_chain_head, action, data);
}
EXPORT_SYMBOL_GPL(dell_laptop_call_notifier);
bool dell_smbios_class_is_supported(u16 class)
{
/* Classes over 30 always unsupported */
if (class > 30)
return false;
return da_supported_commands & (1 << class);
}
EXPORT_SYMBOL_GPL(dell_smbios_class_is_supported);
static void __init parse_da_table(const struct dmi_header *dm)
{
/* Final token is a terminator, so we don't want to copy it */
int tokens = (dm->length-11)/sizeof(struct calling_interface_token)-1;
struct calling_interface_token *new_da_tokens;
struct calling_interface_structure *table =
container_of(dm, struct calling_interface_structure, header);
/*
* 4 bytes of table header, plus 7 bytes of Dell header
* plus at least 6 bytes of entry
*/
if (dm->length < 17)
return;
da_supported_commands = table->supportedCmds;
new_da_tokens = krealloc(da_tokens, (da_num_tokens + tokens) *
sizeof(struct calling_interface_token),
GFP_KERNEL);
if (!new_da_tokens)
return;
da_tokens = new_da_tokens;
memcpy(da_tokens+da_num_tokens, table->tokens,
sizeof(struct calling_interface_token) * tokens);
da_num_tokens += tokens;
}
static void zero_duplicates(struct device *dev)
{
int i, j;
for (i = 0; i < da_num_tokens; i++) {
if (da_tokens[i].tokenID == 0)
continue;
for (j = i+1; j < da_num_tokens; j++) {
if (da_tokens[j].tokenID == 0)
continue;
if (da_tokens[i].tokenID == da_tokens[j].tokenID) {
dev_dbg(dev, "Zeroing dup token ID %x(%x/%x)\n",
da_tokens[j].tokenID,
da_tokens[j].location,
da_tokens[j].value);
da_tokens[j].tokenID = 0;
}
}
}
}
static void __init find_tokens(const struct dmi_header *dm, void *dummy)
{
switch (dm->type) {
case 0xd4: /* Indexed IO */
case 0xd5: /* Protected Area Type 1 */
case 0xd6: /* Protected Area Type 2 */
break;
case 0xda: /* Calling interface */
parse_da_table(dm);
break;
}
}
static ssize_t location_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct token_sysfs_data *data = container_of(attr, struct token_sysfs_data, location_attr);
if (!capable(CAP_SYS_ADMIN))
return -EPERM;
return sysfs_emit(buf, "%08x", data->token->location);
}
static ssize_t value_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct token_sysfs_data *data = container_of(attr, struct token_sysfs_data, value_attr);
if (!capable(CAP_SYS_ADMIN))
return -EPERM;
return sysfs_emit(buf, "%08x", data->token->value);
}
static struct attribute_group smbios_attribute_group = {
.name = "tokens"
};
static struct platform_driver platform_driver = {
.driver = {
.name = "dell-smbios",
},
};
static int build_tokens_sysfs(struct platform_device *dev)
{
char *location_name;
char *value_name;
int ret;
int i, j;
token_entries = kzalloc_objs(*token_entries, da_num_tokens);
if (!token_entries)
return -ENOMEM;
/* need to store both location and value + terminator*/
token_attrs = kzalloc_objs(*token_attrs, (2 * da_num_tokens) + 1);
if (!token_attrs)
goto out_allocate_attrs;
for (i = 0, j = 0; i < da_num_tokens; i++) {
/* skip empty */
if (da_tokens[i].tokenID == 0)
continue;
token_entries[i].token = &da_tokens[i];
/* add location */
location_name = kasprintf(GFP_KERNEL, "%04x_location",
da_tokens[i].tokenID);
if (location_name == NULL)
goto out_unwind_strings;
sysfs_attr_init(&token_entries[i].location_attr.attr);
token_entries[i].location_attr.attr.name = location_name;
token_entries[i].location_attr.attr.mode = 0444;
token_entries[i].location_attr.show = location_show;
token_attrs[j++] = &token_entries[i].location_attr.attr;
/* add value */
value_name = kasprintf(GFP_KERNEL, "%04x_value",
da_tokens[i].tokenID);
if (!value_name) {
kfree(location_name);
goto out_unwind_strings;
}
sysfs_attr_init(&token_entries[i].value_attr.attr);
token_entries[i].value_attr.attr.name = value_name;
token_entries[i].value_attr.attr.mode = 0444;
token_entries[i].value_attr.show = value_show;
token_attrs[j++] = &token_entries[i].value_attr.attr;
}
smbios_attribute_group.attrs = token_attrs;
ret = sysfs_create_group(&dev->dev.kobj, &smbios_attribute_group);
if (ret)
goto out_unwind_strings;
return 0;
out_unwind_strings:
while (i--) {
kfree(token_entries[i].location_attr.attr.name);
kfree(token_entries[i].value_attr.attr.name);
}
kfree(token_attrs);
out_allocate_attrs:
kfree(token_entries);
return -ENOMEM;
}
static void free_group(struct platform_device *pdev)
{
int i;
sysfs_remove_group(&pdev->dev.kobj,
&smbios_attribute_group);
for (i = 0; i < da_num_tokens; i++) {
kfree(token_entries[i].location_attr.attr.name);
kfree(token_entries[i].value_attr.attr.name);
}
kfree(token_attrs);
kfree(token_entries);
}
static int __init dell_smbios_init(void)
{
int ret, wmi, smm;
if (!dmi_find_device(DMI_DEV_TYPE_OEM_STRING, "Dell System", NULL) &&
!dmi_find_device(DMI_DEV_TYPE_OEM_STRING, "Alienware", NULL) &&
!dmi_find_device(DMI_DEV_TYPE_OEM_STRING, "www.dell.com", NULL)) {
pr_err("Unable to run on non-Dell system\n");
return -ENODEV;
}
dmi_walk(find_tokens, NULL);
ret = platform_driver_register(&platform_driver);
if (ret)
goto fail_platform_driver;
platform_device = platform_device_alloc("dell-smbios", 0);
if (!platform_device) {
ret = -ENOMEM;
goto fail_platform_device_alloc;
}
ret = platform_device_add(platform_device);
if (ret)
goto fail_platform_device_add;
/* register backends */
wmi = init_dell_smbios_wmi();
if (wmi)
pr_debug("Failed to initialize WMI backend: %d\n", wmi);
smm = init_dell_smbios_smm();
if (smm)
pr_debug("Failed to initialize SMM backend: %d\n", smm);
if (wmi && smm) {
pr_err("No SMBIOS backends available (wmi: %d, smm: %d)\n",
wmi, smm);
ret = -ENODEV;
goto fail_create_group;
}
if (da_tokens) {
/* duplicate tokens will cause problems building sysfs files */
zero_duplicates(&platform_device->dev);
ret = build_tokens_sysfs(platform_device);
if (ret)
goto fail_sysfs;
}
return 0;
fail_sysfs:
if (!wmi)
exit_dell_smbios_wmi();
if (!smm)
exit_dell_smbios_smm();
fail_create_group:
platform_device_del(platform_device);
fail_platform_device_add:
platform_device_put(platform_device);
fail_platform_device_alloc:
platform_driver_unregister(&platform_driver);
fail_platform_driver:
kfree(da_tokens);
return ret;
}
static void __exit dell_smbios_exit(void)
{
exit_dell_smbios_wmi();
exit_dell_smbios_smm();
mutex_lock(&smbios_mutex);
if (platform_device) {
if (da_tokens)
free_group(platform_device);
platform_device_unregister(platform_device);
platform_driver_unregister(&platform_driver);
}
kfree(da_tokens);
mutex_unlock(&smbios_mutex);
}
module_init(dell_smbios_init);
module_exit(dell_smbios_exit);
MODULE_AUTHOR("Matthew Garrett <mjg@redhat.com>");
MODULE_AUTHOR("Gabriele Mazzotta <gabriele.mzt@gmail.com>");
MODULE_AUTHOR("Pali Rohár <pali@kernel.org>");
MODULE_AUTHOR("Mario Limonciello <mario.limonciello@outlook.com>");
MODULE_DESCRIPTION("Common functions for kernel modules using Dell SMBIOS");
MODULE_LICENSE("GPL");
@@ -0,0 +1,153 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* SMI methods for use with dell-smbios
*
* Copyright (c) Red Hat <mjg@redhat.com>
* Copyright (c) 2014 Gabriele Mazzotta <gabriele.mzt@gmail.com>
* Copyright (c) 2014 Pali Rohár <pali@kernel.org>
* Copyright (c) 2017 Dell Inc.
*/
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/dmi.h>
#include <linux/gfp.h>
#include <linux/io.h>
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/platform_device.h>
#include "dcdbas.h"
#include "dell-smbios.h"
static int da_command_address;
static int da_command_code;
static struct smi_buffer smi_buf;
static struct calling_interface_buffer *buffer;
static struct platform_device *platform_device;
static DEFINE_MUTEX(smm_mutex);
static void parse_da_table(const struct dmi_header *dm)
{
struct calling_interface_structure *table =
container_of(dm, struct calling_interface_structure, header);
/* 4 bytes of table header, plus 7 bytes of Dell header, plus at least
* 6 bytes of entry
*/
if (dm->length < 17)
return;
da_command_address = table->cmdIOAddress;
da_command_code = table->cmdIOCode;
}
static void find_cmd_address(const struct dmi_header *dm, void *dummy)
{
switch (dm->type) {
case 0xda: /* Calling interface */
parse_da_table(dm);
break;
}
}
static int dell_smbios_smm_call(struct calling_interface_buffer *input)
{
struct smi_cmd command;
size_t size;
size = sizeof(struct calling_interface_buffer);
command.magic = SMI_CMD_MAGIC;
command.command_address = da_command_address;
command.command_code = da_command_code;
command.ebx = smi_buf.dma;
command.ecx = 0x42534931;
mutex_lock(&smm_mutex);
memcpy(buffer, input, size);
dcdbas_smi_request(&command);
memcpy(input, buffer, size);
mutex_unlock(&smm_mutex);
return 0;
}
/* When enabled this indicates that SMM won't work */
static bool test_wsmt_enabled(void)
{
struct calling_interface_token *wsmt;
/* if token doesn't exist, SMM will work */
wsmt = dell_smbios_find_token(WSMT_EN_TOKEN);
if (!wsmt)
return false;
/* If token exists, try to access over SMM but set a dummy return.
* - If WSMT disabled it will be overwritten by SMM
* - If WSMT enabled then dummy value will remain
*/
buffer->cmd_class = CLASS_TOKEN_READ;
buffer->cmd_select = SELECT_TOKEN_STD;
memset(buffer, 0, sizeof(struct calling_interface_buffer));
buffer->input[0] = wsmt->location;
buffer->output[0] = 99;
dell_smbios_smm_call(buffer);
if (buffer->output[0] == 99)
return true;
return false;
}
int init_dell_smbios_smm(void)
{
int ret;
/*
* Allocate buffer below 4GB for SMI data--only 32-bit physical addr
* is passed to SMI handler.
*/
ret = dcdbas_smi_alloc(&smi_buf, PAGE_SIZE);
if (ret)
return ret;
buffer = (void *)smi_buf.virt;
dmi_walk(find_cmd_address, NULL);
if (test_wsmt_enabled()) {
pr_debug("Disabling due to WSMT enabled\n");
ret = -ENODEV;
goto fail_wsmt;
}
platform_device = platform_device_alloc("dell-smbios", 1);
if (!platform_device) {
ret = -ENOMEM;
goto fail_platform_device_alloc;
}
ret = platform_device_add(platform_device);
if (ret)
goto fail_platform_device_add;
ret = dell_smbios_register_device(&platform_device->dev, 0, &dell_smbios_smm_call);
if (ret)
goto fail_register;
return 0;
fail_register:
platform_device_del(platform_device);
fail_platform_device_add:
platform_device_put(platform_device);
fail_wsmt:
fail_platform_device_alloc:
dcdbas_smi_free(&smi_buf);
return ret;
}
void exit_dell_smbios_smm(void)
{
if (platform_device) {
dell_smbios_unregister_device(&platform_device->dev);
platform_device_unregister(platform_device);
dcdbas_smi_free(&smi_buf);
}
}
@@ -0,0 +1,337 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* WMI methods for use with dell-smbios
*
* Copyright (c) 2017 Dell Inc.
*/
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/device.h>
#include <linux/dmi.h>
#include <linux/fs.h>
#include <linux/list.h>
#include <linux/miscdevice.h>
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/uaccess.h>
#include <linux/wmi.h>
#include <uapi/linux/wmi.h>
#include "dell-smbios.h"
#include "dell-wmi-descriptor.h"
static DEFINE_MUTEX(call_mutex);
static DEFINE_MUTEX(list_mutex);
static int wmi_supported;
struct misc_bios_flags_structure {
struct dmi_header header;
u16 flags0;
} __packed;
#define FLAG_HAS_ACPI_WMI 0x02
#define DELL_WMI_SMBIOS_GUID "A80593CE-A997-11DA-B012-B622A1EF5492"
struct wmi_smbios_priv {
struct dell_wmi_smbios_buffer *buf;
struct list_head list;
struct wmi_device *wdev;
struct device *child;
u64 req_buf_size;
struct miscdevice char_dev;
};
static LIST_HEAD(wmi_list);
static inline struct wmi_smbios_priv *get_first_smbios_priv(void)
{
return list_first_entry_or_null(&wmi_list,
struct wmi_smbios_priv,
list);
}
static int run_smbios_call(struct wmi_device *wdev)
{
struct wmi_smbios_priv *priv = dev_get_drvdata(&wdev->dev);
const struct wmi_buffer input = {
.length = priv->req_buf_size - sizeof(u64),
.data = &priv->buf->std,
};
struct wmi_buffer output;
int ret;
dev_dbg(&wdev->dev, "evaluating: %u/%u [%x,%x,%x,%x]\n",
priv->buf->std.cmd_class, priv->buf->std.cmd_select,
priv->buf->std.input[0], priv->buf->std.input[1],
priv->buf->std.input[2], priv->buf->std.input[3]);
/*
* The output buffer returned by the WMI method should have at least the size
* of the input buffer.
*/
ret = wmidev_invoke_method(wdev, 0, 1, &input, &output, input.length);
if (ret < 0)
return ret;
memcpy(input.data, output.data, input.length);
kfree(output.data);
dev_dbg(&wdev->dev, "result: [%08x,%08x,%08x,%08x]\n",
priv->buf->std.output[0], priv->buf->std.output[1],
priv->buf->std.output[2], priv->buf->std.output[3]);
return 0;
}
static int dell_smbios_wmi_call(struct calling_interface_buffer *buffer)
{
struct wmi_smbios_priv *priv;
size_t difference;
size_t size;
int ret;
mutex_lock(&call_mutex);
priv = get_first_smbios_priv();
if (!priv) {
ret = -ENODEV;
goto out_wmi_call;
}
size = sizeof(struct calling_interface_buffer);
difference = priv->req_buf_size - sizeof(u64) - size;
memset(&priv->buf->ext, 0, difference);
memcpy(&priv->buf->std, buffer, size);
ret = run_smbios_call(priv->wdev);
memcpy(buffer, &priv->buf->std, size);
out_wmi_call:
mutex_unlock(&call_mutex);
return ret;
}
static int dell_smbios_wmi_open(struct inode *inode, struct file *filp)
{
struct wmi_smbios_priv *priv;
priv = container_of(filp->private_data, struct wmi_smbios_priv, char_dev);
filp->private_data = priv;
return nonseekable_open(inode, filp);
}
static ssize_t dell_smbios_wmi_read(struct file *filp, char __user *buffer, size_t length,
loff_t *offset)
{
struct wmi_smbios_priv *priv = filp->private_data;
return simple_read_from_buffer(buffer, length, offset, &priv->req_buf_size,
sizeof(priv->req_buf_size));
}
static long dell_smbios_wmi_do_ioctl(struct wmi_smbios_priv *priv,
struct dell_wmi_smbios_buffer __user *arg)
{
long ret;
if (get_user(priv->buf->length, &arg->length))
return -EFAULT;
if (priv->buf->length < priv->req_buf_size)
return -EINVAL;
/* if it's too big, warn, driver will only use what is needed */
if (priv->buf->length > priv->req_buf_size)
dev_err(&priv->wdev->dev, "Buffer %llu is bigger than required %llu\n",
priv->buf->length, priv->req_buf_size);
if (copy_from_user(priv->buf, arg, priv->req_buf_size))
return -EFAULT;
if (dell_smbios_call_filter(&priv->wdev->dev, &priv->buf->std)) {
dev_err(&priv->wdev->dev, "Invalid call %d/%d:%8x\n",
priv->buf->std.cmd_class,
priv->buf->std.cmd_select,
priv->buf->std.input[0]);
return -EINVAL;
}
ret = run_smbios_call(priv->wdev);
if (ret)
return ret;
if (copy_to_user(arg, priv->buf, priv->req_buf_size))
return -EFAULT;
return 0;
}
static long dell_smbios_wmi_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
struct dell_wmi_smbios_buffer __user *input = (struct dell_wmi_smbios_buffer __user *)arg;
struct wmi_smbios_priv *priv = filp->private_data;
long ret;
if (cmd != DELL_WMI_SMBIOS_CMD)
return -ENOIOCTLCMD;
mutex_lock(&call_mutex);
ret = dell_smbios_wmi_do_ioctl(priv, input);
mutex_unlock(&call_mutex);
return ret;
}
static const struct file_operations dell_smbios_wmi_fops = {
.owner = THIS_MODULE,
.open = dell_smbios_wmi_open,
.read = dell_smbios_wmi_read,
.unlocked_ioctl = dell_smbios_wmi_ioctl,
.compat_ioctl = compat_ptr_ioctl,
};
static void dell_smbios_wmi_unregister_chardev(void *data)
{
struct miscdevice *char_dev = data;
misc_deregister(char_dev);
}
static int dell_smbios_wmi_register_chardev(struct wmi_smbios_priv *priv)
{
int ret;
priv->char_dev.minor = MISC_DYNAMIC_MINOR;
priv->char_dev.name = "wmi/dell-smbios";
priv->char_dev.fops = &dell_smbios_wmi_fops;
priv->char_dev.mode = 0444;
ret = misc_register(&priv->char_dev);
if (ret < 0)
return ret;
return devm_add_action_or_reset(&priv->wdev->dev, dell_smbios_wmi_unregister_chardev,
&priv->char_dev);
}
static int dell_smbios_wmi_probe(struct wmi_device *wdev, const void *context)
{
struct wmi_smbios_priv *priv;
u32 buffer_size, hotfix;
int count;
int ret;
ret = dell_wmi_get_descriptor_valid();
if (ret)
return ret;
priv = devm_kzalloc(&wdev->dev, sizeof(struct wmi_smbios_priv),
GFP_KERNEL);
if (!priv)
return -ENOMEM;
priv->wdev = wdev;
dev_set_drvdata(&wdev->dev, priv);
/* WMI buffer size will be either 4k or 32k depending on machine */
if (!dell_wmi_get_size(&buffer_size))
return -EPROBE_DEFER;
priv->req_buf_size = buffer_size;
/* some SMBIOS calls fail unless BIOS contains hotfix */
if (!dell_wmi_get_hotfix(&hotfix))
return -EPROBE_DEFER;
if (!hotfix)
dev_warn(&wdev->dev,
"WMI SMBIOS userspace interface not supported(%u), try upgrading to a newer BIOS\n",
hotfix);
/* add in the length object we will use internally with ioctl */
priv->req_buf_size += sizeof(u64);
count = get_order(priv->req_buf_size);
priv->buf = (void *)devm_get_free_pages(&wdev->dev, GFP_KERNEL, count);
if (!priv->buf)
return -ENOMEM;
ret = dell_smbios_wmi_register_chardev(priv);
if (ret)
return ret;
ret = dell_smbios_register_device(&wdev->dev, 1, &dell_smbios_wmi_call);
if (ret)
return ret;
mutex_lock(&list_mutex);
list_add_tail(&priv->list, &wmi_list);
mutex_unlock(&list_mutex);
return 0;
}
static void dell_smbios_wmi_remove(struct wmi_device *wdev)
{
struct wmi_smbios_priv *priv = dev_get_drvdata(&wdev->dev);
mutex_lock(&call_mutex);
mutex_lock(&list_mutex);
list_del(&priv->list);
mutex_unlock(&list_mutex);
dell_smbios_unregister_device(&wdev->dev);
mutex_unlock(&call_mutex);
}
static const struct wmi_device_id dell_smbios_wmi_id_table[] = {
{ .guid_string = DELL_WMI_SMBIOS_GUID },
{ },
};
static void parse_b1_table(const struct dmi_header *dm)
{
struct misc_bios_flags_structure *flags =
container_of(dm, struct misc_bios_flags_structure, header);
/* 4 bytes header, 8 bytes flags */
if (dm->length < 12)
return;
if (dm->handle != 0xb100)
return;
if ((flags->flags0 & FLAG_HAS_ACPI_WMI))
wmi_supported = 1;
}
static void find_b1(const struct dmi_header *dm, void *dummy)
{
switch (dm->type) {
case 0xb1: /* misc bios flags */
parse_b1_table(dm);
break;
}
}
static struct wmi_driver dell_smbios_wmi_driver = {
.driver = {
.name = "dell-smbios",
},
.probe = dell_smbios_wmi_probe,
.remove = dell_smbios_wmi_remove,
.id_table = dell_smbios_wmi_id_table,
};
int init_dell_smbios_wmi(void)
{
dmi_walk(find_b1, NULL);
if (!wmi_supported)
return -ENODEV;
return wmi_driver_register(&dell_smbios_wmi_driver);
}
void exit_dell_smbios_wmi(void)
{
if (wmi_supported)
wmi_driver_unregister(&dell_smbios_wmi_driver);
}
MODULE_DEVICE_TABLE(wmi, dell_smbios_wmi_id_table);
@@ -0,0 +1,116 @@
/* SPDX-License-Identifier: GPL-2.0-only */
/*
* Common functions for kernel modules using Dell SMBIOS
*
* Copyright (c) Red Hat <mjg@redhat.com>
* Copyright (c) 2014 Gabriele Mazzotta <gabriele.mzt@gmail.com>
* Copyright (c) 2014 Pali Rohár <pali@kernel.org>
*
* Based on documentation in the libsmbios package:
* Copyright (C) 2005-2014 Dell Inc.
*/
#ifndef _DELL_SMBIOS_H_
#define _DELL_SMBIOS_H_
#include <linux/device.h>
#include <uapi/linux/wmi.h>
/* Classes and selects used only in kernel drivers */
#define CLASS_KBD_BACKLIGHT 4
#define SELECT_KBD_BACKLIGHT 11
#define SELECT_THERMAL_MANAGEMENT 19
/* Tokens used in kernel drivers, any of these
* should be filtered from userspace access
*/
#define BRIGHTNESS_TOKEN 0x007d
#define KBD_LED_AC_TOKEN 0x0451
#define KBD_LED_OFF_TOKEN 0x01E1
#define KBD_LED_ON_TOKEN 0x01E2
#define KBD_LED_AUTO_TOKEN 0x01E3
#define KBD_LED_AUTO_25_TOKEN 0x02EA
#define KBD_LED_AUTO_50_TOKEN 0x02EB
#define KBD_LED_AUTO_75_TOKEN 0x02EC
#define KBD_LED_AUTO_100_TOKEN 0x02F6
#define BAT_PRI_AC_MODE_TOKEN 0x0341
#define BAT_ADAPTIVE_MODE_TOKEN 0x0342
#define BAT_CUSTOM_MODE_TOKEN 0x0343
#define BAT_STANDARD_MODE_TOKEN 0x0346
#define BAT_EXPRESS_MODE_TOKEN 0x0347
#define BAT_CUSTOM_CHARGE_START 0x0349
#define BAT_CUSTOM_CHARGE_END 0x034A
#define GLOBAL_MIC_MUTE_ENABLE 0x0364
#define GLOBAL_MIC_MUTE_DISABLE 0x0365
#define GLOBAL_MUTE_ENABLE 0x058C
#define GLOBAL_MUTE_DISABLE 0x058D
struct notifier_block;
struct calling_interface_token {
u16 tokenID;
u16 location;
union {
u16 value;
u16 stringlength;
};
};
struct calling_interface_structure {
struct dmi_header header;
u16 cmdIOAddress;
u8 cmdIOCode;
u32 supportedCmds;
struct calling_interface_token tokens[];
} __packed;
int dell_smbios_register_device(struct device *d, int priority, void *call_fn);
void dell_smbios_unregister_device(struct device *d);
int dell_smbios_error(int value);
int dell_smbios_call_filter(struct device *d,
struct calling_interface_buffer *buffer);
int dell_smbios_call(struct calling_interface_buffer *buffer);
void dell_fill_request(struct calling_interface_buffer *buffer,
u32 arg0, u32 arg1, u32 arg2, u32 arg3);
int dell_send_request(struct calling_interface_buffer *buffer,
u16 class, u16 select);
struct calling_interface_token *dell_smbios_find_token(int tokenid);
enum dell_laptop_notifier_actions {
DELL_LAPTOP_KBD_BACKLIGHT_BRIGHTNESS_CHANGED,
};
int dell_laptop_register_notifier(struct notifier_block *nb);
int dell_laptop_unregister_notifier(struct notifier_block *nb);
void dell_laptop_call_notifier(unsigned long action, void *data);
bool dell_smbios_class_is_supported(u16 class);
/* for the supported backends */
#ifdef CONFIG_DELL_SMBIOS_WMI
int init_dell_smbios_wmi(void);
void exit_dell_smbios_wmi(void);
#else /* CONFIG_DELL_SMBIOS_WMI */
static inline int init_dell_smbios_wmi(void)
{
return -ENODEV;
}
static inline void exit_dell_smbios_wmi(void)
{}
#endif /* CONFIG_DELL_SMBIOS_WMI */
#ifdef CONFIG_DELL_SMBIOS_SMM
int init_dell_smbios_smm(void);
void exit_dell_smbios_smm(void);
#else /* CONFIG_DELL_SMBIOS_SMM */
static inline int init_dell_smbios_smm(void)
{
return -ENODEV;
}
static inline void exit_dell_smbios_smm(void)
{}
#endif /* CONFIG_DELL_SMBIOS_SMM */
#endif /* _DELL_SMBIOS_H_ */
@@ -0,0 +1,27 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* ACPI SMO88XX lis3lv02d freefall / accelerometer device-ids.
*
* Copyright (C) 2012 Sonal Santan <sonal.santan@gmail.com>
* Copyright (C) 2014 Pali Rohár <pali@kernel.org>
*/
#ifndef _DELL_SMO8800_IDS_H_
#define _DELL_SMO8800_IDS_H_
#include <linux/mod_devicetable.h>
#include <linux/module.h>
static const struct acpi_device_id smo8800_ids[] = {
{ "SMO8800" },
{ "SMO8801" },
{ "SMO8810" },
{ "SMO8811" },
{ "SMO8820" },
{ "SMO8821" },
{ "SMO8830" },
{ "SMO8831" },
{ }
};
MODULE_DEVICE_TABLE(acpi, smo8800_ids);
#endif
@@ -0,0 +1,178 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* dell-smo8800.c - Dell Latitude ACPI SMO88XX freefall sensor driver
*
* Copyright (C) 2012 Sonal Santan <sonal.santan@gmail.com>
* Copyright (C) 2014 Pali Rohár <pali@kernel.org>
*
* This is loosely based on lis3lv02d driver.
*/
#define DRIVER_NAME "smo8800"
#include <linux/fs.h>
#include <linux/interrupt.h>
#include <linux/kernel.h>
#include <linux/miscdevice.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/uaccess.h>
#include "dell-smo8800-ids.h"
struct smo8800_device {
u32 irq; /* acpi device irq */
atomic_t counter; /* count after last read */
struct miscdevice miscdev; /* for /dev/freefall */
unsigned long misc_opened; /* whether the device is open */
wait_queue_head_t misc_wait; /* Wait queue for the misc dev */
struct device *dev; /* acpi device */
};
static irqreturn_t smo8800_interrupt_quick(int irq, void *data)
{
struct smo8800_device *smo8800 = data;
atomic_inc(&smo8800->counter);
wake_up_interruptible(&smo8800->misc_wait);
return IRQ_WAKE_THREAD;
}
static irqreturn_t smo8800_interrupt_thread(int irq, void *data)
{
struct smo8800_device *smo8800 = data;
dev_info(smo8800->dev, "detected free fall\n");
return IRQ_HANDLED;
}
static ssize_t smo8800_misc_read(struct file *file, char __user *buf,
size_t count, loff_t *pos)
{
struct smo8800_device *smo8800 = container_of(file->private_data,
struct smo8800_device, miscdev);
u32 data = 0;
unsigned char byte_data;
ssize_t retval = 1;
if (count < 1)
return -EINVAL;
atomic_set(&smo8800->counter, 0);
retval = wait_event_interruptible(smo8800->misc_wait,
(data = atomic_xchg(&smo8800->counter, 0)));
if (retval)
return retval;
retval = 1;
byte_data = min_t(u32, data, 255);
if (put_user(byte_data, buf))
retval = -EFAULT;
return retval;
}
static int smo8800_misc_open(struct inode *inode, struct file *file)
{
struct smo8800_device *smo8800 = container_of(file->private_data,
struct smo8800_device, miscdev);
if (test_and_set_bit(0, &smo8800->misc_opened))
return -EBUSY; /* already open */
atomic_set(&smo8800->counter, 0);
return 0;
}
static int smo8800_misc_release(struct inode *inode, struct file *file)
{
struct smo8800_device *smo8800 = container_of(file->private_data,
struct smo8800_device, miscdev);
clear_bit(0, &smo8800->misc_opened); /* release the device */
return 0;
}
static const struct file_operations smo8800_misc_fops = {
.owner = THIS_MODULE,
.read = smo8800_misc_read,
.open = smo8800_misc_open,
.release = smo8800_misc_release,
};
static int smo8800_probe(struct platform_device *device)
{
int err;
struct smo8800_device *smo8800;
smo8800 = devm_kzalloc(&device->dev, sizeof(*smo8800), GFP_KERNEL);
if (!smo8800) {
dev_err(&device->dev, "failed to allocate device data\n");
return -ENOMEM;
}
smo8800->dev = &device->dev;
smo8800->miscdev.minor = MISC_DYNAMIC_MINOR;
smo8800->miscdev.name = "freefall";
smo8800->miscdev.fops = &smo8800_misc_fops;
init_waitqueue_head(&smo8800->misc_wait);
err = misc_register(&smo8800->miscdev);
if (err) {
dev_err(&device->dev, "failed to register misc dev: %d\n", err);
return err;
}
platform_set_drvdata(device, smo8800);
err = platform_get_irq(device, 0);
if (err < 0)
goto error;
smo8800->irq = err;
err = request_threaded_irq(smo8800->irq, smo8800_interrupt_quick,
smo8800_interrupt_thread,
IRQF_TRIGGER_RISING | IRQF_ONESHOT,
DRIVER_NAME, smo8800);
if (err) {
dev_err(&device->dev,
"failed to request thread for IRQ %d: %d\n",
smo8800->irq, err);
goto error;
}
dev_dbg(&device->dev, "device /dev/freefall registered with IRQ %d\n",
smo8800->irq);
return 0;
error:
misc_deregister(&smo8800->miscdev);
return err;
}
static void smo8800_remove(struct platform_device *device)
{
struct smo8800_device *smo8800 = platform_get_drvdata(device);
free_irq(smo8800->irq, smo8800);
misc_deregister(&smo8800->miscdev);
dev_dbg(&device->dev, "device /dev/freefall unregistered\n");
}
static struct platform_driver smo8800_driver = {
.probe = smo8800_probe,
.remove = smo8800_remove,
.driver = {
.name = DRIVER_NAME,
.acpi_match_table = smo8800_ids,
},
};
module_platform_driver(smo8800_driver);
MODULE_DESCRIPTION("Dell Latitude freefall driver (ACPI SMO88XX)");
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Sonal Santan, Pali Rohár");
@@ -0,0 +1,407 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Dell AIO Serial Backlight Driver
*
* Copyright (C) 2024 Hans de Goede <hansg@kernel.org>
* Copyright (C) 2017 AceLan Kao <acelan.kao@canonical.com>
*/
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/acpi.h>
#include <linux/backlight.h>
#include <linux/delay.h>
#include <linux/device.h>
#include <linux/err.h>
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/platform_device.h>
#include <linux/serdev.h>
#include <linux/string.h>
#include <linux/types.h>
#include <linux/wait.h>
#include <acpi/video.h>
#include "../serdev_helpers.h"
/* The backlight controller must respond within 1 second */
#define DELL_BL_TIMEOUT msecs_to_jiffies(1000)
#define DELL_BL_MAX_BRIGHTNESS 100
/* Defines for the commands send to the controller */
/* 1st byte Start Of Frame 3 MSB bits: cmd-len + 01010 SOF marker */
#define DELL_SOF(len) (((len) << 5) | 0x0a)
#define GET_CMD_LEN 3
#define SET_CMD_LEN 4
/* 2nd byte command */
#define CMD_GET_VERSION 0x06
#define CMD_SET_BRIGHTNESS 0x0b
#define CMD_GET_BRIGHTNESS 0x0c
#define CMD_SET_BL_POWER 0x0e
/* Indexes and other defines for response received from the controller */
#define RESP_LEN 0
#define RESP_CMD 1 /* Echo of CMD byte from command */
#define RESP_DATA 2 /* Start of received data */
#define SET_RESP_LEN 3
#define GET_RESP_LEN 4
#define MIN_RESP_LEN 3
#define MAX_RESP_LEN 80
struct dell_uart_backlight {
struct mutex mutex;
wait_queue_head_t wait_queue;
struct device *dev;
struct backlight_device *bl;
u8 *resp;
u8 resp_idx;
u8 resp_len;
u8 resp_max_len;
u8 pending_cmd;
int status;
int power;
};
/* Checksum: SUM(Length and Cmd and Data) xor 0xFF */
static u8 dell_uart_checksum(u8 *buf, int len)
{
u8 val = 0;
while (len-- > 0)
val += buf[len];
return val ^ 0xff;
}
static int dell_uart_bl_command(struct dell_uart_backlight *dell_bl,
const u8 *cmd, int cmd_len,
u8 *resp, int resp_max_len)
{
int ret;
ret = mutex_lock_killable(&dell_bl->mutex);
if (ret)
return ret;
dell_bl->status = -EBUSY;
dell_bl->resp = resp;
dell_bl->resp_idx = 0;
dell_bl->resp_len = -1; /* Invalid / unset */
dell_bl->resp_max_len = resp_max_len;
dell_bl->pending_cmd = cmd[1];
/* The TTY buffer should be big enough to take the entire cmd in one go */
ret = serdev_device_write_buf(to_serdev_device(dell_bl->dev), cmd, cmd_len);
if (ret != cmd_len) {
dev_err(dell_bl->dev, "Error writing command: %d\n", ret);
dell_bl->status = (ret < 0) ? ret : -EIO;
goto out;
}
ret = wait_event_timeout(dell_bl->wait_queue, dell_bl->status != -EBUSY,
DELL_BL_TIMEOUT);
if (ret == 0) {
dev_err(dell_bl->dev, "Timed out waiting for response.\n");
/* Clear busy status to discard bytes received after this */
dell_bl->status = -ETIMEDOUT;
}
out:
mutex_unlock(&dell_bl->mutex);
return dell_bl->status;
}
static int dell_uart_set_brightness(struct dell_uart_backlight *dell_bl, int brightness)
{
u8 set_brightness[SET_CMD_LEN], resp[SET_RESP_LEN];
set_brightness[0] = DELL_SOF(SET_CMD_LEN);
set_brightness[1] = CMD_SET_BRIGHTNESS;
set_brightness[2] = brightness;
set_brightness[3] = dell_uart_checksum(set_brightness, 3);
return dell_uart_bl_command(dell_bl, set_brightness, SET_CMD_LEN, resp, SET_RESP_LEN);
}
static int dell_uart_get_brightness(struct dell_uart_backlight *dell_bl)
{
struct device *dev = dell_bl->dev;
u8 get_brightness[GET_CMD_LEN], resp[GET_RESP_LEN];
int ret;
get_brightness[0] = DELL_SOF(GET_CMD_LEN);
get_brightness[1] = CMD_GET_BRIGHTNESS;
get_brightness[2] = dell_uart_checksum(get_brightness, 2);
ret = dell_uart_bl_command(dell_bl, get_brightness, GET_CMD_LEN, resp, GET_RESP_LEN);
if (ret)
return ret;
if (resp[RESP_LEN] != GET_RESP_LEN) {
dev_err(dev, "Unexpected get brightness response length: %d\n", resp[RESP_LEN]);
return -EIO;
}
if (resp[RESP_DATA] > DELL_BL_MAX_BRIGHTNESS) {
dev_err(dev, "Unexpected get brightness response: %d\n", resp[RESP_DATA]);
return -EIO;
}
return resp[RESP_DATA];
}
static int dell_uart_set_bl_power(struct dell_uart_backlight *dell_bl, int power)
{
u8 set_power[SET_CMD_LEN], resp[SET_RESP_LEN];
int ret;
set_power[0] = DELL_SOF(SET_CMD_LEN);
set_power[1] = CMD_SET_BL_POWER;
set_power[2] = (power == BACKLIGHT_POWER_ON) ? 1 : 0;
set_power[3] = dell_uart_checksum(set_power, 3);
ret = dell_uart_bl_command(dell_bl, set_power, SET_CMD_LEN, resp, SET_RESP_LEN);
if (ret)
return ret;
dell_bl->power = power;
return 0;
}
/*
* There is no command to get backlight power status,
* so we set the backlight power to "on" while initializing,
* and then track and report its status by power variable.
*/
static int dell_uart_get_bl_power(struct dell_uart_backlight *dell_bl)
{
return dell_bl->power;
}
static int dell_uart_update_status(struct backlight_device *bd)
{
struct dell_uart_backlight *dell_bl = bl_get_data(bd);
int ret;
ret = dell_uart_set_brightness(dell_bl, bd->props.brightness);
if (ret)
return ret;
if (bd->props.power != dell_uart_get_bl_power(dell_bl))
return dell_uart_set_bl_power(dell_bl, bd->props.power);
return 0;
}
static int dell_uart_get_brightness_op(struct backlight_device *bd)
{
return dell_uart_get_brightness(bl_get_data(bd));
}
static const struct backlight_ops dell_uart_backlight_ops = {
.update_status = dell_uart_update_status,
.get_brightness = dell_uart_get_brightness_op,
};
static size_t dell_uart_bl_receive(struct serdev_device *serdev, const u8 *data, size_t len)
{
struct dell_uart_backlight *dell_bl = serdev_device_get_drvdata(serdev);
size_t i;
u8 csum;
dev_dbg(dell_bl->dev, "Recv: %*ph\n", (int)len, data);
/* Throw away unexpected bytes / remainder of response after an error */
if (dell_bl->status != -EBUSY) {
dev_warn(dell_bl->dev, "Bytes received out of band, dropping them.\n");
return len;
}
i = 0;
while (i < len && dell_bl->resp_idx != dell_bl->resp_len) {
dell_bl->resp[dell_bl->resp_idx] = data[i++];
switch (dell_bl->resp_idx) {
case RESP_LEN: /* Length byte */
dell_bl->resp_len = dell_bl->resp[RESP_LEN];
if (dell_bl->resp_len < MIN_RESP_LEN ||
dell_bl->resp_len > dell_bl->resp_max_len) {
dev_err(dell_bl->dev, "Response length %d out if range %d - %d\n",
dell_bl->resp_len, MIN_RESP_LEN, dell_bl->resp_max_len);
dell_bl->status = -EIO;
goto wakeup;
}
break;
case RESP_CMD: /* CMD byte */
if (dell_bl->resp[RESP_CMD] != dell_bl->pending_cmd) {
dev_err(dell_bl->dev, "Response cmd 0x%02x != pending 0x%02x\n",
dell_bl->resp[RESP_CMD], dell_bl->pending_cmd);
dell_bl->status = -EIO;
goto wakeup;
}
break;
}
dell_bl->resp_idx++;
}
if (dell_bl->resp_idx != dell_bl->resp_len)
return len; /* Response not complete yet */
csum = dell_uart_checksum(dell_bl->resp, dell_bl->resp_len - 1);
if (dell_bl->resp[dell_bl->resp_len - 1] == csum) {
dell_bl->status = 0; /* Success */
} else {
dev_err(dell_bl->dev, "Checksum mismatch got 0x%02x expected 0x%02x\n",
dell_bl->resp[dell_bl->resp_len - 1], csum);
dell_bl->status = -EIO;
}
wakeup:
wake_up(&dell_bl->wait_queue);
return i;
}
static const struct serdev_device_ops dell_uart_bl_serdev_ops = {
.receive_buf = dell_uart_bl_receive,
.write_wakeup = serdev_device_write_wakeup,
};
static int dell_uart_bl_serdev_probe(struct serdev_device *serdev)
{
u8 get_version[GET_CMD_LEN], resp[MAX_RESP_LEN];
struct backlight_properties props = {};
struct dell_uart_backlight *dell_bl;
struct device *dev = &serdev->dev;
int ret;
dell_bl = devm_kzalloc(dev, sizeof(*dell_bl), GFP_KERNEL);
if (!dell_bl)
return -ENOMEM;
mutex_init(&dell_bl->mutex);
init_waitqueue_head(&dell_bl->wait_queue);
dell_bl->dev = dev;
serdev_device_set_drvdata(serdev, dell_bl);
serdev_device_set_client_ops(serdev, &dell_uart_bl_serdev_ops);
ret = devm_serdev_device_open(dev, serdev);
if (ret)
return dev_err_probe(dev, ret, "opening UART device\n");
/* 9600 bps, no flow control, these are the default but set them to be sure */
serdev_device_set_baudrate(serdev, 9600);
serdev_device_set_flow_control(serdev, false);
get_version[0] = DELL_SOF(GET_CMD_LEN);
get_version[1] = CMD_GET_VERSION;
get_version[2] = dell_uart_checksum(get_version, 2);
ret = dell_uart_bl_command(dell_bl, get_version, GET_CMD_LEN, resp, MAX_RESP_LEN);
if (ret)
return dev_err_probe(dev, ret, "getting firmware version\n");
dev_dbg(dev, "Firmware version: %.*s\n", resp[RESP_LEN] - 3, resp + RESP_DATA);
/* Initialize bl_power to a known value */
ret = dell_uart_set_bl_power(dell_bl, BACKLIGHT_POWER_ON);
if (ret)
return ret;
ret = dell_uart_get_brightness(dell_bl);
if (ret < 0)
return ret;
props.type = BACKLIGHT_PLATFORM;
props.brightness = ret;
props.max_brightness = DELL_BL_MAX_BRIGHTNESS;
props.power = dell_bl->power;
dell_bl->bl = devm_backlight_device_register(dev, "dell_uart_backlight",
dev, dell_bl,
&dell_uart_backlight_ops,
&props);
return PTR_ERR_OR_ZERO(dell_bl->bl);
}
static struct serdev_device_driver dell_uart_bl_serdev_driver = {
.probe = dell_uart_bl_serdev_probe,
.driver = {
.name = KBUILD_MODNAME,
},
};
static int dell_uart_bl_pdev_probe(struct platform_device *pdev)
{
enum acpi_backlight_type bl_type;
struct serdev_device *serdev;
struct device *ctrl_dev;
int ret;
bl_type = acpi_video_get_backlight_type();
if (bl_type != acpi_backlight_dell_uart) {
dev_dbg(&pdev->dev, "Not loading (ACPI backlight type = %d)\n", bl_type);
return -ENODEV;
}
ctrl_dev = get_serdev_controller("DELL0501", NULL, 0, "serial0");
if (IS_ERR(ctrl_dev))
return PTR_ERR(ctrl_dev);
serdev = serdev_device_alloc(to_serdev_controller(ctrl_dev));
put_device(ctrl_dev);
if (!serdev)
return -ENOMEM;
ret = serdev_device_add(serdev);
if (ret) {
dev_err(&pdev->dev, "error %d adding serdev\n", ret);
serdev_device_put(serdev);
return ret;
}
ret = serdev_device_driver_register(&dell_uart_bl_serdev_driver);
if (ret)
goto err_remove_serdev;
/*
* serdev device <-> driver matching relies on OF or ACPI matches and
* neither is available here, manually bind the driver.
*/
ret = device_driver_attach(&dell_uart_bl_serdev_driver.driver, &serdev->dev);
if (ret)
goto err_unregister_serdev_driver;
/* So that dell_uart_bl_pdev_remove() can remove the serdev */
platform_set_drvdata(pdev, serdev);
return 0;
err_unregister_serdev_driver:
serdev_device_driver_unregister(&dell_uart_bl_serdev_driver);
err_remove_serdev:
serdev_device_remove(serdev);
return ret;
}
static void dell_uart_bl_pdev_remove(struct platform_device *pdev)
{
struct serdev_device *serdev = platform_get_drvdata(pdev);
serdev_device_driver_unregister(&dell_uart_bl_serdev_driver);
serdev_device_remove(serdev);
}
static struct platform_driver dell_uart_bl_pdev_driver = {
.probe = dell_uart_bl_pdev_probe,
.remove = dell_uart_bl_pdev_remove,
.driver = {
.name = "dell-uart-backlight",
},
};
module_platform_driver(dell_uart_bl_pdev_driver);
MODULE_ALIAS("platform:dell-uart-backlight");
MODULE_DESCRIPTION("Dell AIO Serial Backlight driver");
MODULE_AUTHOR("Hans de Goede <hansg@kernel.org>");
MODULE_LICENSE("GPL");
@@ -0,0 +1,186 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* WMI hotkeys support for Dell All-In-One series
*/
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/types.h>
#include <linux/input.h>
#include <linux/input/sparse-keymap.h>
#include <linux/acpi.h>
#include <linux/string.h>
MODULE_DESCRIPTION("WMI hotkeys driver for Dell All-In-One series");
MODULE_LICENSE("GPL");
#define EVENT_GUID1 "284A0E6B-380E-472A-921F-E52786257FB4"
#define EVENT_GUID2 "02314822-307C-4F66-BF0E-48AEAEB26CC8"
struct dell_wmi_event {
u16 length;
/* 0x000: A hot key pressed or an event occurred
* 0x00F: A sequence of hot keys are pressed */
u16 type;
u16 event[];
};
static const char *dell_wmi_aio_guids[] = {
EVENT_GUID1,
EVENT_GUID2,
NULL
};
MODULE_ALIAS("wmi:"EVENT_GUID1);
MODULE_ALIAS("wmi:"EVENT_GUID2);
static const struct key_entry dell_wmi_aio_keymap[] = {
{ KE_KEY, 0xc0, { KEY_VOLUMEUP } },
{ KE_KEY, 0xc1, { KEY_VOLUMEDOWN } },
{ KE_KEY, 0xe030, { KEY_VOLUMEUP } },
{ KE_KEY, 0xe02e, { KEY_VOLUMEDOWN } },
{ KE_KEY, 0xe020, { KEY_MUTE } },
{ KE_KEY, 0xe027, { KEY_DISPLAYTOGGLE } },
{ KE_KEY, 0xe006, { KEY_BRIGHTNESSUP } },
{ KE_KEY, 0xe005, { KEY_BRIGHTNESSDOWN } },
{ KE_KEY, 0xe00b, { KEY_SWITCHVIDEOMODE } },
{ KE_END, 0 }
};
static struct input_dev *dell_wmi_aio_input_dev;
/*
* The new WMI event data format will follow the dell_wmi_event structure
* So, we will check if the buffer matches the format
*/
static bool dell_wmi_aio_event_check(u8 *buffer, int length)
{
struct dell_wmi_event *event = (struct dell_wmi_event *)buffer;
if (event == NULL || length < 6)
return false;
if ((event->type == 0 || event->type == 0xf) &&
event->length >= 2)
return true;
return false;
}
static void dell_wmi_aio_notify(union acpi_object *obj, void *context)
{
struct dell_wmi_event *event;
if (obj) {
unsigned int scancode = 0;
switch (obj->type) {
case ACPI_TYPE_INTEGER:
/* Most All-In-One correctly return integer scancode */
scancode = obj->integer.value;
sparse_keymap_report_event(dell_wmi_aio_input_dev,
scancode, 1, true);
break;
case ACPI_TYPE_BUFFER:
if (dell_wmi_aio_event_check(obj->buffer.pointer,
obj->buffer.length)) {
event = (struct dell_wmi_event *)
obj->buffer.pointer;
scancode = event->event[0];
} else {
/* Broken machines return the scancode in a
buffer */
if (obj->buffer.pointer &&
obj->buffer.length > 0)
scancode = obj->buffer.pointer[0];
}
if (scancode)
sparse_keymap_report_event(
dell_wmi_aio_input_dev,
scancode, 1, true);
break;
}
}
}
static int __init dell_wmi_aio_input_setup(void)
{
int err;
dell_wmi_aio_input_dev = input_allocate_device();
if (!dell_wmi_aio_input_dev)
return -ENOMEM;
dell_wmi_aio_input_dev->name = "Dell AIO WMI hotkeys";
dell_wmi_aio_input_dev->phys = "wmi/input0";
dell_wmi_aio_input_dev->id.bustype = BUS_HOST;
err = sparse_keymap_setup(dell_wmi_aio_input_dev,
dell_wmi_aio_keymap, NULL);
if (err) {
pr_err("Unable to setup input device keymap\n");
goto err_free_dev;
}
err = input_register_device(dell_wmi_aio_input_dev);
if (err) {
pr_info("Unable to register input device\n");
goto err_free_dev;
}
return 0;
err_free_dev:
input_free_device(dell_wmi_aio_input_dev);
return err;
}
static const char *dell_wmi_aio_find(void)
{
int i;
for (i = 0; dell_wmi_aio_guids[i] != NULL; i++)
if (wmi_has_guid(dell_wmi_aio_guids[i]))
return dell_wmi_aio_guids[i];
return NULL;
}
static int __init dell_wmi_aio_init(void)
{
int err;
const char *guid;
guid = dell_wmi_aio_find();
if (!guid) {
pr_warn("No known WMI GUID found\n");
return -ENXIO;
}
err = dell_wmi_aio_input_setup();
if (err)
return err;
err = wmi_install_notify_handler(guid, dell_wmi_aio_notify, NULL);
if (err) {
pr_err("Unable to register notify handler - %d\n", err);
input_unregister_device(dell_wmi_aio_input_dev);
return err;
}
return 0;
}
static void __exit dell_wmi_aio_exit(void)
{
const char *guid;
guid = dell_wmi_aio_find();
wmi_remove_notify_handler(guid);
input_unregister_device(dell_wmi_aio_input_dev);
}
module_init(dell_wmi_aio_init);
module_exit(dell_wmi_aio_exit);
@@ -0,0 +1,862 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Dell WMI hotkeys
*
* Copyright (C) 2008 Red Hat <mjg@redhat.com>
* Copyright (C) 2014-2015 Pali Rohár <pali@kernel.org>
*
* Portions based on wistron_btns.c:
* Copyright (C) 2005 Miloslav Trmac <mitr@volny.cz>
* Copyright (C) 2005 Bernhard Rosenkraenzer <bero@arklinux.org>
* Copyright (C) 2005 Dmitry Torokhov <dtor@mail.ru>
*/
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/compiler_attributes.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/types.h>
#include <linux/input.h>
#include <linux/input/sparse-keymap.h>
#include <linux/acpi.h>
#include <linux/string.h>
#include <linux/dmi.h>
#include <linux/wmi.h>
#include <acpi/video.h>
#include "dell-smbios.h"
#include "dell-wmi-descriptor.h"
#include "dell-wmi-privacy.h"
MODULE_AUTHOR("Matthew Garrett <mjg@redhat.com>");
MODULE_AUTHOR("Pali Rohár <pali@kernel.org>");
MODULE_DESCRIPTION("Dell laptop WMI hotkeys driver");
MODULE_LICENSE("GPL");
#define DELL_EVENT_GUID "9DBB5994-A997-11DA-B012-B622A1EF5492"
static bool wmi_requires_smbios_request;
struct dell_wmi_priv {
struct input_dev *input_dev;
struct input_dev *tabletswitch_dev;
u32 interface_version;
};
static int __init dmi_matched(const struct dmi_system_id *dmi)
{
wmi_requires_smbios_request = 1;
return 1;
}
static const struct dmi_system_id dell_wmi_smbios_list[] __initconst = {
{
.callback = dmi_matched,
.ident = "Dell Inspiron M5110",
.matches = {
DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."),
DMI_MATCH(DMI_PRODUCT_NAME, "Inspiron M5110"),
},
},
{
.callback = dmi_matched,
.ident = "Dell Vostro V131",
.matches = {
DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."),
DMI_MATCH(DMI_PRODUCT_NAME, "Vostro V131"),
},
},
{ }
};
/*
* Keymap for WMI events of type 0x0000
*
* Certain keys are flagged as KE_IGNORE. All of these are either
* notifications (rather than requests for change) or are also sent
* via the keyboard controller so should not be sent again.
*/
static const struct key_entry dell_wmi_keymap_type_0000[] = {
{ KE_IGNORE, 0x003a, { KEY_CAPSLOCK } },
/* Audio mute toggle */
{ KE_KEY, 0x0109, { KEY_MUTE } },
/* Mic mute toggle */
{ KE_KEY, 0x0150, { KEY_MICMUTE } },
/* Meta key lock */
{ KE_IGNORE, 0xe000, { KEY_RIGHTMETA } },
/* Meta key unlock */
{ KE_IGNORE, 0xe001, { KEY_RIGHTMETA } },
/* Key code is followed by brightness level */
{ KE_KEY, 0xe005, { KEY_BRIGHTNESSDOWN } },
{ KE_KEY, 0xe006, { KEY_BRIGHTNESSUP } },
/* Battery health status button */
{ KE_KEY, 0xe007, { KEY_BATTERY } },
/* Radio devices state change, key code is followed by other values */
{ KE_IGNORE, 0xe008, { KEY_RFKILL } },
{ KE_KEY, 0xe009, { KEY_EJECTCD } },
/* Key code is followed by: next, active and attached devices */
{ KE_KEY, 0xe00b, { KEY_SWITCHVIDEOMODE } },
/* Key code is followed by keyboard illumination level */
{ KE_IGNORE, 0xe00c, { KEY_KBDILLUMTOGGLE } },
/* BIOS error detected */
{ KE_IGNORE, 0xe00d, { KEY_RESERVED } },
/* Battery was removed or inserted */
{ KE_IGNORE, 0xe00e, { KEY_RESERVED } },
/* Wifi Catcher */
{ KE_KEY, 0xe011, { KEY_WLAN } },
/* Ambient light sensor toggle */
{ KE_IGNORE, 0xe013, { KEY_RESERVED } },
{ KE_IGNORE, 0xe020, { KEY_MUTE } },
/* Unknown, defined in ACPI DSDT */
/* { KE_IGNORE, 0xe023, { KEY_RESERVED } }, */
/* Untested, Dell Instant Launch key on Inspiron 7520 */
/* { KE_IGNORE, 0xe024, { KEY_RESERVED } }, */
/* Dell Instant Launch key */
{ KE_KEY, 0xe025, { KEY_PROG4 } },
/* Audio panel key */
{ KE_IGNORE, 0xe026, { KEY_RESERVED } },
/* LCD Display On/Off Control key */
{ KE_KEY, 0xe027, { KEY_DISPLAYTOGGLE } },
/* Untested, Multimedia key on Dell Vostro 3560 */
/* { KE_IGNORE, 0xe028, { KEY_RESERVED } }, */
/* Dell Instant Launch key */
{ KE_KEY, 0xe029, { KEY_PROG4 } },
/* Untested, Windows Mobility Center button on Inspiron 7520 */
/* { KE_IGNORE, 0xe02a, { KEY_RESERVED } }, */
/* Unknown, defined in ACPI DSDT */
/* { KE_IGNORE, 0xe02b, { KEY_RESERVED } }, */
/* Untested, Dell Audio With Preset Switch button on Inspiron 7520 */
/* { KE_IGNORE, 0xe02c, { KEY_RESERVED } }, */
{ KE_IGNORE, 0xe02e, { KEY_VOLUMEDOWN } },
{ KE_IGNORE, 0xe030, { KEY_VOLUMEUP } },
{ KE_IGNORE, 0xe033, { KEY_KBDILLUMUP } },
{ KE_IGNORE, 0xe034, { KEY_KBDILLUMDOWN } },
{ KE_IGNORE, 0xe03a, { KEY_CAPSLOCK } },
/* NIC Link is Up */
{ KE_IGNORE, 0xe043, { KEY_RESERVED } },
/* NIC Link is Down */
{ KE_IGNORE, 0xe044, { KEY_RESERVED } },
/*
* This entry is very suspicious!
* Originally Matthew Garrett created this dell-wmi driver specially for
* "button with a picture of a battery" which has event code 0xe045.
* Later Mario Limonciello from Dell told us that event code 0xe045 is
* reported by Num Lock and should be ignored because key is send also
* by keyboard controller.
* So for now we will ignore this event to prevent potential double
* Num Lock key press.
*/
{ KE_IGNORE, 0xe045, { KEY_NUMLOCK } },
/* Scroll lock and also going to tablet mode on portable devices */
{ KE_IGNORE, 0xe046, { KEY_SCROLLLOCK } },
/* Untested, going from tablet mode on portable devices */
/* { KE_IGNORE, 0xe047, { KEY_RESERVED } }, */
/* Dell Support Center key */
{ KE_IGNORE, 0xe06e, { KEY_RESERVED } },
{ KE_IGNORE, 0xe0f7, { KEY_MUTE } },
{ KE_IGNORE, 0xe0f8, { KEY_VOLUMEDOWN } },
{ KE_IGNORE, 0xe0f9, { KEY_VOLUMEUP } },
};
struct dell_bios_keymap_entry {
u16 scancode;
u16 keycode;
};
struct dell_bios_hotkey_table {
struct dmi_header header;
struct dell_bios_keymap_entry keymap[];
};
struct dell_dmi_results {
int err;
int keymap_size;
struct key_entry *keymap;
};
/* Uninitialized entries here are KEY_RESERVED == 0. */
static const u16 bios_to_linux_keycode[256] = {
[0] = KEY_MEDIA,
[1] = KEY_NEXTSONG,
[2] = KEY_PLAYPAUSE,
[3] = KEY_PREVIOUSSONG,
[4] = KEY_STOPCD,
[5] = KEY_UNKNOWN,
[6] = KEY_UNKNOWN,
[7] = KEY_UNKNOWN,
[8] = KEY_WWW,
[9] = KEY_UNKNOWN,
[10] = KEY_VOLUMEDOWN,
[11] = KEY_MUTE,
[12] = KEY_VOLUMEUP,
[13] = KEY_UNKNOWN,
[14] = KEY_BATTERY,
[15] = KEY_EJECTCD,
[16] = KEY_UNKNOWN,
[17] = KEY_SLEEP,
[18] = KEY_PROG1,
[19] = KEY_BRIGHTNESSDOWN,
[20] = KEY_BRIGHTNESSUP,
[21] = KEY_BRIGHTNESS_AUTO,
[22] = KEY_KBDILLUMTOGGLE,
[23] = KEY_UNKNOWN,
[24] = KEY_SWITCHVIDEOMODE,
[25] = KEY_UNKNOWN,
[26] = KEY_UNKNOWN,
[27] = KEY_SWITCHVIDEOMODE,
[28] = KEY_UNKNOWN,
[29] = KEY_UNKNOWN,
[30] = KEY_PROG2,
[31] = KEY_UNKNOWN,
[32] = KEY_UNKNOWN,
[33] = KEY_UNKNOWN,
[34] = KEY_UNKNOWN,
[35] = KEY_UNKNOWN,
[36] = KEY_UNKNOWN,
[37] = KEY_UNKNOWN,
[38] = KEY_MICMUTE,
[255] = KEY_PROG3,
};
/*
* Keymap for WMI events of type 0x0010
*
* These are applied if the 0xB2 DMI hotkey table is present and doesn't
* override them.
*/
static const struct key_entry dell_wmi_keymap_type_0010[] = {
/* Fn-lock switched to function keys */
{ KE_IGNORE, 0x0, { KEY_RESERVED } },
/* Fn-lock switched to multimedia keys */
{ KE_IGNORE, 0x1, { KEY_RESERVED } },
/* Keyboard backlight change notification */
{ KE_IGNORE, 0x3f, { KEY_RESERVED } },
/* Backlight brightness level */
{ KE_KEY, 0x57, { KEY_BRIGHTNESSDOWN } },
{ KE_KEY, 0x58, { KEY_BRIGHTNESSUP } },
/*Speaker Mute*/
{ KE_KEY, 0x109, { KEY_MUTE} },
/* S2Idle screen off */
{ KE_IGNORE, 0x120, { KEY_RESERVED }},
/* Leaving S4 or S2Idle suspend */
{ KE_IGNORE, 0x130, { KEY_RESERVED }},
/* Entering S2Idle suspend */
{ KE_IGNORE, 0x140, { KEY_RESERVED }},
/* Mic mute */
{ KE_KEY, 0x150, { KEY_MICMUTE } },
/* Fn-lock */
{ KE_IGNORE, 0x151, { KEY_RESERVED } },
/* Change keyboard illumination */
{ KE_IGNORE, 0x152, { KEY_KBDILLUMTOGGLE } },
/*
* Radio disable (notify only -- there is no model for which the
* WMI event is supposed to trigger an action).
*/
{ KE_IGNORE, 0x153, { KEY_RFKILL } },
/* RGB keyboard backlight control */
{ KE_IGNORE, 0x154, { KEY_RESERVED } },
/*
* Stealth mode toggle. This will "disable all lights and sounds".
* The action is performed by the BIOS and EC; the WMI event is just
* a notification. On the XPS 13 9350, this is Fn+F7, and there's
* a BIOS setting to enable and disable the hotkey.
*/
{ KE_IGNORE, 0x155, { KEY_RESERVED } },
/* Rugged magnetic dock attach/detach events */
{ KE_IGNORE, 0x156, { KEY_RESERVED } },
{ KE_IGNORE, 0x157, { KEY_RESERVED } },
/* Rugged programmable (P1/P2/P3 keys) */
{ KE_KEY, 0x850, { KEY_PROG1 } },
{ KE_KEY, 0x851, { KEY_PROG2 } },
{ KE_KEY, 0x852, { KEY_PROG3 } },
/*
* Radio disable (notify only -- there is no model for which the
* WMI event is supposed to trigger an action).
*/
{ KE_IGNORE, 0xe008, { KEY_RFKILL } },
/* Fn-lock */
{ KE_IGNORE, 0xe035, { KEY_RESERVED } },
};
/*
* Keymap for WMI events of type 0x0011
*/
static const struct key_entry dell_wmi_keymap_type_0011[] = {
/* Reflex keyboard switch on 2n1 devices */
{ KE_IGNORE, 0xe070, { KEY_RESERVED } },
/* Battery unplugged */
{ KE_IGNORE, 0xfff0, { KEY_RESERVED } },
/* Battery inserted */
{ KE_IGNORE, 0xfff1, { KEY_RESERVED } },
/*
* Detachable keyboard detached / undocked
* Note SW_TABLET_MODE is already reported through the intel_vbtn
* driver for this, so we ignore it.
*/
{ KE_IGNORE, 0xfff2, { KEY_RESERVED } },
/* Detachable keyboard attached / docked */
{ KE_IGNORE, 0xfff3, { KEY_RESERVED } },
/* Keyboard backlight level changed */
{ KE_IGNORE, KBD_LED_OFF_TOKEN, { KEY_RESERVED } },
{ KE_IGNORE, KBD_LED_ON_TOKEN, { KEY_RESERVED } },
{ KE_IGNORE, KBD_LED_AUTO_TOKEN, { KEY_RESERVED } },
{ KE_IGNORE, KBD_LED_AUTO_25_TOKEN, { KEY_RESERVED } },
{ KE_IGNORE, KBD_LED_AUTO_50_TOKEN, { KEY_RESERVED } },
{ KE_IGNORE, KBD_LED_AUTO_75_TOKEN, { KEY_RESERVED } },
{ KE_IGNORE, KBD_LED_AUTO_100_TOKEN, { KEY_RESERVED } },
};
/*
* Keymap for WMI events of type 0x0012
* They are events with extended data
*/
static const struct key_entry dell_wmi_keymap_type_0012[] = {
/* Backlight brightness change event */
{ KE_IGNORE, 0x0003, { KEY_RESERVED } },
/*
* Electronic privacy screen toggled, extended data gives state,
* separate entries for on/off see handling in dell_wmi_process_key().
*/
{ KE_KEY, 0x000c, { KEY_EPRIVACY_SCREEN_OFF } },
{ KE_KEY, 0x000c, { KEY_EPRIVACY_SCREEN_ON } },
/* Ultra-performance mode switch request */
{ KE_IGNORE, 0x000d, { KEY_RESERVED } },
/* Fn-lock button pressed */
{ KE_IGNORE, 0xe035, { KEY_RESERVED } },
};
static void dell_wmi_switch_event(struct input_dev **subdev,
const char *devname,
int switchid,
int value)
{
if (!*subdev) {
struct input_dev *dev = input_allocate_device();
if (!dev) {
pr_warn("could not allocate device for %s\n", devname);
return;
}
__set_bit(EV_SW, (dev)->evbit);
__set_bit(switchid, (dev)->swbit);
(dev)->name = devname;
(dev)->id.bustype = BUS_HOST;
if (input_register_device(dev)) {
input_free_device(dev);
pr_warn("could not register device for %s\n", devname);
return;
}
*subdev = dev;
}
input_report_switch(*subdev, switchid, value);
input_sync(*subdev);
}
static int dell_wmi_process_key(struct wmi_device *wdev, int type, int code, __le16 *buffer,
int remaining)
{
struct dell_wmi_priv *priv = dev_get_drvdata(&wdev->dev);
const struct key_entry *key;
int used = 0;
int value = 1;
key = sparse_keymap_entry_from_scancode(priv->input_dev,
(type << 16) | code);
if (!key) {
pr_info("Unknown key with type 0x%04x and code 0x%04x pressed\n",
type, code);
return 0;
}
pr_debug("Key with type 0x%04x and code 0x%04x pressed\n", type, code);
/* Don't report brightness notifications that will also come via ACPI */
if ((key->keycode == KEY_BRIGHTNESSUP ||
key->keycode == KEY_BRIGHTNESSDOWN) &&
acpi_video_handles_brightness_key_presses())
return 0;
if (type == 0x0000 && code == 0xe025 && !wmi_requires_smbios_request)
return 0;
if (key->keycode == KEY_KBDILLUMTOGGLE) {
dell_laptop_call_notifier(
DELL_LAPTOP_KBD_BACKLIGHT_BRIGHTNESS_CHANGED, NULL);
} else if (type == 0x0011 && code == 0xe070 && remaining > 0) {
dell_wmi_switch_event(&priv->tabletswitch_dev,
"Dell tablet mode switch",
SW_TABLET_MODE, !le16_to_cpu(buffer[0]));
return 1;
} else if (type == 0x0012 && code == 0x000c && remaining > 0) {
/* Eprivacy toggle, switch to "on" key entry for on events */
if (le16_to_cpu(buffer[0]) == 2)
key++;
used = 1;
} else if (type == 0x0012 && code == 0x000d && remaining > 0) {
value = (le16_to_cpu(buffer[2]) == 2);
used = 1;
}
sparse_keymap_report_entry(priv->input_dev, key, value, true);
return used;
}
static void dell_wmi_notify(struct wmi_device *wdev, const struct wmi_buffer *buffer)
{
struct dell_wmi_priv *priv = dev_get_drvdata(&wdev->dev);
__le16 *buffer_entry, *buffer_end;
size_t buffer_size;
int len, i;
pr_debug("Received WMI event (%*ph)\n", (int)buffer->length, buffer->data);
buffer_entry = buffer->data;
buffer_size = buffer->length / 2;
buffer_end = buffer_entry + buffer_size;
/*
* BIOS/ACPI on devices with WMI interface version 0 does not clear
* buffer before filling it. So next time when BIOS/ACPI send WMI event
* which is smaller as previous then it contains garbage in buffer from
* previous event.
*
* BIOS/ACPI on devices with WMI interface version 1 clears buffer and
* sometimes send more events in buffer at one call.
*
* So to prevent reading garbage from buffer we will process only first
* one event on devices with WMI interface version 0.
*/
if (priv->interface_version == 0 && buffer_entry < buffer_end)
if (buffer_end > buffer_entry + le16_to_cpu(buffer_entry[0]) + 1)
buffer_end = buffer_entry + le16_to_cpu(buffer_entry[0]) + 1;
while (buffer_entry < buffer_end) {
len = le16_to_cpu(buffer_entry[0]);
if (len == 0)
break;
len++;
if (buffer_entry + len > buffer_end) {
pr_warn("Invalid length of WMI event\n");
break;
}
pr_debug("Process buffer (%*ph)\n", len*2, buffer_entry);
switch (le16_to_cpu(buffer_entry[1])) {
case 0x0000: /* One key pressed or event occurred */
if (len > 2)
dell_wmi_process_key(wdev, le16_to_cpu(buffer_entry[1]),
le16_to_cpu(buffer_entry[2]),
buffer_entry + 3,
len - 3);
/* Extended data is currently ignored */
break;
case 0x0010: /* Sequence of keys pressed */
case 0x0011: /* Sequence of events occurred */
for (i = 2; i < len; ++i)
i += dell_wmi_process_key(wdev, le16_to_cpu(buffer_entry[1]),
le16_to_cpu(buffer_entry[i]),
buffer_entry + i,
len - i - 1);
break;
case 0x0012:
if ((len > 4) && dell_privacy_process_event(le16_to_cpu(buffer_entry[1]),
le16_to_cpu(buffer_entry[3]),
le16_to_cpu(buffer_entry[4])))
/* dell_privacy_process_event has handled the event */;
else if (len > 2)
dell_wmi_process_key(wdev, le16_to_cpu(buffer_entry[1]),
le16_to_cpu(buffer_entry[2]),
buffer_entry + 3, len - 3);
break;
default: /* Unknown event */
pr_info("Unknown WMI event type 0x%x\n", le16_to_cpu(buffer_entry[1]));
break;
}
buffer_entry += len;
}
}
static bool have_scancode(u32 scancode, const struct key_entry *keymap, int len)
{
int i;
for (i = 0; i < len; i++)
if (keymap[i].code == scancode)
return true;
return false;
}
static void handle_dmi_entry(const struct dmi_header *dm, void *opaque)
{
struct dell_dmi_results *results = opaque;
struct dell_bios_hotkey_table *table;
int hotkey_num, i, pos = 0;
struct key_entry *keymap;
if (results->err || results->keymap)
return; /* We already found the hotkey table. */
/* The Dell hotkey table is type 0xB2. Scan until we find it. */
if (dm->type != 0xb2)
return;
table = container_of(dm, struct dell_bios_hotkey_table, header);
hotkey_num = (table->header.length -
sizeof(struct dell_bios_hotkey_table)) /
sizeof(struct dell_bios_keymap_entry);
if (hotkey_num < 1) {
/*
* Historically, dell-wmi would ignore a DMI entry of
* fewer than 7 bytes. Sizes between 4 and 8 bytes are
* nonsensical (both the header and all entries are 4
* bytes), so we approximate the old behavior by
* ignoring tables with fewer than one entry.
*/
return;
}
keymap = kzalloc_objs(struct key_entry, hotkey_num);
if (!keymap) {
results->err = -ENOMEM;
return;
}
for (i = 0; i < hotkey_num; i++) {
const struct dell_bios_keymap_entry *bios_entry =
&table->keymap[i];
/* Uninitialized entries are 0 aka KEY_RESERVED. */
u16 keycode = (bios_entry->keycode <
ARRAY_SIZE(bios_to_linux_keycode)) ?
bios_to_linux_keycode[bios_entry->keycode] :
(bios_entry->keycode == 0xffff ? KEY_UNKNOWN : KEY_RESERVED);
/*
* Log if we find an entry in the DMI table that we don't
* understand. If this happens, we should figure out what
* the entry means and add it to bios_to_linux_keycode.
*/
if (keycode == KEY_RESERVED) {
pr_info("firmware scancode 0x%x maps to unrecognized keycode 0x%x\n",
bios_entry->scancode, bios_entry->keycode);
continue;
}
if (keycode == KEY_KBDILLUMTOGGLE)
keymap[pos].type = KE_IGNORE;
else
keymap[pos].type = KE_KEY;
keymap[pos].code = bios_entry->scancode;
keymap[pos].keycode = keycode;
pos++;
}
results->keymap = keymap;
results->keymap_size = pos;
}
static int dell_wmi_input_setup(struct wmi_device *wdev)
{
struct dell_wmi_priv *priv = dev_get_drvdata(&wdev->dev);
struct dell_dmi_results dmi_results = {};
struct key_entry *keymap;
int err, i, pos = 0;
priv->input_dev = input_allocate_device();
if (!priv->input_dev)
return -ENOMEM;
priv->input_dev->name = "Dell WMI hotkeys";
priv->input_dev->id.bustype = BUS_HOST;
priv->input_dev->dev.parent = &wdev->dev;
if (dmi_walk(handle_dmi_entry, &dmi_results)) {
/*
* Historically, dell-wmi ignored dmi_walk errors. A failure
* is certainly surprising, but it probably just indicates
* a very old laptop.
*/
pr_warn("no DMI; using the old-style hotkey interface\n");
}
if (dmi_results.err) {
err = dmi_results.err;
goto err_free_dev;
}
keymap = kzalloc_objs(struct key_entry,
dmi_results.keymap_size + ARRAY_SIZE(dell_wmi_keymap_type_0000) + ARRAY_SIZE(dell_wmi_keymap_type_0010) + ARRAY_SIZE(dell_wmi_keymap_type_0011) + ARRAY_SIZE(dell_wmi_keymap_type_0012) + 1);
if (!keymap) {
kfree(dmi_results.keymap);
err = -ENOMEM;
goto err_free_dev;
}
/* Append table with events of type 0x0010 which comes from DMI */
for (i = 0; i < dmi_results.keymap_size; i++) {
keymap[pos] = dmi_results.keymap[i];
keymap[pos].code |= (0x0010 << 16);
pos++;
}
kfree(dmi_results.keymap);
/* Append table with extra events of type 0x0010 which are not in DMI */
for (i = 0; i < ARRAY_SIZE(dell_wmi_keymap_type_0010); i++) {
const struct key_entry *entry = &dell_wmi_keymap_type_0010[i];
/*
* Check if we've already found this scancode. This takes
* quadratic time, but it doesn't matter unless the list
* of extra keys gets very long.
*/
if (dmi_results.keymap_size &&
have_scancode(entry->code | (0x0010 << 16),
keymap, dmi_results.keymap_size)
)
continue;
keymap[pos] = *entry;
keymap[pos].code |= (0x0010 << 16);
pos++;
}
/* Append table with events of type 0x0011 */
for (i = 0; i < ARRAY_SIZE(dell_wmi_keymap_type_0011); i++) {
keymap[pos] = dell_wmi_keymap_type_0011[i];
keymap[pos].code |= (0x0011 << 16);
pos++;
}
/* Append table with events of type 0x0012 */
for (i = 0; i < ARRAY_SIZE(dell_wmi_keymap_type_0012); i++) {
keymap[pos] = dell_wmi_keymap_type_0012[i];
keymap[pos].code |= (0x0012 << 16);
pos++;
}
/*
* Now append also table with "legacy" events of type 0x0000. Some of
* them are reported also on laptops which have scancodes in DMI.
*/
for (i = 0; i < ARRAY_SIZE(dell_wmi_keymap_type_0000); i++) {
keymap[pos] = dell_wmi_keymap_type_0000[i];
pos++;
}
keymap[pos].type = KE_END;
err = sparse_keymap_setup(priv->input_dev, keymap, NULL);
/*
* Sparse keymap library makes a copy of keymap so we don't need the
* original one that was allocated.
*/
kfree(keymap);
if (err)
goto err_free_dev;
err = input_register_device(priv->input_dev);
if (err)
goto err_free_dev;
return 0;
err_free_dev:
input_free_device(priv->input_dev);
return err;
}
static void dell_wmi_input_destroy(struct wmi_device *wdev)
{
struct dell_wmi_priv *priv = dev_get_drvdata(&wdev->dev);
input_unregister_device(priv->input_dev);
if (priv->tabletswitch_dev)
input_unregister_device(priv->tabletswitch_dev);
}
/*
* According to Dell SMBIOS documentation:
*
* 17 3 Application Program Registration
*
* cbArg1 Application ID 1 = 0x00010000
* cbArg2 Application ID 2
* QUICKSET/DCP = 0x51534554 "QSET"
* ALS Driver = 0x416c7353 "AlsS"
* Latitude ON = 0x4c6f6e52 "LonR"
* cbArg3 Application version or revision number
* cbArg4 0 = Unregister application
* 1 = Register application
* cbRes1 Standard return codes (0, -1, -2)
*/
static int dell_wmi_events_set_enabled(bool enable)
{
struct calling_interface_buffer *buffer;
int ret;
buffer = kzalloc_obj(struct calling_interface_buffer);
if (!buffer)
return -ENOMEM;
buffer->cmd_class = CLASS_INFO;
buffer->cmd_select = SELECT_APP_REGISTRATION;
buffer->input[0] = 0x10000;
buffer->input[1] = 0x51534554;
buffer->input[3] = enable;
ret = dell_smbios_call(buffer);
if (ret == 0)
ret = buffer->output[0];
kfree(buffer);
return dell_smbios_error(ret);
}
static int dell_wmi_probe(struct wmi_device *wdev, const void *context)
{
struct dell_wmi_priv *priv;
int ret;
ret = dell_wmi_get_descriptor_valid();
if (ret)
return ret;
priv = devm_kzalloc(
&wdev->dev, sizeof(struct dell_wmi_priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
dev_set_drvdata(&wdev->dev, priv);
if (!dell_wmi_get_interface_version(&priv->interface_version))
return -EPROBE_DEFER;
return dell_wmi_input_setup(wdev);
}
static void dell_wmi_remove(struct wmi_device *wdev)
{
dell_wmi_input_destroy(wdev);
}
static const struct wmi_device_id dell_wmi_id_table[] = {
{ .guid_string = DELL_EVENT_GUID },
{ },
};
static struct wmi_driver dell_wmi_driver = {
.driver = {
.name = "dell-wmi",
},
.id_table = dell_wmi_id_table,
.min_event_size = sizeof(__le16),
.probe = dell_wmi_probe,
.remove = dell_wmi_remove,
.notify_new = dell_wmi_notify,
};
static int __init dell_wmi_init(void)
{
int err;
dmi_check_system(dell_wmi_smbios_list);
if (wmi_requires_smbios_request) {
err = dell_wmi_events_set_enabled(true);
if (err) {
pr_err("Failed to enable WMI events\n");
return err;
}
}
err = dell_privacy_register_driver();
if (err)
return err;
return wmi_driver_register(&dell_wmi_driver);
}
late_initcall(dell_wmi_init);
static void __exit dell_wmi_exit(void)
{
if (wmi_requires_smbios_request)
dell_wmi_events_set_enabled(false);
wmi_driver_unregister(&dell_wmi_driver);
dell_privacy_unregister_driver();
}
module_exit(dell_wmi_exit);
MODULE_DEVICE_TABLE(wmi, dell_wmi_id_table);
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,193 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Dell WMI descriptor driver
*
* Copyright (C) 2017 Dell Inc. All Rights Reserved.
*/
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/cleanup.h>
#include <linux/compiler_attributes.h>
#include <linux/list.h>
#include <linux/module.h>
#include <linux/types.h>
#include <linux/wmi.h>
#include "dell-wmi-descriptor.h"
#define DELL_WMI_DESCRIPTOR_GUID "8D9DDCBC-A997-11DA-B012-B622A1EF5492"
/*
* Descriptor buffer is 128 byte long and contains:
*
* Name Offset Length Value
* Vendor Signature 0 4 "DELL"
* Object Signature 4 4 " WMI"
* WMI Interface Version 8 4 <version>
* WMI buffer length 12 4 <length>
* WMI hotfix number 16 4 <hotfix>
*/
struct descriptor {
/* Both fields are NOT null-terminated */
char vendor_signature[4] __nonstring;
char object_signature[4] __nonstring;
__le32 interface_version;
__le32 buffer_length;
__le32 hotfix_number;
} __packed;
struct descriptor_priv {
struct list_head list;
u32 interface_version;
u32 size;
u32 hotfix;
};
static int descriptor_valid = -EPROBE_DEFER;
static LIST_HEAD(wmi_list);
static DEFINE_MUTEX(list_mutex);
int dell_wmi_get_descriptor_valid(void)
{
if (!wmi_has_guid(DELL_WMI_DESCRIPTOR_GUID))
return -ENODEV;
return descriptor_valid;
}
EXPORT_SYMBOL_GPL(dell_wmi_get_descriptor_valid);
bool dell_wmi_get_interface_version(u32 *version)
{
struct descriptor_priv *priv;
bool ret = false;
mutex_lock(&list_mutex);
priv = list_first_entry_or_null(&wmi_list,
struct descriptor_priv,
list);
if (priv) {
*version = priv->interface_version;
ret = true;
}
mutex_unlock(&list_mutex);
return ret;
}
EXPORT_SYMBOL_GPL(dell_wmi_get_interface_version);
bool dell_wmi_get_size(u32 *size)
{
struct descriptor_priv *priv;
bool ret = false;
mutex_lock(&list_mutex);
priv = list_first_entry_or_null(&wmi_list,
struct descriptor_priv,
list);
if (priv) {
*size = priv->size;
ret = true;
}
mutex_unlock(&list_mutex);
return ret;
}
EXPORT_SYMBOL_GPL(dell_wmi_get_size);
bool dell_wmi_get_hotfix(u32 *hotfix)
{
struct descriptor_priv *priv;
bool ret = false;
mutex_lock(&list_mutex);
priv = list_first_entry_or_null(&wmi_list,
struct descriptor_priv,
list);
if (priv) {
*hotfix = priv->hotfix;
ret = true;
}
mutex_unlock(&list_mutex);
return ret;
}
EXPORT_SYMBOL_GPL(dell_wmi_get_hotfix);
static int dell_wmi_descriptor_probe(struct wmi_device *wdev, const void *context)
{
struct descriptor_priv *priv;
struct wmi_buffer buffer;
int ret;
ret = wmidev_query_block(wdev, 0, &buffer, sizeof(struct descriptor));
if (ret < 0) {
descriptor_valid = ret;
return ret;
}
struct descriptor *desc __free(kfree) = buffer.data;
if (strncmp(desc->vendor_signature, "DELL", sizeof(desc->vendor_signature))) {
dev_err(&wdev->dev, "Dell descriptor buffer has invalid vendor signature (%4ph)\n",
desc->vendor_signature);
descriptor_valid = -ENOMSG;
return -ENOMSG;
}
if (strncmp(desc->object_signature, " WMI", sizeof(desc->object_signature))) {
dev_err(&wdev->dev, "Dell descriptor buffer has invalid object signature (%4ph)\n",
desc->object_signature);
descriptor_valid = -ENOMSG;
return -ENOMSG;
}
descriptor_valid = 0;
if (le32_to_cpu(desc->interface_version) > 1)
dev_warn(&wdev->dev, "Dell descriptor buffer has unknown version (%u)\n",
le32_to_cpu(desc->interface_version));
priv = devm_kzalloc(&wdev->dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
priv->interface_version = le32_to_cpu(desc->interface_version);
priv->size = le32_to_cpu(desc->buffer_length);
priv->hotfix = le32_to_cpu(desc->hotfix_number);
dev_set_drvdata(&wdev->dev, priv);
mutex_lock(&list_mutex);
list_add_tail(&priv->list, &wmi_list);
mutex_unlock(&list_mutex);
dev_dbg(&wdev->dev, "Detected Dell WMI interface version %lu, buffer size %lu, hotfix %lu\n",
(unsigned long) priv->interface_version,
(unsigned long) priv->size,
(unsigned long) priv->hotfix);
return ret;
}
static void dell_wmi_descriptor_remove(struct wmi_device *wdev)
{
struct descriptor_priv *priv = dev_get_drvdata(&wdev->dev);
mutex_lock(&list_mutex);
list_del(&priv->list);
mutex_unlock(&list_mutex);
}
static const struct wmi_device_id dell_wmi_descriptor_id_table[] = {
{ .guid_string = DELL_WMI_DESCRIPTOR_GUID },
{ },
};
static struct wmi_driver dell_wmi_descriptor_driver = {
.driver = {
.name = "dell-wmi-descriptor",
},
.probe = dell_wmi_descriptor_probe,
.remove = dell_wmi_descriptor_remove,
.id_table = dell_wmi_descriptor_id_table,
};
module_wmi_driver(dell_wmi_descriptor_driver);
MODULE_DEVICE_TABLE(wmi, dell_wmi_descriptor_id_table);
MODULE_AUTHOR("Mario Limonciello <mario.limonciello@outlook.com>");
MODULE_DESCRIPTION("Dell WMI descriptor driver");
MODULE_LICENSE("GPL");
@@ -0,0 +1,25 @@
/* SPDX-License-Identifier: GPL-2.0-only */
/*
* Dell WMI descriptor driver
*
* Copyright (c) 2017 Dell Inc.
*/
#ifndef _DELL_WMI_DESCRIPTOR_H_
#define _DELL_WMI_DESCRIPTOR_H_
#include <linux/wmi.h>
/* possible return values:
* -ENODEV: Descriptor GUID missing from WMI bus
* -EPROBE_DEFER: probing for dell-wmi-descriptor not yet run
* 0: valid descriptor, successfully probed
* < 0: invalid descriptor, don't probe dependent devices
*/
int dell_wmi_get_descriptor_valid(void);
bool dell_wmi_get_interface_version(u32 *version);
bool dell_wmi_get_size(u32 *size);
bool dell_wmi_get_hotfix(u32 *hotfix);
#endif
@@ -0,0 +1,186 @@
/*
* Copyright (C) 2010 Dell Inc.
* Louis Davis <louis_davis@dell.com>
* Jim Dailey <jim_dailey@dell.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as
* published by the Free Software Foundation.
*
*/
#include <linux/acpi.h>
#include <linux/leds.h>
#include <linux/slab.h>
#include <linux/module.h>
MODULE_AUTHOR("Louis Davis/Jim Dailey");
MODULE_DESCRIPTION("Dell LED Control Driver");
MODULE_LICENSE("GPL");
#define DELL_LED_BIOS_GUID "F6E4FE6E-909D-47cb-8BAB-C9F6F2F8D396"
MODULE_ALIAS("wmi:" DELL_LED_BIOS_GUID);
/* Error Result Codes: */
#define INVALID_DEVICE_ID 250
#define INVALID_PARAMETER 251
#define INVALID_BUFFER 252
#define INTERFACE_ERROR 253
#define UNSUPPORTED_COMMAND 254
#define UNSPECIFIED_ERROR 255
/* Device ID */
#define DEVICE_ID_PANEL_BACK 1
/* LED Commands */
#define CMD_LED_ON 16
#define CMD_LED_OFF 17
#define CMD_LED_BLINK 18
struct bios_args {
unsigned char length;
unsigned char result_code;
unsigned char device_id;
unsigned char command;
unsigned char on_time;
unsigned char off_time;
};
static int dell_led_perform_fn(u8 length, u8 result_code, u8 device_id,
u8 command, u8 on_time, u8 off_time)
{
struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL };
struct bios_args *bios_return;
struct acpi_buffer input;
union acpi_object *obj;
acpi_status status;
u8 return_code;
struct bios_args args = {
.length = length,
.result_code = result_code,
.device_id = device_id,
.command = command,
.on_time = on_time,
.off_time = off_time
};
input.length = sizeof(struct bios_args);
input.pointer = &args;
status = wmi_evaluate_method(DELL_LED_BIOS_GUID, 0, 1, &input, &output);
if (ACPI_FAILURE(status))
return status;
obj = output.pointer;
if (!obj)
return -EINVAL;
if (obj->type != ACPI_TYPE_BUFFER) {
kfree(obj);
return -EINVAL;
}
bios_return = ((struct bios_args *)obj->buffer.pointer);
return_code = bios_return->result_code;
kfree(obj);
return return_code;
}
static int led_on(void)
{
return dell_led_perform_fn(3, /* Length of command */
INTERFACE_ERROR, /* Init to INTERFACE_ERROR */
DEVICE_ID_PANEL_BACK, /* Device ID */
CMD_LED_ON, /* Command */
0, /* not used */
0); /* not used */
}
static int led_off(void)
{
return dell_led_perform_fn(3, /* Length of command */
INTERFACE_ERROR, /* Init to INTERFACE_ERROR */
DEVICE_ID_PANEL_BACK, /* Device ID */
CMD_LED_OFF, /* Command */
0, /* not used */
0); /* not used */
}
static int led_blink(unsigned char on_eighths, unsigned char off_eighths)
{
return dell_led_perform_fn(5, /* Length of command */
INTERFACE_ERROR, /* Init to INTERFACE_ERROR */
DEVICE_ID_PANEL_BACK, /* Device ID */
CMD_LED_BLINK, /* Command */
on_eighths, /* blink on in eigths of a second */
off_eighths); /* blink off in eights of a second */
}
static void dell_led_set(struct led_classdev *led_cdev,
enum led_brightness value)
{
if (value == LED_OFF)
led_off();
else
led_on();
}
static int dell_led_blink(struct led_classdev *led_cdev,
unsigned long *delay_on, unsigned long *delay_off)
{
unsigned long on_eighths;
unsigned long off_eighths;
/*
* The Dell LED delay is based on 125ms intervals.
* Need to round up to next interval.
*/
on_eighths = DIV_ROUND_UP(*delay_on, 125);
on_eighths = clamp_t(unsigned long, on_eighths, 1, 255);
*delay_on = on_eighths * 125;
off_eighths = DIV_ROUND_UP(*delay_off, 125);
off_eighths = clamp_t(unsigned long, off_eighths, 1, 255);
*delay_off = off_eighths * 125;
led_blink(on_eighths, off_eighths);
return 0;
}
static struct led_classdev dell_led = {
.name = "dell::lid",
.brightness = LED_OFF,
.max_brightness = 1,
.brightness_set = dell_led_set,
.blink_set = dell_led_blink,
.flags = LED_CORE_SUSPENDRESUME,
};
static int __init dell_led_init(void)
{
int error = 0;
if (!wmi_has_guid(DELL_LED_BIOS_GUID))
return -ENODEV;
error = led_off();
if (error != 0)
return -ENODEV;
return led_classdev_register(NULL, &dell_led);
}
static void __exit dell_led_exit(void)
{
led_classdev_unregister(&dell_led);
led_off();
}
module_init(dell_led_init);
module_exit(dell_led_exit);
@@ -0,0 +1,392 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Dell privacy notification driver
*
* Copyright (C) 2021 Dell Inc. All Rights Reserved.
*/
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/acpi.h>
#include <linux/bitops.h>
#include <linux/cleanup.h>
#include <linux/compiler_attributes.h>
#include <linux/input.h>
#include <linux/input/sparse-keymap.h>
#include <linux/list.h>
#include <linux/leds.h>
#include <linux/module.h>
#include <linux/types.h>
#include <linux/wmi.h>
#include "dell-wmi-privacy.h"
#define DELL_PRIVACY_GUID "6932965F-1671-4CEB-B988-D3AB0A901919"
#define MICROPHONE_STATUS BIT(0)
#define CAMERA_STATUS BIT(1)
#define DELL_PRIVACY_AUDIO_EVENT 0x1
#define DELL_PRIVACY_CAMERA_EVENT 0x2
#define led_to_priv(c) container_of(c, struct privacy_wmi_data, cdev)
/*
* Describes the Device State class exposed by BIOS which can be consumed by
* various applications interested in knowing the Privacy feature capabilities.
* class DeviceState
* {
* [key, read] string InstanceName;
* [read] boolean ReadOnly;
*
* [WmiDataId(1), read] uint32 DevicesSupported;
* 0 - None; 0x1 - Microphone; 0x2 - Camera; 0x4 - ePrivacy Screen
*
* [WmiDataId(2), read] uint32 CurrentState;
* 0 - Off; 1 - On; Bit0 - Microphone; Bit1 - Camera; Bit2 - ePrivacyScreen
* };
*/
struct device_state {
__le32 devices_supported;
__le32 current_state;
} __packed;
/*
* The wmi_list is used to store the privacy_priv struct with mutex protecting
*/
static LIST_HEAD(wmi_list);
static DEFINE_MUTEX(list_mutex);
struct privacy_wmi_data {
struct input_dev *input_dev;
struct wmi_device *wdev;
struct list_head list;
struct led_classdev cdev;
u32 features_present;
u32 last_status;
};
/* DELL Privacy Type */
enum dell_hardware_privacy_type {
DELL_PRIVACY_TYPE_AUDIO = 0,
DELL_PRIVACY_TYPE_CAMERA,
DELL_PRIVACY_TYPE_SCREEN,
DELL_PRIVACY_TYPE_MAX,
};
static const char * const privacy_types[DELL_PRIVACY_TYPE_MAX] = {
[DELL_PRIVACY_TYPE_AUDIO] = "Microphone",
[DELL_PRIVACY_TYPE_CAMERA] = "Camera Shutter",
[DELL_PRIVACY_TYPE_SCREEN] = "ePrivacy Screen",
};
/*
* Keymap for WMI privacy events of type 0x0012
*/
static const struct key_entry dell_wmi_keymap_type_0012[] = {
/* privacy mic mute */
{ KE_KEY, 0x0001, { KEY_MICMUTE } },
/* privacy camera mute */
{ KE_VSW, 0x0002, { SW_CAMERA_LENS_COVER } },
{ KE_END, 0},
};
bool dell_privacy_has_mic_mute(void)
{
struct privacy_wmi_data *priv;
mutex_lock(&list_mutex);
priv = list_first_entry_or_null(&wmi_list,
struct privacy_wmi_data,
list);
mutex_unlock(&list_mutex);
return priv && (priv->features_present & BIT(DELL_PRIVACY_TYPE_AUDIO));
}
EXPORT_SYMBOL_GPL(dell_privacy_has_mic_mute);
/*
* The flow of privacy event:
* 1) User presses key. HW does stuff with this key (timeout is started)
* 2) WMI event is emitted from BIOS
* 3) WMI event is received by dell-privacy
* 4) KEY_MICMUTE emitted from dell-privacy
* 5) Userland picks up key and modifies kcontrol for SW mute
* 6) Codec kernel driver catches and calls ledtrig_audio_set which will call
* led_set_brightness() on the LED registered by dell_privacy_leds_setup()
* 7) dell-privacy notifies EC, the timeout is cancelled and the HW mute activates.
* If the EC is not notified then the HW mic mute will activate when the timeout
* triggers, just a bit later than with the active ack.
*/
bool dell_privacy_process_event(int type, int code, int status)
{
struct privacy_wmi_data *priv;
const struct key_entry *key;
bool ret = false;
mutex_lock(&list_mutex);
priv = list_first_entry_or_null(&wmi_list,
struct privacy_wmi_data,
list);
if (!priv)
goto error;
key = sparse_keymap_entry_from_scancode(priv->input_dev, (type << 16) | code);
if (!key) {
dev_warn(&priv->wdev->dev, "Unknown key with type 0x%04x and code 0x%04x pressed\n",
type, code);
goto error;
}
dev_dbg(&priv->wdev->dev, "Key with type 0x%04x and code 0x%04x pressed\n", type, code);
switch (code) {
case DELL_PRIVACY_AUDIO_EVENT: /* Mic mute */
priv->last_status = status;
sparse_keymap_report_entry(priv->input_dev, key, 1, true);
ret = true;
break;
case DELL_PRIVACY_CAMERA_EVENT: /* Camera mute */
priv->last_status = status;
sparse_keymap_report_entry(priv->input_dev, key, !(status & CAMERA_STATUS), false);
ret = true;
break;
default:
dev_dbg(&priv->wdev->dev, "unknown event type 0x%04x 0x%04x\n", type, code);
}
error:
mutex_unlock(&list_mutex);
return ret;
}
static ssize_t dell_privacy_supported_type_show(struct device *dev,
struct device_attribute *attr,
char *buf)
{
struct privacy_wmi_data *priv = dev_get_drvdata(dev);
enum dell_hardware_privacy_type type;
u32 privacy_list;
int len = 0;
privacy_list = priv->features_present;
for (type = DELL_PRIVACY_TYPE_AUDIO; type < DELL_PRIVACY_TYPE_MAX; type++) {
if (privacy_list & BIT(type))
len += sysfs_emit_at(buf, len, "[%s] [supported]\n", privacy_types[type]);
else
len += sysfs_emit_at(buf, len, "[%s] [unsupported]\n", privacy_types[type]);
}
return len;
}
static ssize_t dell_privacy_current_state_show(struct device *dev,
struct device_attribute *attr,
char *buf)
{
struct privacy_wmi_data *priv = dev_get_drvdata(dev);
u32 privacy_supported = priv->features_present;
enum dell_hardware_privacy_type type;
u32 privacy_state = priv->last_status;
int len = 0;
for (type = DELL_PRIVACY_TYPE_AUDIO; type < DELL_PRIVACY_TYPE_MAX; type++) {
if (privacy_supported & BIT(type)) {
if (privacy_state & BIT(type))
len += sysfs_emit_at(buf, len, "[%s] [unmuted]\n", privacy_types[type]);
else
len += sysfs_emit_at(buf, len, "[%s] [muted]\n", privacy_types[type]);
}
}
return len;
}
static DEVICE_ATTR_RO(dell_privacy_supported_type);
static DEVICE_ATTR_RO(dell_privacy_current_state);
static struct attribute *privacy_attrs[] = {
&dev_attr_dell_privacy_supported_type.attr,
&dev_attr_dell_privacy_current_state.attr,
NULL,
};
ATTRIBUTE_GROUPS(privacy);
static int get_current_status(struct wmi_device *wdev)
{
struct privacy_wmi_data *priv = dev_get_drvdata(&wdev->dev);
struct wmi_buffer buffer;
int ret;
if (!priv) {
dev_err(&wdev->dev, "dell privacy priv is NULL\n");
return -EINVAL;
}
/* check privacy support features and device states */
ret = wmidev_query_block(wdev, 0, &buffer, sizeof(struct device_state));
if (ret < 0)
return ret;
struct device_state *state __free(kfree) = buffer.data;
priv->features_present = le32_to_cpu(state->devices_supported);
priv->last_status = le32_to_cpu(state->current_state);
return 0;
}
static int dell_privacy_micmute_led_set(struct led_classdev *led_cdev,
enum led_brightness brightness)
{
struct privacy_wmi_data *priv = led_to_priv(led_cdev);
static char *acpi_method = (char *)"ECAK";
acpi_status status;
acpi_handle handle;
handle = ec_get_handle();
if (!handle)
return -EIO;
if (!acpi_has_method(handle, acpi_method))
return -EIO;
status = acpi_evaluate_object(handle, acpi_method, NULL, NULL);
if (ACPI_FAILURE(status)) {
dev_err(&priv->wdev->dev, "Error setting privacy EC ack value: %s\n",
acpi_format_exception(status));
return -EIO;
}
return 0;
}
/*
* Pressing the mute key activates a time delayed circuit to physically cut
* off the mute. The LED is in the same circuit, so it reflects the true
* state of the HW mute. The reason for the EC "ack" is so that software
* can first invoke a SW mute before the HW circuit is cut off. Without SW
* cutting this off first does not affect the time delayed muting or status
* of the LED but there is a possibility of a "popping" noise.
*
* If the EC receives the SW ack, the circuit will be activated before the
* delay completed.
*
* Exposing as an LED device allows the codec drivers notification path to
* EC ACK to work
*/
static int dell_privacy_leds_setup(struct device *dev)
{
struct privacy_wmi_data *priv = dev_get_drvdata(dev);
priv->cdev.name = "dell-privacy::micmute";
priv->cdev.max_brightness = 1;
priv->cdev.brightness_set_blocking = dell_privacy_micmute_led_set;
priv->cdev.default_trigger = "audio-micmute";
return devm_led_classdev_register(dev, &priv->cdev);
}
static int dell_privacy_wmi_probe(struct wmi_device *wdev, const void *context)
{
struct privacy_wmi_data *priv;
struct key_entry *keymap;
int ret, i, j;
priv = devm_kzalloc(&wdev->dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
dev_set_drvdata(&wdev->dev, priv);
priv->wdev = wdev;
ret = get_current_status(priv->wdev);
if (ret)
return ret;
/* create evdev passing interface */
priv->input_dev = devm_input_allocate_device(&wdev->dev);
if (!priv->input_dev)
return -ENOMEM;
/* remap the wmi keymap event to new keymap */
keymap = kzalloc_objs(struct key_entry,
ARRAY_SIZE(dell_wmi_keymap_type_0012));
if (!keymap)
return -ENOMEM;
/* remap the keymap code with Dell privacy key type 0x12 as prefix
* KEY_MICMUTE scancode will be reported as 0x120001
*/
for (i = 0, j = 0; i < ARRAY_SIZE(dell_wmi_keymap_type_0012); i++) {
/*
* Unlike keys where only presses matter, userspace may act
* on switches in both of their positions. Only register
* SW_CAMERA_LENS_COVER if it is actually there.
*/
if (dell_wmi_keymap_type_0012[i].type == KE_VSW &&
dell_wmi_keymap_type_0012[i].sw.code == SW_CAMERA_LENS_COVER &&
!(priv->features_present & BIT(DELL_PRIVACY_TYPE_CAMERA)))
continue;
keymap[j] = dell_wmi_keymap_type_0012[i];
keymap[j].code |= (0x0012 << 16);
j++;
}
ret = sparse_keymap_setup(priv->input_dev, keymap, NULL);
kfree(keymap);
if (ret)
return ret;
priv->input_dev->dev.parent = &wdev->dev;
priv->input_dev->name = "Dell Privacy Driver";
priv->input_dev->id.bustype = BUS_HOST;
/* Report initial camera-cover status */
if (priv->features_present & BIT(DELL_PRIVACY_TYPE_CAMERA))
input_report_switch(priv->input_dev, SW_CAMERA_LENS_COVER,
!(priv->last_status & CAMERA_STATUS));
ret = input_register_device(priv->input_dev);
if (ret)
return ret;
if (priv->features_present & BIT(DELL_PRIVACY_TYPE_AUDIO)) {
ret = dell_privacy_leds_setup(&priv->wdev->dev);
if (ret)
return ret;
}
mutex_lock(&list_mutex);
list_add_tail(&priv->list, &wmi_list);
mutex_unlock(&list_mutex);
return 0;
}
static void dell_privacy_wmi_remove(struct wmi_device *wdev)
{
struct privacy_wmi_data *priv = dev_get_drvdata(&wdev->dev);
mutex_lock(&list_mutex);
list_del(&priv->list);
mutex_unlock(&list_mutex);
}
static const struct wmi_device_id dell_wmi_privacy_wmi_id_table[] = {
{ .guid_string = DELL_PRIVACY_GUID },
{ },
};
static struct wmi_driver dell_privacy_wmi_driver = {
.driver = {
.name = "dell-privacy",
.dev_groups = privacy_groups,
},
.probe = dell_privacy_wmi_probe,
.remove = dell_privacy_wmi_remove,
.id_table = dell_wmi_privacy_wmi_id_table,
};
int dell_privacy_register_driver(void)
{
return wmi_driver_register(&dell_privacy_wmi_driver);
}
void dell_privacy_unregister_driver(void)
{
wmi_driver_unregister(&dell_privacy_wmi_driver);
}
@@ -0,0 +1,36 @@
/* SPDX-License-Identifier: GPL-2.0-only */
/*
* Dell privacy notification driver
*
* Copyright (C) 2021 Dell Inc. All Rights Reserved.
*/
#ifndef _DELL_PRIVACY_WMI_H_
#define _DELL_PRIVACY_WMI_H_
#if IS_ENABLED(CONFIG_DELL_WMI_PRIVACY)
bool dell_privacy_has_mic_mute(void);
bool dell_privacy_process_event(int type, int code, int status);
int dell_privacy_register_driver(void);
void dell_privacy_unregister_driver(void);
#else /* CONFIG_DELL_WMI_PRIVACY */
static inline bool dell_privacy_has_mic_mute(void)
{
return false;
}
static inline bool dell_privacy_process_event(int type, int code, int status)
{
return false;
}
static inline int dell_privacy_register_driver(void)
{
return 0;
}
static inline void dell_privacy_unregister_driver(void)
{
}
#endif /* CONFIG_DELL_WMI_PRIVACY */
#endif
@@ -0,0 +1,8 @@
obj-$(CONFIG_DELL_WMI_SYSMAN) += dell-wmi-sysman.o
dell-wmi-sysman-y := sysman.o \
enum-attributes.o \
int-attributes.o \
string-attributes.o \
passobj-attributes.o \
biosattr-interface.o \
passwordattr-interface.o
@@ -0,0 +1,185 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Functions corresponding to SET methods under BIOS attributes interface GUID for use
* with dell-wmi-sysman
*
* Copyright (c) 2020 Dell Inc.
*/
#include <linux/wmi.h>
#include "dell-wmi-sysman.h"
#define SETDEFAULTVALUES_METHOD_ID 0x02
#define SETBIOSDEFAULTS_METHOD_ID 0x03
#define SETATTRIBUTE_METHOD_ID 0x04
static int call_biosattributes_interface(struct wmi_device *wdev, u8 *in_args,
size_t size, int method_id)
{
struct acpi_buffer output = {ACPI_ALLOCATE_BUFFER, NULL};
struct acpi_buffer input;
union acpi_object *obj;
acpi_status status;
int ret = -EIO;
input.length = (acpi_size) size;
input.pointer = in_args;
status = wmidev_evaluate_method(wdev, 0, method_id, &input, &output);
if (ACPI_FAILURE(status))
return -EIO;
obj = (union acpi_object *)output.pointer;
if (obj->type == ACPI_TYPE_INTEGER)
ret = obj->integer.value;
if (wmi_priv.pending_changes == 0) {
wmi_priv.pending_changes = 1;
/* let userland know it may need to check reboot pending again */
kobject_uevent(&wmi_priv.class_dev->kobj, KOBJ_CHANGE);
}
kfree(output.pointer);
return map_wmi_error(ret);
}
/**
* set_attribute() - Update an attribute value
* @a_name: The attribute name
* @a_value: The attribute value
*
* Sets an attribute to new value
*/
int set_attribute(const char *a_name, const char *a_value)
{
size_t security_area_size, buffer_size;
size_t a_name_size, a_value_size;
u8 *buffer = NULL, *start;
int ret;
mutex_lock(&wmi_priv.mutex);
if (!wmi_priv.bios_attr_wdev) {
ret = -ENODEV;
goto out;
}
/* build/calculate buffer */
security_area_size = calculate_security_buffer(wmi_priv.current_admin_password);
a_name_size = calculate_string_buffer(a_name);
a_value_size = calculate_string_buffer(a_value);
buffer_size = security_area_size + a_name_size + a_value_size;
buffer = kzalloc(buffer_size, GFP_KERNEL);
if (!buffer) {
ret = -ENOMEM;
goto out;
}
/* build security area */
populate_security_buffer(buffer, wmi_priv.current_admin_password);
/* build variables to set */
start = buffer + security_area_size;
ret = populate_string_buffer(start, a_name_size, a_name);
if (ret < 0)
goto out;
start += ret;
ret = populate_string_buffer(start, a_value_size, a_value);
if (ret < 0)
goto out;
print_hex_dump_bytes("set attribute data: ", DUMP_PREFIX_NONE, buffer, buffer_size);
ret = call_biosattributes_interface(wmi_priv.bios_attr_wdev,
buffer, buffer_size,
SETATTRIBUTE_METHOD_ID);
if (ret == -EOPNOTSUPP)
dev_err(&wmi_priv.bios_attr_wdev->dev, "admin password must be configured\n");
else if (ret == -EACCES)
dev_err(&wmi_priv.bios_attr_wdev->dev, "invalid password\n");
out:
kfree(buffer);
mutex_unlock(&wmi_priv.mutex);
return ret;
}
/**
* set_bios_defaults() - Resets BIOS defaults
* @deftype: the type of BIOS value reset to issue.
*
* Resets BIOS defaults
*/
int set_bios_defaults(u8 deftype)
{
size_t security_area_size, buffer_size;
size_t integer_area_size = sizeof(u8);
u8 *buffer = NULL;
u8 *defaultType;
int ret;
mutex_lock(&wmi_priv.mutex);
if (!wmi_priv.bios_attr_wdev) {
ret = -ENODEV;
goto out;
}
security_area_size = calculate_security_buffer(wmi_priv.current_admin_password);
buffer_size = security_area_size + integer_area_size;
buffer = kzalloc(buffer_size, GFP_KERNEL);
if (!buffer) {
ret = -ENOMEM;
goto out;
}
/* build security area */
populate_security_buffer(buffer, wmi_priv.current_admin_password);
defaultType = buffer + security_area_size;
*defaultType = deftype;
ret = call_biosattributes_interface(wmi_priv.bios_attr_wdev, buffer, buffer_size,
SETBIOSDEFAULTS_METHOD_ID);
if (ret)
dev_err(&wmi_priv.bios_attr_wdev->dev, "reset BIOS defaults failed: %d\n", ret);
kfree(buffer);
out:
mutex_unlock(&wmi_priv.mutex);
return ret;
}
static int bios_attr_set_interface_probe(struct wmi_device *wdev, const void *context)
{
mutex_lock(&wmi_priv.mutex);
wmi_priv.bios_attr_wdev = wdev;
mutex_unlock(&wmi_priv.mutex);
return 0;
}
static void bios_attr_set_interface_remove(struct wmi_device *wdev)
{
mutex_lock(&wmi_priv.mutex);
wmi_priv.bios_attr_wdev = NULL;
mutex_unlock(&wmi_priv.mutex);
}
static const struct wmi_device_id bios_attr_set_interface_id_table[] = {
{ .guid_string = DELL_WMI_BIOS_ATTRIBUTES_INTERFACE_GUID },
{ },
};
static struct wmi_driver bios_attr_set_interface_driver = {
.driver = {
.name = DRIVER_NAME
},
.probe = bios_attr_set_interface_probe,
.remove = bios_attr_set_interface_remove,
.id_table = bios_attr_set_interface_id_table,
};
int init_bios_attr_set_interface(void)
{
return wmi_driver_register(&bios_attr_set_interface_driver);
}
void exit_bios_attr_set_interface(void)
{
wmi_driver_unregister(&bios_attr_set_interface_driver);
}
MODULE_DEVICE_TABLE(wmi, bios_attr_set_interface_id_table);
@@ -0,0 +1,199 @@
/* SPDX-License-Identifier: GPL-2.0
* Definitions for kernel modules using Dell WMI System Management Driver
*
* Copyright (c) 2020 Dell Inc.
*/
#ifndef _DELL_WMI_BIOS_ATTR_H_
#define _DELL_WMI_BIOS_ATTR_H_
#include <linux/wmi.h>
#include <linux/device.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/capability.h>
#define DRIVER_NAME "dell-wmi-sysman"
#define MAX_BUFF 512
#define DELL_WMI_BIOS_ENUMERATION_ATTRIBUTE_GUID "F1DDEE52-063C-4784-A11E-8A06684B9BF5"
#define DELL_WMI_BIOS_INTEGER_ATTRIBUTE_GUID "F1DDEE52-063C-4784-A11E-8A06684B9BFA"
#define DELL_WMI_BIOS_STRING_ATTRIBUTE_GUID "F1DDEE52-063C-4784-A11E-8A06684B9BF9"
#define DELL_WMI_BIOS_PASSOBJ_ATTRIBUTE_GUID "0894B8D6-44A6-4719-97D7-6AD24108BFD4"
#define DELL_WMI_BIOS_ATTRIBUTES_INTERFACE_GUID "F1DDEE52-063C-4784-A11E-8A06684B9BF4"
#define DELL_WMI_BIOS_PASSWORD_INTERFACE_GUID "70FE8229-D03B-4214-A1C6-1F884B1A892A"
struct enumeration_data {
struct kobject *attr_name_kobj;
char display_name_language_code[MAX_BUFF];
char dell_value_modifier[MAX_BUFF];
char possible_values[MAX_BUFF];
char attribute_name[MAX_BUFF];
char default_value[MAX_BUFF];
char dell_modifier[MAX_BUFF];
char display_name[MAX_BUFF];
};
struct integer_data {
struct kobject *attr_name_kobj;
char display_name_language_code[MAX_BUFF];
char attribute_name[MAX_BUFF];
char dell_modifier[MAX_BUFF];
char display_name[MAX_BUFF];
int scalar_increment;
int default_value;
int min_value;
int max_value;
};
struct str_data {
struct kobject *attr_name_kobj;
char display_name_language_code[MAX_BUFF];
char attribute_name[MAX_BUFF];
char display_name[MAX_BUFF];
char default_value[MAX_BUFF];
char dell_modifier[MAX_BUFF];
int min_length;
int max_length;
};
struct po_data {
struct kobject *attr_name_kobj;
char attribute_name[MAX_BUFF];
int min_password_length;
int max_password_length;
};
struct wmi_sysman_priv {
char current_admin_password[MAX_BUFF];
char current_system_password[MAX_BUFF];
struct wmi_device *password_attr_wdev;
struct wmi_device *bios_attr_wdev;
struct kset *authentication_dir_kset;
struct kset *main_dir_kset;
struct device *class_dev;
struct enumeration_data *enumeration_data;
int enumeration_instances_count;
struct integer_data *integer_data;
int integer_instances_count;
struct str_data *str_data;
int str_instances_count;
struct po_data *po_data;
int po_instances_count;
bool pending_changes;
struct mutex mutex;
};
/* global structure used by multiple WMI interfaces */
extern struct wmi_sysman_priv wmi_priv;
enum { ENUM, INT, STR, PO };
#define ENUM_MIN_ELEMENTS 8
#define INT_MIN_ELEMENTS 9
#define STR_MIN_ELEMENTS 8
#define PO_MIN_ELEMENTS 4
enum {
ATTR_NAME,
DISPL_NAME_LANG_CODE,
DISPLAY_NAME,
DEFAULT_VAL,
CURRENT_VAL,
MODIFIER
};
#define get_instance_id(type) \
static int get_##type##_instance_id(struct kobject *kobj) \
{ \
int i; \
for (i = 0; i <= wmi_priv.type##_instances_count; i++) { \
if (!(strcmp(kobj->name, wmi_priv.type##_data[i].attribute_name)))\
return i; \
} \
return -EIO; \
}
#define attribute_s_property_show(name, type) \
static ssize_t name##_show(struct kobject *kobj, struct kobj_attribute *attr, \
char *buf) \
{ \
int i = get_##type##_instance_id(kobj); \
if (i >= 0) \
return sprintf(buf, "%s\n", wmi_priv.type##_data[i].name); \
return 0; \
}
#define attribute_n_property_show(name, type) \
static ssize_t name##_show(struct kobject *kobj, struct kobj_attribute *attr, \
char *buf) \
{ \
int i = get_##type##_instance_id(kobj); \
if (i >= 0) \
return sprintf(buf, "%d\n", wmi_priv.type##_data[i].name); \
return 0; \
}
#define attribute_property_store(curr_val, type) \
static ssize_t curr_val##_store(struct kobject *kobj, \
struct kobj_attribute *attr, \
const char *buf, size_t count) \
{ \
char *p, *buf_cp; \
int i, ret = -EIO; \
buf_cp = kstrdup(buf, GFP_KERNEL); \
if (!buf_cp) \
return -ENOMEM; \
p = memchr(buf_cp, '\n', count); \
\
if (p != NULL) \
*p = '\0'; \
i = get_##type##_instance_id(kobj); \
if (i >= 0) \
ret = validate_##type##_input(i, buf_cp); \
if (!ret) \
ret = set_attribute(kobj->name, buf_cp); \
kfree(buf_cp); \
return ret ? ret : count; \
}
#define check_property_type(attr, prop, valuetype) \
(attr##_obj[prop].type != valuetype)
union acpi_object *get_wmiobj_pointer(int instance_id, const char *guid_string);
int get_instance_count(const char *guid_string);
void strlcpy_attr(char *dest, char *src);
int populate_enum_data(union acpi_object *enumeration_obj, int instance_id,
struct kobject *attr_name_kobj, u32 enum_property_count);
int alloc_enum_data(void);
void exit_enum_attributes(void);
int populate_int_data(union acpi_object *integer_obj, int instance_id,
struct kobject *attr_name_kobj);
int alloc_int_data(void);
void exit_int_attributes(void);
int populate_str_data(union acpi_object *str_obj, int instance_id, struct kobject *attr_name_kobj);
int alloc_str_data(void);
void exit_str_attributes(void);
int populate_po_data(union acpi_object *po_obj, int instance_id, struct kobject *attr_name_kobj);
int alloc_po_data(void);
void exit_po_attributes(void);
int set_attribute(const char *a_name, const char *a_value);
int set_bios_defaults(u8 defType);
void exit_bios_attr_set_interface(void);
int init_bios_attr_set_interface(void);
int map_wmi_error(int error_code);
size_t calculate_string_buffer(const char *str);
size_t calculate_security_buffer(const char *authentication);
void populate_security_buffer(u8 *buffer, const char *authentication);
ssize_t populate_string_buffer(u8 *buffer, size_t buffer_len, const char *str);
int set_new_password(const char *password_type, const char *new);
int init_bios_attr_pass_interface(void);
void exit_bios_attr_pass_interface(void);
#endif
@@ -0,0 +1,244 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Functions corresponding to enumeration type attributes under
* BIOS Enumeration GUID for use with dell-wmi-sysman
*
* Copyright (c) 2020 Dell Inc.
*/
#include <linux/bug.h>
#include "dell-wmi-sysman.h"
get_instance_id(enumeration);
static int append_enum_string(char *dest, const char *src)
{
size_t dest_len = strlen(dest);
ssize_t copied;
if (WARN_ON_ONCE(dest_len >= MAX_BUFF))
return -EINVAL;
copied = strscpy(dest + dest_len, src, MAX_BUFF - dest_len);
if (copied < 0)
return -EINVAL;
dest_len += copied;
copied = strscpy(dest + dest_len, ";", MAX_BUFF - dest_len);
if (copied < 0)
return -EINVAL;
return 0;
}
static ssize_t current_value_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf)
{
int instance_id = get_enumeration_instance_id(kobj);
union acpi_object *obj;
ssize_t ret;
if (instance_id < 0)
return instance_id;
/* need to use specific instance_id and guid combination to get right data */
obj = get_wmiobj_pointer(instance_id, DELL_WMI_BIOS_ENUMERATION_ATTRIBUTE_GUID);
if (!obj)
return -EIO;
if (obj->type != ACPI_TYPE_PACKAGE || obj->package.count < ENUM_MIN_ELEMENTS ||
obj->package.elements[CURRENT_VAL].type != ACPI_TYPE_STRING) {
kfree(obj);
return -EIO;
}
ret = snprintf(buf, PAGE_SIZE, "%s\n", obj->package.elements[CURRENT_VAL].string.pointer);
kfree(obj);
return ret;
}
/**
* validate_enumeration_input() - Validate input of current_value against possible values
* @instance_id: The instance on which input is validated
* @buf: Input value
*/
static int validate_enumeration_input(int instance_id, const char *buf)
{
char *options, *tmp, *p;
int ret = -EINVAL;
options = tmp = kstrdup(wmi_priv.enumeration_data[instance_id].possible_values,
GFP_KERNEL);
if (!options)
return -ENOMEM;
while ((p = strsep(&options, ";")) != NULL) {
if (!*p)
continue;
if (!strcasecmp(p, buf)) {
ret = 0;
break;
}
}
kfree(tmp);
return ret;
}
attribute_s_property_show(display_name_language_code, enumeration);
static struct kobj_attribute displ_langcode =
__ATTR_RO(display_name_language_code);
attribute_s_property_show(display_name, enumeration);
static struct kobj_attribute displ_name =
__ATTR_RO(display_name);
attribute_s_property_show(default_value, enumeration);
static struct kobj_attribute default_val =
__ATTR_RO(default_value);
attribute_property_store(current_value, enumeration);
static struct kobj_attribute current_val =
__ATTR_RW_MODE(current_value, 0600);
attribute_s_property_show(dell_modifier, enumeration);
static struct kobj_attribute modifier =
__ATTR_RO(dell_modifier);
attribute_s_property_show(dell_value_modifier, enumeration);
static struct kobj_attribute value_modfr =
__ATTR_RO(dell_value_modifier);
attribute_s_property_show(possible_values, enumeration);
static struct kobj_attribute poss_val =
__ATTR_RO(possible_values);
static ssize_t type_show(struct kobject *kobj, struct kobj_attribute *attr,
char *buf)
{
return sprintf(buf, "enumeration\n");
}
static struct kobj_attribute type =
__ATTR_RO(type);
static struct attribute *enumeration_attrs[] = {
&displ_langcode.attr,
&displ_name.attr,
&default_val.attr,
&current_val.attr,
&modifier.attr,
&value_modfr.attr,
&poss_val.attr,
&type.attr,
NULL,
};
static const struct attribute_group enumeration_attr_group = {
.attrs = enumeration_attrs,
};
int alloc_enum_data(void)
{
int ret = 0;
wmi_priv.enumeration_instances_count =
get_instance_count(DELL_WMI_BIOS_ENUMERATION_ATTRIBUTE_GUID);
wmi_priv.enumeration_data = kzalloc_objs(struct enumeration_data,
wmi_priv.enumeration_instances_count);
if (!wmi_priv.enumeration_data) {
wmi_priv.enumeration_instances_count = 0;
ret = -ENOMEM;
}
return ret;
}
/**
* populate_enum_data() - Populate all properties of an instance under enumeration attribute
* @enumeration_obj: ACPI object with enumeration data
* @instance_id: The instance to enumerate
* @attr_name_kobj: The parent kernel object
* @enum_property_count: Total properties count under enumeration type
*/
int populate_enum_data(union acpi_object *enumeration_obj, int instance_id,
struct kobject *attr_name_kobj, u32 enum_property_count)
{
int i, next_obj, value_modifier_count, possible_values_count;
wmi_priv.enumeration_data[instance_id].attr_name_kobj = attr_name_kobj;
if (check_property_type(enumeration, ATTR_NAME, ACPI_TYPE_STRING))
return -EINVAL;
strlcpy_attr(wmi_priv.enumeration_data[instance_id].attribute_name,
enumeration_obj[ATTR_NAME].string.pointer);
if (check_property_type(enumeration, DISPL_NAME_LANG_CODE, ACPI_TYPE_STRING))
return -EINVAL;
strlcpy_attr(wmi_priv.enumeration_data[instance_id].display_name_language_code,
enumeration_obj[DISPL_NAME_LANG_CODE].string.pointer);
if (check_property_type(enumeration, DISPLAY_NAME, ACPI_TYPE_STRING))
return -EINVAL;
strlcpy_attr(wmi_priv.enumeration_data[instance_id].display_name,
enumeration_obj[DISPLAY_NAME].string.pointer);
if (check_property_type(enumeration, DEFAULT_VAL, ACPI_TYPE_STRING))
return -EINVAL;
strlcpy_attr(wmi_priv.enumeration_data[instance_id].default_value,
enumeration_obj[DEFAULT_VAL].string.pointer);
if (check_property_type(enumeration, MODIFIER, ACPI_TYPE_STRING))
return -EINVAL;
strlcpy_attr(wmi_priv.enumeration_data[instance_id].dell_modifier,
enumeration_obj[MODIFIER].string.pointer);
next_obj = MODIFIER + 1;
if (next_obj >= enum_property_count)
return -EINVAL;
if (check_property_type(enumeration, next_obj, ACPI_TYPE_INTEGER))
return -EINVAL;
value_modifier_count = (uintptr_t)enumeration_obj[next_obj++].string.pointer;
for (i = 0; i < value_modifier_count; i++) {
if (next_obj >= enum_property_count)
return -EINVAL;
if (check_property_type(enumeration, next_obj, ACPI_TYPE_STRING))
return -EINVAL;
if (append_enum_string(wmi_priv.enumeration_data[instance_id].dell_value_modifier,
enumeration_obj[next_obj++].string.pointer))
return -EINVAL;
}
if (next_obj >= enum_property_count)
return -EINVAL;
if (check_property_type(enumeration, next_obj, ACPI_TYPE_INTEGER))
return -EINVAL;
possible_values_count = (uintptr_t) enumeration_obj[next_obj++].string.pointer;
for (i = 0; i < possible_values_count; i++) {
if (next_obj >= enum_property_count)
return -EINVAL;
if (check_property_type(enumeration, next_obj, ACPI_TYPE_STRING))
return -EINVAL;
if (append_enum_string(wmi_priv.enumeration_data[instance_id].possible_values,
enumeration_obj[next_obj++].string.pointer))
return -EINVAL;
}
return sysfs_create_group(attr_name_kobj, &enumeration_attr_group);
}
/**
* exit_enum_attributes() - Clear all attribute data
*
* Clears all data allocated for this group of attributes
*/
void exit_enum_attributes(void)
{
int instance_id;
for (instance_id = 0; instance_id < wmi_priv.enumeration_instances_count; instance_id++) {
if (wmi_priv.enumeration_data[instance_id].attr_name_kobj)
sysfs_remove_group(wmi_priv.enumeration_data[instance_id].attr_name_kobj,
&enumeration_attr_group);
}
wmi_priv.enumeration_instances_count = 0;
kfree(wmi_priv.enumeration_data);
wmi_priv.enumeration_data = NULL;
}
@@ -0,0 +1,199 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Functions corresponding to integer type attributes under BIOS Integer GUID for use with
* dell-wmi-sysman
*
* Copyright (c) 2020 Dell Inc.
*/
#include "dell-wmi-sysman.h"
enum int_properties {MIN_VALUE = 6, MAX_VALUE, SCALAR_INCR};
get_instance_id(integer);
static ssize_t current_value_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf)
{
int instance_id = get_integer_instance_id(kobj);
union acpi_object *obj;
ssize_t ret;
if (instance_id < 0)
return instance_id;
/* need to use specific instance_id and guid combination to get right data */
obj = get_wmiobj_pointer(instance_id, DELL_WMI_BIOS_INTEGER_ATTRIBUTE_GUID);
if (!obj)
return -EIO;
if (obj->type != ACPI_TYPE_PACKAGE || obj->package.count < INT_MIN_ELEMENTS ||
obj->package.elements[CURRENT_VAL].type != ACPI_TYPE_INTEGER) {
kfree(obj);
return -EIO;
}
ret = snprintf(buf, PAGE_SIZE, "%lld\n", obj->package.elements[CURRENT_VAL].integer.value);
kfree(obj);
return ret;
}
/**
* validate_integer_input() - Validate input of current_value against lower and upper bound
* @instance_id: The instance on which input is validated
* @buf: Input value
*/
static int validate_integer_input(int instance_id, char *buf)
{
int in_val;
int ret;
ret = kstrtoint(buf, 0, &in_val);
if (ret)
return ret;
if (in_val < wmi_priv.integer_data[instance_id].min_value ||
in_val > wmi_priv.integer_data[instance_id].max_value)
return -EINVAL;
/* workaround for BIOS error.
* validate input to avoid setting 0 when integer input passed with + sign
*/
if (*buf == '+')
memmove(buf, (buf + 1), strlen(buf + 1) + 1);
return ret;
}
attribute_s_property_show(display_name_language_code, integer);
static struct kobj_attribute integer_displ_langcode =
__ATTR_RO(display_name_language_code);
attribute_s_property_show(display_name, integer);
static struct kobj_attribute integer_displ_name =
__ATTR_RO(display_name);
attribute_n_property_show(default_value, integer);
static struct kobj_attribute integer_default_val =
__ATTR_RO(default_value);
attribute_property_store(current_value, integer);
static struct kobj_attribute integer_current_val =
__ATTR_RW_MODE(current_value, 0600);
attribute_s_property_show(dell_modifier, integer);
static struct kobj_attribute integer_modifier =
__ATTR_RO(dell_modifier);
attribute_n_property_show(min_value, integer);
static struct kobj_attribute integer_lower_bound =
__ATTR_RO(min_value);
attribute_n_property_show(max_value, integer);
static struct kobj_attribute integer_upper_bound =
__ATTR_RO(max_value);
attribute_n_property_show(scalar_increment, integer);
static struct kobj_attribute integer_scalar_increment =
__ATTR_RO(scalar_increment);
static ssize_t type_show(struct kobject *kobj, struct kobj_attribute *attr,
char *buf)
{
return sprintf(buf, "integer\n");
}
static struct kobj_attribute integer_type =
__ATTR_RO(type);
static struct attribute *integer_attrs[] = {
&integer_displ_langcode.attr,
&integer_displ_name.attr,
&integer_default_val.attr,
&integer_current_val.attr,
&integer_modifier.attr,
&integer_lower_bound.attr,
&integer_upper_bound.attr,
&integer_scalar_increment.attr,
&integer_type.attr,
NULL,
};
static const struct attribute_group integer_attr_group = {
.attrs = integer_attrs,
};
int alloc_int_data(void)
{
int ret = 0;
wmi_priv.integer_instances_count = get_instance_count(DELL_WMI_BIOS_INTEGER_ATTRIBUTE_GUID);
wmi_priv.integer_data = kzalloc_objs(struct integer_data,
wmi_priv.integer_instances_count);
if (!wmi_priv.integer_data) {
wmi_priv.integer_instances_count = 0;
ret = -ENOMEM;
}
return ret;
}
/**
* populate_int_data() - Populate all properties of an instance under integer attribute
* @integer_obj: ACPI object with integer data
* @instance_id: The instance to enumerate
* @attr_name_kobj: The parent kernel object
*/
int populate_int_data(union acpi_object *integer_obj, int instance_id,
struct kobject *attr_name_kobj)
{
wmi_priv.integer_data[instance_id].attr_name_kobj = attr_name_kobj;
if (check_property_type(integer, ATTR_NAME, ACPI_TYPE_STRING))
return -EINVAL;
strlcpy_attr(wmi_priv.integer_data[instance_id].attribute_name,
integer_obj[ATTR_NAME].string.pointer);
if (check_property_type(integer, DISPL_NAME_LANG_CODE, ACPI_TYPE_STRING))
return -EINVAL;
strlcpy_attr(wmi_priv.integer_data[instance_id].display_name_language_code,
integer_obj[DISPL_NAME_LANG_CODE].string.pointer);
if (check_property_type(integer, DISPLAY_NAME, ACPI_TYPE_STRING))
return -EINVAL;
strlcpy_attr(wmi_priv.integer_data[instance_id].display_name,
integer_obj[DISPLAY_NAME].string.pointer);
if (check_property_type(integer, DEFAULT_VAL, ACPI_TYPE_INTEGER))
return -EINVAL;
wmi_priv.integer_data[instance_id].default_value =
(uintptr_t)integer_obj[DEFAULT_VAL].string.pointer;
if (check_property_type(integer, MODIFIER, ACPI_TYPE_STRING))
return -EINVAL;
strlcpy_attr(wmi_priv.integer_data[instance_id].dell_modifier,
integer_obj[MODIFIER].string.pointer);
if (check_property_type(integer, MIN_VALUE, ACPI_TYPE_INTEGER))
return -EINVAL;
wmi_priv.integer_data[instance_id].min_value =
(uintptr_t)integer_obj[MIN_VALUE].string.pointer;
if (check_property_type(integer, MAX_VALUE, ACPI_TYPE_INTEGER))
return -EINVAL;
wmi_priv.integer_data[instance_id].max_value =
(uintptr_t)integer_obj[MAX_VALUE].string.pointer;
if (check_property_type(integer, SCALAR_INCR, ACPI_TYPE_INTEGER))
return -EINVAL;
wmi_priv.integer_data[instance_id].scalar_increment =
(uintptr_t)integer_obj[SCALAR_INCR].string.pointer;
return sysfs_create_group(attr_name_kobj, &integer_attr_group);
}
/**
* exit_int_attributes() - Clear all attribute data
*
* Clears all data allocated for this group of attributes
*/
void exit_int_attributes(void)
{
int instance_id;
for (instance_id = 0; instance_id < wmi_priv.integer_instances_count; instance_id++) {
if (wmi_priv.integer_data[instance_id].attr_name_kobj)
sysfs_remove_group(wmi_priv.integer_data[instance_id].attr_name_kobj,
&integer_attr_group);
}
wmi_priv.integer_instances_count = 0;
kfree(wmi_priv.integer_data);
wmi_priv.integer_data = NULL;
}
@@ -0,0 +1,198 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Functions corresponding to password object type attributes under BIOS Password Object GUID for
* use with dell-wmi-sysman
*
* Copyright (c) 2020 Dell Inc.
*/
#include "dell-wmi-sysman.h"
enum po_properties {IS_PASS_SET = 1, MIN_PASS_LEN, MAX_PASS_LEN};
get_instance_id(po);
static ssize_t is_enabled_show(struct kobject *kobj, struct kobj_attribute *attr,
char *buf)
{
int instance_id = get_po_instance_id(kobj);
union acpi_object *obj;
ssize_t ret;
if (instance_id < 0)
return instance_id;
/* need to use specific instance_id and guid combination to get right data */
obj = get_wmiobj_pointer(instance_id, DELL_WMI_BIOS_PASSOBJ_ATTRIBUTE_GUID);
if (!obj)
return -EIO;
if (obj->type != ACPI_TYPE_PACKAGE || obj->package.count < PO_MIN_ELEMENTS ||
obj->package.elements[IS_PASS_SET].type != ACPI_TYPE_INTEGER) {
kfree(obj);
return -EIO;
}
ret = snprintf(buf, PAGE_SIZE, "%lld\n", obj->package.elements[IS_PASS_SET].integer.value);
kfree(obj);
return ret;
}
static struct kobj_attribute po_is_pass_set = __ATTR_RO(is_enabled);
static ssize_t current_password_store(struct kobject *kobj,
struct kobj_attribute *attr,
const char *buf, size_t count)
{
char *target = NULL;
int length;
length = strlen(buf);
if (length && buf[length - 1] == '\n')
length--;
/* firmware does verifiation of min/max password length,
* hence only check for not exceeding MAX_BUFF here.
*/
if (length >= MAX_BUFF)
return -EINVAL;
if (strcmp(kobj->name, "Admin") == 0)
target = wmi_priv.current_admin_password;
else if (strcmp(kobj->name, "System") == 0)
target = wmi_priv.current_system_password;
if (!target)
return -EIO;
memcpy(target, buf, length);
target[length] = '\0';
return count;
}
static struct kobj_attribute po_current_password = __ATTR_WO(current_password);
static ssize_t new_password_store(struct kobject *kobj,
struct kobj_attribute *attr,
const char *buf, size_t count)
{
char *p, *buf_cp;
int ret;
buf_cp = kstrdup(buf, GFP_KERNEL);
if (!buf_cp)
return -ENOMEM;
p = memchr(buf_cp, '\n', count);
if (p != NULL)
*p = '\0';
if (strlen(buf_cp) > MAX_BUFF) {
ret = -EINVAL;
goto out;
}
ret = set_new_password(kobj->name, buf_cp);
out:
kfree(buf_cp);
return ret ? ret : count;
}
static struct kobj_attribute po_new_password = __ATTR_WO(new_password);
attribute_n_property_show(min_password_length, po);
static struct kobj_attribute po_min_pass_length = __ATTR_RO(min_password_length);
attribute_n_property_show(max_password_length, po);
static struct kobj_attribute po_max_pass_length = __ATTR_RO(max_password_length);
static ssize_t mechanism_show(struct kobject *kobj, struct kobj_attribute *attr,
char *buf)
{
return sprintf(buf, "password\n");
}
static struct kobj_attribute po_mechanism = __ATTR_RO(mechanism);
static ssize_t role_show(struct kobject *kobj, struct kobj_attribute *attr,
char *buf)
{
if (strcmp(kobj->name, "Admin") == 0)
return sprintf(buf, "bios-admin\n");
else if (strcmp(kobj->name, "System") == 0)
return sprintf(buf, "power-on\n");
return -EIO;
}
static struct kobj_attribute po_role = __ATTR_RO(role);
static struct attribute *po_attrs[] = {
&po_is_pass_set.attr,
&po_min_pass_length.attr,
&po_max_pass_length.attr,
&po_current_password.attr,
&po_new_password.attr,
&po_role.attr,
&po_mechanism.attr,
NULL,
};
static const struct attribute_group po_attr_group = {
.attrs = po_attrs,
};
int alloc_po_data(void)
{
int ret = 0;
wmi_priv.po_instances_count = get_instance_count(DELL_WMI_BIOS_PASSOBJ_ATTRIBUTE_GUID);
wmi_priv.po_data = kzalloc_objs(struct po_data,
wmi_priv.po_instances_count);
if (!wmi_priv.po_data) {
wmi_priv.po_instances_count = 0;
ret = -ENOMEM;
}
return ret;
}
/**
* populate_po_data() - Populate all properties of an instance under password object attribute
* @po_obj: ACPI object with password object data
* @instance_id: The instance to enumerate
* @attr_name_kobj: The parent kernel object
*/
int populate_po_data(union acpi_object *po_obj, int instance_id, struct kobject *attr_name_kobj)
{
wmi_priv.po_data[instance_id].attr_name_kobj = attr_name_kobj;
if (check_property_type(po, ATTR_NAME, ACPI_TYPE_STRING))
return -EINVAL;
strlcpy_attr(wmi_priv.po_data[instance_id].attribute_name,
po_obj[ATTR_NAME].string.pointer);
if (check_property_type(po, MIN_PASS_LEN, ACPI_TYPE_INTEGER))
return -EINVAL;
wmi_priv.po_data[instance_id].min_password_length =
(uintptr_t)po_obj[MIN_PASS_LEN].string.pointer;
if (check_property_type(po, MAX_PASS_LEN, ACPI_TYPE_INTEGER))
return -EINVAL;
wmi_priv.po_data[instance_id].max_password_length =
(uintptr_t) po_obj[MAX_PASS_LEN].string.pointer;
return sysfs_create_group(attr_name_kobj, &po_attr_group);
}
/**
* exit_po_attributes() - Clear all attribute data
*
* Clears all data allocated for this group of attributes
*/
void exit_po_attributes(void)
{
int instance_id;
for (instance_id = 0; instance_id < wmi_priv.po_instances_count; instance_id++) {
if (wmi_priv.po_data[instance_id].attr_name_kobj)
sysfs_remove_group(wmi_priv.po_data[instance_id].attr_name_kobj,
&po_attr_group);
}
wmi_priv.po_instances_count = 0;
kfree(wmi_priv.po_data);
wmi_priv.po_data = NULL;
}
@@ -0,0 +1,151 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Functions corresponding to SET password methods under BIOS attributes interface GUID
*
* Copyright (c) 2020 Dell Inc.
*/
#include <linux/wmi.h>
#include "dell-wmi-sysman.h"
static int call_password_interface(struct wmi_device *wdev, u8 *in_args, size_t size)
{
struct acpi_buffer output = {ACPI_ALLOCATE_BUFFER, NULL};
struct acpi_buffer input;
union acpi_object *obj;
acpi_status status;
int ret = -EIO;
input.length = (acpi_size) size;
input.pointer = in_args;
status = wmidev_evaluate_method(wdev, 0, 1, &input, &output);
if (ACPI_FAILURE(status))
return -EIO;
obj = (union acpi_object *)output.pointer;
if (obj->type == ACPI_TYPE_INTEGER)
ret = obj->integer.value;
kfree(output.pointer);
/* let userland know it may need to check is_password_set again */
kobject_uevent(&wmi_priv.class_dev->kobj, KOBJ_CHANGE);
return map_wmi_error(ret);
}
/**
* set_new_password() - Sets a system admin password
* @password_type: The type of password to set
* @new: The new password
*
* Sets the password using plaintext interface
*/
int set_new_password(const char *password_type, const char *new)
{
size_t password_type_size, current_password_size, new_size;
size_t security_area_size, buffer_size;
u8 *buffer = NULL, *start;
char *current_password;
int ret;
mutex_lock(&wmi_priv.mutex);
if (!wmi_priv.password_attr_wdev) {
ret = -ENODEV;
goto out;
}
if (strcmp(password_type, "Admin") == 0) {
current_password = wmi_priv.current_admin_password;
} else if (strcmp(password_type, "System") == 0) {
current_password = wmi_priv.current_system_password;
} else {
ret = -EINVAL;
dev_err(&wmi_priv.password_attr_wdev->dev, "unknown password type %s\n",
password_type);
goto out;
}
/* build/calculate buffer */
security_area_size = calculate_security_buffer(wmi_priv.current_admin_password);
password_type_size = calculate_string_buffer(password_type);
current_password_size = calculate_string_buffer(current_password);
new_size = calculate_string_buffer(new);
buffer_size = security_area_size + password_type_size + current_password_size + new_size;
buffer = kzalloc(buffer_size, GFP_KERNEL);
if (!buffer) {
ret = -ENOMEM;
goto out;
}
/* build security area */
populate_security_buffer(buffer, wmi_priv.current_admin_password);
/* build variables to set */
start = buffer + security_area_size;
ret = populate_string_buffer(start, password_type_size, password_type);
if (ret < 0)
goto out;
start += ret;
ret = populate_string_buffer(start, current_password_size, current_password);
if (ret < 0)
goto out;
start += ret;
ret = populate_string_buffer(start, new_size, new);
if (ret < 0)
goto out;
ret = call_password_interface(wmi_priv.password_attr_wdev, buffer, buffer_size);
/* on success copy the new password to current password */
if (!ret)
strscpy(current_password, new, MAX_BUFF);
/* explain to user the detailed failure reason */
else if (ret == -EOPNOTSUPP)
dev_err(&wmi_priv.password_attr_wdev->dev, "admin password must be configured\n");
else if (ret == -EACCES)
dev_err(&wmi_priv.password_attr_wdev->dev, "invalid password\n");
out:
kfree(buffer);
mutex_unlock(&wmi_priv.mutex);
return ret;
}
static int bios_attr_pass_interface_probe(struct wmi_device *wdev, const void *context)
{
mutex_lock(&wmi_priv.mutex);
wmi_priv.password_attr_wdev = wdev;
mutex_unlock(&wmi_priv.mutex);
return 0;
}
static void bios_attr_pass_interface_remove(struct wmi_device *wdev)
{
mutex_lock(&wmi_priv.mutex);
wmi_priv.password_attr_wdev = NULL;
mutex_unlock(&wmi_priv.mutex);
}
static const struct wmi_device_id bios_attr_pass_interface_id_table[] = {
{ .guid_string = DELL_WMI_BIOS_PASSWORD_INTERFACE_GUID },
{ },
};
static struct wmi_driver bios_attr_pass_interface_driver = {
.driver = {
.name = DRIVER_NAME"-password"
},
.probe = bios_attr_pass_interface_probe,
.remove = bios_attr_pass_interface_remove,
.id_table = bios_attr_pass_interface_id_table,
};
int init_bios_attr_pass_interface(void)
{
return wmi_driver_register(&bios_attr_pass_interface_driver);
}
void exit_bios_attr_pass_interface(void)
{
wmi_driver_unregister(&bios_attr_pass_interface_driver);
}
MODULE_DEVICE_TABLE(wmi, bios_attr_pass_interface_id_table);
@@ -0,0 +1,177 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Functions corresponding to string type attributes under BIOS String GUID for use with
* dell-wmi-sysman
*
* Copyright (c) 2020 Dell Inc.
*/
#include "dell-wmi-sysman.h"
enum string_properties {MIN_LEN = 6, MAX_LEN};
get_instance_id(str);
static ssize_t current_value_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf)
{
int instance_id = get_str_instance_id(kobj);
union acpi_object *obj;
ssize_t ret;
if (instance_id < 0)
return -EIO;
/* need to use specific instance_id and guid combination to get right data */
obj = get_wmiobj_pointer(instance_id, DELL_WMI_BIOS_STRING_ATTRIBUTE_GUID);
if (!obj)
return -EIO;
if (obj->type != ACPI_TYPE_PACKAGE || obj->package.count < STR_MIN_ELEMENTS ||
obj->package.elements[CURRENT_VAL].type != ACPI_TYPE_STRING) {
kfree(obj);
return -EIO;
}
ret = snprintf(buf, PAGE_SIZE, "%s\n", obj->package.elements[CURRENT_VAL].string.pointer);
kfree(obj);
return ret;
}
/**
* validate_str_input() - Validate input of current_value against min and max lengths
* @instance_id: The instance on which input is validated
* @buf: Input value
*/
static int validate_str_input(int instance_id, const char *buf)
{
int in_len = strlen(buf);
if ((in_len < wmi_priv.str_data[instance_id].min_length) ||
(in_len > wmi_priv.str_data[instance_id].max_length))
return -EINVAL;
return 0;
}
attribute_s_property_show(display_name_language_code, str);
static struct kobj_attribute str_displ_langcode =
__ATTR_RO(display_name_language_code);
attribute_s_property_show(display_name, str);
static struct kobj_attribute str_displ_name =
__ATTR_RO(display_name);
attribute_s_property_show(default_value, str);
static struct kobj_attribute str_default_val =
__ATTR_RO(default_value);
attribute_property_store(current_value, str);
static struct kobj_attribute str_current_val =
__ATTR_RW_MODE(current_value, 0600);
attribute_s_property_show(dell_modifier, str);
static struct kobj_attribute str_modifier =
__ATTR_RO(dell_modifier);
attribute_n_property_show(min_length, str);
static struct kobj_attribute str_min_length =
__ATTR_RO(min_length);
attribute_n_property_show(max_length, str);
static struct kobj_attribute str_max_length =
__ATTR_RO(max_length);
static ssize_t type_show(struct kobject *kobj, struct kobj_attribute *attr,
char *buf)
{
return sprintf(buf, "string\n");
}
static struct kobj_attribute str_type =
__ATTR_RO(type);
static struct attribute *str_attrs[] = {
&str_displ_langcode.attr,
&str_displ_name.attr,
&str_default_val.attr,
&str_current_val.attr,
&str_modifier.attr,
&str_min_length.attr,
&str_max_length.attr,
&str_type.attr,
NULL,
};
static const struct attribute_group str_attr_group = {
.attrs = str_attrs,
};
int alloc_str_data(void)
{
int ret = 0;
wmi_priv.str_instances_count = get_instance_count(DELL_WMI_BIOS_STRING_ATTRIBUTE_GUID);
wmi_priv.str_data = kzalloc_objs(struct str_data,
wmi_priv.str_instances_count);
if (!wmi_priv.str_data) {
wmi_priv.str_instances_count = 0;
ret = -ENOMEM;
}
return ret;
}
/**
* populate_str_data() - Populate all properties of an instance under string attribute
* @str_obj: ACPI object with string data
* @instance_id: The instance to enumerate
* @attr_name_kobj: The parent kernel object
*/
int populate_str_data(union acpi_object *str_obj, int instance_id, struct kobject *attr_name_kobj)
{
wmi_priv.str_data[instance_id].attr_name_kobj = attr_name_kobj;
if (check_property_type(str, ATTR_NAME, ACPI_TYPE_STRING))
return -EINVAL;
strlcpy_attr(wmi_priv.str_data[instance_id].attribute_name,
str_obj[ATTR_NAME].string.pointer);
if (check_property_type(str, DISPL_NAME_LANG_CODE, ACPI_TYPE_STRING))
return -EINVAL;
strlcpy_attr(wmi_priv.str_data[instance_id].display_name_language_code,
str_obj[DISPL_NAME_LANG_CODE].string.pointer);
if (check_property_type(str, DISPLAY_NAME, ACPI_TYPE_STRING))
return -EINVAL;
strlcpy_attr(wmi_priv.str_data[instance_id].display_name,
str_obj[DISPLAY_NAME].string.pointer);
if (check_property_type(str, DEFAULT_VAL, ACPI_TYPE_STRING))
return -EINVAL;
strlcpy_attr(wmi_priv.str_data[instance_id].default_value,
str_obj[DEFAULT_VAL].string.pointer);
if (check_property_type(str, MODIFIER, ACPI_TYPE_STRING))
return -EINVAL;
strlcpy_attr(wmi_priv.str_data[instance_id].dell_modifier,
str_obj[MODIFIER].string.pointer);
if (check_property_type(str, MIN_LEN, ACPI_TYPE_INTEGER))
return -EINVAL;
wmi_priv.str_data[instance_id].min_length = (uintptr_t)str_obj[MIN_LEN].string.pointer;
if (check_property_type(str, MAX_LEN, ACPI_TYPE_INTEGER))
return -EINVAL;
wmi_priv.str_data[instance_id].max_length = (uintptr_t) str_obj[MAX_LEN].string.pointer;
return sysfs_create_group(attr_name_kobj, &str_attr_group);
}
/**
* exit_str_attributes() - Clear all attribute data
*
* Clears all data allocated for this group of attributes
*/
void exit_str_attributes(void)
{
int instance_id;
for (instance_id = 0; instance_id < wmi_priv.str_instances_count; instance_id++) {
if (wmi_priv.str_data[instance_id].attr_name_kobj)
sysfs_remove_group(wmi_priv.str_data[instance_id].attr_name_kobj,
&str_attr_group);
}
wmi_priv.str_instances_count = 0;
kfree(wmi_priv.str_data);
wmi_priv.str_data = NULL;
}
@@ -0,0 +1,596 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Common methods for use with dell-wmi-sysman
*
* Copyright (c) 2020 Dell Inc.
*/
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/align.h>
#include <linux/fs.h>
#include <linux/dmi.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/string.h>
#include <linux/sysfs.h>
#include <linux/wmi.h>
#include "dell-wmi-sysman.h"
#include "../../firmware_attributes_class.h"
#define MAX_TYPES 4
#include <linux/nls.h>
struct wmi_sysman_priv wmi_priv = {
.mutex = __MUTEX_INITIALIZER(wmi_priv.mutex),
};
/* reset bios to defaults */
static const char * const reset_types[] = {"builtinsafe", "lastknowngood", "factory", "custom"};
static int reset_option = -1;
/**
* populate_string_buffer() - populates a string buffer
* @buffer: the start of the destination buffer
* @buffer_len: length of the destination buffer
* @str: the string to insert into buffer
*/
ssize_t populate_string_buffer(u8 *buffer, size_t buffer_len, const char *str)
{
u16 *length = (u16 *)buffer;
u16 *target = length + 1;
int ret;
ret = utf8s_to_utf16s(str, strlen(str), UTF16_HOST_ENDIAN,
target, buffer_len - sizeof(u16));
if (ret < 0) {
dev_err(wmi_priv.class_dev, "UTF16 conversion failed\n");
return ret;
}
if ((ret * sizeof(u16)) > U16_MAX) {
dev_err(wmi_priv.class_dev, "Error string too long\n");
return -ERANGE;
}
*length = ret * sizeof(u16);
return sizeof(u16) + *length;
}
/**
* calculate_string_buffer() - determines size of string buffer for use with BIOS communication
* @str: the string to calculate based upon
*
*/
size_t calculate_string_buffer(const char *str)
{
/* u16 length field + one UTF16 char for each input char */
return sizeof(u16) + strlen(str) * sizeof(u16);
}
/**
* calculate_security_buffer() - determines size of security buffer for authentication scheme
* @authentication: the authentication content
*
* Currently only supported type is Admin password
*/
size_t calculate_security_buffer(const char *authentication)
{
return sizeof(u32) * 2 + ALIGN(strlen(authentication), 2);
}
/**
* populate_security_buffer() - builds a security buffer for authentication scheme
* @buffer: the buffer to populate
* @authentication: the authentication content
*
* Currently only supported type is PLAIN TEXT
*/
void populate_security_buffer(u8 *buffer, const char *authentication)
{
size_t seclen = strlen(authentication);
u8 *auth = buffer + sizeof(u32) * 2;
u32 *sectype = (u32 *) buffer;
u32 *seclenp = sectype + 1;
*sectype = !!seclen;
*seclenp = seclen;
/* plain text */
memcpy(auth, authentication, seclen);
}
/**
* map_wmi_error() - map errors from WMI methods to kernel error codes
* @error_code: integer error code returned from Dell's firmware
*/
int map_wmi_error(int error_code)
{
switch (error_code) {
case 0:
/* success */
return 0;
case 1:
/* failed */
return -EIO;
case 2:
/* invalid parameter */
return -EINVAL;
case 3:
/* access denied */
return -EACCES;
case 4:
/* not supported */
return -EOPNOTSUPP;
case 5:
/* memory error */
return -ENOMEM;
case 6:
/* protocol error */
return -EPROTO;
}
/* unspecified error */
return -EIO;
}
/**
* reset_bios_show() - sysfs implementaton for read reset_bios
* @kobj: Kernel object for this attribute
* @attr: Kernel object attribute
* @buf: The buffer to display to userspace
*/
static ssize_t reset_bios_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf)
{
ssize_t len = 0;
int i;
for (i = 0; i < MAX_TYPES; i++) {
if (i == reset_option)
len += sysfs_emit_at(buf, len, "[%s] ", reset_types[i]);
else
len += sysfs_emit_at(buf, len, "%s ", reset_types[i]);
}
len += sysfs_emit_at(buf, len, "\n");
return len;
}
/**
* reset_bios_store() - sysfs implementaton for write reset_bios
* @kobj: Kernel object for this attribute
* @attr: Kernel object attribute
* @buf: The buffer from userspace
* @count: the size of the buffer from userspace
*/
static ssize_t reset_bios_store(struct kobject *kobj,
struct kobj_attribute *attr, const char *buf, size_t count)
{
int type = sysfs_match_string(reset_types, buf);
int ret;
if (type < 0)
return type;
ret = set_bios_defaults(type);
pr_debug("reset all attributes request type %d: %d\n", type, ret);
if (!ret) {
reset_option = type;
ret = count;
}
return ret;
}
/**
* pending_reboot_show() - sysfs implementaton for read pending_reboot
* @kobj: Kernel object for this attribute
* @attr: Kernel object attribute
* @buf: The buffer to display to userspace
*
* Stores default value as 0
* When current_value is changed this attribute is set to 1 to notify reboot may be required
*/
static ssize_t pending_reboot_show(struct kobject *kobj, struct kobj_attribute *attr,
char *buf)
{
return sysfs_emit(buf, "%d\n", wmi_priv.pending_changes);
}
static struct kobj_attribute reset_bios = __ATTR_RW(reset_bios);
static struct kobj_attribute pending_reboot = __ATTR_RO(pending_reboot);
/**
* create_attributes_level_sysfs_files() - Creates reset_bios and
* pending_reboot attributes
*/
static int create_attributes_level_sysfs_files(void)
{
int ret;
ret = sysfs_create_file(&wmi_priv.main_dir_kset->kobj, &reset_bios.attr);
if (ret)
return ret;
ret = sysfs_create_file(&wmi_priv.main_dir_kset->kobj, &pending_reboot.attr);
if (ret)
return ret;
return 0;
}
static void attr_name_release(struct kobject *kobj)
{
kfree(kobj);
}
static const struct kobj_type attr_name_ktype = {
.release = attr_name_release,
.sysfs_ops = &kobj_sysfs_ops,
};
/**
* strlcpy_attr - Copy a length-limited, NULL-terminated string with bound checks
* @dest: Where to copy the string to
* @src: Where to copy the string from
*/
void strlcpy_attr(char *dest, char *src)
{
size_t len = strlen(src) + 1;
if (len > 1 && len <= MAX_BUFF)
strscpy(dest, src, len);
/*len can be zero because any property not-applicable to attribute can
* be empty so check only for too long buffers and log error
*/
if (len > MAX_BUFF)
pr_err("Source string returned from BIOS is out of bound!\n");
}
/**
* get_wmiobj_pointer() - Get Content of WMI block for particular instance
* @instance_id: WMI instance ID
* @guid_string: WMI GUID (in str form)
*
* Fetches the content for WMI block (instance_id) under GUID (guid_string)
* Caller must kfree the return
*/
union acpi_object *get_wmiobj_pointer(int instance_id, const char *guid_string)
{
struct acpi_buffer out = { ACPI_ALLOCATE_BUFFER, NULL };
acpi_status status;
status = wmi_query_block(guid_string, instance_id, &out);
return ACPI_SUCCESS(status) ? (union acpi_object *)out.pointer : NULL;
}
/**
* get_instance_count() - Compute total number of instances under guid_string
* @guid_string: WMI GUID (in string form)
*/
int get_instance_count(const char *guid_string)
{
int ret;
ret = wmi_instance_count(guid_string);
if (ret < 0)
return 0;
return ret;
}
/**
* alloc_attributes_data() - Allocate attributes data for a particular type
* @attr_type: Attribute type to allocate
*/
static int alloc_attributes_data(int attr_type)
{
int retval = 0;
switch (attr_type) {
case ENUM:
retval = alloc_enum_data();
break;
case INT:
retval = alloc_int_data();
break;
case STR:
retval = alloc_str_data();
break;
case PO:
retval = alloc_po_data();
break;
default:
break;
}
return retval;
}
/**
* destroy_attribute_objs() - Free a kset of kobjects
* @kset: The kset to destroy
*
* Frees kobjects created for each attribute_name under attribute type kset.
*/
static void destroy_attribute_objs(struct kset *kset)
{
struct kobject *pos, *next;
list_for_each_entry_safe(pos, next, &kset->list, entry) {
kobject_put(pos);
}
}
/**
* release_attributes_data() - Clean-up all sysfs directories and files created
*/
static void release_attributes_data(void)
{
mutex_lock(&wmi_priv.mutex);
exit_enum_attributes();
exit_int_attributes();
exit_str_attributes();
exit_po_attributes();
if (wmi_priv.authentication_dir_kset) {
destroy_attribute_objs(wmi_priv.authentication_dir_kset);
kset_unregister(wmi_priv.authentication_dir_kset);
wmi_priv.authentication_dir_kset = NULL;
}
if (wmi_priv.main_dir_kset) {
sysfs_remove_file(&wmi_priv.main_dir_kset->kobj, &reset_bios.attr);
sysfs_remove_file(&wmi_priv.main_dir_kset->kobj, &pending_reboot.attr);
destroy_attribute_objs(wmi_priv.main_dir_kset);
kset_unregister(wmi_priv.main_dir_kset);
wmi_priv.main_dir_kset = NULL;
}
mutex_unlock(&wmi_priv.mutex);
}
/**
* init_bios_attributes() - Initialize all attributes for a type
* @attr_type: The attribute type to initialize
* @guid: The WMI GUID associated with this type to initialize
*
* Initialiaze all 4 types of attributes enumeration, integer, string and password object.
* Populates each attrbute typ's respective properties under sysfs files
*/
static int init_bios_attributes(int attr_type, const char *guid)
{
struct kobject *attr_name_kobj; //individual attribute names
union acpi_object *obj = NULL;
union acpi_object *elements;
struct kobject *duplicate;
struct kset *tmp_set;
int min_elements;
/* instance_id needs to be reset for each type GUID
* also, instance IDs are unique within GUID but not across
*/
int instance_id = 0;
int retval = 0;
retval = alloc_attributes_data(attr_type);
if (retval)
return retval;
switch (attr_type) {
case ENUM: min_elements = ENUM_MIN_ELEMENTS; break;
case INT: min_elements = INT_MIN_ELEMENTS; break;
case STR: min_elements = STR_MIN_ELEMENTS; break;
case PO: min_elements = PO_MIN_ELEMENTS; break;
default:
pr_err("Error: Unknown attr_type: %d\n", attr_type);
return -EINVAL;
}
/* need to use specific instance_id and guid combination to get right data */
obj = get_wmiobj_pointer(instance_id, guid);
if (!obj)
return -ENODEV;
mutex_lock(&wmi_priv.mutex);
while (obj) {
if (obj->type != ACPI_TYPE_PACKAGE) {
pr_err("Error: Expected ACPI-package type, got: %d\n", obj->type);
retval = -EIO;
goto err_attr_init;
}
if (obj->package.count < min_elements) {
pr_err("Error: ACPI-package does not have enough elements: %d < %d\n",
obj->package.count, min_elements);
goto nextobj;
}
elements = obj->package.elements;
/* sanity checking */
if (elements[ATTR_NAME].type != ACPI_TYPE_STRING) {
pr_debug("incorrect element type\n");
goto nextobj;
}
if (strlen(elements[ATTR_NAME].string.pointer) == 0) {
pr_debug("empty attribute found\n");
goto nextobj;
}
if (attr_type == PO)
tmp_set = wmi_priv.authentication_dir_kset;
else
tmp_set = wmi_priv.main_dir_kset;
duplicate = kset_find_obj(tmp_set, elements[ATTR_NAME].string.pointer);
if (duplicate) {
pr_debug("Duplicate attribute name found - %s\n",
elements[ATTR_NAME].string.pointer);
kobject_put(duplicate);
goto nextobj;
}
/* build attribute */
attr_name_kobj = kzalloc_obj(*attr_name_kobj);
if (!attr_name_kobj) {
retval = -ENOMEM;
goto err_attr_init;
}
attr_name_kobj->kset = tmp_set;
retval = kobject_init_and_add(attr_name_kobj, &attr_name_ktype, NULL, "%s",
elements[ATTR_NAME].string.pointer);
if (retval) {
kobject_put(attr_name_kobj);
goto err_attr_init;
}
/* enumerate all of this attribute */
switch (attr_type) {
case ENUM:
retval = populate_enum_data(elements, instance_id, attr_name_kobj,
obj->package.count);
break;
case INT:
retval = populate_int_data(elements, instance_id, attr_name_kobj);
break;
case STR:
retval = populate_str_data(elements, instance_id, attr_name_kobj);
break;
case PO:
retval = populate_po_data(elements, instance_id, attr_name_kobj);
break;
default:
break;
}
if (retval) {
pr_debug("failed to populate %s\n",
elements[ATTR_NAME].string.pointer);
goto err_attr_init;
}
nextobj:
kfree(obj);
instance_id++;
obj = get_wmiobj_pointer(instance_id, guid);
}
mutex_unlock(&wmi_priv.mutex);
return 0;
err_attr_init:
mutex_unlock(&wmi_priv.mutex);
kfree(obj);
return retval;
}
static int __init sysman_init(void)
{
int ret = 0;
if (!dmi_find_device(DMI_DEV_TYPE_OEM_STRING, "Dell System", NULL) &&
!dmi_find_device(DMI_DEV_TYPE_OEM_STRING, "Alienware", NULL) &&
!dmi_find_device(DMI_DEV_TYPE_OEM_STRING, "www.dell.com", NULL)) {
pr_err("Unable to run on non-Dell system\n");
return -ENODEV;
}
ret = init_bios_attr_set_interface();
if (ret)
return ret;
ret = init_bios_attr_pass_interface();
if (ret)
goto err_exit_bios_attr_set_interface;
if (!wmi_priv.bios_attr_wdev || !wmi_priv.password_attr_wdev) {
pr_debug("failed to find set or pass interface\n");
ret = -ENODEV;
goto err_exit_bios_attr_pass_interface;
}
wmi_priv.class_dev = device_create(&firmware_attributes_class, NULL, MKDEV(0, 0),
NULL, "%s", DRIVER_NAME);
if (IS_ERR(wmi_priv.class_dev)) {
ret = PTR_ERR(wmi_priv.class_dev);
goto err_exit_bios_attr_pass_interface;
}
wmi_priv.main_dir_kset = kset_create_and_add("attributes", NULL,
&wmi_priv.class_dev->kobj);
if (!wmi_priv.main_dir_kset) {
ret = -ENOMEM;
goto err_destroy_classdev;
}
wmi_priv.authentication_dir_kset = kset_create_and_add("authentication", NULL,
&wmi_priv.class_dev->kobj);
if (!wmi_priv.authentication_dir_kset) {
ret = -ENOMEM;
goto err_release_attributes_data;
}
ret = create_attributes_level_sysfs_files();
if (ret) {
pr_debug("could not create reset BIOS attribute\n");
goto err_release_attributes_data;
}
ret = init_bios_attributes(ENUM, DELL_WMI_BIOS_ENUMERATION_ATTRIBUTE_GUID);
if (ret) {
pr_debug("failed to populate enumeration type attributes\n");
goto err_release_attributes_data;
}
ret = init_bios_attributes(INT, DELL_WMI_BIOS_INTEGER_ATTRIBUTE_GUID);
if (ret) {
pr_debug("failed to populate integer type attributes\n");
goto err_release_attributes_data;
}
ret = init_bios_attributes(STR, DELL_WMI_BIOS_STRING_ATTRIBUTE_GUID);
if (ret) {
pr_debug("failed to populate string type attributes\n");
goto err_release_attributes_data;
}
ret = init_bios_attributes(PO, DELL_WMI_BIOS_PASSOBJ_ATTRIBUTE_GUID);
if (ret) {
pr_debug("failed to populate pass object type attributes\n");
goto err_release_attributes_data;
}
return 0;
err_release_attributes_data:
release_attributes_data();
err_destroy_classdev:
device_unregister(wmi_priv.class_dev);
err_exit_bios_attr_pass_interface:
exit_bios_attr_pass_interface();
err_exit_bios_attr_set_interface:
exit_bios_attr_set_interface();
return ret;
}
static void __exit sysman_exit(void)
{
release_attributes_data();
device_unregister(wmi_priv.class_dev);
exit_bios_attr_set_interface();
exit_bios_attr_pass_interface();
}
module_init(sysman_init);
module_exit(sysman_exit);
MODULE_AUTHOR("Mario Limonciello <mario.limonciello@outlook.com>");
MODULE_AUTHOR("Prasanth Ksr <prasanth.ksr@dell.com>");
MODULE_AUTHOR("Divya Bharathi <divya.bharathi@dell.com>");
MODULE_DESCRIPTION("Dell platform setting control interface");
MODULE_LICENSE("GPL");
@@ -0,0 +1,683 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* dell_rbu.c
* Bios Update driver for Dell systems
* Author: Dell Inc
* Abhay Salunke <abhay_salunke@dell.com>
*
* Copyright (C) 2005 Dell Inc.
*
* Remote BIOS Update (rbu) driver is used for updating DELL BIOS by
* creating entries in the /sys file systems on Linux 2.6 and higher
* kernels. The driver supports two mechanism to update the BIOS namely
* contiguous and packetized. Both these methods still require having some
* application to set the CMOS bit indicating the BIOS to update itself
* after a reboot.
*
* Contiguous method:
* This driver writes the incoming data in a monolithic image by allocating
* contiguous physical pages large enough to accommodate the incoming BIOS
* image size.
*
* Packetized method:
* The driver writes the incoming packet image by allocating a new packet
* on every time the packet data is written. This driver requires an
* application to break the BIOS image in to fixed sized packet chunks.
*
* See Documentation/admin-guide/dell_rbu.rst for more info.
*/
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/init.h>
#include <linux/kstrtox.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/string.h>
#include <linux/errno.h>
#include <linux/blkdev.h>
#include <linux/platform_device.h>
#include <linux/spinlock.h>
#include <linux/moduleparam.h>
#include <linux/firmware.h>
#include <linux/dma-mapping.h>
#include <asm/set_memory.h>
MODULE_AUTHOR("Abhay Salunke <abhay_salunke@dell.com>");
MODULE_DESCRIPTION("Driver for updating BIOS image on DELL systems");
MODULE_LICENSE("GPL");
MODULE_VERSION("3.3");
#define BIOS_SCAN_LIMIT 0xffffffff
#define MAX_IMAGE_LENGTH 16
static struct _rbu_data {
void *image_update_buffer;
unsigned long image_update_buffer_size;
unsigned long bios_image_size;
int image_update_ordernum;
spinlock_t lock;
unsigned long packet_read_count;
unsigned long num_packets;
unsigned long packetsize;
unsigned long imagesize;
int entry_created;
} rbu_data;
static char image_type[MAX_IMAGE_LENGTH + 1] = "mono";
module_param_string(image_type, image_type, sizeof (image_type), 0);
MODULE_PARM_DESC(image_type, "BIOS image type. choose- mono or packet or init");
static unsigned long allocation_floor = 0x100000;
module_param(allocation_floor, ulong, 0644);
MODULE_PARM_DESC(allocation_floor, "Minimum address for allocations when using Packet mode");
struct packet_data {
struct list_head list;
size_t length;
void *data;
int ordernum;
};
static struct list_head packet_data_list;
static struct platform_device *rbu_device;
static int context;
static void init_packet_head(void)
{
INIT_LIST_HEAD(&packet_data_list);
rbu_data.packet_read_count = 0;
rbu_data.num_packets = 0;
rbu_data.packetsize = 0;
rbu_data.imagesize = 0;
}
static int create_packet(void *data, size_t length) __must_hold(&rbu_data.lock)
{
struct packet_data *newpacket;
int ordernum = 0;
int retval = 0;
unsigned int packet_array_size = 0;
void **invalid_addr_packet_array = NULL;
void *packet_data_temp_buf = NULL;
unsigned int idx = 0;
pr_debug("entry\n");
if (!rbu_data.packetsize) {
pr_debug("packetsize not specified\n");
retval = -EINVAL;
goto out_noalloc;
}
spin_unlock(&rbu_data.lock);
newpacket = kzalloc_obj(struct packet_data);
if (!newpacket) {
pr_warn("failed to allocate new packet\n");
retval = -ENOMEM;
spin_lock(&rbu_data.lock);
goto out_noalloc;
}
ordernum = get_order(length);
/*
* BIOS errata mean we cannot allocate packets below 1MB or they will
* be overwritten by BIOS.
*
* array to temporarily hold packets
* that are below the allocation floor
*
* NOTE: very simplistic because we only need the floor to be at 1MB
* due to BIOS errata. This shouldn't be used for higher floors
* or you will run out of mem trying to allocate the array.
*/
packet_array_size = max_t(unsigned int, allocation_floor / rbu_data.packetsize, 1);
invalid_addr_packet_array = kcalloc(packet_array_size, sizeof(void *),
GFP_KERNEL);
if (!invalid_addr_packet_array) {
pr_warn("failed to allocate invalid_addr_packet_array\n");
retval = -ENOMEM;
spin_lock(&rbu_data.lock);
goto out_alloc_packet;
}
while (!packet_data_temp_buf) {
packet_data_temp_buf = (unsigned char *)
__get_free_pages(GFP_KERNEL, ordernum);
if (!packet_data_temp_buf) {
pr_warn("failed to allocate new packet\n");
retval = -ENOMEM;
spin_lock(&rbu_data.lock);
goto out_alloc_packet_array;
}
if ((unsigned long)virt_to_phys(packet_data_temp_buf)
< allocation_floor) {
pr_debug("packet 0x%lx below floor at 0x%lx\n",
(unsigned long)virt_to_phys(
packet_data_temp_buf),
allocation_floor);
invalid_addr_packet_array[idx++] = packet_data_temp_buf;
packet_data_temp_buf = NULL;
}
}
/*
* set to uncachable or it may never get written back before reboot
*/
set_memory_uc((unsigned long)packet_data_temp_buf, 1 << ordernum);
spin_lock(&rbu_data.lock);
newpacket->data = packet_data_temp_buf;
pr_debug("newpacket at physical addr %lx\n",
(unsigned long)virt_to_phys(newpacket->data));
/* packets may not have fixed size */
newpacket->length = length;
newpacket->ordernum = ordernum;
++rbu_data.num_packets;
/* initialize the newly created packet headers */
INIT_LIST_HEAD(&newpacket->list);
list_add_tail(&newpacket->list, &packet_data_list);
memcpy(newpacket->data, data, length);
pr_debug("exit\n");
out_alloc_packet_array:
/* always free packet array */
while (idx--) {
pr_debug("freeing unused packet below floor 0x%lx\n",
(unsigned long)virt_to_phys(invalid_addr_packet_array[idx]));
free_pages((unsigned long)invalid_addr_packet_array[idx], ordernum);
}
kfree(invalid_addr_packet_array);
out_alloc_packet:
/* if error, free data */
if (retval)
kfree(newpacket);
out_noalloc:
return retval;
}
static int packetize_data(const u8 *data, size_t length)
{
int rc = 0;
int done = 0;
int packet_length;
u8 *temp;
u8 *end = (u8 *) data + length;
pr_debug("data length %zd\n", length);
if (!rbu_data.packetsize) {
pr_warn("packetsize not specified\n");
return -EIO;
}
temp = (u8 *) data;
/* packetize the hunk */
while (!done) {
if ((temp + rbu_data.packetsize) < end)
packet_length = rbu_data.packetsize;
else {
/* this is the last packet */
packet_length = end - temp;
done = 1;
}
rc = create_packet(temp, packet_length);
if (rc)
return rc;
pr_debug("%p:%td\n", temp, (end - temp));
temp += packet_length;
}
rbu_data.imagesize = length;
return rc;
}
static int do_packet_read(char *data, struct packet_data *newpacket,
int length, int bytes_read, int *list_read_count)
{
void *ptemp_buf;
int bytes_copied = 0;
int j = 0;
*list_read_count += newpacket->length;
if (*list_read_count > bytes_read) {
/* point to the start of unread data */
j = newpacket->length - (*list_read_count - bytes_read);
/* point to the offset in the packet buffer */
ptemp_buf = (u8 *) newpacket->data + j;
/*
* check if there is enough room in
* * the incoming buffer
*/
if (length > (*list_read_count - bytes_read))
/*
* copy what ever is there in this
* packet and move on
*/
bytes_copied = (*list_read_count - bytes_read);
else
/* copy the remaining */
bytes_copied = length;
memcpy(data, ptemp_buf, bytes_copied);
}
return bytes_copied;
}
static int packet_read_list(char *data, size_t *pread_length)
{
struct packet_data *newpacket;
int temp_count = 0;
int bytes_copied = 0;
int bytes_read = 0;
int remaining_bytes = 0;
char *pdest = data;
/* check if we have any packets */
if (0 == rbu_data.num_packets)
return -ENOMEM;
remaining_bytes = *pread_length;
bytes_read = rbu_data.packet_read_count;
list_for_each_entry(newpacket, &packet_data_list, list) {
bytes_copied = do_packet_read(pdest, newpacket,
remaining_bytes, bytes_read, &temp_count);
remaining_bytes -= bytes_copied;
bytes_read += bytes_copied;
pdest += bytes_copied;
/*
* check if we reached end of buffer before reaching the
* last packet
*/
if (remaining_bytes == 0)
break;
}
/*finally set the bytes read */
*pread_length = bytes_read - rbu_data.packet_read_count;
rbu_data.packet_read_count = bytes_read;
return 0;
}
static void packet_empty_list(void)
{
struct packet_data *newpacket, *tmp;
list_for_each_entry_safe(newpacket, tmp, &packet_data_list, list) {
list_del(&newpacket->list);
/*
* zero out the RBU packet memory before freeing
* to make sure there are no stale RBU packets left in memory
*/
memset(newpacket->data, 0, newpacket->length);
set_memory_wb((unsigned long)newpacket->data,
1 << newpacket->ordernum);
free_pages((unsigned long) newpacket->data,
newpacket->ordernum);
kfree(newpacket);
}
rbu_data.packet_read_count = 0;
rbu_data.num_packets = 0;
rbu_data.imagesize = 0;
}
/*
* img_update_free: Frees the buffer allocated for storing BIOS image
* Always called with lock held and returned with lock held
*/
static void img_update_free(void)
{
if (!rbu_data.image_update_buffer)
return;
/*
* zero out this buffer before freeing it to get rid of any stale
* BIOS image copied in memory.
*/
memset(rbu_data.image_update_buffer, 0,
rbu_data.image_update_buffer_size);
free_pages((unsigned long) rbu_data.image_update_buffer,
rbu_data.image_update_ordernum);
/*
* Re-initialize the rbu_data variables after a free
*/
rbu_data.image_update_ordernum = -1;
rbu_data.image_update_buffer = NULL;
rbu_data.image_update_buffer_size = 0;
rbu_data.bios_image_size = 0;
}
/*
* img_update_realloc: This function allocates the contiguous pages to
* accommodate the requested size of data. The memory address and size
* values are stored globally and on every call to this function the new
* size is checked to see if more data is required than the existing size.
* If true the previous memory is freed and new allocation is done to
* accommodate the new size. If the incoming size is less then than the
* already allocated size, then that memory is reused. This function is
* called with lock held and returns with lock held.
*/
static int img_update_realloc(unsigned long size)
{
unsigned char *image_update_buffer = NULL;
unsigned long img_buf_phys_addr;
int ordernum;
/*
* check if the buffer of sufficient size has been
* already allocated
*/
if (rbu_data.image_update_buffer_size >= size) {
/*
* check for corruption
*/
if ((size != 0) && (rbu_data.image_update_buffer == NULL)) {
pr_err("corruption check failed\n");
return -EINVAL;
}
/*
* we have a valid pre-allocated buffer with
* sufficient size
*/
return 0;
}
/*
* free any previously allocated buffer
*/
img_update_free();
spin_unlock(&rbu_data.lock);
ordernum = get_order(size);
image_update_buffer =
(unsigned char *)__get_free_pages(GFP_DMA32, ordernum);
spin_lock(&rbu_data.lock);
if (!image_update_buffer) {
pr_debug("Not enough memory for image update: size = %ld\n", size);
return -ENOMEM;
}
img_buf_phys_addr = (unsigned long)virt_to_phys(image_update_buffer);
if (WARN_ON_ONCE(img_buf_phys_addr > BIOS_SCAN_LIMIT))
return -EINVAL; /* can't happen per definition */
rbu_data.image_update_buffer = image_update_buffer;
rbu_data.image_update_buffer_size = size;
rbu_data.bios_image_size = rbu_data.image_update_buffer_size;
rbu_data.image_update_ordernum = ordernum;
return 0;
}
static ssize_t read_packet_data(char *buffer, loff_t pos, size_t count)
{
int retval;
size_t bytes_left;
size_t data_length;
char *ptempBuf = buffer;
/* check to see if we have something to return */
if (rbu_data.num_packets == 0) {
pr_debug("no packets written\n");
retval = -ENOMEM;
goto read_rbu_data_exit;
}
if (pos > rbu_data.imagesize) {
retval = 0;
pr_warn("data underrun\n");
goto read_rbu_data_exit;
}
bytes_left = rbu_data.imagesize - pos;
data_length = min(bytes_left, count);
retval = packet_read_list(ptempBuf, &data_length);
if (retval < 0)
goto read_rbu_data_exit;
if ((pos + count) > rbu_data.imagesize) {
rbu_data.packet_read_count = 0;
/* this was the last copy */
retval = bytes_left;
} else
retval = count;
read_rbu_data_exit:
return retval;
}
static ssize_t read_rbu_mono_data(char *buffer, loff_t pos, size_t count)
{
/* check to see if we have something to return */
if ((rbu_data.image_update_buffer == NULL) ||
(rbu_data.bios_image_size == 0)) {
pr_debug("image_update_buffer %p, bios_image_size %lu\n",
rbu_data.image_update_buffer,
rbu_data.bios_image_size);
return -ENOMEM;
}
return memory_read_from_buffer(buffer, count, &pos,
rbu_data.image_update_buffer, rbu_data.bios_image_size);
}
static ssize_t data_read(struct file *filp, struct kobject *kobj,
const struct bin_attribute *bin_attr,
char *buffer, loff_t pos, size_t count)
{
ssize_t ret_count = 0;
spin_lock(&rbu_data.lock);
if (!strcmp(image_type, "mono"))
ret_count = read_rbu_mono_data(buffer, pos, count);
else if (!strcmp(image_type, "packet"))
ret_count = read_packet_data(buffer, pos, count);
else
pr_debug("invalid image type specified\n");
spin_unlock(&rbu_data.lock);
return ret_count;
}
static const BIN_ATTR_RO(data, 0);
static void callbackfn_rbu(const struct firmware *fw, void *context)
{
rbu_data.entry_created = 0;
if (!fw)
return;
if (!fw->size)
goto out;
spin_lock(&rbu_data.lock);
if (!strcmp(image_type, "mono")) {
if (!img_update_realloc(fw->size))
memcpy(rbu_data.image_update_buffer,
fw->data, fw->size);
} else if (!strcmp(image_type, "packet")) {
/*
* we need to free previous packets if a
* new hunk of packets needs to be downloaded
*/
packet_empty_list();
if (packetize_data(fw->data, fw->size))
/* Incase something goes wrong when we are
* in middle of packetizing the data, we
* need to free up whatever packets might
* have been created before we quit.
*/
packet_empty_list();
} else
pr_debug("invalid image type specified\n");
spin_unlock(&rbu_data.lock);
out:
release_firmware(fw);
}
static ssize_t image_type_read(struct file *filp, struct kobject *kobj,
const struct bin_attribute *bin_attr,
char *buffer, loff_t pos, size_t count)
{
int size = 0;
if (!pos)
size = scnprintf(buffer, count, "%s\n", image_type);
return size;
}
static ssize_t image_type_write(struct file *filp, struct kobject *kobj,
const struct bin_attribute *bin_attr,
char *buffer, loff_t pos, size_t count)
{
int rc = count;
int req_firm_rc = 0;
int i;
spin_lock(&rbu_data.lock);
/*
* Find the first newline or space
*/
for (i = 0; i < count; ++i)
if (buffer[i] == '\n' || buffer[i] == ' ') {
buffer[i] = '\0';
break;
}
if (i == count)
buffer[count] = '\0';
if (strstr(buffer, "mono"))
strscpy(image_type, "mono");
else if (strstr(buffer, "packet"))
strscpy(image_type, "packet");
else if (strstr(buffer, "init")) {
/*
* If due to the user error the driver gets in a bad
* state where even though it is loaded , the
* /sys/class/firmware/dell_rbu entries are missing.
* to cover this situation the user can recreate entries
* by writing init to image_type.
*/
if (!rbu_data.entry_created) {
spin_unlock(&rbu_data.lock);
req_firm_rc = request_firmware_nowait(THIS_MODULE,
FW_ACTION_NOUEVENT, "dell_rbu",
&rbu_device->dev, GFP_KERNEL, &context,
callbackfn_rbu);
if (req_firm_rc) {
pr_err("request_firmware_nowait failed %d\n", rc);
rc = -EIO;
} else
rbu_data.entry_created = 1;
spin_lock(&rbu_data.lock);
}
} else {
pr_warn("image_type is invalid\n");
spin_unlock(&rbu_data.lock);
return -EINVAL;
}
/* we must free all previous allocations */
packet_empty_list();
img_update_free();
spin_unlock(&rbu_data.lock);
return rc;
}
static const BIN_ATTR_RW(image_type, 0);
static ssize_t packet_size_read(struct file *filp, struct kobject *kobj,
const struct bin_attribute *bin_attr,
char *buffer, loff_t pos, size_t count)
{
int size = 0;
if (!pos) {
spin_lock(&rbu_data.lock);
size = scnprintf(buffer, count, "%lu\n", rbu_data.packetsize);
spin_unlock(&rbu_data.lock);
}
return size;
}
static ssize_t packet_size_write(struct file *filp, struct kobject *kobj,
const struct bin_attribute *bin_attr,
char *buffer, loff_t pos, size_t count)
{
unsigned long temp;
if (kstrtoul(buffer, 10, &temp))
return -EINVAL;
spin_lock(&rbu_data.lock);
packet_empty_list();
if (temp < 0xffffffff)
rbu_data.packetsize = temp;
spin_unlock(&rbu_data.lock);
return count;
}
static const BIN_ATTR_RW(packet_size, 0);
static const struct bin_attribute *const rbu_bin_attrs[] = {
&bin_attr_data,
&bin_attr_image_type,
&bin_attr_packet_size,
NULL
};
static const struct attribute_group rbu_group = {
.bin_attrs = rbu_bin_attrs,
};
static int __init dcdrbu_init(void)
{
int rc;
spin_lock_init(&rbu_data.lock);
init_packet_head();
rbu_device = platform_device_register_simple("dell_rbu", PLATFORM_DEVID_NONE, NULL, 0);
if (IS_ERR(rbu_device)) {
pr_err("platform_device_register_simple failed\n");
return PTR_ERR(rbu_device);
}
rc = sysfs_create_group(&rbu_device->dev.kobj, &rbu_group);
if (rc)
goto out_devreg;
rbu_data.entry_created = 0;
return 0;
out_devreg:
platform_device_unregister(rbu_device);
return rc;
}
static __exit void dcdrbu_exit(void)
{
spin_lock(&rbu_data.lock);
packet_empty_list();
img_update_free();
spin_unlock(&rbu_data.lock);
sysfs_remove_group(&rbu_device->dev.kobj, &rbu_group);
platform_device_unregister(rbu_device);
}
module_exit(dcdrbu_exit);
module_init(dcdrbu_init);