[REGRESSION] Display freeze on VT switch back to X11 since v6.16

From: Andrés Pérez

Date: Sun Feb 22 2026 - 16:56:24 EST


# OVERVIEW

Since kernel v6.16.1, switching from an X11 session to a text VT and back
freezes the display on a ThinkPad P15 Gen 2. The system remains responsive
over SSH; only the display is frozen. Bisecting identified commit
d1b618e7954802fe ("media: uvcvideo: Do not turn on the camera for some
ioctls") as the trigger. Reverting the logic change in that commit
fixes VT switching
on v6.16.1, v6.17.9, and v6.18.9, but that is not an actual solution. Wayland
compositors (e.g., river and sway) are not affected.

Last good: v6.15.9
First bad: v6.16.1
Bisect result: d1b618e7954802fe media: uvcvideo: Do not turn on the
camera for some ioctls

## Hardware: Lenovo ThinkPad P15 Gen 2i (20YQ0031US)
CPU: Intel Core i7-11800H (Tiger Lake-H)
iGPU: Intel UHD Graphics (TGL GT1)
dGPU: NVIDIA T1200 (not involved in eDP output; driver: nvidia-open)
Display: 15.6" 1920x1080 eDP, 10 bpc capable (EDID 1.4)
Webcam: Integrated Camera on PCH xHCI (Bus 003 Port 004)
Firmware: LENOVO N37ET61W (1.97)
OS: Arch Linux, Nix home-manager, X11 + xmonad, no display manager

## Symptoms and reproduction steps:
1. Boot, start X11 on tty1 (startx).
2. Switch to tty2 (Ctrl+Alt+F2): works.
3. Switch back to tty1 (Ctrl+Alt+F1): display freezes.
- Frozen on the last frame shown before switching away.
- System is fully responsive over SSH.
- Other VTs switch normally between each other as long as X11 is
not active on them.
- Killing X does not recover the display. A reboot is required.

# DEBUG ANALYSIS

On v6.16.1, the VT switch back to X triggers a full modeset due to pipe
configuration mismatches detected by intel_pipe_config_compare:

[drm:intel_pipe_config_compare] fastset requirement not met in pipe_bpp
(expected 30, found 24)
[drm:intel_pipe_config_compare] fastset requirement not met in dp_m_n
(expected link 269484/524288, found link 336855/524288)
[drm:intel_pipe_config_compare] fastset requirement not met in dpll_hw_state
(expected cfgcr0: 0xe001a5, found cfgcr0: 0x1c2)
[drm:intel_pipe_config_compare] fastset requirement not met in port_clock
(expected 270000, found 216000)
[drm:intel_atomic_check] forcing full modeset

On v6.15.9, the same VT switch shows no such messages.
no pipe_config_compare runs, no modeset, no freeze.

# BISECT AND VERIFICATION

The bisect converged on d1b618e7954802fe in the uvcvideo driver. This
commit adds a switch statement to uvc_v4l2_unlocked_ioctl that allows
certain V4L2 IOCTLS to call video_ioctl2 directly without first calling
uvc_pm_get/uvc_pm_put. Prior to this commit, all ioctls called uvc_pm_get
before video_ioctl2.

## VT switching verification across kernel versions:

v6.12.74 arch pkg: WORKS
v6.15.9 arch pkg: WORKS
v6.15.9 from source: WORKS
v6.16.1 with d1b618e reverted: WORKS
v6.17.9 with PM wrapping restored: WORKS
v6.18.9 with PM wrapping restored: WORKS

v6.16.1 from source: FREEZES
v6.16.1 arch pkg: FREEZES
v6.17.9 arch pkg: FREEZES
v6.18.9 from source: FREEZES
v6.18.9 arch pkg: FREEZES

## Things that do not eliminate the freeze

- module_blacklist=uvcvideo on boot
- CONFIG_USB_VIDEO_CLASS=n (compiled out)
- i915.enable_psr=0
- Bypassing intel_vrr_transcoder_enable/disable (no-op)
- xrandr --output eDP-1 --set "max bpc" 10
- Xorg config FBDepth 30 (No effect on pipe_bpp)

## Workaround patch

Reverting the optimization from d1b618e to restore the unconditional
uvc_pm_get/put wrapping for all ioctls. This is not a proper fix.

diff --git a/drivers/media/usb/uvc/uvc_v4l2.c b/drivers/media/usb/uvc/uvc_v4l2.c
index 9e4a251eca88..15057b47ec4f 100644
--- a/drivers/media/usb/uvc/uvc_v4l2.c
+++ b/drivers/media/usb/uvc/uvc_v4l2.c
@@ -1199,33 +1199,12 @@ static long uvc_v4l2_unlocked_ioctl(struct file *file,
unsigned int converted_cmd = v4l2_translate_cmd(cmd);
int ret;

- /* The following IOCTLs need to turn on the camera. */
- switch (converted_cmd) {
- case UVCIOC_CTRL_MAP:
- case UVCIOC_CTRL_QUERY:
- case VIDIOC_G_CTRL:
- case VIDIOC_G_EXT_CTRLS:
- case VIDIOC_G_INPUT:
- case VIDIOC_QUERYCTRL:
- case VIDIOC_QUERYMENU:
- case VIDIOC_QUERY_EXT_CTRL:
- case VIDIOC_S_CTRL:
- case VIDIOC_S_EXT_CTRLS:
- case VIDIOC_S_FMT:
- case VIDIOC_S_INPUT:
- case VIDIOC_S_PARM:
- case VIDIOC_TRY_EXT_CTRLS:
- case VIDIOC_TRY_FMT:
- ret = uvc_pm_get(handle->stream->dev);
- if (ret)
- return ret;
- ret = video_ioctl2(file, cmd, arg);
- uvc_pm_put(handle->stream->dev);
+ ret = uvc_pm_get(handle->stream->dev);
+ if (ret)
return ret;
- }
-
- /* The other IOCTLs can run with the camera off. */
- return video_ioctl2(file, cmd, arg);
+ ret = video_ioctl2(file, cmd, arg);
+ uvc_pm_put(handle->stream->dev);
+ return ret;
}

const struct v4l2_ioctl_ops uvc_ioctl_ops = {

Andrés