xserver/hw/xwayland/xwayland-drm-lease.c
Olivier Fourdan 4053782443 xwayland: Do not remove output on withdraw if leased
On DRM lease connector withdrawn event, Xwayland would free the
corresponding xwl_output offered for lease.

However, the pointer is still referenced from the rrLease->outputs[],
meaning that trying to clean up the RANDR DRM leases as done with commit
ef181265 (xwayland: Clean up drm lease when terminating) would cause a
use after free and random crashes.

To avoid that issue, on the connector withdraw event, set the connector
withdrawn flag but do not to remove (i.e. free) the xwayland output if
its is offered for lease.

Then, once the lease is terminated, check for the xwl_outputs with a
withdrawn connector and remove them (once we have no use for them
anymore.

Note that we cannot do that cleanup from xwl_randr_terminate_lease() as
removing the xwl_output will free the RRcrtc resources, which checks for
leases in XRANDR, and calls RRTerminateLease(), which chains back to
xwl_randr_terminate_lease().

v2: Use a "withdrawn_connector" flag to mark outputs to remove (Xaver)

Signed-off-by: Olivier Fourdan <ofourdan@redhat.com>
Reviewed-by: Xaver Hugl <xaver.hugl@kde.org>
fixes: ef181265 - xwayland: Clean up drm lease when terminating

See-also: https://gitlab.freedesktop.org/xorg/xserver/-/issues/946
See-also: https://gitlab.freedesktop.org/xorg/xserver/-/merge_requests/1130
Part-of: <https://gitlab.freedesktop.org/xorg/xserver/-/merge_requests/1482>
2024-04-30 09:13:34 +02:00

484 lines
16 KiB
C

/*
* Copyright © 2020 Drew Devault
* Copyright © 2021 Xaver Hugl
*
* Permission to use, copy, modify, distribute, and sell this software
* and its documentation for any purpose is hereby granted without
* fee, provided that the above copyright notice appear in all copies
* and that both that copyright notice and this permission notice
* appear in supporting documentation, and that the name of the
* copyright holders not be used in advertising or publicity
* pertaining to distribution of the software without specific,
* written prior permission. The copyright holders make no
* representations about the suitability of this software for any
* purpose. It is provided "as is" without express or implied
* warranty.
*
* THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
* SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
* FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
* SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
* AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
* SOFTWARE.
*/
#include <xwayland-config.h>
#ifdef WITH_LIBDRM
#include <xf86drm.h>
#include <xf86drmMode.h>
#endif
#include "randrstr_priv.h"
#include "xwayland-drm-lease.h"
#include "xwayland-screen.h"
#include "xwayland-output.h"
static void
xwl_randr_lease_cleanup_outputs(RRLeasePtr rrLease)
{
struct xwl_output *output;
int i;
for (i = 0; i < rrLease->numOutputs; ++i) {
output = rrLease->outputs[i]->devPrivate;
if (output) {
output->lease = NULL;
}
}
}
static void
xwl_randr_lease_free_outputs(RRLeasePtr rrLease)
{
struct xwl_output *xwl_output;
int i;
for (i = 0; i < rrLease->numOutputs; ++i) {
xwl_output = rrLease->outputs[i]->devPrivate;
if (xwl_output && xwl_output->withdrawn_connector) {
rrLease->outputs[i]->devPrivate = NULL;
xwl_output_remove(xwl_output);
}
}
}
static void
drm_lease_handle_lease_fd(void *data,
struct wp_drm_lease_v1 *wp_drm_lease_v1,
int32_t lease_fd)
{
struct xwl_drm_lease *lease = (struct xwl_drm_lease *)data;
lease->fd = lease_fd;
AttendClient(lease->client);
}
static void
drm_lease_handle_finished(void *data,
struct wp_drm_lease_v1 *wp_drm_lease_v1)
{
struct xwl_drm_lease *lease = (struct xwl_drm_lease *)data;
if (lease->fd >= 0) {
RRTerminateLease(lease->rrLease);
} else {
AttendClient(lease->client);
xwl_randr_lease_cleanup_outputs(lease->rrLease);
}
/* Free the xwl_outputs that have been withdrawn while lease-able */
xwl_randr_lease_free_outputs(lease->rrLease);
}
static struct wp_drm_lease_v1_listener drm_lease_listener = {
.lease_fd = drm_lease_handle_lease_fd,
.finished = drm_lease_handle_finished,
};
void
xwl_randr_get_lease(ClientPtr client, ScreenPtr screen, RRLeasePtr *rrLease, int *fd)
{
struct xwl_screen *xwl_screen;
struct xwl_drm_lease *lease;
xwl_screen = xwl_screen_get(screen);
xorg_list_for_each_entry(lease, &xwl_screen->drm_leases, link) {
if (lease->client == client) {
*rrLease = lease->rrLease;
*fd = lease->fd;
if (lease->fd < 0)
xorg_list_del(&lease->link);
return;
}
}
*rrLease = NULL;
*fd = -1;
}
int
xwl_randr_request_lease(ClientPtr client, ScreenPtr screen, RRLeasePtr rrLease)
{
struct xwl_screen *xwl_screen;
struct wp_drm_lease_request_v1 *req;
struct xwl_drm_lease *lease_private;
struct xwl_drm_lease_device *lease_device = NULL;
struct xwl_drm_lease_device *device_data;
struct xwl_output *output;
int i;
xwl_screen = xwl_screen_get(screen);
if (xorg_list_is_empty(&xwl_screen->drm_lease_devices)) {
ErrorF("Attempted to create DRM lease without wp_drm_lease_device_v1\n");
return BadMatch;
}
for (i = 0; i < rrLease->numOutputs; ++i) {
output = rrLease->outputs[i]->devPrivate;
if (!output || !output->lease_connector || output->lease) {
return BadValue;
}
}
xorg_list_for_each_entry(device_data, &xwl_screen->drm_lease_devices, link) {
Bool connectors_of_device = FALSE;
for (i = 0; i < rrLease->numOutputs; ++i) {
output = rrLease->outputs[i]->devPrivate;
if (output->lease_device == device_data) {
connectors_of_device = TRUE;
break;
}
}
if (connectors_of_device) {
if (lease_device != NULL) {
ErrorF("Attempted to create DRM lease from multiple devices\n");
return BadValue;
}
lease_device = device_data;
}
}
req = wp_drm_lease_device_v1_create_lease_request(
lease_device->drm_lease_device);
lease_private = calloc(1, sizeof(struct xwl_drm_lease));
for (i = 0; i < rrLease->numOutputs; ++i) {
output = rrLease->outputs[i]->devPrivate;
output->lease = lease_private;
wp_drm_lease_request_v1_request_connector(req, output->lease_connector);
}
lease_private->fd = -1;
lease_private->lease = wp_drm_lease_request_v1_submit(req);
lease_private->rrLease = rrLease;
lease_private->client = client;
rrLease->devPrivate = lease_private;
wp_drm_lease_v1_add_listener(lease_private->lease,
&drm_lease_listener, lease_private);
xorg_list_add(&lease_private->link, &xwl_screen->drm_leases);
ResetCurrentRequest(client);
client->sequence--;
IgnoreClient(client);
return Success;
}
void
xwl_randr_terminate_lease(ScreenPtr screen, RRLeasePtr lease)
{
struct xwl_drm_lease *lease_private = lease->devPrivate;
if (lease_private) {
xwl_randr_lease_cleanup_outputs(lease);
xorg_list_del(&lease_private->link);
if (lease_private->fd >= 0)
close(lease_private->fd);
wp_drm_lease_v1_destroy(lease_private->lease);
free(lease_private);
lease->devPrivate = NULL;
}
RRLeaseTerminated(lease);
}
static void
lease_connector_handle_name(void *data,
struct wp_drm_lease_connector_v1 *wp_drm_lease_connector_v1,
const char *name)
{
struct xwl_output *xwl_output = data;
char rr_output_name[MAX_OUTPUT_NAME] = { 0 };
snprintf(rr_output_name, MAX_OUTPUT_NAME, "lease-%s", name);
xwl_output_set_name(xwl_output, rr_output_name);
}
static void
lease_connector_handle_description(void *data,
struct wp_drm_lease_connector_v1 *wp_drm_lease_connector_v1,
const char *description)
{
/* This space is deliberately left blank */
}
static RRModePtr *
xwl_get_rrmodes_from_connector_id(int drm, int32_t connector_id, int *nmode, int *npref)
{
#ifdef WITH_LIBDRM
drmModeConnectorPtr conn;
drmModeModeInfoPtr kmode;
RRModePtr *rrmodes;
int pref, i;
*nmode = *npref = 0;
conn = drmModeGetConnectorCurrent(drm, connector_id);
if (!conn) {
ErrorF("drmModeGetConnector for connector %d failed\n", connector_id);
return NULL;
}
rrmodes = xallocarray(conn->count_modes, sizeof(RRModePtr));
if (!rrmodes) {
ErrorF("Failed to allocate connector modes\n");
drmModeFreeConnector(conn);
return NULL;
}
/* This spaghetti brought to you courtesey of xf86RandrR12.c
* It adds preferred modes first, then non-preferred modes */
for (pref = 1; pref >= 0; pref--) {
for (i = 0; i < conn->count_modes; ++i) {
kmode = &conn->modes[i];
if ((pref != 0) == ((kmode->type & DRM_MODE_TYPE_PREFERRED) != 0)) {
xRRModeInfo modeInfo;
RRModePtr rrmode;
modeInfo.nameLength = strlen(kmode->name);
modeInfo.width = kmode->hdisplay;
modeInfo.dotClock = kmode->clock * 1000;
modeInfo.hSyncStart = kmode->hsync_start;
modeInfo.hSyncEnd = kmode->hsync_end;
modeInfo.hTotal = kmode->htotal;
modeInfo.hSkew = kmode->hskew;
modeInfo.height = kmode->vdisplay;
modeInfo.vSyncStart = kmode->vsync_start;
modeInfo.vSyncEnd = kmode->vsync_end;
modeInfo.vTotal = kmode->vtotal;
modeInfo.modeFlags = kmode->flags;
rrmode = RRModeGet(&modeInfo, kmode->name);
if (rrmode) {
rrmodes[*nmode] = rrmode;
*nmode = *nmode + 1;
*npref = *npref + pref;
}
}
}
}
/* workaround: there could be no preferred mode that got added */
if (*nmode > 0 && *npref == 0)
*npref = 1;
drmModeFreeConnector(conn);
return rrmodes;
#else
*nmode = *npref = 0;
return NULL;
#endif
}
static void
lease_connector_handle_connector_id(void *data,
struct wp_drm_lease_connector_v1 *wp_drm_lease_connector_v1,
uint32_t connector_id)
{
struct xwl_output *output;
Atom name;
INT32 value;
int err;
int nmode, npref;
RRModePtr *rrmodes;
value = connector_id;
output = (struct xwl_output *)data;
name = MakeAtom("CONNECTOR_ID", 12, TRUE);
if (name != BAD_RESOURCE) {
err = RRConfigureOutputProperty(output->randr_output, name,
FALSE, FALSE, TRUE,
1, &value);
if (err != 0) {
ErrorF("RRConfigureOutputProperty error, %d\n", err);
return;
}
err = RRChangeOutputProperty(output->randr_output, name,
XA_INTEGER, 32, PropModeReplace, 1,
&value, FALSE, FALSE);
if (err != 0) {
ErrorF("RRChangeOutputProperty error, %d\n", err);
return;
}
}
rrmodes = xwl_get_rrmodes_from_connector_id(output->lease_device->drm_read_only_fd,
connector_id, &nmode, &npref);
if (rrmodes != NULL)
RROutputSetModes(output->randr_output, rrmodes, nmode, npref);
free(rrmodes);
}
static void
lease_connector_handle_done(void *data,
struct wp_drm_lease_connector_v1 *wp_drm_lease_connector_v1)
{
/* This space is deliberately left blank */
}
static void
lease_connector_handle_withdrawn(void *data,
struct wp_drm_lease_connector_v1 *wp_drm_lease_connector_v1)
{
struct xwl_output *xwl_output = data;
xwl_output->withdrawn_connector = TRUE;
/* Not removing the xwl_output if currently leased with Wayland */
if (xwl_output->lease)
return;
xwl_output_remove(xwl_output);
}
static const struct wp_drm_lease_connector_v1_listener lease_connector_listener = {
.name = lease_connector_handle_name,
.description = lease_connector_handle_description,
.connector_id = lease_connector_handle_connector_id,
.withdrawn = lease_connector_handle_withdrawn,
.done = lease_connector_handle_done,
};
static void
drm_lease_device_handle_drm_fd(void *data,
struct wp_drm_lease_device_v1 *wp_drm_lease_device_v1,
int fd)
{
((struct xwl_drm_lease_device *)data)->drm_read_only_fd = fd;
}
static void
drm_lease_device_handle_connector(void *data,
struct wp_drm_lease_device_v1 *wp_drm_lease_device_v1,
struct wp_drm_lease_connector_v1 *connector)
{
struct xwl_drm_lease_device *lease_device = data;
struct xwl_screen *xwl_screen = lease_device->xwl_screen;
struct xwl_output *xwl_output;
char name[MAX_OUTPUT_NAME] = { 0 };
xwl_output = calloc(1, sizeof *xwl_output);
if (xwl_output == NULL) {
ErrorF("%s ENOMEM\n", __func__);
return;
}
xwl_output->lease_device = lease_device;
xwl_output->xwl_screen = xwl_screen;
xwl_output->lease_connector = connector;
xwl_output->randr_crtc = RRCrtcCreate(xwl_screen->screen, xwl_output);
if (!xwl_output->randr_crtc) {
ErrorF("Failed creating RandR CRTC\n");
goto err;
}
RRCrtcSetRotations(xwl_output->randr_crtc, ALL_ROTATIONS);
xwl_output->randr_output = RROutputCreate(xwl_screen->screen,
name, MAX_OUTPUT_NAME, xwl_output);
snprintf(name, MAX_OUTPUT_NAME, "XWAYLAND%d",
xwl_screen_get_next_output_serial(xwl_screen));
xwl_output_set_name(xwl_output, name);
if (!xwl_output->randr_output) {
ErrorF("Failed creating RandR Output\n");
goto err;
}
RRCrtcGammaSetSize(xwl_output->randr_crtc, 256);
RROutputSetCrtcs(xwl_output->randr_output, &xwl_output->randr_crtc, 1);
RROutputSetConnection(xwl_output->randr_output, RR_Connected);
RROutputSetNonDesktop(xwl_output->randr_output, TRUE);
xwl_output->randr_output->devPrivate = xwl_output;
wp_drm_lease_connector_v1_add_listener(connector,
&lease_connector_listener,
xwl_output);
xorg_list_append(&xwl_output->link, &xwl_screen->output_list);
return;
err:
if (xwl_output->randr_crtc)
RRCrtcDestroy(xwl_output->randr_crtc);
free(xwl_output);
}
static void
drm_lease_device_handle_released(void *data,
struct wp_drm_lease_device_v1 *wp_drm_lease_device_v1)
{
struct xwl_drm_lease_device *lease_device = data;
xwl_screen_destroy_drm_lease_device(lease_device->xwl_screen, wp_drm_lease_device_v1);
}
static void
drm_lease_device_handle_done(void *data,
struct wp_drm_lease_device_v1 *wp_drm_lease_device_v1)
{
/* This space is deliberately left blank */
}
static const struct wp_drm_lease_device_v1_listener drm_lease_device_listener = {
.drm_fd = drm_lease_device_handle_drm_fd,
.connector = drm_lease_device_handle_connector,
.released = drm_lease_device_handle_released,
.done = drm_lease_device_handle_done,
};
void
xwl_screen_add_drm_lease_device(struct xwl_screen *xwl_screen, uint32_t id)
{
struct wp_drm_lease_device_v1 *lease_device = wl_registry_bind(
xwl_screen->registry, id, &wp_drm_lease_device_v1_interface, 1);
struct xwl_drm_lease_device *device_data = malloc(sizeof(struct xwl_drm_lease_device));
device_data->drm_lease_device = lease_device;
device_data->xwl_screen = xwl_screen;
device_data->drm_read_only_fd = -1;
device_data->id = id;
xorg_list_add(&device_data->link, &xwl_screen->drm_lease_devices);
wp_drm_lease_device_v1_add_listener(lease_device,
&drm_lease_device_listener,
device_data);
}
void
xwl_screen_destroy_drm_lease_device(struct xwl_screen *xwl_screen,
struct wp_drm_lease_device_v1 *wp_drm_lease_device_v1)
{
struct xwl_drm_lease_device *device_data;
xorg_list_for_each_entry(device_data, &xwl_screen->drm_lease_devices, link) {
if (device_data->drm_lease_device == wp_drm_lease_device_v1) {
wp_drm_lease_device_v1_destroy(wp_drm_lease_device_v1);
xorg_list_del(&device_data->link);
if (device_data->drm_read_only_fd >= 0)
close(device_data->drm_read_only_fd);
free(device_data);
return;
}
}
}