1 | // SPDX-License-Identifier: GPL-2.0 |
2 | /* |
3 | * Telemetry communication for Wilco EC |
4 | * |
5 | * Copyright 2019 Google LLC |
6 | * |
7 | * The Wilco Embedded Controller is able to send telemetry data |
8 | * which is useful for enterprise applications. A daemon running on |
9 | * the OS sends a command to the EC via a write() to a char device, |
10 | * and can read the response with a read(). The write() request is |
11 | * verified by the driver to ensure that it is performing only one |
12 | * of the allowlisted commands, and that no extraneous data is |
13 | * being transmitted to the EC. The response is passed directly |
14 | * back to the reader with no modification. |
15 | * |
16 | * The character device will appear as /dev/wilco_telemN, where N |
17 | * is some small non-negative integer, starting with 0. Only one |
18 | * process may have the file descriptor open at a time. The calling |
19 | * userspace program needs to keep the device file descriptor open |
20 | * between the calls to write() and read() in order to preserve the |
21 | * response. Up to 32 bytes will be available for reading. |
22 | * |
23 | * For testing purposes, try requesting the EC's firmware build |
24 | * date, by sending the WILCO_EC_TELEM_GET_VERSION command with |
25 | * argument index=3. i.e. write [0x38, 0x00, 0x03] |
26 | * to the device node. An ASCII string of the build date is |
27 | * returned. |
28 | */ |
29 | |
30 | #include <linux/cdev.h> |
31 | #include <linux/device.h> |
32 | #include <linux/fs.h> |
33 | #include <linux/module.h> |
34 | #include <linux/platform_data/wilco-ec.h> |
35 | #include <linux/platform_device.h> |
36 | #include <linux/slab.h> |
37 | #include <linux/types.h> |
38 | #include <linux/uaccess.h> |
39 | |
40 | #define TELEM_DEV_NAME "wilco_telem" |
41 | #define TELEM_CLASS_NAME TELEM_DEV_NAME |
42 | #define DRV_NAME TELEM_DEV_NAME |
43 | #define TELEM_DEV_NAME_FMT (TELEM_DEV_NAME "%d") |
44 | static struct class telem_class = { |
45 | .name = TELEM_CLASS_NAME, |
46 | }; |
47 | |
48 | /* Keep track of all the device numbers used. */ |
49 | #define TELEM_MAX_DEV 128 |
50 | static int telem_major; |
51 | static DEFINE_IDA(telem_ida); |
52 | |
53 | /* EC telemetry command codes */ |
54 | #define WILCO_EC_TELEM_GET_LOG 0x99 |
55 | #define WILCO_EC_TELEM_GET_VERSION 0x38 |
56 | #define WILCO_EC_TELEM_GET_FAN_INFO 0x2E |
57 | #define WILCO_EC_TELEM_GET_DIAG_INFO 0xFA |
58 | #define WILCO_EC_TELEM_GET_TEMP_INFO 0x95 |
59 | #define WILCO_EC_TELEM_GET_TEMP_READ 0x2C |
60 | #define WILCO_EC_TELEM_GET_BATT_EXT_INFO 0x07 |
61 | #define WILCO_EC_TELEM_GET_BATT_PPID_INFO 0x8A |
62 | |
63 | #define TELEM_ARGS_SIZE_MAX 30 |
64 | |
65 | /* |
66 | * The following telem_args_get_* structs are embedded within the |args| field |
67 | * of wilco_ec_telem_request. |
68 | */ |
69 | |
70 | struct telem_args_get_log { |
71 | u8 log_type; |
72 | u8 log_index; |
73 | } __packed; |
74 | |
75 | /* |
76 | * Get a piece of info about the EC firmware version: |
77 | * 0 = label |
78 | * 1 = svn_rev |
79 | * 2 = model_no |
80 | * 3 = build_date |
81 | * 4 = frio_version |
82 | */ |
83 | struct telem_args_get_version { |
84 | u8 index; |
85 | } __packed; |
86 | |
87 | struct telem_args_get_fan_info { |
88 | u8 command; |
89 | u8 fan_number; |
90 | u8 arg; |
91 | } __packed; |
92 | |
93 | struct telem_args_get_diag_info { |
94 | u8 type; |
95 | u8 sub_type; |
96 | } __packed; |
97 | |
98 | struct telem_args_get_temp_info { |
99 | u8 command; |
100 | u8 index; |
101 | u8 field; |
102 | u8 zone; |
103 | } __packed; |
104 | |
105 | struct telem_args_get_temp_read { |
106 | u8 sensor_index; |
107 | } __packed; |
108 | |
109 | struct telem_args_get_batt_ext_info { |
110 | u8 var_args[5]; |
111 | } __packed; |
112 | |
113 | struct telem_args_get_batt_ppid_info { |
114 | u8 always1; /* Should always be 1 */ |
115 | } __packed; |
116 | |
117 | /** |
118 | * struct wilco_ec_telem_request - Telemetry command and arguments sent to EC. |
119 | * @command: One of WILCO_EC_TELEM_GET_* command codes. |
120 | * @reserved: Must be 0. |
121 | * @args: The first N bytes are one of telem_args_get_* structs, the rest is 0. |
122 | */ |
123 | struct wilco_ec_telem_request { |
124 | u8 command; |
125 | u8 reserved; |
126 | union { |
127 | u8 buf[TELEM_ARGS_SIZE_MAX]; |
128 | struct telem_args_get_log get_log; |
129 | struct telem_args_get_version get_version; |
130 | struct telem_args_get_fan_info get_fan_info; |
131 | struct telem_args_get_diag_info get_diag_info; |
132 | struct telem_args_get_temp_info get_temp_info; |
133 | struct telem_args_get_temp_read get_temp_read; |
134 | struct telem_args_get_batt_ext_info get_batt_ext_info; |
135 | struct telem_args_get_batt_ppid_info get_batt_ppid_info; |
136 | } args; |
137 | } __packed; |
138 | |
139 | /** |
140 | * check_telem_request() - Ensure that a request from userspace is valid. |
141 | * @rq: Request buffer copied from userspace. |
142 | * @size: Number of bytes copied from userspace. |
143 | * |
144 | * Return: 0 if valid, -EINVAL if bad command or reserved byte is non-zero, |
145 | * -EMSGSIZE if the request is too long. |
146 | * |
147 | * We do not want to allow userspace to send arbitrary telemetry commands to |
148 | * the EC. Therefore we check to ensure that |
149 | * 1. The request follows the format of struct wilco_ec_telem_request. |
150 | * 2. The supplied command code is one of the allowlisted commands. |
151 | * 3. The request only contains the necessary data for the header and arguments. |
152 | */ |
153 | static int check_telem_request(struct wilco_ec_telem_request *rq, |
154 | size_t size) |
155 | { |
156 | size_t max_size = offsetof(struct wilco_ec_telem_request, args); |
157 | |
158 | if (rq->reserved) |
159 | return -EINVAL; |
160 | |
161 | switch (rq->command) { |
162 | case WILCO_EC_TELEM_GET_LOG: |
163 | max_size += sizeof(rq->args.get_log); |
164 | break; |
165 | case WILCO_EC_TELEM_GET_VERSION: |
166 | max_size += sizeof(rq->args.get_version); |
167 | break; |
168 | case WILCO_EC_TELEM_GET_FAN_INFO: |
169 | max_size += sizeof(rq->args.get_fan_info); |
170 | break; |
171 | case WILCO_EC_TELEM_GET_DIAG_INFO: |
172 | max_size += sizeof(rq->args.get_diag_info); |
173 | break; |
174 | case WILCO_EC_TELEM_GET_TEMP_INFO: |
175 | max_size += sizeof(rq->args.get_temp_info); |
176 | break; |
177 | case WILCO_EC_TELEM_GET_TEMP_READ: |
178 | max_size += sizeof(rq->args.get_temp_read); |
179 | break; |
180 | case WILCO_EC_TELEM_GET_BATT_EXT_INFO: |
181 | max_size += sizeof(rq->args.get_batt_ext_info); |
182 | break; |
183 | case WILCO_EC_TELEM_GET_BATT_PPID_INFO: |
184 | if (rq->args.get_batt_ppid_info.always1 != 1) |
185 | return -EINVAL; |
186 | |
187 | max_size += sizeof(rq->args.get_batt_ppid_info); |
188 | break; |
189 | default: |
190 | return -EINVAL; |
191 | } |
192 | |
193 | return (size <= max_size) ? 0 : -EMSGSIZE; |
194 | } |
195 | |
196 | /** |
197 | * struct telem_device_data - Data for a Wilco EC device that queries telemetry. |
198 | * @cdev: Char dev that userspace reads and polls from. |
199 | * @dev: Device associated with the %cdev. |
200 | * @ec: Wilco EC that we will be communicating with using the mailbox interface. |
201 | * @available: Boolean of if the device can be opened. |
202 | */ |
203 | struct telem_device_data { |
204 | struct device dev; |
205 | struct cdev cdev; |
206 | struct wilco_ec_device *ec; |
207 | atomic_t available; |
208 | }; |
209 | |
210 | #define TELEM_RESPONSE_SIZE EC_MAILBOX_DATA_SIZE |
211 | |
212 | /** |
213 | * struct telem_session_data - Data that exists between open() and release(). |
214 | * @dev_data: Pointer to get back to the device data and EC. |
215 | * @request: Command and arguments sent to EC. |
216 | * @response: Response buffer of data from EC. |
217 | * @has_msg: Is there data available to read from a previous write? |
218 | */ |
219 | struct telem_session_data { |
220 | struct telem_device_data *dev_data; |
221 | struct wilco_ec_telem_request request; |
222 | u8 response[TELEM_RESPONSE_SIZE]; |
223 | bool has_msg; |
224 | }; |
225 | |
226 | /** |
227 | * telem_open() - Callback for when the device node is opened. |
228 | * @inode: inode for this char device node. |
229 | * @filp: file for this char device node. |
230 | * |
231 | * We need to ensure that after writing a command to the device, |
232 | * the same userspace process reads the corresponding result. |
233 | * Therefore, we increment a refcount on opening the device, so that |
234 | * only one process can communicate with the EC at a time. |
235 | * |
236 | * Return: 0 on success, or negative error code on failure. |
237 | */ |
238 | static int telem_open(struct inode *inode, struct file *filp) |
239 | { |
240 | struct telem_device_data *dev_data; |
241 | struct telem_session_data *sess_data; |
242 | |
243 | /* Ensure device isn't already open */ |
244 | dev_data = container_of(inode->i_cdev, struct telem_device_data, cdev); |
245 | if (atomic_cmpxchg(v: &dev_data->available, old: 1, new: 0) == 0) |
246 | return -EBUSY; |
247 | |
248 | get_device(dev: &dev_data->dev); |
249 | |
250 | sess_data = kzalloc(size: sizeof(*sess_data), GFP_KERNEL); |
251 | if (!sess_data) { |
252 | atomic_set(v: &dev_data->available, i: 1); |
253 | return -ENOMEM; |
254 | } |
255 | sess_data->dev_data = dev_data; |
256 | sess_data->has_msg = false; |
257 | |
258 | stream_open(inode, filp); |
259 | filp->private_data = sess_data; |
260 | |
261 | return 0; |
262 | } |
263 | |
264 | static ssize_t telem_write(struct file *filp, const char __user *buf, |
265 | size_t count, loff_t *pos) |
266 | { |
267 | struct telem_session_data *sess_data = filp->private_data; |
268 | struct wilco_ec_message msg = {}; |
269 | int ret; |
270 | |
271 | if (count > sizeof(sess_data->request)) |
272 | return -EMSGSIZE; |
273 | memset(&sess_data->request, 0, sizeof(sess_data->request)); |
274 | if (copy_from_user(to: &sess_data->request, from: buf, n: count)) |
275 | return -EFAULT; |
276 | ret = check_telem_request(rq: &sess_data->request, size: count); |
277 | if (ret < 0) |
278 | return ret; |
279 | |
280 | memset(sess_data->response, 0, sizeof(sess_data->response)); |
281 | msg.type = WILCO_EC_MSG_TELEMETRY; |
282 | msg.request_data = &sess_data->request; |
283 | msg.request_size = sizeof(sess_data->request); |
284 | msg.response_data = sess_data->response; |
285 | msg.response_size = sizeof(sess_data->response); |
286 | |
287 | ret = wilco_ec_mailbox(ec: sess_data->dev_data->ec, msg: &msg); |
288 | if (ret < 0) |
289 | return ret; |
290 | if (ret != sizeof(sess_data->response)) |
291 | return -EMSGSIZE; |
292 | |
293 | sess_data->has_msg = true; |
294 | |
295 | return count; |
296 | } |
297 | |
298 | static ssize_t telem_read(struct file *filp, char __user *buf, size_t count, |
299 | loff_t *pos) |
300 | { |
301 | struct telem_session_data *sess_data = filp->private_data; |
302 | |
303 | if (!sess_data->has_msg) |
304 | return -ENODATA; |
305 | if (count > sizeof(sess_data->response)) |
306 | return -EINVAL; |
307 | |
308 | if (copy_to_user(to: buf, from: sess_data->response, n: count)) |
309 | return -EFAULT; |
310 | |
311 | sess_data->has_msg = false; |
312 | |
313 | return count; |
314 | } |
315 | |
316 | static int telem_release(struct inode *inode, struct file *filp) |
317 | { |
318 | struct telem_session_data *sess_data = filp->private_data; |
319 | |
320 | atomic_set(v: &sess_data->dev_data->available, i: 1); |
321 | put_device(dev: &sess_data->dev_data->dev); |
322 | kfree(objp: sess_data); |
323 | |
324 | return 0; |
325 | } |
326 | |
327 | static const struct file_operations telem_fops = { |
328 | .open = telem_open, |
329 | .write = telem_write, |
330 | .read = telem_read, |
331 | .release = telem_release, |
332 | .llseek = no_llseek, |
333 | .owner = THIS_MODULE, |
334 | }; |
335 | |
336 | /** |
337 | * telem_device_free() - Callback to free the telem_device_data structure. |
338 | * @d: The device embedded in our device data, which we have been ref counting. |
339 | * |
340 | * Once all open file descriptors are closed and the device has been removed, |
341 | * the refcount of the device will fall to 0 and this will be called. |
342 | */ |
343 | static void telem_device_free(struct device *d) |
344 | { |
345 | struct telem_device_data *dev_data; |
346 | |
347 | dev_data = container_of(d, struct telem_device_data, dev); |
348 | kfree(objp: dev_data); |
349 | } |
350 | |
351 | /** |
352 | * telem_device_probe() - Callback when creating a new device. |
353 | * @pdev: platform device that we will be receiving telems from. |
354 | * |
355 | * This finds a free minor number for the device, allocates and initializes |
356 | * some device data, and creates a new device and char dev node. |
357 | * |
358 | * Return: 0 on success, negative error code on failure. |
359 | */ |
360 | static int telem_device_probe(struct platform_device *pdev) |
361 | { |
362 | struct telem_device_data *dev_data; |
363 | int error, minor; |
364 | |
365 | /* Get the next available device number */ |
366 | minor = ida_alloc_max(ida: &telem_ida, TELEM_MAX_DEV-1, GFP_KERNEL); |
367 | if (minor < 0) { |
368 | error = minor; |
369 | dev_err(&pdev->dev, "Failed to find minor number: %d\n" , error); |
370 | return error; |
371 | } |
372 | |
373 | dev_data = kzalloc(size: sizeof(*dev_data), GFP_KERNEL); |
374 | if (!dev_data) { |
375 | ida_free(&telem_ida, id: minor); |
376 | return -ENOMEM; |
377 | } |
378 | |
379 | /* Initialize the device data */ |
380 | dev_data->ec = dev_get_platdata(dev: &pdev->dev); |
381 | atomic_set(v: &dev_data->available, i: 1); |
382 | platform_set_drvdata(pdev, data: dev_data); |
383 | |
384 | /* Initialize the device */ |
385 | dev_data->dev.devt = MKDEV(telem_major, minor); |
386 | dev_data->dev.class = &telem_class; |
387 | dev_data->dev.release = telem_device_free; |
388 | dev_set_name(dev: &dev_data->dev, TELEM_DEV_NAME_FMT, minor); |
389 | device_initialize(dev: &dev_data->dev); |
390 | |
391 | /* Initialize the character device and add it to userspace */; |
392 | cdev_init(&dev_data->cdev, &telem_fops); |
393 | error = cdev_device_add(cdev: &dev_data->cdev, dev: &dev_data->dev); |
394 | if (error) { |
395 | put_device(dev: &dev_data->dev); |
396 | ida_free(&telem_ida, id: minor); |
397 | return error; |
398 | } |
399 | |
400 | return 0; |
401 | } |
402 | |
403 | static void telem_device_remove(struct platform_device *pdev) |
404 | { |
405 | struct telem_device_data *dev_data = platform_get_drvdata(pdev); |
406 | |
407 | cdev_device_del(cdev: &dev_data->cdev, dev: &dev_data->dev); |
408 | ida_free(&telem_ida, MINOR(dev_data->dev.devt)); |
409 | put_device(dev: &dev_data->dev); |
410 | } |
411 | |
412 | static struct platform_driver telem_driver = { |
413 | .probe = telem_device_probe, |
414 | .remove_new = telem_device_remove, |
415 | .driver = { |
416 | .name = DRV_NAME, |
417 | }, |
418 | }; |
419 | |
420 | static int __init telem_module_init(void) |
421 | { |
422 | dev_t dev_num = 0; |
423 | int ret; |
424 | |
425 | ret = class_register(class: &telem_class); |
426 | if (ret) { |
427 | pr_err(DRV_NAME ": Failed registering class: %d\n" , ret); |
428 | return ret; |
429 | } |
430 | |
431 | /* Request the kernel for device numbers, starting with minor=0 */ |
432 | ret = alloc_chrdev_region(&dev_num, 0, TELEM_MAX_DEV, TELEM_DEV_NAME); |
433 | if (ret) { |
434 | pr_err(DRV_NAME ": Failed allocating dev numbers: %d\n" , ret); |
435 | goto destroy_class; |
436 | } |
437 | telem_major = MAJOR(dev_num); |
438 | |
439 | ret = platform_driver_register(&telem_driver); |
440 | if (ret < 0) { |
441 | pr_err(DRV_NAME ": Failed registering driver: %d\n" , ret); |
442 | goto unregister_region; |
443 | } |
444 | |
445 | return 0; |
446 | |
447 | unregister_region: |
448 | unregister_chrdev_region(MKDEV(telem_major, 0), TELEM_MAX_DEV); |
449 | destroy_class: |
450 | class_unregister(class: &telem_class); |
451 | ida_destroy(ida: &telem_ida); |
452 | return ret; |
453 | } |
454 | |
455 | static void __exit telem_module_exit(void) |
456 | { |
457 | platform_driver_unregister(&telem_driver); |
458 | unregister_chrdev_region(MKDEV(telem_major, 0), TELEM_MAX_DEV); |
459 | class_unregister(class: &telem_class); |
460 | ida_destroy(ida: &telem_ida); |
461 | } |
462 | |
463 | module_init(telem_module_init); |
464 | module_exit(telem_module_exit); |
465 | |
466 | MODULE_AUTHOR("Nick Crews <ncrews@chromium.org>" ); |
467 | MODULE_DESCRIPTION("Wilco EC telemetry driver" ); |
468 | MODULE_LICENSE("GPL" ); |
469 | MODULE_ALIAS("platform:" DRV_NAME); |
470 | |