git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

commit4138adaa998dc89f368e89e731b22b7de7e27b1c
parent9b1a0e0ea8
authorIsaac Freund <[email protected]>
date2026-02-13 15:37
OutputManager: reject coordinates too large for X11

We already reject negative coordinates when Xwayland is enabled but
do not yet reject coordinates too large for Xwayland to handle.

 river/Output.zig        | 26 +++++++++++++++++++++
 river/OutputManager.zig | 61 ++++++++++++++++++++++---------------------------
 2 files changed, 53 insertions(+), 34 deletions(-)

diff --git a/river/Output.zig b/river/Output.zig
index f2f9530..7ede781 100644
--- a/river/Output.zig
+++ b/river/Output.zig
@@ -57,6 +57,32 @@ pub const State = struct {
     adaptive_sync: bool,
     auto_layout: bool,
 
+    pub fn fromHeadState(state: *const wlr.OutputHeadV1.State) State {
+        assert(state.enabled);
+        return .{
+            .state = .enabled,
+            .mode = blk: {
+                if (state.mode) |mode| {
+                    break :blk .{ .standard = mode };
+                } else {
+                    break :blk .{ .custom = .{
+                        .width = state.custom_mode.width,
+                        .height = state.custom_mode.height,
+                        .refresh = state.custom_mode.refresh,
+                    } };
+                }
+            },
+            .x = state.x,
+            .y = state.y,
+            // Round to nearest 1/120 to ensure the scale is exactly represented
+            // in the fractional-scale-v1 protocol.
+            .scale = @round(state.scale * 120) / 120,
+            .transform = state.transform,
+            .adaptive_sync = state.adaptive_sync_enabled,
+            .auto_layout = false,
+        };
+    }
+
     /// Width/height in the logical coordinate space
     pub fn dimensions(state: *const State) struct { u31, u31 } {
         var w: i32, var h: i32 = switch (state.mode) {
diff --git a/river/OutputManager.zig b/river/OutputManager.zig
index 1d8a1c2..9db205a 100644
--- a/river/OutputManager.zig
+++ b/river/OutputManager.zig
@@ -6,6 +6,7 @@ const OutputManager = @This();
 const build_options = @import("build_options");
 const std = @import("std");
 const assert = std.debug.assert;
+const math = std.math;
 const mem = std.mem;
 const wlr = @import("wlroots");
 const wl = @import("wayland").server.wl;
@@ -134,33 +135,12 @@ fn handleManagerApply(_: *wl.Listener(*wlr.OutputConfigurationV1), config: *wlr.
     var it = config.heads.iterator(.forward);
     while (it.next()) |head| {
         const output: *Output = @ptrCast(@alignCast(head.state.output.data));
-        if (!head.state.enabled) {
+        if (head.state.enabled) {
+            output.scheduled = .fromHeadState(&head.state);
+        } else {
             // Avoid overwriting and losing all other output state on disable.
             output.scheduled.state = .disabled_hard;
-            continue;
         }
-        output.scheduled = .{
-            .state = .enabled,
-            .mode = blk: {
-                if (head.state.mode) |mode| {
-                    break :blk .{ .standard = mode };
-                } else {
-                    break :blk .{ .custom = .{
-                        .width = head.state.custom_mode.width,
-                        .height = head.state.custom_mode.height,
-                        .refresh = head.state.custom_mode.refresh,
-                    } };
-                }
-            },
-            .x = head.state.x,
-            .y = head.state.y,
-            // Round to nearest 1/120 to ensure the scale is exactly represented
-            // in the fractional-scale-v1 protocol.
-            .scale = @round(head.state.scale * 120) / 120,
-            .transform = head.state.transform,
-            .adaptive_sync = head.state.adaptive_sync_enabled,
-            .auto_layout = false,
-        };
     }
 
     if (server.wm.scheduled.output_config) |old| {
@@ -175,16 +155,29 @@ fn handleManagerApply(_: *wl.Listener(*wlr.OutputConfigurationV1), config: *wlr.
 fn validateConfigCoordinates(config: *wlr.OutputConfigurationV1) bool {
     var it = config.heads.iterator(.forward);
     while (it.next()) |head| {
-        // Negative output coordinates currently cause Xwayland clients to not receive click events.
-        // See: https://gitlab.freedesktop.org/xorg/xserver/-/issues/899
-        if (build_options.xwayland and server.xwayland != null and
-            (head.state.x < 0 or head.state.y < 0))
-        {
-            log.err(
-                \\Attempted to set negative coordinates for output {s}.
-                \\Negative output coordinates are disallowed if Xwayland is enabled due to a limitation of Xwayland.
-            , .{head.state.output.name});
-            return false;
+        if (!head.state.enabled) continue;
+
+        const proposed: Output.State = .fromHeadState(&head.state);
+        if (build_options.xwayland and server.xwayland != null) {
+            // Negative output coordinates currently cause Xwayland clients to not receive click events.
+            // See: https://gitlab.freedesktop.org/xorg/xserver/-/issues/899
+            if (proposed.x < 0 or proposed.y < 0) {
+                log.err(
+                    \\Attempted to set negative coordinates for output {s}.
+                    \\Negative output coordinates are disallowed if Xwayland is enabled due to a limitation of Xwayland.
+                , .{head.state.output.name});
+                return false;
+            }
+            const width, const height = proposed.dimensions();
+            if (proposed.x + width > math.maxInt(i16) or
+                proposed.y + height > math.maxInt(i16))
+            {
+                log.err(
+                    \\Attempted to set too-large coordinates for output {s}.
+                    \\Coordinates greater than {d} are disallowed if Xwayland is enabled due to a limitation of X11.
+                , .{ head.state.output.name, math.maxInt(i16) });
+                return false;
+            }
         }
     }
     return true;