1 | // SPDX-License-Identifier: GPL-2.0 |
2 | |
3 | #include <linux/compat.h> |
4 | #include <linux/console.h> |
5 | #include <linux/fb.h> |
6 | #include <linux/fbcon.h> |
7 | #include <linux/major.h> |
8 | |
9 | #include "fb_internal.h" |
10 | |
11 | /* |
12 | * We hold a reference to the fb_info in file->private_data, |
13 | * but if the current registered fb has changed, we don't |
14 | * actually want to use it. |
15 | * |
16 | * So look up the fb_info using the inode minor number, |
17 | * and just verify it against the reference we have. |
18 | */ |
19 | static struct fb_info *file_fb_info(struct file *file) |
20 | { |
21 | struct inode *inode = file_inode(f: file); |
22 | int fbidx = iminor(inode); |
23 | struct fb_info *info = registered_fb[fbidx]; |
24 | |
25 | if (info != file->private_data) |
26 | info = NULL; |
27 | return info; |
28 | } |
29 | |
30 | static ssize_t fb_read(struct file *file, char __user *buf, size_t count, loff_t *ppos) |
31 | { |
32 | struct fb_info *info = file_fb_info(file); |
33 | |
34 | if (!info) |
35 | return -ENODEV; |
36 | |
37 | if (fb_WARN_ON_ONCE(info, !info->fbops->fb_read)) |
38 | return -EINVAL; |
39 | |
40 | if (info->state != FBINFO_STATE_RUNNING) |
41 | return -EPERM; |
42 | |
43 | return info->fbops->fb_read(info, buf, count, ppos); |
44 | } |
45 | |
46 | static ssize_t fb_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) |
47 | { |
48 | struct fb_info *info = file_fb_info(file); |
49 | |
50 | if (!info) |
51 | return -ENODEV; |
52 | |
53 | if (fb_WARN_ON_ONCE(info, !info->fbops->fb_write)) |
54 | return -EINVAL; |
55 | |
56 | if (info->state != FBINFO_STATE_RUNNING) |
57 | return -EPERM; |
58 | |
59 | return info->fbops->fb_write(info, buf, count, ppos); |
60 | } |
61 | |
62 | static long do_fb_ioctl(struct fb_info *info, unsigned int cmd, |
63 | unsigned long arg) |
64 | { |
65 | const struct fb_ops *fb; |
66 | struct fb_var_screeninfo var; |
67 | struct fb_fix_screeninfo fix; |
68 | struct fb_cmap cmap_from; |
69 | struct fb_cmap_user cmap; |
70 | void __user *argp = (void __user *)arg; |
71 | long ret = 0; |
72 | |
73 | switch (cmd) { |
74 | case FBIOGET_VSCREENINFO: |
75 | lock_fb_info(info); |
76 | var = info->var; |
77 | unlock_fb_info(info); |
78 | |
79 | ret = copy_to_user(to: argp, from: &var, n: sizeof(var)) ? -EFAULT : 0; |
80 | break; |
81 | case FBIOPUT_VSCREENINFO: |
82 | if (copy_from_user(to: &var, from: argp, n: sizeof(var))) |
83 | return -EFAULT; |
84 | /* only for kernel-internal use */ |
85 | var.activate &= ~FB_ACTIVATE_KD_TEXT; |
86 | console_lock(); |
87 | lock_fb_info(info); |
88 | ret = fbcon_modechange_possible(info, var: &var); |
89 | if (!ret) |
90 | ret = fb_set_var(info, var: &var); |
91 | if (!ret) |
92 | fbcon_update_vcs(info, all: var.activate & FB_ACTIVATE_ALL); |
93 | unlock_fb_info(info); |
94 | console_unlock(); |
95 | if (!ret && copy_to_user(to: argp, from: &var, n: sizeof(var))) |
96 | ret = -EFAULT; |
97 | break; |
98 | case FBIOGET_FSCREENINFO: |
99 | lock_fb_info(info); |
100 | memcpy(&fix, &info->fix, sizeof(fix)); |
101 | if (info->flags & FBINFO_HIDE_SMEM_START) |
102 | fix.smem_start = 0; |
103 | unlock_fb_info(info); |
104 | |
105 | ret = copy_to_user(to: argp, from: &fix, n: sizeof(fix)) ? -EFAULT : 0; |
106 | break; |
107 | case FBIOPUTCMAP: |
108 | if (copy_from_user(to: &cmap, from: argp, n: sizeof(cmap))) |
109 | return -EFAULT; |
110 | ret = fb_set_user_cmap(cmap: &cmap, fb_info: info); |
111 | break; |
112 | case FBIOGETCMAP: |
113 | if (copy_from_user(to: &cmap, from: argp, n: sizeof(cmap))) |
114 | return -EFAULT; |
115 | lock_fb_info(info); |
116 | cmap_from = info->cmap; |
117 | unlock_fb_info(info); |
118 | ret = fb_cmap_to_user(from: &cmap_from, to: &cmap); |
119 | break; |
120 | case FBIOPAN_DISPLAY: |
121 | if (copy_from_user(to: &var, from: argp, n: sizeof(var))) |
122 | return -EFAULT; |
123 | console_lock(); |
124 | lock_fb_info(info); |
125 | ret = fb_pan_display(info, var: &var); |
126 | unlock_fb_info(info); |
127 | console_unlock(); |
128 | if (ret == 0 && copy_to_user(to: argp, from: &var, n: sizeof(var))) |
129 | return -EFAULT; |
130 | break; |
131 | case FBIO_CURSOR: |
132 | ret = -EINVAL; |
133 | break; |
134 | case FBIOGET_CON2FBMAP: |
135 | ret = fbcon_get_con2fb_map_ioctl(argp); |
136 | break; |
137 | case FBIOPUT_CON2FBMAP: |
138 | ret = fbcon_set_con2fb_map_ioctl(argp); |
139 | break; |
140 | case FBIOBLANK: |
141 | if (arg > FB_BLANK_POWERDOWN) |
142 | return -EINVAL; |
143 | console_lock(); |
144 | lock_fb_info(info); |
145 | ret = fb_blank(info, blank: arg); |
146 | /* might again call into fb_blank */ |
147 | fbcon_fb_blanked(info, blank: arg); |
148 | unlock_fb_info(info); |
149 | console_unlock(); |
150 | break; |
151 | default: |
152 | lock_fb_info(info); |
153 | fb = info->fbops; |
154 | if (fb->fb_ioctl) |
155 | ret = fb->fb_ioctl(info, cmd, arg); |
156 | else |
157 | ret = -ENOTTY; |
158 | unlock_fb_info(info); |
159 | } |
160 | return ret; |
161 | } |
162 | |
163 | static long fb_ioctl(struct file *file, unsigned int cmd, unsigned long arg) |
164 | { |
165 | struct fb_info *info = file_fb_info(file); |
166 | |
167 | if (!info) |
168 | return -ENODEV; |
169 | return do_fb_ioctl(info, cmd, arg); |
170 | } |
171 | |
172 | #ifdef CONFIG_COMPAT |
173 | struct fb_fix_screeninfo32 { |
174 | char id[16]; |
175 | compat_caddr_t smem_start; |
176 | u32 smem_len; |
177 | u32 type; |
178 | u32 type_aux; |
179 | u32 visual; |
180 | u16 xpanstep; |
181 | u16 ypanstep; |
182 | u16 ywrapstep; |
183 | u32 line_length; |
184 | compat_caddr_t mmio_start; |
185 | u32 mmio_len; |
186 | u32 accel; |
187 | u16 reserved[3]; |
188 | }; |
189 | |
190 | struct fb_cmap32 { |
191 | u32 start; |
192 | u32 len; |
193 | compat_caddr_t red; |
194 | compat_caddr_t green; |
195 | compat_caddr_t blue; |
196 | compat_caddr_t transp; |
197 | }; |
198 | |
199 | static int fb_getput_cmap(struct fb_info *info, unsigned int cmd, |
200 | unsigned long arg) |
201 | { |
202 | struct fb_cmap32 cmap32; |
203 | struct fb_cmap cmap_from; |
204 | struct fb_cmap_user cmap; |
205 | |
206 | if (copy_from_user(to: &cmap32, from: compat_ptr(uptr: arg), n: sizeof(cmap32))) |
207 | return -EFAULT; |
208 | |
209 | cmap = (struct fb_cmap_user) { |
210 | .start = cmap32.start, |
211 | .len = cmap32.len, |
212 | .red = compat_ptr(uptr: cmap32.red), |
213 | .green = compat_ptr(uptr: cmap32.green), |
214 | .blue = compat_ptr(uptr: cmap32.blue), |
215 | .transp = compat_ptr(uptr: cmap32.transp), |
216 | }; |
217 | |
218 | if (cmd == FBIOPUTCMAP) |
219 | return fb_set_user_cmap(cmap: &cmap, fb_info: info); |
220 | |
221 | lock_fb_info(info); |
222 | cmap_from = info->cmap; |
223 | unlock_fb_info(info); |
224 | |
225 | return fb_cmap_to_user(from: &cmap_from, to: &cmap); |
226 | } |
227 | |
228 | static int do_fscreeninfo_to_user(struct fb_fix_screeninfo *fix, |
229 | struct fb_fix_screeninfo32 __user *fix32) |
230 | { |
231 | __u32 data; |
232 | int err; |
233 | |
234 | err = copy_to_user(to: &fix32->id, from: &fix->id, n: sizeof(fix32->id)); |
235 | |
236 | data = (__u32) (unsigned long) fix->smem_start; |
237 | err |= put_user(data, &fix32->smem_start); |
238 | |
239 | err |= put_user(fix->smem_len, &fix32->smem_len); |
240 | err |= put_user(fix->type, &fix32->type); |
241 | err |= put_user(fix->type_aux, &fix32->type_aux); |
242 | err |= put_user(fix->visual, &fix32->visual); |
243 | err |= put_user(fix->xpanstep, &fix32->xpanstep); |
244 | err |= put_user(fix->ypanstep, &fix32->ypanstep); |
245 | err |= put_user(fix->ywrapstep, &fix32->ywrapstep); |
246 | err |= put_user(fix->line_length, &fix32->line_length); |
247 | |
248 | data = (__u32) (unsigned long) fix->mmio_start; |
249 | err |= put_user(data, &fix32->mmio_start); |
250 | |
251 | err |= put_user(fix->mmio_len, &fix32->mmio_len); |
252 | err |= put_user(fix->accel, &fix32->accel); |
253 | err |= copy_to_user(to: fix32->reserved, from: fix->reserved, |
254 | n: sizeof(fix->reserved)); |
255 | |
256 | if (err) |
257 | return -EFAULT; |
258 | return 0; |
259 | } |
260 | |
261 | static int fb_get_fscreeninfo(struct fb_info *info, unsigned int cmd, |
262 | unsigned long arg) |
263 | { |
264 | struct fb_fix_screeninfo fix; |
265 | |
266 | lock_fb_info(info); |
267 | fix = info->fix; |
268 | if (info->flags & FBINFO_HIDE_SMEM_START) |
269 | fix.smem_start = 0; |
270 | unlock_fb_info(info); |
271 | return do_fscreeninfo_to_user(fix: &fix, fix32: compat_ptr(uptr: arg)); |
272 | } |
273 | |
274 | static long fb_compat_ioctl(struct file *file, unsigned int cmd, |
275 | unsigned long arg) |
276 | { |
277 | struct fb_info *info = file_fb_info(file); |
278 | const struct fb_ops *fb; |
279 | long ret = -ENOIOCTLCMD; |
280 | |
281 | if (!info) |
282 | return -ENODEV; |
283 | fb = info->fbops; |
284 | switch (cmd) { |
285 | case FBIOGET_VSCREENINFO: |
286 | case FBIOPUT_VSCREENINFO: |
287 | case FBIOPAN_DISPLAY: |
288 | case FBIOGET_CON2FBMAP: |
289 | case FBIOPUT_CON2FBMAP: |
290 | arg = (unsigned long) compat_ptr(uptr: arg); |
291 | fallthrough; |
292 | case FBIOBLANK: |
293 | ret = do_fb_ioctl(info, cmd, arg); |
294 | break; |
295 | |
296 | case FBIOGET_FSCREENINFO: |
297 | ret = fb_get_fscreeninfo(info, cmd, arg); |
298 | break; |
299 | |
300 | case FBIOGETCMAP: |
301 | case FBIOPUTCMAP: |
302 | ret = fb_getput_cmap(info, cmd, arg); |
303 | break; |
304 | |
305 | default: |
306 | if (fb->fb_compat_ioctl) |
307 | ret = fb->fb_compat_ioctl(info, cmd, arg); |
308 | break; |
309 | } |
310 | return ret; |
311 | } |
312 | #endif |
313 | |
314 | static int fb_mmap(struct file *file, struct vm_area_struct *vma) |
315 | { |
316 | struct fb_info *info = file_fb_info(file); |
317 | int res; |
318 | |
319 | if (!info) |
320 | return -ENODEV; |
321 | |
322 | if (fb_WARN_ON_ONCE(info, !info->fbops->fb_mmap)) |
323 | return -ENODEV; |
324 | |
325 | mutex_lock(&info->mm_lock); |
326 | res = info->fbops->fb_mmap(info, vma); |
327 | mutex_unlock(lock: &info->mm_lock); |
328 | |
329 | return res; |
330 | } |
331 | |
332 | static int fb_open(struct inode *inode, struct file *file) |
333 | __acquires(&info->lock) |
334 | __releases(&info->lock) |
335 | { |
336 | int fbidx = iminor(inode); |
337 | struct fb_info *info; |
338 | int res = 0; |
339 | |
340 | info = get_fb_info(idx: fbidx); |
341 | if (!info) { |
342 | request_module("fb%d" , fbidx); |
343 | info = get_fb_info(idx: fbidx); |
344 | if (!info) |
345 | return -ENODEV; |
346 | } |
347 | if (IS_ERR(ptr: info)) |
348 | return PTR_ERR(ptr: info); |
349 | |
350 | lock_fb_info(info); |
351 | if (!try_module_get(module: info->fbops->owner)) { |
352 | res = -ENODEV; |
353 | goto out; |
354 | } |
355 | file->private_data = info; |
356 | if (info->fbops->fb_open) { |
357 | res = info->fbops->fb_open(info, 1); |
358 | if (res) |
359 | module_put(module: info->fbops->owner); |
360 | } |
361 | #ifdef CONFIG_FB_DEFERRED_IO |
362 | if (info->fbdefio) |
363 | fb_deferred_io_open(info, inode, file); |
364 | #endif |
365 | out: |
366 | unlock_fb_info(info); |
367 | if (res) |
368 | put_fb_info(fb_info: info); |
369 | return res; |
370 | } |
371 | |
372 | static int fb_release(struct inode *inode, struct file *file) |
373 | __acquires(&info->lock) |
374 | __releases(&info->lock) |
375 | { |
376 | struct fb_info * const info = file->private_data; |
377 | |
378 | lock_fb_info(info); |
379 | #if IS_ENABLED(CONFIG_FB_DEFERRED_IO) |
380 | if (info->fbdefio) |
381 | fb_deferred_io_release(info); |
382 | #endif |
383 | if (info->fbops->fb_release) |
384 | info->fbops->fb_release(info, 1); |
385 | module_put(module: info->fbops->owner); |
386 | unlock_fb_info(info); |
387 | put_fb_info(fb_info: info); |
388 | return 0; |
389 | } |
390 | |
391 | #if defined(CONFIG_FB_PROVIDE_GET_FB_UNMAPPED_AREA) && !defined(CONFIG_MMU) |
392 | static unsigned long get_fb_unmapped_area(struct file *filp, |
393 | unsigned long addr, unsigned long len, |
394 | unsigned long pgoff, unsigned long flags) |
395 | { |
396 | struct fb_info * const info = filp->private_data; |
397 | unsigned long fb_size = PAGE_ALIGN(info->fix.smem_len); |
398 | |
399 | if (pgoff > fb_size || len > fb_size - pgoff) |
400 | return -EINVAL; |
401 | |
402 | return (unsigned long)info->screen_base + pgoff; |
403 | } |
404 | #endif |
405 | |
406 | static const struct file_operations fb_fops = { |
407 | .owner = THIS_MODULE, |
408 | .read = fb_read, |
409 | .write = fb_write, |
410 | .unlocked_ioctl = fb_ioctl, |
411 | #ifdef CONFIG_COMPAT |
412 | .compat_ioctl = fb_compat_ioctl, |
413 | #endif |
414 | .mmap = fb_mmap, |
415 | .open = fb_open, |
416 | .release = fb_release, |
417 | #if defined(HAVE_ARCH_FB_UNMAPPED_AREA) || \ |
418 | (defined(CONFIG_FB_PROVIDE_GET_FB_UNMAPPED_AREA) && \ |
419 | !defined(CONFIG_MMU)) |
420 | .get_unmapped_area = get_fb_unmapped_area, |
421 | #endif |
422 | #ifdef CONFIG_FB_DEFERRED_IO |
423 | .fsync = fb_deferred_io_fsync, |
424 | #endif |
425 | .llseek = default_llseek, |
426 | }; |
427 | |
428 | int fb_register_chrdev(void) |
429 | { |
430 | int ret; |
431 | |
432 | ret = register_chrdev(FB_MAJOR, name: "fb" , fops: &fb_fops); |
433 | if (ret) { |
434 | pr_err("Unable to get major %d for fb devs\n" , FB_MAJOR); |
435 | return ret; |
436 | } |
437 | |
438 | return ret; |
439 | } |
440 | |
441 | void fb_unregister_chrdev(void) |
442 | { |
443 | unregister_chrdev(FB_MAJOR, name: "fb" ); |
444 | } |
445 | |