From patchwork Sat Oct 21 06:41:02 2017 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: Corentin Chary X-Patchwork-Id: 10021263 X-Patchwork-Delegate: andy.shevchenko@gmail.com Return-Path: Received: from mail.wl.linuxfoundation.org (pdx-wl-mail.web.codeaurora.org [172.30.200.125]) by pdx-korg-patchwork.web.codeaurora.org (Postfix) with ESMTP id BEAB1603B5 for ; Sat, 21 Oct 2017 06:41:16 +0000 (UTC) Received: from mail.wl.linuxfoundation.org (localhost [127.0.0.1]) by mail.wl.linuxfoundation.org (Postfix) with ESMTP id A880327F8C for ; Sat, 21 Oct 2017 06:41:16 +0000 (UTC) Received: by mail.wl.linuxfoundation.org (Postfix, from userid 486) id 8928328D83; Sat, 21 Oct 2017 06:41:16 +0000 (UTC) X-Spam-Checker-Version: SpamAssassin 3.3.1 (2010-03-16) on pdx-wl-mail.web.codeaurora.org X-Spam-Level: X-Spam-Status: No, score=-6.5 required=2.0 tests=BAYES_00,DKIM_SIGNED, DKIM_VALID, DKIM_VALID_AU, FREEMAIL_FROM, RCVD_IN_DNSWL_HI, RCVD_IN_SORBS_SPAM autolearn=ham version=3.3.1 Received: from vger.kernel.org (vger.kernel.org [209.132.180.67]) by mail.wl.linuxfoundation.org (Postfix) with ESMTP id 73D9C27F8C for ; Sat, 21 Oct 2017 06:41:14 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S1751124AbdJUGlM (ORCPT ); Sat, 21 Oct 2017 02:41:12 -0400 Received: from mail-wm0-f48.google.com ([74.125.82.48]:47475 "EHLO mail-wm0-f48.google.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1751111AbdJUGlK (ORCPT ); Sat, 21 Oct 2017 02:41:10 -0400 Received: by mail-wm0-f48.google.com with SMTP id t69so1234275wmt.2; Fri, 20 Oct 2017 23:41:09 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20161025; h=from:to:cc:subject:date:message-id:in-reply-to:references :mime-version:content-transfer-encoding; bh=cGFPDDr+2J/upgknj+DSDrtSoEoriLviKvtDWtuCuOw=; b=nynG1kPYV4V1Zclrdzek2LeEvUgoVbTS3uPDyR41sCjq5djlBY8Qo+RBRAtd1dOCwn G4gdHGsRi7CDb91ZzmXJC6CVZCJZCVHPitdvkMZF8Ux65OA+1N3cBvVJA9UJreo0c8hx j31Q3JOBLNQ2S3qgGSM1sWLV66jn/eAjbmAqrk1/fJBASguuqvgvrQiW07s8o9PW1SMW UlWklU5gkADOsV7WzaJSCGXudzrXjpoqoKYKACSYAmN1XrTlFgjBb4KSWkK3M+Gh7cqM PnPmX4llmirfp5dR3WNbbb1xEvVbzcBsPTg0aDxwcy8nkY55EkasG7Sb66FssjeO9haY aimQ== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20161025; h=x-gm-message-state:from:to:cc:subject:date:message-id:in-reply-to :references:mime-version:content-transfer-encoding; bh=cGFPDDr+2J/upgknj+DSDrtSoEoriLviKvtDWtuCuOw=; b=MSFQ+8RImCQDuR1uEhBIhCizrxBBxNQHqxnwGcFe1Z8i2gk6ueIrM6CY2ICHzJ2tPx tV/b8ZHXqrXxvXiXY7twA3QAEGmtx2A9yqR5E+toSI2SK2uToOqBRiMYukBtFOmjpyRM WrdCCNOdpukBZDRY3dY1jQLMCj0EJIUf8ObRC3tkyg8B2T4qgOJ8eGSs5R/o3YVJs2Na YZXQPvAl1Natg/ocE3Cm2ctAJ5mky6wfNDRDLWC1/fpgemqy3Zf+jFCknPXqiFmgnUez Lht1U6RFo3XUSeHJ0jCFpWAgy12kLTxPIcGRHu4pEsPesIhKzuzhClMmrhzfWz/TesYC Kp6g== X-Gm-Message-State: AMCzsaXmIm2tBYaWWpFMo/neYhfLZh/ybHPpYl39Tq0pEERwJNpmqhqV ZS7uSAqBnZR/t9nOUEcA6J8= X-Google-Smtp-Source: ABhQp+SEgcdeT5EBK1ofLR+ZJdLey3BPqg4ps7w0L31PovLKhOSRSN4z2P+nV1ADdN7Cg7hcd9bFyw== X-Received: by 10.80.230.24 with SMTP id y24mr9042749edm.211.1508568068327; Fri, 20 Oct 2017 23:41:08 -0700 (PDT) Received: from coulomier.criteo.prod ([91.199.242.238]) by smtp.gmail.com with ESMTPSA id l9sm1719497edi.31.2017.10.20.23.41.06 (version=TLS1_2 cipher=ECDHE-RSA-AES128-GCM-SHA256 bits=128/128); Fri, 20 Oct 2017 23:41:07 -0700 (PDT) From: Corentin Chary To: Darren Hart Cc: Andy Shevchenko , corentin.chary@gmail.com, platform-driver-x86@vger.kernel.org, linux-kernel@vger.kernel.org Subject: [PATCH] drivers/x86: add thinkpad-wmi Date: Sat, 21 Oct 2017 08:41:02 +0200 Message-Id: <20171021064102.15166-1-corentin.chary@gmail.com> X-Mailer: git-send-email 2.14.1 In-Reply-To: <20170904082110.30925-1-corentin.chary@gmail.com> References: <20170904082110.30925-1-corentin.chary@gmail.com> MIME-Version: 1.0 Sender: platform-driver-x86-owner@vger.kernel.org Precedence: bulk List-ID: X-Mailing-List: platform-driver-x86@vger.kernel.org X-Virus-Scanned: ClamAV using ClamSMTP This driver has been available on https://github.com/iksaif/thinkpad-wmi for a few year and is already deployed on large fleets of thinkpad laptops. The WMI interface is documented here: http://download.lenovo.com/ibmdl/pub/pc/pccbbs/thinkcentre_pdf/hrdeploy_en.pdf It mostly focused on changing BIOS/Firmware settings. Signed-off-by: Corentin Chary --- .../ABI/testing/sysfs-platform-thinkpad-wmi | 50 + Documentation/platform/thinkpad-wmi.txt | 92 ++ drivers/platform/x86/Kconfig | 10 + drivers/platform/x86/Makefile | 1 + drivers/platform/x86/thinkpad-wmi.c | 1210 ++++++++++++++++++++ 5 files changed, 1363 insertions(+) create mode 100644 Documentation/ABI/testing/sysfs-platform-thinkpad-wmi create mode 100644 Documentation/platform/thinkpad-wmi.txt create mode 100644 drivers/platform/x86/thinkpad-wmi.c diff --git a/Documentation/ABI/testing/sysfs-platform-thinkpad-wmi b/Documentation/ABI/testing/sysfs-platform-thinkpad-wmi new file mode 100644 index 000000000000..c3673876c5b3 --- /dev/null +++ b/Documentation/ABI/testing/sysfs-platform-thinkpad-wmi @@ -0,0 +1,50 @@ +What: /sys/devices/platform/thinkpad-wmi/password +Date: Aug 2017 +KernelVersion: 4.14 +Contact: "Corentin Chary" +Description: + BIOS password needs to be written in this file if set + to be able to change BIOS settings. + +What: /sys/devices/platform/thinkpad-wmi/password_encoding +Date: Aug 2017 +KernelVersion: 4.14 +Contact: "Corentin Chary" +Description: + Password encoding ('ascii' or 'scanmode'). + +What: /sys/devices/platform/thinkpad-wmi/password_kbd_lang +Date: Aug 2017 +KernelVersion: 4.14 +Contact: "Corentin Chary" +Description: + Keyboard language used for password. One of 'us', 'fr' and 'gr'. + +What: /sys/devices/platform/thinkpad-wmi/password_type +Date: Aug 2017 +KernelVersion: 4.14 +Contact: "Corentin Chary" +Description: + Password type to be changed when password_change is written to, e.g. 'pap'. + +What: /sys/devices/platform/thinkpad-wmi/password_change +Date: Aug 2017 +KernelVersion: 4.14 +Contact: "Corentin Chary" +Description: + Writing to this file will set the password specified in password_type. + The new password will not take effect until the next reboot. + +What: /sys/devices/platform/thinkpad-wmi/password_settings +Date: Oct 2015 +KernelVersion: 4.14 +Contact: "Corentin Chary" +Description: + Display various password settings. + +What: /sys/devices/platform/thinkpad-wmi/load_default_settings +Date: Oct 2015 +KernelVersion: 4.14 +Contact: "Corentin Chary" +Description: + Write anything to this file to load default BIOS settings. diff --git a/Documentation/platform/thinkpad-wmi.txt b/Documentation/platform/thinkpad-wmi.txt new file mode 100644 index 000000000000..40d141aecc7b --- /dev/null +++ b/Documentation/platform/thinkpad-wmi.txt @@ -0,0 +1,92 @@ +# thinkpad-wmi + +Linux Driver for Thinkpad WMI interface, allows you to control most +BIOS settings from Linux, and maybe more. + +## sysfs interface + +Directory: /sys/bus/wmi/drivers/thinkpad-wmi/ + +Each setting exposed by the WMI interface is available under its own name +in this sysfs directory. Read from the file to get the current value (line 1) +and list of options (line 2), and write an option to the file to set it. + +Additionally, there are some extra files for querying and managing BIOS +password(s). + +### password + +Must contain the BIOS supervisor password (aka 'pap'), if set, to be able to do +any change. + +Every subsequent password change will be authorized with this password. The +password may be unloaded by writing an empty string. Writing an invalid +password may trigger the BIOS' invalid password limit, such that a reboot will +be required in order to make any further BIOS changes. + +### password_encoding + +Encoding used for the password, either '', 'scancode' or 'ascii'. + +Scan-code encoding appears to require the key-down scan codes, e.g. 0x1e, 0x30, +0x2e for the ASCII encoded password 'abc'. + +### password_kbd_lang + +Keyboard language mapping, can be '', 'us', 'fr' or 'gr'. + +### password_type + +Specify the password type to be changed when password_change is written to. +Can be: +* 'pap': supervisor password +* 'pop': power-on-password + +Other types may be valid, e.g. for user and master disk passwords. + +### password_change + +Writing to this file will change the password specified by password_type. The +new password will not take effect until the next reboot. + +### password_settings + +Display password related settings. This includes: + +* password_state: which passwords are set, if any + * bit 0: user password (power on password) is installed / 'pop' + * bit 1: admin/supervisor password is installed / 'pap' + * bit 2: hdd password(s) installed +* supported_encodings: supported keyboard encoding(s) + * bit 0: ASCII + * bit 1: scancode +* supported_keyboard: support keyboard language(s) + * bit 0: us + * bit 1: fr + * bit 2: gr + +### load_default_settings + +Reset all settings to factory default. + +## debugfs interface + +The debugfs interface maps closely to the WMI Interface (see driver and doc). + +* bios_settings: show all BIOS settings +* bios_setting: show BIOS setting for +* list_valid_choices: list settings for +* set_bios_settings: call set bios settings command with . +* save_bios_settings call save bios settings command with . +* discard_bios_settings: call discard bios settings command with . +* load_default: call load default with . +* set_bios_password: call set BIOS password with . +* argument: argument to be used in various commands. +* instance: setting instance. +* instance_count: number of settings. +* password_settings: password settings. + +## References + +Thinkpad WMI interface documentation: +http://download.lenovo.com/ibmdl/pub/pc/pccbbs/thinkcentre_pdf/hrdeploy_en.pdf diff --git a/drivers/platform/x86/Kconfig b/drivers/platform/x86/Kconfig index 80b87954f6dd..4e2e8a04228a 100644 --- a/drivers/platform/x86/Kconfig +++ b/drivers/platform/x86/Kconfig @@ -511,6 +511,16 @@ config THINKPAD_ACPI_HOTKEY_POLL If you are not sure, say Y here. The driver enables polling only if it is strictly necessary to do so. +config THINKPAD_WMI + tristate "THINKPAD WMI Driver (EXPERIMENTAL)" + depends on ACPI_WMI + ---help--- + This driver allow you to modify BIOS passwords, settings, and boot order + using Windows Management Instrumentation (WMI) through the Lenovo + client-management interface. + + Say Y here if you have a WMI aware Thinkpad. + config SENSORS_HDAPS tristate "Thinkpad Hard Drive Active Protection System (hdaps)" depends on INPUT diff --git a/drivers/platform/x86/Makefile b/drivers/platform/x86/Makefile index 91cec1751461..3b03f0744794 100644 --- a/drivers/platform/x86/Makefile +++ b/drivers/platform/x86/Makefile @@ -28,6 +28,7 @@ obj-$(CONFIG_TC1100_WMI) += tc1100-wmi.o obj-$(CONFIG_SONY_LAPTOP) += sony-laptop.o obj-$(CONFIG_IDEAPAD_LAPTOP) += ideapad-laptop.o obj-$(CONFIG_THINKPAD_ACPI) += thinkpad_acpi.o +obj-$(CONFIG_THINKPAD_WMI) += thinkpad-wmi.o obj-$(CONFIG_SENSORS_HDAPS) += hdaps.o obj-$(CONFIG_FUJITSU_LAPTOP) += fujitsu-laptop.o obj-$(CONFIG_FUJITSU_TABLET) += fujitsu-tablet.o diff --git a/drivers/platform/x86/thinkpad-wmi.c b/drivers/platform/x86/thinkpad-wmi.c new file mode 100644 index 000000000000..c102971f2979 --- /dev/null +++ b/drivers/platform/x86/thinkpad-wmi.c @@ -0,0 +1,1210 @@ +/* + * Thinkpad WMI configuration driver + * + * Copyright(C) 2017 Corentin Chary + * + * 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; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define THINKPAD_WMI_FILE "thinkpad-wmi" + +MODULE_AUTHOR("Corentin Chary "); +MODULE_DESCRIPTION("Thinkpad WMI Driver"); +MODULE_LICENSE("GPL"); + +/* WMI interface */ + +/** + * Name: + * Lenovo_BiosSetting + * Description: + * Get item name and settings for current WMI instance. + * Type: + * Query + * Returns: + * "Item,Value" + * Example: + * "WakeOnLAN,Enable" + */ +#define LENOVO_BIOS_SETTING_GUID \ + "51F5230E-9677-46CD-A1CF-C0B23EE34DB7" + +/** + * Name: + * Lenovo_SetBiosSetting + * Description: + * Change the BIOS setting to the desired value using the Lenovo_SetBiosSetting + * class. To save the settings, use the Lenovo_SaveBiosSetting class. + * BIOS settings and values are case sensitive. + * After making changes to the BIOS settings, you must reboot the computer + * before the changes will take effect. + * Type: + * Method + * Arguments: + * "Item,Value,Password,Encoding,KbdLang;" + * Example: + * "WakeOnLAN,Disable,pswd,ascii,us;" + */ +#define LENOVO_SET_BIOS_SETTINGS_GUID \ + "98479A64-33F5-4E33-A707-8E251EBBC3A1" + +/** + * Name: + * Lenovo_SaveBiosSettings + * Description: + * Save any pending changes in settings. + * Type: + * Method + * Arguments: + * "Password,Encoding,KbdLang;" + * Example: + * "pswd,ascii,us;" + */ +#define LENOVO_SAVE_BIOS_SETTINGS_GUID \ + "6A4B54EF-A5ED-4D33-9455-B0D9B48DF4B3" + + +/** + * Name: + * Lenovo_DiscardBiosSettings + * Description: + * Discard any pending changes in settings. + * Type: + * Method + * Arguments: + * "Password,Encoding,KbdLang;" + * Example: + * "pswd,ascii,us;" + */ +#define LENOVO_DISCARD_BIOS_SETTINGS_GUID \ + "74F1EBB6-927A-4C7D-95DF-698E21E80EB5" + +/** + * Name: + * Lenovo_LoadDefaultSettings + * Description: + * Load default BIOS settings. Use Lenovo_SaveBiosSettings to save the + * settings. + * Type: + * Method + * Arguments: + * "Password,Encoding,KbdLang;" + * Example: + * "pswd,ascii,us;" + */ +#define LENOVO_LOAD_DEFAULT_SETTINGS_GUID \ + "7EEF04FF-4328-447C-B5BB-D449925D538D" + +/** + * Name: + * Lenovo_BiosPasswordSettings + * Description: + * Return BIOS Password settings + * Type: + * Query + * Returns: + * PasswordMode, PasswordState, MinLength, MaxLength, + * SupportedEncoding, SupportedKeyboard + */ +#define LENOVO_BIOS_PASSWORD_SETTINGS_GUID \ + "8ADB159E-1E32-455C-BC93-308A7ED98246" + +/** + * Name: + * Lenovo_SetBiosPassword + * Description: + * Change a specific password. + * - BIOS settings cannot be changed at the same boot as power-on + * passwords (POP) and hard disk passwords (HDP). If you want to change + * BIOS settings and POP or HDP, you must reboot the system after changing + * one of them. + * - A password cannot be set using this method when one does not already + * exist. Passwords can only be updated or cleared. + * Type: + * Method + * Arguments: + * "PasswordType,CurrentPassword,NewPassword,Encoding,KbdLang;" + * Example: + * "pop,oldpop,newpop,ascii,us;” + */ +#define LENOVO_SET_BIOS_PASSWORD_GUID \ + "2651D9FD-911C-4B69-B94E-D0DED5963BD7" + +/** + * Name: + * Lenovo_GetBiosSelections + * Description: + * Return a list valid settings for a given item. + * Type: + * Method + * Arguments: + * "Item" + * Returns: + * "Value1,Value2,Value3,..." + * Example: + * -> "FlashOverLAN" + * <- "Enabled,Disabled" + */ +#define LENOVO_GET_BIOS_SELECTIONS_GUID \ + "7364651A-132F-4FE7-ADAA-40C6C7EE2E3B" + +/** + * Name: + * ??? + * Type: + * Method + * Arguments: + * ??? + * Example: + * ??? + * WMI-Internals: + * Return big chunk of data + */ +#define LENOVO_QUERY_GUID \ + "05901221-D566-11D1-B2F0-00A0C9062910" + +/* Return values */ + +enum { + /* + * "Success" + * Operation completed successfully. + */ + THINKPAD_WMI_SUCCESS = 0, + /* + * "Not Supported" + * The feature is not supported on this system. + */ + THINKPAD_WMI_NOT_SUPPORTED = -ENODEV, + /* + * "Invalid" + * The item or value provided is not valid parameter + */ + THINKPAD_WMI_INVALID = -EINVAL, + /* + * "Access Denied" + * The change could not be made due to an authentication problem. + * If a supervisor password exists, the correct supervisor password + * must be provided. + */ + THINKPAD_WMI_ACCESS_DENIED = -EPERM, + /* "System Busy" + * BIOS changes have already been made that need to be committed. + * Reboot the system and try again. + */ + THINKPAD_WMI_SYSTEM_BUSY = -EBUSY +}; + +/* Only add an alias on this one, since it's the one used + * in thinkpad_wmi_probe. + */ +MODULE_ALIAS("wmi:"LENOVO_BIOS_SETTING_GUID); + +struct thinkpad_wmi_pcfg { + uint32_t password_mode; + uint32_t password_state; + uint32_t min_length; + uint32_t max_length; + uint32_t supported_encodings; + uint32_t supported_keyboard; +}; + +/* + * thinkpad_wmi/ - debugfs root directory + * bios_settings + * bios_setting + * list_valid_choices + * set_bios_settings + * save_bios_settings + * discard_bios_settings + * load_default + * set_bios_password + * argument + * instance + * instance_count + * bios_password_settings + */ +struct thinkpad_wmi_debug { + struct dentry *root; + + u8 instances_count; + u8 instance; + char argument[512]; +}; + +struct thinkpad_wmi { + struct wmi_device *wmi_device; + + int settings_count; + + char password[64]; + char password_encoding[64]; + char password_kbdlang[4]; /* 2 bytes for \n\0 */ + char auth_string[256]; + char password_type[64]; + + bool can_set_bios_settings; + bool can_discard_bios_settings; + bool can_load_default_settings; + bool can_get_bios_selections; + bool can_set_bios_password; + bool can_get_password_settings; + + char *settings[256]; + struct dev_ext_attribute *devattrs; + struct thinkpad_wmi_debug debug; +}; + +/* helpers */ +static int thinkpad_wmi_errstr_to_err(const char *errstr) +{ + if (!strcmp(errstr, "Success")) + return THINKPAD_WMI_SUCCESS; + if (!strcmp(errstr, "Not Supported")) + return THINKPAD_WMI_NOT_SUPPORTED; + if (!strcmp(errstr, "Invalid")) + return THINKPAD_WMI_INVALID; + if (!strcmp(errstr, "Access Denied")) + return THINKPAD_WMI_ACCESS_DENIED; + if (!strcmp(errstr, "System Busy")) + return THINKPAD_WMI_SYSTEM_BUSY; + + pr_debug("Unknown error string: '%s'", errstr); + + return -EINVAL; +} + +static int thinkpad_wmi_extract_error(const struct acpi_buffer *output) +{ + const union acpi_object *obj; + int ret; + + obj = output->pointer; + if (!obj || obj->type != ACPI_TYPE_STRING || !obj->string.pointer) + return -EIO; + + ret = thinkpad_wmi_errstr_to_err(obj->string.pointer); + kfree(obj); + return ret; +} + +static int thinkpad_wmi_simple_call(const char *guid, + const char *arg) +{ + const struct acpi_buffer input = { strlen(arg), (char *)arg }; + struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL }; + acpi_status status; + + status = wmi_evaluate_method(guid, 0, 0, &input, &output); + + if (ACPI_FAILURE(status)) + return -EIO; + + return thinkpad_wmi_extract_error(&output); +} + +static int thinkpad_wmi_extract_output_string(const struct acpi_buffer *output, + char **string) +{ + const union acpi_object *obj; + + obj = output->pointer; + if (!obj || obj->type != ACPI_TYPE_STRING || !obj->string.pointer) + return -EIO; + + *string = kstrdup(obj->string.pointer, GFP_KERNEL); + kfree(obj); + return *string ? 0 : -ENOMEM; +} + +static int thinkpad_wmi_bios_setting(int item, char **value) +{ + struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL }; + acpi_status status; + + status = wmi_query_block(LENOVO_BIOS_SETTING_GUID, item, &output); + if (ACPI_FAILURE(status)) + return -EIO; + + return thinkpad_wmi_extract_output_string(&output, value); +} + +static int thinkpad_wmi_get_bios_selections(const char *item, char **value) +{ + const struct acpi_buffer input = { strlen(item), (char *)item }; + struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL }; + acpi_status status; + + status = wmi_evaluate_method(LENOVO_GET_BIOS_SELECTIONS_GUID, + 0, 0, &input, &output); + + if (ACPI_FAILURE(status)) + return -EIO; + + return thinkpad_wmi_extract_output_string(&output, value); +} + +static int thinkpad_wmi_set_bios_settings(const char *settings) +{ + return thinkpad_wmi_simple_call(LENOVO_SET_BIOS_SETTINGS_GUID, + settings); +} + +static int thinkpad_wmi_save_bios_settings(const char *password) +{ + return thinkpad_wmi_simple_call(LENOVO_SAVE_BIOS_SETTINGS_GUID, + password); +} + +static int thinkpad_wmi_discard_bios_settings(const char *password) +{ + return thinkpad_wmi_simple_call(LENOVO_DISCARD_BIOS_SETTINGS_GUID, + password); +} + +static int thinkpad_wmi_load_default(const char *password) +{ + return thinkpad_wmi_simple_call(LENOVO_LOAD_DEFAULT_SETTINGS_GUID, + password); +} + +static int thinkpad_wmi_set_bios_password(const char *settings) +{ + return thinkpad_wmi_simple_call(LENOVO_SET_BIOS_PASSWORD_GUID, + settings); +} + +static int thinkpad_wmi_password_settings(struct thinkpad_wmi_pcfg *pcfg) +{ + struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL }; + const union acpi_object *obj; + acpi_status status; + + status = wmi_query_block(LENOVO_BIOS_PASSWORD_SETTINGS_GUID, 0, + &output); + if (ACPI_FAILURE(status)) + return -EIO; + + obj = output.pointer; + if (!obj || obj->type != ACPI_TYPE_BUFFER || !obj->buffer.pointer) + return -EIO; + if (obj->buffer.length != sizeof(*pcfg)) { + pr_warn("Unknown pcfg buffer length %d\n", obj->buffer.length); + kfree(obj); + return -EIO; + } + + memcpy(pcfg, obj->buffer.pointer, obj->buffer.length); + kfree(obj); + return 0; +} + +/* sysfs */ + +#define to_ext_attr(x) container_of(x, struct dev_ext_attribute, attr) + +static ssize_t show_setting(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct thinkpad_wmi *thinkpad = dev_get_drvdata(dev); + struct dev_ext_attribute *ea = to_ext_attr(attr); + int item = (uintptr_t)ea->var; + char *name = thinkpad->settings[item]; + char *settings = NULL, *choices = NULL, *value; + ssize_t count = 0; + int ret; + + ret = thinkpad_wmi_bios_setting(item, &settings); + if (ret) + return ret; + if (!settings) + return -EIO; + + if (thinkpad->can_get_bios_selections) { + ret = thinkpad_wmi_get_bios_selections(name, &choices); + if (ret) + goto error; + if (!choices || !*choices) { + ret = -EIO; + goto error; + } + } + + value = strchr(settings, ','); + if (!value) + goto error; + value++; + + count = sprintf(buf, "%s\n", value); + if (choices) + count += sprintf(buf + count, "%s\n", choices); + +error: + kfree(settings); + kfree(choices); + return ret ? ret : count; +} + +static ssize_t store_setting(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct thinkpad_wmi *thinkpad = dev_get_drvdata(dev); + struct dev_ext_attribute *ea = to_ext_attr(attr); + int item_idx = (uintptr_t)ea->var; + const char *item = thinkpad->settings[item_idx]; + int ret; + size_t buffer_size; + char *buffer; + + /* Format: 'Item,Value,Authstring;' */ + buffer_size = (strlen(item) + 1 + count + 1 + + sizeof(thinkpad->auth_string) + 2); + buffer = kmalloc(buffer_size, GFP_KERNEL); + if (!buffer) + return -ENOMEM; + + strcpy(buffer, item); + strcat(buffer, ","); + strncat(buffer, buf, count); + if (count) + strim(buffer); + if (*thinkpad->auth_string) { + strcat(buffer, ","); + strcat(buffer, thinkpad->auth_string); + } + strcat(buffer, ";"); + + ret = thinkpad_wmi_set_bios_settings(buffer); + if (ret) + goto end; + + ret = thinkpad_wmi_save_bios_settings(thinkpad->auth_string); + if (ret) { + /* Try to discard the settings if we failed to apply them. */ + thinkpad_wmi_discard_bios_settings(thinkpad->auth_string); + goto end; + } + ret = count; + +end: + kfree(buffer); + return ret; +} + + +/* Password related sysfs methods */ +static ssize_t show_auth(struct thinkpad_wmi *thinkpad, char *buf, + const char *data, size_t size) +{ + if (!capable(CAP_SYS_ADMIN)) + return -EPERM; + + return sprintf(buf, "%s\n", data ? : "(nil)"); +} + +/* Create the auth string from password chunks */ +static void update_auth_string(struct thinkpad_wmi *thinkpad) +{ + if (!*thinkpad->password) { + /* No password at all */ + thinkpad->auth_string[0] = '\0'; + return; + } + strcpy(thinkpad->auth_string, thinkpad->password); + + if (*thinkpad->password_encoding) { + strcat(thinkpad->auth_string, ","); + strcat(thinkpad->auth_string, thinkpad->password_encoding); + } + + if (*thinkpad->password_kbdlang) { + strcat(thinkpad->auth_string, ","); + strcat(thinkpad->auth_string, thinkpad->password_kbdlang); + } +} + +static ssize_t store_auth(struct thinkpad_wmi *thinkpad, + const char *buf, size_t count, + char *dst, size_t size) +{ + ssize_t ret; + + if (!capable(CAP_SYS_ADMIN)) + return -EPERM; + + if (count > size - 1) + return -EINVAL; + + /* dst may be being reused, NUL-terminate */ + ret = strscpy(dst, buf, size); + if (ret < 0) + return ret; + if (count) + strim(dst); + + update_auth_string(thinkpad); + + return count; +} + +#define THINKPAD_WMI_CREATE_AUTH_ATTR(_name, _uname, _mode) \ + static ssize_t show_##_name(struct device *dev, \ + struct device_attribute *attr, \ + char *buf) \ + { \ + struct thinkpad_wmi *thinkpad = dev_get_drvdata(dev); \ + \ + return show_auth(thinkpad, buf, \ + thinkpad->_name, \ + sizeof(thinkpad->_name)); \ + } \ + static ssize_t store_##_name(struct device *dev, \ + struct device_attribute *attr, \ + const char *buf, size_t count) \ + { \ + struct thinkpad_wmi *thinkpad = dev_get_drvdata(dev); \ + \ + return store_auth(thinkpad, buf, count, \ + thinkpad->_name, \ + sizeof(thinkpad->_name)); \ + } \ + static struct device_attribute dev_attr_##_name = { \ + .attr = { \ + .name = _uname, \ + .mode = _mode }, \ + .show = show_##_name, \ + .store = store_##_name, \ + } + +THINKPAD_WMI_CREATE_AUTH_ATTR(password, "password", 0600); +THINKPAD_WMI_CREATE_AUTH_ATTR(password_encoding, "password_encoding", + 0600); +THINKPAD_WMI_CREATE_AUTH_ATTR(password_kbdlang, "password_kbd_lang", + 0600); +THINKPAD_WMI_CREATE_AUTH_ATTR(password_type, "password_type", 0600); + +static ssize_t show_password_settings(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct thinkpad_wmi_pcfg pcfg; + ssize_t ret; + + ret = thinkpad_wmi_password_settings(&pcfg); + if (ret) + return ret; + ret += sprintf(buf, "password_mode: %#x\n", pcfg.password_mode); + ret += sprintf(buf + ret, "password_state: %#x\n", + pcfg.password_state); + ret += sprintf(buf + ret, "min_length: %d\n", pcfg.min_length); + ret += sprintf(buf + ret, "max_length: %d\n", pcfg.max_length); + ret += sprintf(buf + ret, "supported_encodings: %#x\n", + pcfg.supported_encodings); + ret += sprintf(buf + ret, "supported_keyboard: %#x\n", + pcfg.supported_keyboard); + return ret; +} + +static DEVICE_ATTR(password_settings, 0400, show_password_settings, NULL); + +static ssize_t store_password_change(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct thinkpad_wmi *thinkpad = dev_get_drvdata(dev); + size_t buffer_size; + char *buffer; + ssize_t ret; + + if (!capable(CAP_SYS_ADMIN)) + return -EPERM; + + /* Format: 'PasswordType,CurrentPw,NewPw,Encoding,KbdLang;' */ + + /* auth_string is the size of CurrentPassword,Encoding,KbdLang */ + buffer_size = (sizeof(thinkpad->password_type) + 1 + count + 1 + + sizeof(thinkpad->auth_string) + 2); + buffer = kmalloc(buffer_size, GFP_KERNEL); + if (!buffer) + return -ENOMEM; + + strcpy(buffer, thinkpad->password_type); + + if (*thinkpad->password) { + strcat(buffer, ","); + strcat(buffer, thinkpad->password); + } + strcat(buffer, ","); + strncat(buffer, buf, count); + if (count) + strim(buffer); + + if (*thinkpad->password_encoding) { + strcat(buffer, ","); + strcat(buffer, thinkpad->password_encoding); + } + if (*thinkpad->password_kbdlang) { + strcat(buffer, ","); + strcat(buffer, thinkpad->password_kbdlang); + } + strcat(buffer, ";"); + + ret = thinkpad_wmi_set_bios_password(buffer); + if (ret) + return ret; + + return count; +} + +static struct device_attribute dev_attr_password_change = { + .attr = { + .name = "password_change", + .mode = 0200 }, + .store = store_password_change, +}; + + +static ssize_t store_load_default(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct thinkpad_wmi *thinkpad = dev_get_drvdata(dev); + + return thinkpad_wmi_load_default(thinkpad->auth_string); +} + +static DEVICE_ATTR(load_default_settings, 0200, NULL, store_load_default); + +static struct attribute *platform_attributes[] = { + &dev_attr_password_settings.attr, + &dev_attr_password.attr, + &dev_attr_password_encoding.attr, + &dev_attr_password_kbdlang.attr, + &dev_attr_password_type.attr, + &dev_attr_password_change.attr, + &dev_attr_load_default_settings.attr, + NULL +}; + +static umode_t thinkpad_sysfs_is_visible(struct kobject *kobj, + struct attribute *attr, + int idx) +{ + bool supported = true; + + return supported ? attr->mode : 0; +} + +static struct attribute_group platform_attribute_group = { + .is_visible = thinkpad_sysfs_is_visible, + .attrs = platform_attributes +}; + +static void thinkpad_wmi_sysfs_exit(struct wmi_device *wdev) +{ + struct thinkpad_wmi *thinkpad = dev_get_drvdata(&wdev->dev); + int i; + + sysfs_remove_group(&wdev->dev.kobj, &platform_attribute_group); + + if (!thinkpad->devattrs) + return; + + for (i = 0; i < thinkpad->settings_count; ++i) { + struct dev_ext_attribute *deveattr = &thinkpad->devattrs[i]; + struct device_attribute *devattr = &deveattr->attr; + + if (devattr->attr.name) + device_remove_file(&wdev->dev, devattr); + } + kfree(thinkpad->devattrs); + thinkpad->devattrs = NULL; +} + +static int __init thinkpad_wmi_sysfs_init(struct wmi_device *wdev) +{ + struct thinkpad_wmi *thinkpad = dev_get_drvdata(&wdev->dev); + struct dev_ext_attribute *devattrs; + int count = thinkpad->settings_count; + int i, ret; + + devattrs = kmalloc_array(count, sizeof(*devattrs), GFP_KERNEL); + if (!devattrs) + return -ENOMEM; + thinkpad->devattrs = devattrs; + + for (i = 0; i < count; ++i) { + struct dev_ext_attribute *deveattr = &devattrs[i]; + struct device_attribute *devattr = &deveattr->attr; + + sysfs_attr_init(&devattr->attr); + devattr->attr.name = thinkpad->settings[i]; + devattr->attr.mode = 0644; + devattr->show = show_setting; + devattr->store = store_setting; + deveattr->var = (void *)(uintptr_t)i; + ret = device_create_file(&wdev->dev, devattr); + if (ret) { + /* Name is used to check is file has been created. */ + devattr->attr.name = NULL; + return ret; + } + } + + return sysfs_create_group(&wdev->dev.kobj, &platform_attribute_group); +} + +/* + * Platform device + */ +static int __init thinkpad_wmi_platform_init(struct thinkpad_wmi *thinkpad) +{ + return thinkpad_wmi_sysfs_init(thinkpad->wmi_device); +} + +static void thinkpad_wmi_platform_exit(struct thinkpad_wmi *thinkpad) +{ + thinkpad_wmi_sysfs_exit(thinkpad->wmi_device); +} + +/* debugfs */ + +static ssize_t dbgfs_write_argument(struct file *file, + const char __user *userbuf, + size_t count, loff_t *pos) +{ + struct thinkpad_wmi *thinkpad = file->f_path.dentry->d_inode->i_private; + char *kernbuf = thinkpad->debug.argument; + size_t size = sizeof(thinkpad->debug.argument); + + if (count > PAGE_SIZE - 1) + return -EINVAL; + + if (count > size - 1) + return -EINVAL; + + if (copy_from_user(kernbuf, userbuf, count)) + return -EFAULT; + + kernbuf[count] = 0; + + strim(kernbuf); + + return count; +} + +static int dbgfs_show_argument(struct seq_file *m, void *v) +{ + struct thinkpad_wmi *thinkpad = m->private; + + seq_printf(m, "%s\n", thinkpad->debug.argument); + return 0; +} + +static int thinkpad_wmi_debugfs_argument_open(struct inode *inode, + struct file *file) +{ + struct thinkpad_wmi *thinkpad = inode->i_private; + + return single_open(file, dbgfs_show_argument, thinkpad); +} + +static const struct file_operations thinkpad_wmi_debugfs_argument_fops = { + .open = thinkpad_wmi_debugfs_argument_open, + .read = seq_read, + .llseek = seq_lseek, + .release = single_release, + .write = dbgfs_write_argument, +}; + +struct thinkpad_wmi_debugfs_node { + struct thinkpad_wmi *thinkpad; + char *name; + int (*show)(struct seq_file *m, void *data); +}; + +static void show_bios_setting_line(struct thinkpad_wmi *thinkpad, + struct seq_file *m, int i, bool list_valid) +{ + int ret; + char *settings = NULL, *choices = NULL, *p; + + ret = thinkpad_wmi_bios_setting(i, &settings); + if (ret || !settings) + return; + + p = strchr(settings, ','); + if (p) + *p = '='; + seq_printf(m, "%s", settings); + + + if (!thinkpad->can_get_bios_selections) + goto line_feed; + + if (p) + *p = '\0'; + + ret = thinkpad_wmi_get_bios_selections(settings, &choices); + if (ret || !choices || !*choices) + goto line_feed; + + seq_printf(m, "\t[%s]", choices); + +line_feed: + kfree(settings); + kfree(choices); + seq_puts(m, "\n"); +} + +static int dbgfs_bios_settings(struct seq_file *m, void *data) +{ + struct thinkpad_wmi *thinkpad = m->private; + int i; + + for (i = 0; i < thinkpad->settings_count; ++i) + show_bios_setting_line(thinkpad, m, i, true); + + return 0; +} + +static int dbgfs_bios_setting(struct seq_file *m, void *data) +{ + struct thinkpad_wmi *thinkpad = m->private; + + show_bios_setting_line(m->private, m, thinkpad->debug.instance, false); + return 0; +} + +static int dbgfs_list_valid_choices(struct seq_file *m, void *data) +{ + struct thinkpad_wmi *thinkpad = m->private; + char *choices = NULL; + int ret; + + ret = thinkpad_wmi_get_bios_selections(thinkpad->debug.argument, + &choices); + + if (ret || !choices || !*choices) { + kfree(choices); + return -EIO; + } + + seq_printf(m, "%s\n", choices); + kfree(choices); + return 0; +} + +static int dbgfs_set_bios_settings(struct seq_file *m, void *data) +{ + struct thinkpad_wmi *thinkpad = m->private; + + return thinkpad_wmi_set_bios_settings(thinkpad->debug.argument); +} + +static int dbgfs_save_bios_settings(struct seq_file *m, void *data) +{ + struct thinkpad_wmi *thinkpad = m->private; + + return thinkpad_wmi_save_bios_settings(thinkpad->debug.argument); +} + +static int dbgfs_discard_bios_settings(struct seq_file *m, void *data) +{ + struct thinkpad_wmi *thinkpad = m->private; + + return thinkpad_wmi_discard_bios_settings(thinkpad->debug.argument); +} + +static int dbgfs_load_default(struct seq_file *m, void *data) +{ + struct thinkpad_wmi *thinkpad = m->private; + + return thinkpad_wmi_load_default(thinkpad->debug.argument); +} + +static int dbgfs_set_bios_password(struct seq_file *m, void *data) +{ + struct thinkpad_wmi *thinkpad = m->private; + + return thinkpad_wmi_set_bios_password(thinkpad->debug.argument); +} + +static int dbgfs_bios_password_settings(struct seq_file *m, void *data) +{ + struct thinkpad_wmi_pcfg pcfg; + int ret; + + ret = thinkpad_wmi_password_settings(&pcfg); + if (ret) + return ret; + seq_printf(m, "password_mode: %#x\n", pcfg.password_mode); + seq_printf(m, "password_state: %#x\n", pcfg.password_state); + seq_printf(m, "min_length: %d\n", pcfg.min_length); + seq_printf(m, "max_length: %d\n", pcfg.max_length); + seq_printf(m, "supported_encodings: %#x\n", pcfg.supported_encodings); + seq_printf(m, "supported_keyboard: %#x\n", pcfg.supported_keyboard); + return 0; +} + +static struct thinkpad_wmi_debugfs_node thinkpad_wmi_debug_files[] = { + { NULL, "bios_settings", dbgfs_bios_settings }, + { NULL, "bios_setting", dbgfs_bios_setting }, + { NULL, "list_valid_choices", dbgfs_list_valid_choices }, + { NULL, "set_bios_settings", dbgfs_set_bios_settings }, + { NULL, "save_bios_settings", dbgfs_save_bios_settings }, + { NULL, "discard_bios_settings", dbgfs_discard_bios_settings }, + { NULL, "load_default", dbgfs_load_default }, + { NULL, "set_bios_password", dbgfs_set_bios_password }, + { NULL, "bios_password_settings", dbgfs_bios_password_settings }, +}; + +static int thinkpad_wmi_debugfs_open(struct inode *inode, struct file *file) +{ + struct thinkpad_wmi_debugfs_node *node = inode->i_private; + + return single_open(file, node->show, node->thinkpad); +} + +static const struct file_operations thinkpad_wmi_debugfs_io_ops = { + .owner = THIS_MODULE, + .open = thinkpad_wmi_debugfs_open, + .read = seq_read, + .llseek = seq_lseek, + .release = single_release, +}; + +static void __init thinkpad_wmi_debugfs_exit(struct thinkpad_wmi *thinkpad) +{ + debugfs_remove_recursive(thinkpad->debug.root); +} + +static int thinkpad_wmi_debugfs_init(struct thinkpad_wmi *thinkpad) +{ + struct dentry *dent; + int i; + + thinkpad->debug.instances_count = thinkpad->settings_count; + + thinkpad->debug.root = debugfs_create_dir(THINKPAD_WMI_FILE, NULL); + if (!thinkpad->debug.root) { + pr_err("failed to create debugfs directory"); + goto error_debugfs; + } + + dent = debugfs_create_file("argument", 0644, + thinkpad->debug.root, thinkpad, + &thinkpad_wmi_debugfs_argument_fops); + if (!dent) + goto error_debugfs; + + dent = debugfs_create_u8("instance", 0644, + thinkpad->debug.root, + &thinkpad->debug.instance); + if (!dent) + goto error_debugfs; + + dent = debugfs_create_u8("instances_count", 0444, + thinkpad->debug.root, + &thinkpad->debug.instances_count); + if (!dent) + goto error_debugfs; + + for (i = 0; i < ARRAY_SIZE(thinkpad_wmi_debug_files); i++) { + struct thinkpad_wmi_debugfs_node *node; + + node = &thinkpad_wmi_debug_files[i]; + + /* Filter non-present interfaces */ + if (!strcmp(node->name, "set_bios_settings") && + !thinkpad->can_set_bios_settings) + continue; + if (!strcmp(node->name, "dicard_bios_settings") && + !thinkpad->can_discard_bios_settings) + continue; + if (!strcmp(node->name, "load_default_settings") && + !thinkpad->can_load_default_settings) + continue; + if (!strcmp(node->name, "get_bios_selections") && + !thinkpad->can_get_bios_selections) + continue; + if (!strcmp(node->name, "set_bios_password") && + !thinkpad->can_set_bios_password) + continue; + if (!strcmp(node->name, "bios_password_settings") && + !thinkpad->can_get_password_settings) + continue; + + node->thinkpad = thinkpad; + dent = debugfs_create_file(node->name, S_IFREG | 0444, + thinkpad->debug.root, node, + &thinkpad_wmi_debugfs_io_ops); + if (!dent) { + pr_err("failed to create debug file: %s\n", node->name); + goto error_debugfs; + } + } + + + return 0; + +error_debugfs: + thinkpad_wmi_debugfs_exit(thinkpad); + return -ENOMEM; +} + +/* Base driver */ +static void __init thinkpad_wmi_analyze(struct thinkpad_wmi *thinkpad) +{ + acpi_status status; + int i = 0; + + /* Try to find the number of valid settings of this machine + * and use it to create sysfs attributes. + */ + for (i = 0; i < 0xFF; ++i) { + char *item = NULL; + char *p; + + status = thinkpad_wmi_bios_setting(i, &item); + if (ACPI_FAILURE(status)) + break; + if (!item || !*item) + break; + /* Remove the value part */ + p = strchr(item, ','); + if (p) + *p = '\0'; + thinkpad->settings[i] = item; /* Cache setting name */ + } + + thinkpad->settings_count = i; + pr_info("Found %d settings", thinkpad->settings_count); + + if (wmi_has_guid(LENOVO_SET_BIOS_SETTINGS_GUID) && + wmi_has_guid(LENOVO_SAVE_BIOS_SETTINGS_GUID)) { + thinkpad->can_set_bios_settings = true; + } + + if (wmi_has_guid(LENOVO_DISCARD_BIOS_SETTINGS_GUID)) + thinkpad->can_discard_bios_settings = true; + + if (wmi_has_guid(LENOVO_LOAD_DEFAULT_SETTINGS_GUID)) + thinkpad->can_load_default_settings = true; + + if (wmi_has_guid(LENOVO_GET_BIOS_SELECTIONS_GUID)) + thinkpad->can_get_bios_selections = true; + + if (wmi_has_guid(LENOVO_SET_BIOS_PASSWORD_GUID)) + thinkpad->can_set_bios_password = true; + + if (wmi_has_guid(LENOVO_BIOS_PASSWORD_SETTINGS_GUID)) + thinkpad->can_get_password_settings = true; +} + +static int __init thinkpad_wmi_add(struct wmi_device *wdev) +{ + struct thinkpad_wmi *thinkpad; + int err; + + thinkpad = kzalloc(sizeof(struct thinkpad_wmi), GFP_KERNEL); + if (!thinkpad) + return -ENOMEM; + + thinkpad->wmi_device = wdev; + dev_set_drvdata(&wdev->dev, thinkpad); + + thinkpad_wmi_analyze(thinkpad); + + err = thinkpad_wmi_platform_init(thinkpad); + if (err) + goto error_platform; + + err = thinkpad_wmi_debugfs_init(thinkpad); + if (err) + goto error_debugfs; + + return 0; + +error_debugfs: + thinkpad_wmi_platform_exit(thinkpad); +error_platform: + kfree(thinkpad); + return err; +} + +static int __exit thinkpad_wmi_remove(struct wmi_device *wdev) +{ + struct thinkpad_wmi *thinkpad; + int i; + + thinkpad = dev_get_drvdata(&wdev->dev); + thinkpad_wmi_debugfs_exit(thinkpad); + thinkpad_wmi_platform_exit(thinkpad); + + for (i = 0; thinkpad->settings[i]; ++i) { + kfree(thinkpad->settings[i]); + thinkpad->settings[i] = NULL; + } + + kfree(thinkpad); + return 0; +} + +static int __init thinkpad_wmi_probe(struct wmi_device *wdev) +{ + return thinkpad_wmi_add(wdev); +} + +static const struct wmi_device_id thinkpad_wmi_id_table[] = { + // Search for Lenovo_BiosSetting + { .guid_string = LENOVO_BIOS_SETTING_GUID }, + { }, +}; + +static struct wmi_driver thinkpad_wmi_driver = { + .driver = { + .name = "thinkpad-wmi", + }, + .id_table = thinkpad_wmi_id_table, + .probe = thinkpad_wmi_probe, + .remove = thinkpad_wmi_remove, +}; + +static int __init thinkpad_wmi_init(void) +{ + return wmi_driver_register(&thinkpad_wmi_driver); +} + +static void __exit thinkpad_wmi_exit(void) +{ + wmi_driver_unregister(&thinkpad_wmi_driver); +} + +module_init(thinkpad_wmi_init); +module_exit(thinkpad_wmi_exit);