Re: [PATCH v4 1/2] tests/intel: Add gem_change_handle_race test suite

Kamil Konieczny <[email protected]> Wed, 29 Jul 2026 18:12:20 +0200
Newsgroups org.freedesktop.lists.igt-dev
Message-ID <[email protected]>
Hi Vitaly,
On 2026-07-27 at 23:29:23 -0400, [email protected] wrote:
> From: Vitaly Prosyak <[email protected]>
> 
> This test suite validates concurrent operation handling in GEM handle
> management through the proposed DRM_IOCTL_GEM_CHANGE_HANDLE ioctl.
> 
> The test is GPU-agnostic and works with both Intel i915 and AMD amdgpu
> drivers, focusing on proper locking and handle lifecycle management during
> concurrent GEM operations.
> 
> Test coverage (7 race condition subtests):
> - race-change-vs-close: CHANGE_HANDLE races against GEM_CLOSE
> - race-change-vs-change: Two CHANGE_HANDLE ops race on same handle
> - race-change-vs-prime: CHANGE_HANDLE races against PRIME_HANDLE_TO_FD
> - race-aggressive-change-vs-close: High-iteration close vs change
> - race-exploit-single-thread: Sequential swap+close pattern
> - race-exploit-random-handles: Random handle stress test
> - race-close-before-lock: Close-before-lock scenario with CPU pinning
> 
> The test uses i915/gem.h infrastructure while remaining vendor-agnostic
> via DRM core, following IGT convention of placing all gem_* tests in
> tests/intel/ directory.

Please add here Cc and your s-o-b, then after

---

add very datailed description of changes like below.

> 
> v4 changes (addressing Kamil Konieczny's review feedback):
>  - Removed running_under_gdb() function entirely (was wastefully
>    added in patch 1 and removed in patch 2 in v3)
>  - Fixed all double newlines throughout
>  - All formatting/style fixes now in this patch (not in patch 2):
>    * Removed #define _GNU_SOURCE (already defined by meson build system)
>    * Added header comment explaining tests/intel/ location
>    * Fixed check_kernel_traces() brace style
>    * Enhanced pin_to_cpu() with error handling and ARM compatibility
>    * Fixed variable declarations (C89 style)
>    * Removed unnecessary braces in if statements
>    * Fixed static variable initialization
>  - Added git format-patch -v4 for version tracking
>  - Added meson.build entry
> 
> v3 changes (addressing Kamil Konieczny's review feedback):
>  - Removed reference to external documentation (RACE_CONDITIONS_EXPLAINED.md)
>  - Sanitized test descriptions and comments (security consideration)
>  - Renamed 'race-darknavy-cve' to 'race-close-before-lock'
>  - Renamed internal functions and variables for clarity
>  - No functional changes to test logic
> 
> v2 changes (addressing Kamil Konieczny's review feedback):
>  - Added header comment explaining why test is in tests/intel/ directory
>  - ARM compatibility fixes (graceful CPU pinning failure handling)
>  - Made pin_to_cpu() handle failures gracefully with igt_debug()
> 
> v1:
>  - Initial submission with 7 race condition subtests
> 
> Signed-off-by: Vitaly Prosyak <[email protected]>
> Change-Id: I29ff7f6ede7180aa84b617232fad8e149554b44a

Please do not include Change-Id.

> ---
>  tests/intel/gem_change_handle_race.c | 1743 ++++++++++++++++++++++++++
>  tests/meson.build                    |    1 +
>  2 files changed, 1744 insertions(+)
>  create mode 100644 tests/intel/gem_change_handle_race.c
> 
> diff --git a/tests/intel/gem_change_handle_race.c b/tests/intel/gem_change_handle_race.c
> new file mode 100644
> index 000000000..5fde7b1aa
> --- /dev/null
> +++ b/tests/intel/gem_change_handle_race.c
> @@ -0,0 +1,1743 @@
> +// SPDX-License-Identifier: MIT
> +/*
> + * Copyright 2026 Advanced Micro Devices, Inc.
> + * Copyright 2026 Intel Corporation
> + *
> + * GPU-agnostic race condition tests for GEM_CHANGE_HANDLE ioctl
> + *
> + * NOTE: This test is located in tests/intel/ directory because:
> + * 1. All gem_* tests are traditionally placed in tests/intel/ regardless
> + *    of GPU vendor, following IGT convention for GEM-related tests
> + * 2. The test uses i915/gem.h and i915/gem_create.h for GEM object creation
> + *    on Intel platforms, though it works vendor-agnostically via DRM core
> + * 3. Some subtest designs (noop-same-handle, invalid-*, edge-*, functional-*)
> + *    were proposed by Simona Vetter (Intel), hence Intel copyright header
> + *
> + * ARM compatibility: This test uses pthread_setaffinity_np() which requires
> + * _GNU_SOURCE and may behave differently on ARM due to different CPU topology.
> + * The test gracefully handles CPU pinning failures and continues execution.
> + */
> +
> +#include <errno.h>
> +#include <fcntl.h>
> +#include <limits.h>
> +#include <pthread.h>
> +#include <sched.h>
> +#include <semaphore.h>
> +#include <signal.h>
> +#include <string.h>
> +#include <sys/stat.h>
> +
> +#include "igt.h"
> +#include "igt_device.h"
> +#include "i915/gem.h"
> +#include "i915/gem_create.h"
> +
> +/* AMDGPU includes (if available) */
> +#if __has_include("igt_amd.h")
> +#include "lib/amdgpu/amd_memory.h"
> +#include "igt_amd.h"
> +#define HAS_AMDGPU 1
> +#else
> +#define HAS_AMDGPU 0
> +#endif
> +
> +/* Helper to check for concurrent access traces in kernel log */
> +static int check_kernel_traces(void)
> +{
> +	FILE *fp;
> +	char line[1024];
> +	int trace_count = 0;
> +
> +	/*
> +	 * concurrent access manifests in kernel logs as:
> +	 * - KASAN reports (if CONFIG_KASAN=y)
> +	 * - Kernel oops/warnings in drm_gem_object_release_handle
> +	 * - Stack traces with drm_gem_object_handle_put_unlocked
> +	 *
> +	 * We grep for function names that appear in the concurrent access path.
> +	 */
> +	fp = popen("dmesg | grep -c -E '(KASAN.*(drm_gem|change_handle)|"
> +		   "drm_gem_object_release_handle|"
> +		   "drm_gem_object_handle_put_unlocked|"
> +		   "drm_gem_change_handle.*RIP)' 2>/dev/null", "r");
> +	if (fp == NULL)
> +		return 0;
> +
> +	if (fgets(line, sizeof(line), fp) != NULL)
> +		trace_count = atoi(line);
> +	pclose(fp);
> +
> +	return trace_count;
> +}
> +
> +static void pin_to_cpu(int cpu)
> +{
> +	cpu_set_t cpuset;
> +	int ret;
> +
> +	CPU_ZERO(&cpuset);
> +	CPU_SET(cpu, &cpuset);
> +	ret = pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &cpuset);
> +	
> +	/*
> +	 * ARM compatibility: pthread_setaffinity_np() may fail on ARM systems
> +	 * due to different CPU topology or when running in containers/VMs.
> +	 * We don't fail the test - CPU pinning is a best-effort optimization
> +	 * to increase race condition probability, not a hard requirement.
> +	 */
> +	if (ret != 0)
> +		igt_debug("Failed to pin to CPU %d (errno=%d), continuing anyway\n",
> +			  cpu, ret);
> +}
> +/* Define DRM_IOCTL_GEM_CHANGE_HANDLE if not available in kernel headers */
> +#ifndef DRM_IOCTL_GEM_CHANGE_HANDLE
> +struct drm_gem_change_handle {
> +	__u32 handle;
> +	__u32 new_handle;
> +};
> +
> +#define DRM_IOCTL_GEM_CHANGE_HANDLE    DRM_IOWR(0xD2, struct drm_gem_change_handle)
> +#endif
> +
> +/**
> + * TEST: gem change handle race
> + * Description: Multi-threaded race condition tests for GEM_CHANGE_HANDLE
> + * Category: Core
> + * Mega feature: General Core features
> + * Sub-category: GEM handles
> + * Functionality: gem change handle
> + * Feature: gem
> + * Run type: FULL
> + *
> + * SUBTEST: race-change-vs-close
> + * Description: Test CHANGE_HANDLE racing with GEM_CLOSE (concurrent access issues detection)
> + *
> + * SUBTEST: race-change-vs-change
> + * Description: Test concurrent CHANGE_HANDLE operations (handle corruption detection)
> + *
> + * SUBTEST: race-change-vs-prime
> + * Description: Test CHANGE_HANDLE racing with Prime ioctls (stale reference detection)
> + *
> + * SUBTEST: race-close-before-lock
> + * Description: GEM_CLOSE before CHANGE_HANDLE table_lock (concurrent access)
> + *
> + * SUBTEST: race-aggressive-change-vs-close
> + * Description: Aggressive concurrent access race with CPU pinning and KASAN detection
> + *
> + * SUBTEST: race-exploit-single-thread
> + * Description: Single BO swapped H to H+1 with periodic FD close for concurrent access
> + *
> + * SUBTEST: race-exploit-random-handles
> + * Description: Random handle probing to find free slots and trigger concurrent access
> + *
> + * SUBTEST: noop-same-handle
> + * Description: Edge case where handle equals new_handle should succeed as noop
> + *
> + * SUBTEST: invalid-new-handle-exceeds-int-max
> + * Description: new_handle exceeding INT_MAX should fail with EINVAL
> + *
> + * SUBTEST: invalid-handle-nonexistent
> + * Description: Non-existent handle should fail with ENOENT
> + *
> + * SUBTEST: edge-new-handle-zero
> + * Description: new_handle of zero exercises the unhandled zero-handle case
> + *
> + * SUBTEST: functional-rename-verification
> + * Description: After rename old handle returns EINVAL on close and new handle closes successfully
> + */
> +
> +IGT_TEST_DESCRIPTION("Multi-threaded race condition tests for GEM_CHANGE_HANDLE");
> +
> +#define BO_SIZE (16 * 1024)
> +#define RACE_DURATION_SEC 1
> +
> +/* Verbose logging - set to false to reduce output */
> +static bool verbose_logging;
> +
> +/* Driver type detection */
> +enum gpu_driver {
> +	DRIVER_TYPE_UNKNOWN = 0,
> +	DRIVER_TYPE_I915,
> +	DRIVER_TYPE_AMDGPU,
> +};
> +
> +/* GPU context structure */
> +struct gpu_ctx {
> +	int fd;
> +	enum gpu_driver driver;
> +#if HAS_AMDGPU
> +	amdgpu_device_handle amdgpu_device;
> +#endif
> +};
> +
> +/* Thread synchronization */
> +struct thread_data {
> +	struct gpu_ctx *ctx;
> +	uint32_t handle;
> +	uint32_t alt_handle;
> +	uint32_t *race_detected;
> +	int *unexpected_errno;  /* Track what errno caused the race */
> +	bool running;
> +	pthread_barrier_t *barrier;
> +};
> +
> +struct aggressive_data {
> +	struct gpu_ctx *ctx;
> +	volatile int go;
> +	uint32_t handle;
> +	uint32_t new_handle;
> +};
> +
> +struct single_thread_data {
> +	struct gpu_ctx *ctx;
> +	int *main_fd;
> +	pthread_mutex_t *fd_lock;
> +	volatile bool running;
> +	volatile bool hammer_ready;  /* Signal when BO created */
> +	volatile uint32_t swap_count;
> +	volatile uint32_t close_count;
> +	uint32_t *race_hits;
> +	pthread_barrier_t *barrier;
> +};
> +
> +struct random_handle_data {
> +	struct gpu_ctx *ctx;
> +	int *main_fd;
> +	pthread_mutex_t *fd_lock;
> +	volatile bool running;
> +	volatile bool hammer_ready;
> +	volatile uint32_t swap_count;
> +	volatile uint32_t close_count;
> +	volatile uint32_t attempt_count;  /* Track total attempts */
> +	uint32_t *race_hits;
> +	pthread_barrier_t *barrier;
> +};
> +
> +struct race_shared {
> +	int fd;
> +	uint32_t old_handle;
> +	uint32_t new_handle;
> +	struct drm_gem_change_handle ch;
> +	struct drm_gem_close cl;
> +	sem_t change_sem;
> +	sem_t close_sem;
> +	int change_success;
> +	int close_success;
> +};
> +
> +static enum gpu_driver detect_driver(int fd)
> +{
> +	/* Check AMDGPU first (primary target) */
> +	if (is_amdgpu_device(fd))
> +		return DRIVER_TYPE_AMDGPU;
> +	if (is_i915_device(fd))
> +		return DRIVER_TYPE_I915;
> +	return DRIVER_TYPE_UNKNOWN;
> +}
> +
> +static const char *gpu_name(enum gpu_driver driver)
> +{
> +	switch (driver) {
> +	case DRIVER_TYPE_I915: return "Intel i915";
> +	case DRIVER_TYPE_AMDGPU: return "AMD GPU";
> +	default: return "Unknown";
> +	}
> +}
> +
> +static uint32_t gem_create_bo(struct gpu_ctx *ctx, uint64_t size)
> +{
> +	uint32_t handle = 0;
> +
> +	switch (ctx->driver) {
> +	case DRIVER_TYPE_I915: {
> +		struct drm_i915_gem_create create = { .size = size };
> +
> +		do_ioctl(ctx->fd, DRM_IOCTL_I915_GEM_CREATE, &create);
> +		handle = create.handle;
> +		break;
> +	}
> +	case DRIVER_TYPE_AMDGPU:
> +#if HAS_AMDGPU
> +		handle = igt_amd_create_bo(ctx->fd, size);
> +#else
> +		igt_require_f(0, "AMDGPU support not compiled in\n");
> +#endif
> +		break;
> +	default:
> +		igt_assert_f(0, "Unsupported driver\n");
> +	}
> +
> +	igt_assert(handle != 0);
> +	return handle;
> +}
> +
> +static int gem_close_bo(struct gpu_ctx *ctx, uint32_t handle)
> +{
> +	struct drm_gem_close close_args = { .handle = handle };
> +
> +	return igt_ioctl(ctx->fd, DRM_IOCTL_GEM_CLOSE, &close_args);
> +}
> +
> +static int gem_change_handle(struct gpu_ctx *ctx, uint32_t old_handle, uint32_t new_handle)
> +{
> +	struct drm_gem_change_handle args = {
> +		.handle = old_handle,
> +		.new_handle = new_handle,
> +	};
> +	return igt_ioctl(ctx->fd, DRM_IOCTL_GEM_CHANGE_HANDLE, &args);
> +}
> +
> +/*
> + * ═══════════════════════════════════════════════════════════════════════
> + * RACE CONDITION #1: CHANGE_HANDLE vs GEM_CLOSE
> + * ═══════════════════════════════════════════════════════════════════════
> + *
> + *
> + * Thread A (CHANGE_HANDLE)           Thread B (GEM_CLOSE)
> + * ────────────────────────           ────────────────────
> + * 1. Lookup handle 0x100
> + *    → Find GEM object
> + *                                     2. Lookup handle 0x100
> + *                                        → Find GEM object
> + *                                     3. Remove from handle table
> + *                                     4. Free GEM object
> + * 5. Change handle 0x100 → 0x200
> + *    → Operate on freed memory! 💥

Is that verbatim copy from kernel? Do it really use
non-ascii chars, utf-8 or utf-16 in report?

> + *
> + * Test validates kernel properly locks to prevent concurrent access issues.
> + */
> +
> +static void *change_handle_thread(void *arg)
> +{
> +	struct thread_data *data = arg;
> +	uint32_t handle = data->handle;
> +	uint32_t new_handle = data->alt_handle;
> +	int ret;
> +
> +	pthread_barrier_wait(data->barrier);
> +
> +	while (data->running) {
> +		ret = gem_change_handle(data->ctx, handle, new_handle);
> +		if (ret == 0) {
> +			/* Successfully changed - try to change back */
> +			ret = gem_change_handle(data->ctx, new_handle, handle);
> +			if (ret != 0 && errno != ENOENT && errno != EINVAL && errno != EEXIST && errno != ENOSPC) {
> +				/* Unexpected error - potential race */
> +				*data->unexpected_errno = errno;
> +				__sync_fetch_and_add(data->race_detected, 1);
> +			}
> +		} else if (errno != ENOENT && errno != EINVAL && errno != EEXIST && errno != ENOSPC) {
> +			/*
> +			 * Expected errors:
> +			 * - ENOENT: handle was closed
> +			 * - EINVAL: invalid handle
> +			 * - EEXIST: target handle already exists (collision detected early)
> +			 * - ENOSPC: target handle occupied (collision detected during idr_alloc)
> +			 */
> +			*data->unexpected_errno = errno;
> +			__sync_fetch_and_add(data->race_detected, 1);
> +		}
> +		sched_yield();
> +	}
> +
> +	return NULL;
> +}
> +
> +static void *close_recreate_thread(void *arg)
> +{
> +	struct thread_data *data = arg;
> +	uint32_t handle;
> +	uint32_t old_handle;
> +	pthread_t tid = pthread_self();
> +
> +	pthread_barrier_wait(data->barrier);
> +
> +	while (data->running) {
> +		/* Close the handle */
> +		old_handle = data->handle;
> +		if (data->handle) {
> +			gem_close_bo(data->ctx, data->handle);
> +			if (verbose_logging) {
> +				igt_info("[CLOSE-TID:%lu] Closed handle %u\n",
> +					 (unsigned long)tid, old_handle);
> +			}
> +		}
> +
> +		/* Recreate BO with new handle */
> +		handle = gem_create_bo(data->ctx, BO_SIZE);
> +		data->handle = handle;
> +		if (verbose_logging) {
> +			igt_info("[CLOSE-TID:%lu] Created new BO with handle %u\n",
> +				 (unsigned long)tid, handle);
> +		}
> +
> +		sched_yield();
> +	}
> +
> +	return NULL;
> +}
> +
> +/**
> + * test_race_change_vs_close - Race CHANGE_HANDLE against GEM_CLOSE
> + *
> + *
> + * Spawns two threads:
> + * - Thread A: Continuously changes handle value
> + * - Thread B: Continuously closes and recreates handle
> + *
> + * Validates kernel prevents concurrent access issues through proper locking.
> + */
> +static void test_race_change_vs_close(struct gpu_ctx *ctx)
> +{
> +	pthread_t thread_change, thread_close;
> +	pthread_barrier_t barrier;
> +	struct thread_data data = {0};
> +	uint32_t race_detected = 0;
> +	int unexpected_errno = 0;
> +	uint32_t handle;
> +
> +	igt_info("Testing RACE #1: CHANGE_HANDLE vs CLOSE on %s\n",
> +		 gpu_name(ctx->driver));
> +	igt_info("  Duration: %d seconds\n", RACE_DURATION_SEC);
> +	igt_info("  Goal: Detect concurrent access issues scenarios\n");
> +
> +	handle = gem_create_bo(ctx, BO_SIZE);
> +	igt_assert(handle != 0);
> +
> +	data.ctx = ctx;
> +	data.handle = handle;
> +	data.alt_handle = handle + 0x1000;
> +	data.race_detected = &race_detected;
> +	data.unexpected_errno = &unexpected_errno;
> +	data.running = true;
> +
> +	pthread_barrier_init(&barrier, NULL, 2);
> +	data.barrier = &barrier;
> +
> +	/* Start both threads simultaneously */
> +	igt_assert_eq(pthread_create(&thread_change, NULL, change_handle_thread, &data), 0);
> +	igt_assert_eq(pthread_create(&thread_close, NULL, close_recreate_thread, &data), 0);
> +
> +	/* Let them race */
> +	sleep(RACE_DURATION_SEC);
> +
> +	data.running = false;
> +	pthread_join(thread_change, NULL);
> +	pthread_join(thread_close, NULL);
> +
> +	pthread_barrier_destroy(&barrier);
> +
> +	/* Clean up final handle if exists */
> +	if (data.handle)
> +		gem_close_bo(ctx, data.handle);
> +
> +	igt_info("  Race test completed. Suspicious conditions: %u\n", race_detected);
> +	if (race_detected > 0) {
> +		igt_info("  ⚠️  RACE DETECTED: Unexpected errno = %d (%s)\n",
> +			 unexpected_errno, strerror(unexpected_errno));
> +		igt_info("  This indicates the kernel may not be properly handling concurrent\n");
> +		igt_info("  CHANGE_HANDLE operations. Kernel should serialize these with\n");
> +		igt_info("  proper locking to prevent corruption and concurrent access issues.\n");
> +	}
> +	igt_assert_f(race_detected == 0,
> +		     "Race condition detected! errno=%d (%s). "
> +		     "Kernel locking may be insufficient.\n",
> +		     unexpected_errno, strerror(unexpected_errno));
> +}
> +
> +/*
> + * ═══════════════════════════════════════════════════════════════════════
> + * RACE CONDITION #2: CHANGE_HANDLE vs CHANGE_HANDLE
> + * ═══════════════════════════════════════════════════════════════════════
> + *
> + *
> + * "I'd just spawn two threads, that constantly try to change the handle
> + *  between just 2 back and forth, to maximize the amount of conflicts"
> + *
> + * Thread A: change_handle(H, H+1)
> + * Thread B: change_handle(H+1, H)
> + * ... continuously swapping ...
> + *
> + * Tests handle table corruption, reference counting, lost handles.
> + */
> +
> +static void *change_handle_thread_a(void *arg)
> +{
> +	struct thread_data *data = arg;
> +	int ret;
> +	pthread_t tid = pthread_self();
> +
> +	pthread_barrier_wait(data->barrier);
> +
> +	while (data->running) {
> +		ret = gem_change_handle(data->ctx, data->handle, data->alt_handle);
> +		if (verbose_logging) {
> +			if (ret == 0) {
> +				igt_info("[THREAD-A-TID:%lu] %u -> %u: SUCCESS\n",
> +					 (unsigned long)tid, data->handle, data->alt_handle);
> +			} else {
> +				igt_info("[THREAD-A-TID:%lu] %u -> %u: FAILED - %s (errno=%d)\n",
> +					 (unsigned long)tid, data->handle, data->alt_handle,
> +					 strerror(errno), errno);
> +			}
> +		}
> +
> +		if (ret != 0 && errno != ENOENT && errno != EINVAL && errno != EEXIST && errno != ENOSPC) {
> +			/* Unexpected error - potential race */
> +			igt_info("[THREAD-A-TID:%lu] *** UNEXPECTED ERROR: %s (errno=%d) ***\n",
> +				 (unsigned long)tid, strerror(errno), errno);
> +			*data->unexpected_errno = errno;
> +			__sync_fetch_and_add(data->race_detected, 1);
> +		}
> +		sched_yield();
> +	}
> +
> +	return NULL;
> +}
> +
> +static void *change_handle_thread_b(void *arg)
> +{
> +	struct thread_data *data = arg;
> +	int ret;
> +	pthread_t tid = pthread_self();
> +
> +	pthread_barrier_wait(data->barrier);
> +
> +	while (data->running) {
> +		ret = gem_change_handle(data->ctx, data->alt_handle, data->handle);
> +		if (verbose_logging) {
> +			if (ret == 0) {
> +				igt_info("[THREAD-B-TID:%lu] %u -> %u: SUCCESS\n",
> +					 (unsigned long)tid, data->alt_handle, data->handle);
> +			} else {
> +				igt_info("[THREAD-B-TID:%lu] %u -> %u: FAILED - %s (errno=%d)\n",
> +					 (unsigned long)tid, data->alt_handle, data->handle,
> +					 strerror(errno), errno);
> +			}
> +		}
> +
> +		if (ret != 0 && errno != ENOENT && errno != EINVAL && errno != EEXIST && errno != ENOSPC) {
> +			/* Unexpected error */
> +			igt_info("[THREAD-B-TID:%lu] *** UNEXPECTED ERROR: %s (errno=%d) ***\n",
> +				 (unsigned long)tid, strerror(errno), errno);
> +			*data->unexpected_errno = errno;
> +			__sync_fetch_and_add(data->race_detected, 1);
> +		}
> +		sched_yield();
> +	}
> +
> +	return NULL;
> +}
> +
> +/**
> + * test_race_change_vs_change - Concurrent CHANGE_HANDLE operations
> + *
> + *
> + * Implements email thread test strategy:
> + * "spawn two threads that constantly try to change the handle between
> + *  just 2 back and forth, to maximize the amount of conflicts"
> + *
> + * Thread A: change(H, H+1)
> + * Thread B: change(H+1, H)
> + *
> + * Validates kernel prevents handle table corruption.
> + */
> +static void test_race_change_vs_change(struct gpu_ctx *ctx)
> +{
> +	pthread_t thread_a, thread_b;
> +	pthread_barrier_t barrier;
> +	struct thread_data data = {0};
> +	uint32_t race_detected = 0;
> +	int unexpected_errno = 0;
> +	uint32_t handle;
> +
> +	igt_info("Testing RACE #2: CHANGE_HANDLE vs CHANGE_HANDLE on %s\n",
> +		 gpu_name(ctx->driver));
> +	igt_info("  Duration: %d seconds\n", RACE_DURATION_SEC);
> +	igt_info("  Strategy: Two threads swapping handle H <-> H+1\n");
> +	igt_info("  Goal: Detect handle corruption and lost handles\n");
> +
> +	handle = gem_create_bo(ctx, BO_SIZE);
> +	igt_assert(handle != 0);
> +
> +	data.ctx = ctx;
> +	data.handle = handle;
> +	data.alt_handle = handle + 1;
> +	data.race_detected = &race_detected;
> +	data.unexpected_errno = &unexpected_errno;
> +	data.running = true;
> +
> +	pthread_barrier_init(&barrier, NULL, 2);
> +	data.barrier = &barrier;
> +
> +	/* Start both threads simultaneously */
> +	igt_assert_eq(pthread_create(&thread_a, NULL, change_handle_thread_a, &data), 0);
> +	igt_assert_eq(pthread_create(&thread_b, NULL, change_handle_thread_b, &data), 0);
> +
> +	/* Let them race */
> +	sleep(RACE_DURATION_SEC);
> +
> +	data.running = false;
> +	pthread_join(thread_a, NULL);
> +	pthread_join(thread_b, NULL);
> +
> +	pthread_barrier_destroy(&barrier);
> +
> +	/* Try to close both possible handles */
> +	gem_close_bo(ctx, handle);
> +	gem_close_bo(ctx, handle + 1);
> +
> +	igt_info("  Race test completed. Suspicious conditions: %u\n", race_detected);
> +	if (race_detected > 0) {
> +		igt_info("  ⚠️  RACE DETECTED: Unexpected errno = %d (%s)\n",
> +			 unexpected_errno, strerror(unexpected_errno));
> +		igt_info("  This indicates the kernel may not be properly handling concurrent\n");
> +		igt_info("  CHANGE_HANDLE operations. Kernel should serialize these with\n");
> +		igt_info("  proper locking to prevent corruption and concurrent access issues.\n");
> +	}
> +	igt_assert_f(race_detected == 0,
> +		     "Race condition detected! errno=%d (%s). "
> +		     "Kernel locking may be insufficient.\n",
> +		     unexpected_errno, strerror(unexpected_errno));
> +}
> +
> +/*
> + * ═══════════════════════════════════════════════════════════════════════
> + * RACE CONDITION #3: CHANGE_HANDLE vs Prime Ioctls
> + * ═══════════════════════════════════════════════════════════════════════
> + *
> + *
> + * "maybe: 1. create bo 2. handle2fd, check that fd2handle gives us
> + *  the same handle back 3. change_handle 4. check that fd2handle
> + *  gives us the new handle back"
> + *
> + * Tests stale references, reference count corruption.
> + */
> +
> +/**
> + * change_handle_prime_thread - Continuously swap handle for prime test
> + *
> + * Like change_handle_thread_a/b but swaps back and forth between
> + * the two handle values to continuously exercise the race.
> + */
> +static void *change_handle_prime_thread(void *arg)
> +{
> +	struct thread_data *data = arg;
> +	uint32_t handle_a = data->handle;
> +	uint32_t handle_b = data->alt_handle;
> +	bool swap_direction = true;
> +	int ret;
> +	pthread_t tid = pthread_self();
> +
> +	pthread_barrier_wait(data->barrier);
> +
> +	while (data->running) {
> +		if (swap_direction) {
> +			/* Try to change from handle_a to handle_b */
> +			ret = gem_change_handle(data->ctx, handle_a, handle_b);
> +			if (verbose_logging) {
> +				if (ret == 0) {
> +					igt_info("[PRIME-CHANGE-TID:%lu] %u -> %u: SUCCESS\n",
> +						 (unsigned long)tid, handle_a, handle_b);
> +				} else {
> +					igt_info("[PRIME-CHANGE-TID:%lu] %u -> %u: FAILED - %s (errno=%d)\n",
> +						 (unsigned long)tid, handle_a, handle_b, strerror(errno), errno);
> +				}
> +			}
> +		} else {
> +			/* Try to change from handle_b to handle_a */
> +			ret = gem_change_handle(data->ctx, handle_b, handle_a);
> +			if (verbose_logging) {
> +				if (ret == 0) {
> +					igt_info("[PRIME-CHANGE-TID:%lu] %u -> %u: SUCCESS\n",
> +						 (unsigned long)tid, handle_b, handle_a);
> +				} else {
> +					igt_info("[PRIME-CHANGE-TID:%lu] %u -> %u: FAILED - %s (errno=%d)\n",
> +						 (unsigned long)tid, handle_b, handle_a, strerror(errno), errno);
> +				}
> +			}
> +		}
> +
> +		/* Flip direction for next iteration to keep swapping */
> +		if (ret == 0)
> +			swap_direction = !swap_direction;
> +		/* All errors are acceptable during race - kernel will serialize */
> +
> +		sched_yield();
> +	}
> +
> +	return NULL;
> +}
> +
> +static void *prime_thread(void *arg)
> +{
> +	struct thread_data *data = arg;
> +	struct drm_prime_handle prime_args;
> +	uint32_t retrieved_handle;
> +	uint32_t handle_a = data->handle;
> +	uint32_t handle_b = data->alt_handle;
> +	int dmabuf_fd;
> +	int ret;
> +	pthread_t tid = pthread_self();
> +	uint32_t tried_handle;
> +
> +	pthread_barrier_wait(data->barrier);
> +
> +	while (data->running) {
> +		/*
> +		 * Try handle_a first. If it fails (handle was changed),
> +		 * try handle_b. One of them should work.
> +		 */
> +		prime_args.handle = handle_a;
> +		prime_args.flags = DRM_CLOEXEC | DRM_RDWR;
> +		tried_handle = handle_a;
> +		ret = igt_ioctl(data->ctx->fd, DRM_IOCTL_PRIME_HANDLE_TO_FD, &prime_args);
> +		if (verbose_logging) {
> +			if (ret == 0) {
> +				igt_info("[PRIME-TID:%lu] HANDLE_TO_FD(handle=%u): SUCCESS (fd=%d)\n",
> +					 (unsigned long)tid, tried_handle, prime_args.fd);
> +			} else {
> +				igt_info("[PRIME-TID:%lu] HANDLE_TO_FD(handle=%u): FAILED - %s (errno=%d)\n",
> +					 (unsigned long)tid, tried_handle, strerror(errno), errno);
> +			}
> +		}
> +		if (ret != 0) {
> +			/* handle_a failed, try handle_b */
> +			prime_args.handle = handle_b;
> +			tried_handle = handle_b;
> +			ret = igt_ioctl(data->ctx->fd, DRM_IOCTL_PRIME_HANDLE_TO_FD, &prime_args);
> +			if (verbose_logging) {
> +				if (ret == 0) {
> +					igt_info("[PRIME-TID:%lu] HANDLE_TO_FD(handle=%u): SUCCESS (fd=%d)\n",
> +						 (unsigned long)tid, tried_handle, prime_args.fd);
> +				} else {
> +					igt_info("[PRIME-TID:%lu] HANDLE_TO_FD(handle=%u): FAILED - %s (errno=%d)\n",
> +						 (unsigned long)tid, tried_handle, strerror(errno), errno);
> +				}
> +			}
> +		}
> +
> +		if (ret == 0) {
> +			dmabuf_fd = prime_args.fd;
> +
> +			/* Convert back to handle */
> +			prime_args.fd = dmabuf_fd;
> +			prime_args.flags = 0;
> +			ret = igt_ioctl(data->ctx->fd, DRM_IOCTL_PRIME_FD_TO_HANDLE, &prime_args);
> +			if (verbose_logging) {
> +				if (ret == 0) {
> +					igt_info("[PRIME-TID:%lu] FD_TO_HANDLE(fd=%d): SUCCESS (handle=%u)\n",
> +						 (unsigned long)tid, dmabuf_fd, prime_args.handle);
> +				} else {
> +					igt_info("[PRIME-TID:%lu] FD_TO_HANDLE(fd=%d): FAILED - %s (errno=%d)\n",
> +						 (unsigned long)tid, dmabuf_fd, strerror(errno), errno);
> +				}
> +			}
> +			if (ret == 0) {
> +				retrieved_handle = prime_args.handle;
> +
> +				/*
> +				 * Handle should be one of our two known handles.
> +				 * If it's neither, Prime reference table is corrupted.
> +				 */
> +				if (retrieved_handle != handle_a &&
> +				    retrieved_handle != handle_b) {
> +					/* Handle corruption detected */
> +					__sync_fetch_and_add(data->race_detected, 1);
> +				}
> +
> +				/* Close retrieved handle */
> +				gem_close_bo(data->ctx, retrieved_handle);
> +			}
> +			close(dmabuf_fd);
> +		}
> +		/* All errors acceptable during race */
> +
> +		usleep(100); /* Prime operations are slower */
> +	}
> +
> +	return NULL;
> +}
> +
> +/**
> + * test_race_change_vs_prime - Race CHANGE_HANDLE against Prime ioctls
> + *
> + *
> + * Implements email thread test:
> + * "create bo, handle2fd, check fd2handle returns same, change_handle,
> + *  check fd2handle returns new handle"
> + *
> + * Thread A: Continuously changes handle
> + * Thread B: Continuously does fd2handle and validates result
> + *
> + * Validates kernel updates Prime references correctly.
> + */
> +static void test_race_change_vs_prime(struct gpu_ctx *ctx)
> +{
> +	pthread_t thread_change, thread_prime;
> +	pthread_barrier_t barrier;
> +	struct thread_data data = {0};
> +	uint32_t race_detected = 0;
> +	int unexpected_errno = 0;
> +	uint32_t handle;
> +	struct drm_prime_handle prime_args;
> +	int dmabuf_fd, ret;
> +	uint32_t retrieved_handle;
> +
> +	igt_info("Testing RACE #3: CHANGE_HANDLE vs Prime on %s\n",
> +		 gpu_name(ctx->driver));
> +	igt_info("  Duration: %d seconds\n", RACE_DURATION_SEC);
> +	igt_info("  Goal: Detect stale references and refcount corruption\n");
> +
> +	handle = gem_create_bo(ctx, BO_SIZE);
> +	igt_assert(handle != 0);
> +
> +	/* Initial prime export - verify it works */
> +	prime_args.handle = handle;
> +	prime_args.flags = DRM_CLOEXEC | DRM_RDWR;
> +	ret = igt_ioctl(ctx->fd, DRM_IOCTL_PRIME_HANDLE_TO_FD, &prime_args);
> +	igt_assert_eq(ret, 0);
> +	dmabuf_fd = prime_args.fd;
> +
> +	prime_args.fd = dmabuf_fd;
> +	prime_args.flags = 0;
> +	ret = igt_ioctl(ctx->fd, DRM_IOCTL_PRIME_FD_TO_HANDLE, &prime_args);
> +	igt_assert_eq(ret, 0);
> +	retrieved_handle = prime_args.handle;
> +	igt_assert_eq(retrieved_handle, handle);
> +
> +	gem_close_bo(ctx, retrieved_handle);
> +	close(dmabuf_fd);
> +
> +	/* Now test concurrent change + prime operations */
> +	data.ctx = ctx;
> +	data.handle = handle;
> +	data.alt_handle = handle + 1;
> +	data.race_detected = &race_detected;
> +	data.unexpected_errno = &unexpected_errno;
> +	data.running = true;
> +
> +	pthread_barrier_init(&barrier, NULL, 2);
> +	data.barrier = &barrier;
> +
> +	igt_assert_eq(pthread_create(&thread_change, NULL, change_handle_prime_thread, &data), 0);
> +	igt_assert_eq(pthread_create(&thread_prime, NULL, prime_thread, &data), 0);
> +
> +	/* Let them race */
> +	sleep(RACE_DURATION_SEC);
> +
> +	data.running = false;
> +	pthread_join(thread_change, NULL);
> +	pthread_join(thread_prime, NULL);
> +
> +	pthread_barrier_destroy(&barrier);
> +
> +	/* Cleanup */
> +	gem_close_bo(ctx, handle);
> +	gem_close_bo(ctx, handle + 1);
> +
> +	igt_info("  Race test completed. Handle corruption: %u\n", race_detected);
> +	igt_assert_eq(race_detected, 0);
> +}
> +
> +/*
> + * Aggressive race test - follows proven concurrent access reproducer pattern.
> + * Thread A: CHANGE_HANDLE(h->nh), Thread B: CLOSE(h)
> + * CPU pinning for maximum race potential.
> + */
> +
> +static void *aggressive_change_thread(void *arg)
> +{
> +	struct aggressive_data *data = arg;
> +	pthread_t tid = pthread_self();
> +	int ret;
> +
> +	pin_to_cpu(0);
> +
> +	while (!data->go)
> +		__asm__ volatile("pause" ::: "memory");
> +
> +	ret = gem_change_handle(data->ctx, data->handle, data->new_handle);
> +
> +	if (verbose_logging) {
> +		if (ret == 0) {
> +			igt_info("[AGG-CHANGE-TID:%lu] %u -> %u: SUCCESS\n",
> +				 (unsigned long)tid, data->handle, data->new_handle);
> +		} else {
> +			igt_info("[AGG-CHANGE-TID:%lu] %u -> %u: FAILED - %s (errno=%d)\n",
> +				 (unsigned long)tid, data->handle, data->new_handle,
> +				 strerror(errno), errno);
> +		}
> +	}
> +
> +	return NULL;
> +}
> +
> +static void *aggressive_close_thread(void *arg)
> +{
> +	struct aggressive_data *data = arg;
> +	pthread_t tid = pthread_self();
> +
> +	pin_to_cpu(1);
> +
> +	while (!data->go)
> +		__asm__ volatile("pause" ::: "memory");
> +
> +	gem_close_bo(data->ctx, data->handle);
> +
> +	if (verbose_logging) {
> +		igt_info("[AGG-CLOSE-TID:%lu] Closed handle %u\n",
> +			 (unsigned long)tid, data->handle);
> +	}
> +
> +	return NULL;
> +}
> +
> +static void test_race_aggressive(struct gpu_ctx *ctx)
> +{
> +	struct aggressive_data data = {};
> +	pthread_t thread_change, thread_close;
> +	int iterations = 5000;
> +	int race_wins = 0;
> +	int kasan_before, kasan_after;
> +	int i;
> +
> +	igt_info("Testing AGGRESSIVE RACE: CHANGE_HANDLE vs CLOSE on %s\n",
> +		 gpu_name(ctx->driver));
> +	igt_info("  Pattern: concurrent access reproducer from DRM mailing list\n");
> +	igt_info("  Iterations: %d\n", iterations);
> +	igt_info("  CPU pinning: Change->CPU0, Close->CPU1\n");
> +	igt_info("\n");
> +	igt_info("  Expected results:\n");
> +	igt_info("    Unpatched kernel: Race wins ~50-60%%, KASAN traces in dmesg\n");
> +	igt_info("    Patched kernel:   0 race wins (mutex serializes ops)\n");
> +	igt_info("\n");
> +	igt_info("  Note: KASAN traces appear in kernel log (dmesg), not test output\n");
> +	igt_info("        Kernel must have CONFIG_KASAN=y to see detailed concurrent access reports\n");
> +	igt_info("\n");
> +
> +	data.ctx = ctx;
> +
> +	/* Clear/mark kernel log before test */
> +	kasan_before = check_kernel_traces();
> +
> +	for (i = 0; i < iterations; i++) {
> +		uint32_t h = gem_create_bo(ctx, BO_SIZE);
> +
> +		data.handle = h;
> +		data.new_handle = 0x4000 + (i & 0x3FFF);
> +		data.go = 0;
> +
> +		__sync_synchronize();
> +
> +		pthread_create(&thread_change, NULL, aggressive_change_thread, &data);
> +		pthread_create(&thread_close, NULL, aggressive_close_thread, &data);
> +
> +		__sync_synchronize();
> +		data.go = 1;
> +
> +		pthread_join(thread_change, NULL);
> +		pthread_join(thread_close, NULL);
> +
> +		/*
> +		 * concurrent access Detection Strategy:
> +		 *
> +		 * The concurrent access happens when CHANGE_HANDLE races with CLOSE.
> +		 * Handle state checking doesn't work because:
> +		 * - With mutex: CHANGE can win cleanly (not a race)
> +		 * - Without mutex: concurrent access happens but handles appear normal
> +		 *
> +		 * ONLY reliable detection: Monitor kernel log for concurrent access traces.
> +		 * concurrent access shows up in drm_gem_object_release_handle path.
> +		 */
> +
> +		/* Clean up handles (one or both may fail) */
> +		drmIoctl(ctx->fd, DRM_IOCTL_GEM_CLOSE,
> +			 &(struct drm_gem_close){ .handle = data.new_handle });
> +		drmIoctl(ctx->fd, DRM_IOCTL_GEM_CLOSE,
> +			 &(struct drm_gem_close){ .handle = h });
> +
> +		if ((i + 1) % 1000 == 0) {
> +			/* Check for KASAN traces periodically */
> +			int kasan_now = check_kernel_traces();
> +			int kasan_new = kasan_now - kasan_before;
> +
> +			igt_info("  Progress: %d/%d, KASAN traces: %d\n",
> +				 i + 1, iterations, kasan_new);
> +
> +			if (kasan_new > 0 && race_wins == 0)
> +				race_wins = kasan_new;
> +		}
> +	}
> +
> +	/* Final KASAN check */
> +	kasan_after = check_kernel_traces();
> +	race_wins = kasan_after - kasan_before;
> +
> +	igt_info("\nAggressive race test completed:\n");
> +	igt_info("  Iterations: %d\n", iterations);
> +	igt_info("  KASAN concurrent access traces detected: %d\n", race_wins);
> +
> +	if (race_wins > 0) {
> +		igt_info("\n*** RACE CONDITION DETECTED (concurrent access) ***\n");
> +		igt_info("  Kernel is UNPATCHED (missing handle_map_lock mutex)\n");
> +		igt_info("  KASAN detected %d concurrent access issues events\n", race_wins);
> +		igt_info("\n");
> +		igt_info("  View concurrent access traces:\n");
> +		igt_info("    dmesg | grep -A40 -E '(drm_gem_object_release_handle|drm_gem_object_handle_put_unlocked)'\n");
> +		igt_info("\n");
> +		igt_info("  The concurrent access happens when CHANGE_HANDLE tries to access\n");
> +		igt_info("  an object that was freed by concurrent CLOSE.\n");
> +		igt_info("\n");
> +		igt_info("  Fix: Apply V5 patch (add handle_map_lock mutex)\n");
> +
> +		/* Fail the test if concurrent access detected */
> +		igt_assert_eq(race_wins, 0);
> +	} else {
> +		igt_info("\n=== NO concurrent access DETECTED ===\n");
> +		igt_info("  Kernel appears PATCHED (handle_map_lock prevents race)\n");
> +		igt_info("  No KASAN traces found after %d iterations\n", iterations);
> +		igt_info("  This is the expected behavior with V5 patch applied\n");
> +		igt_info("\n");
> +		igt_info("  Note: concurrent access detection via kernel log monitoring\n");
> +		igt_info("        Works with KASAN or kernel oops/warnings\n");
> +	}
> +}
> +
> +/*
> + * ═══════════════════════════════════════════════════════════════════════
> + * SINGLE-THREADED TEST - Focused race exploitation
> + * ═══════════════════════════════════════════════════════════════════════
> + */
> +
> +/*
> + * ═══════════════════════════════════════════════════════════════════════
> + * SINGLE-THREADED TEST - Fixed timing
> + * ═══════════════════════════════════════════════════════════════════════
> + */
> +
> +static void *single_thread_hammer(void *arg)
> +{
> +	struct single_thread_data *data = arg;
> +	uint32_t handle, alt_handle;
> +	int local_fd;
> +	int ret;
> +	bool is_base = true;
> +	unsigned long swaps = 0;
> +	struct gpu_ctx local_ctx;
> +
> +	pthread_barrier_wait(data->barrier);
> +
> +	/* Create BO BEFORE closer starts */
> +	pthread_mutex_lock(data->fd_lock);
> +	local_fd = *data->main_fd;
> +	pthread_mutex_unlock(data->fd_lock);
> +
> +	local_ctx = *data->ctx;
> +	local_ctx.fd = local_fd;
> +
> +	handle = gem_create_bo(&local_ctx, 4096);
> +	if (handle == 0) {
> +		igt_info("[HAMMER] Failed to create BO\n");
> +		data->hammer_ready = true;  /* Signal even on failure */
> +		return NULL;
> +	}
> +
> +	alt_handle = handle + 1;
> +	igt_info("[HAMMER] Created handle %u, will swap with %u\n", handle, alt_handle);
> +
> +	/* Signal that BO is ready - closer can start now */
> +	data->hammer_ready = true;
> +
> +	while (data->running) {
> +		/* Rapid swap - no FD checks, just swap */
> +		ret = gem_change_handle(&local_ctx,
> +					is_base ? handle : alt_handle,
> +					is_base ? alt_handle : handle);
> +		if (ret == 0) {
> +			swaps++;
> +			__sync_fetch_and_add(&data->swap_count, 1);
> +			is_base = !is_base;
> +
> +			if (swaps % 10000 == 0)
> +				igt_info("[HAMMER] %lu swaps\n", swaps);
> +		}
> +		/* No error handling - just keep hammering */
> +	}
> +
> +	igt_info("[HAMMER] Final: %lu swaps\n", swaps);
> +	return NULL;
> +}
> +
> +static void *single_thread_closer(void *arg)
> +{
> +	struct single_thread_data *data = arg;
> +	unsigned long closes = 0;
> +	int new_fd;
> +
> +	pthread_barrier_wait(data->barrier);
> +
> +	/* Wait for hammer to create BO first */
> +	while (!data->hammer_ready)
> +		usleep(100);
> +
> +	igt_info("[CLOSER] Hammer ready, starting closes\n");
> +
> +	while (data->running) {
> +		/* Close FD to trigger idr_for_each() */
> +		pthread_mutex_lock(data->fd_lock);
> +		if (*data->main_fd >= 0) {
> +			close(*data->main_fd);
> +			*data->main_fd = -1;
> +			closes++;
> +			__sync_fetch_and_add(&data->close_count, 1);
> +		}
> +		pthread_mutex_unlock(data->fd_lock);
> +
> +		/* Small delay - this is the window for hitting the race */
> +		usleep(1000);
> +
> +		/* Reopen */
> +		new_fd = drm_open_driver_render(DRIVER_ANY);
> +		if (new_fd >= 0) {
> +			pthread_mutex_lock(data->fd_lock);
> +			*data->main_fd = new_fd;
> +			pthread_mutex_unlock(data->fd_lock);
> +		}
> +
> +		/* Check KASAN */
> +		if (closes % 100 == 0) {
> +			int kasan_now = check_kernel_traces();
> +
> +			if (kasan_now > 0) {
> +				__sync_fetch_and_add(data->race_hits, 1);
> +				igt_info("[CLOSER] *** KASAN DETECTED: %d traces ***\n", kasan_now);
> +			}
> +		}
> +	}
> +
> +	igt_info("[CLOSER] %lu closes\n", closes);
> +	return NULL;
> +}
> +
> +static void *single_thread_monitor(void *arg)
> +{
> +	struct single_thread_data *data = arg;
> +	int kasan_last = 0;
> +	int kasan_now;
> +
> +	pthread_barrier_wait(data->barrier);
> +
> +	while (data->running) {
> +		sleep(5);
> +
> +		kasan_now = check_kernel_traces();
> +		if (kasan_now > kasan_last) {
> +			igt_info("[MONITOR] +%d KASAN traces (total: %d)\n",
> +				 kasan_now - kasan_last, kasan_now);
> +			kasan_last = kasan_now;
> +		}
> +
> +		igt_info("[STATS] Swaps:%u Closes:%u KASAN:%u\n",
> +			 data->swap_count, data->close_count, *data->race_hits);
> +	}
> +
> +	return NULL;
> +}
> +
> +static void test_race_single_thread(struct gpu_ctx *ctx)
> +{
> +	pthread_t hammer, closer, monitor;
> +	pthread_barrier_t barrier;
> +	pthread_mutex_t fd_lock = PTHREAD_MUTEX_INITIALIZER;
> +	struct single_thread_data data = {0};
> +	uint32_t race_hits = 0;
> +	int main_fd;
> +	int kasan_before, kasan_after;
> +	int race_detected;
> +
> +	igt_info("═══════════════════════════════════════════════════════════\n");
> +	igt_info("  SINGLE-THREADED FOCUSED TEST (Fixed Timing)\n");
> +	igt_info("═══════════════════════════════════════════════════════════\n");
> +	igt_info("  Duration: 3 seconds\n");
> +	igt_info("  Threads: 1 hammer + 1 closer + 1 monitor\n");
> +	igt_info("  Strategy:\n");
> +	igt_info("    1. Hammer creates ONE BO\n");
> +	igt_info("    2. Hammer swaps H <-> H+1 rapidly in tight loop\n");
> +	igt_info("    3. Closer periodically close(fd) -> idr_for_each()\n");
> +	igt_info("  Race window: idr_alloc(new) ... idr_replace(old, NULL)\n");
> +	igt_info("    Between these two, BOTH handles point to same object\n");
> +	igt_info("    If idr_for_each() runs here -> double release -> concurrent access\n");
> +	igt_info("═══════════════════════════════════════════════════════════\n\n");
> +
> +	kasan_before = check_kernel_traces();
> +	igt_info("KASAN traces before test: %d\n\n", kasan_before);
> +
> +	main_fd = drm_open_driver_render(DRIVER_ANY);
> +	igt_require(main_fd >= 0);
> +
> +	data.ctx = ctx;
> +	data.main_fd = &main_fd;
> +	data.fd_lock = &fd_lock;
> +	data.running = true;
> +	data.hammer_ready = false;
> +	data.race_hits = &race_hits;
> +
> +	pthread_barrier_init(&barrier, NULL, 3);
> +	data.barrier = &barrier;
> +
> +	igt_info("Starting threads...\n");
> +
> +	pthread_create(&hammer, NULL, single_thread_hammer, &data);
> +	pthread_create(&closer, NULL, single_thread_closer, &data);
> +	pthread_create(&monitor, NULL, single_thread_monitor, &data);
> +
> +	igt_info("Racing for 3 seconds...\n\n");
> +	sleep(3);
> +
> +	igt_info("\nStopping threads...\n");
> +	data.running = false;
> +
> +	pthread_join(hammer, NULL);
> +	pthread_join(closer, NULL);
> +	pthread_join(monitor, NULL);
> +
> +	pthread_barrier_destroy(&barrier);
> +	pthread_mutex_destroy(&fd_lock);
> +
> +	if (main_fd >= 0)
> +		close(main_fd);
> +
> +	kasan_after = check_kernel_traces();
> +	race_detected = kasan_after - kasan_before;
> +
> +	igt_info("\n═══════════════════════════════════════════════════════════\n");
> +	igt_info("  SINGLE-THREADED TEST RESULTS\n");
> +	igt_info("═══════════════════════════════════════════════════════════\n");
> +	igt_info("  Total swaps: %u\n", data.swap_count);
> +	igt_info("  Total closes: %u\n", data.close_count);
> +	igt_info("  KASAN concurrent access traces: %d\n", race_detected);
> +	igt_info("═══════════════════════════════════════════════════════════\n\n");
> +
> +	if (race_detected > 0) {
> +		igt_info("*** SUCCESS: RACE CONDITION TRIGGERED! ***\n\n");
> +		igt_info("concurrent access detected with %d KASAN traces!\n\n", race_detected);
> +		igt_info("This confirms the vulnerability in the wrong operation order:\n\n");
> +		igt_info("  Race Timeline:\n");
> +		igt_info("  --------------\n");
> +		igt_info("  T1: Hammer thread calls change_handle(H, H+1)\n");
> +		igt_info("      -> enters drm_gem_change_handle_ioctl()\n");
> +		igt_info("      -> spin_lock(&table_lock)\n");
> +		igt_info("      -> idr_alloc(H+1, obj)\n");
> +		igt_info("         [RACE WINDOW OPENS - Both H and H+1 point to obj]\n\n");
> +		igt_info("  T2: Closer thread calls close(fd)\n");
> +		igt_info("      -> drm_gem_release()\n");
> +		igt_info("      -> idr_for_each(&object_idr, release_handle, ...)\n");
> +		igt_info("         Iteration 1: handle H -> release(obj)\n");
> +		igt_info("         Iteration 2: handle H+1 -> release(obj) AGAIN\n");
> +		igt_info("      -> DOUBLE RELEASE -> USE-AFTER-FREE!\n\n");
> +		igt_info("  T3: Hammer thread continues (if not crashed):\n");
> +		igt_info("      -> idr_replace(H, NULL)\n");
> +		igt_info("         [Too late - obj already freed]\n\n");
> +		igt_info("View KASAN traces:\n");
> +		igt_info("  sudo dmesg | grep -B10 -A40 'concurrent access issues'\n\n");
> +		igt_info("The fix (correct order):\n");
> +		igt_info("  1. idr_replace(H, NULL)  FIRST  <- Make H invisible\n");
> +		igt_info("  2. idr_alloc(H+1, obj)   SECOND <- Create new handle\n");
> +		igt_info("  Result: At no point do both handles exist simultaneously\n");
> +		igt_info("          idr_for_each() can never see both -> no double release\n\n");
> +	} else {
> +		igt_info("=== NO concurrent access DETECTED ===\n\n");
> +		if (data.swap_count == 0) {
> +			igt_info("ERROR: No successful handle swaps!\n");
> +			igt_info("The change_handle ioctl may not be working properly.\n\n");
> +		} else if (data.swap_count < 100) {
> +			igt_info("WARNING: Very few swaps (%u). May not have hit race window.\n\n",
> +				 data.swap_count);
> +		} else {
> +			igt_info("Completed %u swaps and %u closes without detecting concurrent access.\n\n",
> +				 data.swap_count, data.close_count);
> +			igt_info("Possible reasons:\n");
> +			igt_info("  1. Correct patch is applied (NULL old BEFORE alloc new)\n");
> +			igt_info("  2. Race window is extremely small (nanoseconds)\n");
> +			igt_info("  3. Timing didn't align to hit the exact window\n");
> +			igt_info("  4. KASAN may not be sensitive enough to catch it\n");
> +			igt_info("  5. Test needs different timing parameters\n\n");
> +			igt_info("Note: Absence of concurrent access detection doesn't prove correctness.\n");
> +			igt_info("      Code analysis shows the race exists in principle.\n");
> +		}
> +	}
> +}
> +
> +/*
> + * ═══════════════════════════════════════════════════════════════════════
> + * RANDOM HANDLE TEST - Try random handle numbers to find free slots
> + * ═══════════════════════════════════════════════════════════════════════
> + */
> +
> +static void *random_handle_hammer(void *arg)
> +{
> +	struct random_handle_data *data = arg;
> +	uint32_t handle;
> +	int local_fd;
> +	int ret;
> +	unsigned long swaps = 0;
> +	unsigned long attempts = 0;
> +	struct gpu_ctx local_ctx;
> +	uint32_t random_handle;
> +
> +	pthread_barrier_wait(data->barrier);
> +
> +	/* Create BO BEFORE closer starts */
> +	pthread_mutex_lock(data->fd_lock);
> +	local_fd = *data->main_fd;
> +	pthread_mutex_unlock(data->fd_lock);
> +
> +	local_ctx = *data->ctx;
> +	local_ctx.fd = local_fd;
> +
> +	handle = gem_create_bo(&local_ctx, 4096);
> +	if (handle == 0) {
> +		igt_info("[HAMMER] Failed to create BO\n");
> +		data->hammer_ready = true;
> +		return NULL;
> +	}
> +
> +	igt_info("[HAMMER] Created base handle %u\n", handle);
> +	data->hammer_ready = true;
> +
> +	/* Seed random number generator with thread ID + time */
> +	srand(time(NULL) ^ pthread_self());
> +
> +	while (data->running) {
> +		/* Try random handle numbers in different ranges */
> +		int range = attempts % 4;
> +
> +		switch (range) {
> +		case 0:
> +			/* Small random offset (1-100) */
> +			random_handle = handle + (rand() % 100) + 1;
> +			break;
> +		case 1:
> +			/* Medium random offset (100-10000) */
> +			random_handle = handle + (rand() % 9900) + 100;
> +			break;
> +		case 2:
> +			/* Large random offset (1M-2M) */
> +			random_handle = handle + 1000000 + (rand() % 1000000);
> +			break;
> +		case 3:
> +			/* Completely random handle */
> +			random_handle = rand() % 0xFFFFFF;
> +			if (random_handle == handle)
> +				random_handle++;
> +			break;
> +		}
> +
> +		attempts++;
> +		__sync_fetch_and_add(&data->attempt_count, 1);
> +
> +		/* Try to change to random handle */
> +		ret = gem_change_handle(&local_ctx, handle, random_handle);
> +		if (ret == 0) {
> +			swaps++;
> +			__sync_fetch_and_add(&data->swap_count, 1);
> +
> +			/* Successfully swapped, now swap back */
> +			ret = gem_change_handle(&local_ctx, random_handle, handle);
> +			if (ret != 0) {
> +				/* Failed to swap back, update current handle */
> +				handle = random_handle;
> +			}
> +
> +			if (swaps % 100 == 0)
> +				igt_info("[HAMMER] %lu successful swaps (%.2f%% success rate)\n",
> +					 swaps, 100.0 * swaps / attempts);
> +		}
> +
> +		/* Every 10000 attempts, report stats */
> +		if (attempts % 10000 == 0) {
> +			igt_info("[HAMMER] %lu attempts, %lu swaps (%.4f%% success)\n",
> +				 attempts, swaps, 100.0 * swaps / attempts);
> +		}
> +	}
> +
> +	igt_info("[HAMMER] Final: %lu swaps from %lu attempts (%.4f%%)\n",
> +		 swaps, attempts, 100.0 * swaps / attempts);
> +
> +	/* Cleanup current handle */
> +	gem_close_bo(&local_ctx, handle);
> +
> +	return NULL;
> +}
> +
> +static void *random_handle_closer(void *arg)
> +{
> +	struct random_handle_data *data = arg;
> +	unsigned long closes = 0;
> +	int new_fd;
> +
> +	pthread_barrier_wait(data->barrier);
> +
> +	/* Wait for hammer to create BO first */
> +	while (!data->hammer_ready)
> +		usleep(100);
> +
> +	igt_info("[CLOSER] Hammer ready, starting closes\n");
> +
> +	while (data->running) {
> +		/* Close FD to trigger idr_for_each() */
> +		pthread_mutex_lock(data->fd_lock);
> +		if (*data->main_fd >= 0) {
> +			close(*data->main_fd);
> +			*data->main_fd = -1;
> +			closes++;
> +			__sync_fetch_and_add(&data->close_count, 1);
> +		}
> +		pthread_mutex_unlock(data->fd_lock);
> +
> +		/* Small delay - race window */
> +		usleep(1000);
> +
> +		/* Reopen */
> +		new_fd = drm_open_driver_render(DRIVER_ANY);
> +		if (new_fd >= 0) {
> +			pthread_mutex_lock(data->fd_lock);
> +			*data->main_fd = new_fd;
> +			pthread_mutex_unlock(data->fd_lock);
> +		}
> +
> +		/* Check KASAN */
> +		if (closes % 100 == 0) {
> +			int kasan_now = check_kernel_traces();
> +
> +			if (kasan_now > 0) {
> +				__sync_fetch_and_add(data->race_hits, 1);
> +				igt_info("[CLOSER] *** KASAN DETECTED: %d traces ***\n", kasan_now);
> +			}
> +		}
> +	}
> +
> +	igt_info("[CLOSER] %lu closes\n", closes);
> +	return NULL;
> +}
> +
> +static void *random_handle_monitor(void *arg)
> +{
> +	struct random_handle_data *data = arg;
> +	int kasan_last = 0;
> +	int kasan_now;
> +
> +	pthread_barrier_wait(data->barrier);
> +
> +	while (data->running) {
> +		sleep(5);
> +
> +		kasan_now = check_kernel_traces();
> +		if (kasan_now > kasan_last) {
> +			igt_info("[MONITOR] +%d KASAN traces (total: %d)\n",
> +				 kasan_now - kasan_last, kasan_now);
> +			kasan_last = kasan_now;
> +		}
> +
> +		igt_info("[STATS] Attempts:%u Swaps:%u Closes:%u KASAN:%u (%.4f%% success)\n",
> +			 data->attempt_count, data->swap_count, data->close_count,
> +			 *data->race_hits,
> +			 data->attempt_count > 0 ? 100.0 * data->swap_count / data->attempt_count : 0);
> +	}
> +
> +	return NULL;
> +}
> +
> +static void test_race_random_handles(struct gpu_ctx *ctx)
> +{
> +	pthread_t hammer, closer, monitor;
> +	pthread_barrier_t barrier;
> +	pthread_mutex_t fd_lock = PTHREAD_MUTEX_INITIALIZER;
> +	struct random_handle_data data = {0};
> +	uint32_t race_hits = 0;
> +	int main_fd;
> +	int kasan_before, kasan_after;
> +	int race_detected;
> +
> +	igt_info("═══════════════════════════════════════════════════════════\n");
> +	igt_info("  RANDOM HANDLE TEST\n");
> +	igt_info("═══════════════════════════════════════════════════════════\n");
> +	igt_info("  Duration: 3 seconds\n");
> +	igt_info("  Threads: 1 hammer + 1 closer + 1 monitor\n");
> +	igt_info("  Strategy:\n");
> +	igt_info("    1. Hammer creates ONE BO\n");
> +	igt_info("    2. Hammer tries RANDOM new handle numbers\n");
> +	igt_info("    3. Ranges: small (1-100), medium (100-10K),\n");
> +	igt_info("       large (1M-2M), completely random\n");
> +	igt_info("    4. Swaps back to original handle on success\n");
> +	igt_info("    5. Closer periodically close(fd) -> idr_for_each()\n");
> +	igt_info("  Goal: Find free handle slots by random probing\n");
> +	igt_info("═══════════════════════════════════════════════════════════\n\n");
> +
> +	kasan_before = check_kernel_traces();
> +	igt_info("KASAN traces before test: %d\n\n", kasan_before);
> +
> +	main_fd = drm_open_driver_render(DRIVER_ANY);
> +	igt_require(main_fd >= 0);
> +
> +	data.ctx = ctx;
> +	data.main_fd = &main_fd;
> +	data.fd_lock = &fd_lock;
> +	data.running = true;
> +	data.hammer_ready = false;
> +	data.race_hits = &race_hits;
> +
> +	pthread_barrier_init(&barrier, NULL, 3);
> +	data.barrier = &barrier;
> +
> +	igt_info("Starting threads...\n");
> +
> +	pthread_create(&hammer, NULL, random_handle_hammer, &data);
> +	pthread_create(&closer, NULL, random_handle_closer, &data);
> +	pthread_create(&monitor, NULL, random_handle_monitor, &data);
> +
> +	igt_info("Racing for 3 seconds...\n\n");
> +	sleep(3);
> +
> +	igt_info("\nStopping threads...\n");
> +	data.running = false;
> +
> +	pthread_join(hammer, NULL);
> +	pthread_join(closer, NULL);
> +	pthread_join(monitor, NULL);
> +
> +	pthread_barrier_destroy(&barrier);
> +	pthread_mutex_destroy(&fd_lock);
> +
> +	if (main_fd >= 0)
> +		close(main_fd);
> +
> +	kasan_after = check_kernel_traces();
> +	race_detected = kasan_after - kasan_before;
> +
> +	igt_info("\n═══════════════════════════════════════════════════════════\n");
> +	igt_info("  RANDOM HANDLE TEST RESULTS\n");
> +	igt_info("═══════════════════════════════════════════════════════════\n");
> +	igt_info("  Total attempts: %u\n", data.attempt_count);
> +	igt_info("  Successful swaps: %u\n", data.swap_count);
> +	if (data.attempt_count > 0) {
> +		igt_info("  Success rate: %.4f%%\n",
> +			 100.0 * data.swap_count / data.attempt_count);
> +	}
> +	igt_info("  Total closes: %u\n", data.close_count);
> +	igt_info("  KASAN concurrent access traces: %d\n", race_detected);
> +	igt_info("═══════════════════════════════════════════════════════════\n\n");
> +
> +	if (race_detected > 0) {
> +		igt_info("*** SUCCESS: RACE CONDITION TRIGGERED! ***\n\n");
> +		igt_info("concurrent access detected with %d KASAN traces!\n\n", race_detected);
> +		igt_info("This confirms the race with random handle probing.\n\n");
> +		igt_info("View KASAN traces:\n");
> +		igt_info("  sudo dmesg | grep -B10 -A40 'concurrent access issues'\n\n");
> +	} else {
> +		igt_info("=== NO concurrent access DETECTED ===\n\n");
> +		if (data.swap_count == 0) {
> +			igt_info("ERROR: No successful handle swaps!\n");
> +			igt_info("Random probing also failed to find free handles.\n\n");
> +		} else if (data.swap_count < 100) {
> +			igt_info("WARNING: Very few swaps (%u). May not have hit race window.\n\n",
> +				 data.swap_count);
> +		} else {
> +			igt_info("Completed %u swaps and %u closes without detecting concurrent access.\n\n",
> +				 data.swap_count, data.close_count);
> +			igt_info("Random handle probing success rate: %.4f%%\n\n",
> +				 100.0 * data.swap_count / data.attempt_count);
> +			if (data.swap_count > 1000)
> +				igt_info("Good swap rate - race window likely too small to hit.\n");
> +		}
> +	}
> +}
> +/*
> + * Close-before-lock test - Exact race from vulnerability report
> + *
> + * Tests concurrent GEM_CLOSE and CHANGE_HANDLE operations.
> + * The race exploits the window where GEM_CLOSE runs BEFORE change_handle
> + * takes table_lock for idr_alloc:
> + *
> + *   Thread A (change_handle)          Thread B (GEM_CLOSE on H)
> + *   -----------------------          -------------------------
> + *   obj = lookup(H)  refcount 1->2
> + *                                    handle_count 1->0 -> put(obj)
> + *                                      refcount 2->1
> + *                                    idr_remove(H)
> + *   idr_alloc(obj, N)
> + *   idr_replace(NULL, H): H empty,
> + *     returns NULL -- discarded!
> + *   idr_replace(obj, N)
> + *   out: put(obj)  refcount 1->0
> + *     -> obj FREED
> + *
> + * End state: object_idr[N] points at freed obj -> concurrent access
> + *
> + * Uses DRM_IOCTL_MODE_CREATE_DUMB (driver-agnostic, no libdrm needed).
> + * Semaphore sync + CPU pinning for maximum race window exploitation.
> + *
> + * Expected:
> + *   Unpatched kernel: "Race Success" + KASAN traces in dmesg
> + *   Patched kernel (Sima's fix): 0 race wins in 2000 iterations
> + */
> +
> +static void *race_change_worker(void *arg)
> +{
> +	struct race_shared *s = arg;
> +
> +	pin_to_cpu(1);
> +	sem_wait(&s->change_sem);
> +
> +	if (igt_ioctl(s->fd, DRM_IOCTL_GEM_CHANGE_HANDLE, &s->ch) == 0)
> +		s->change_success = 1;
> +
> +	return NULL;
> +}
> +
> +static void *race_close_worker(void *arg)
> +{
> +	struct race_shared *s = arg;
> +
> +	pin_to_cpu(0);
> +	sem_wait(&s->close_sem);
> +
> +	if (igt_ioctl(s->fd, DRM_IOCTL_GEM_CLOSE, &s->cl) == 0)
> +		s->close_success = 1;
> +
> +	return NULL;
> +}
> +
> +static void test_race_close_before_lock(struct gpu_ctx *ctx)
> +{
> +	struct race_shared shared;
> +	int iterations = 2000;
> +	int race_wins = 0;
> +	int kasan_before, kasan_after;
> +	int i;
> +
> +	igt_info("Testing Close-before-lock test on %s\n",
> +		 gpu_name(ctx->driver));
> +	igt_info("  Tests close-before-lock scenario)\n");
> +	igt_info("  Race: GEM_CLOSE wins before CHANGE_HANDLE takes table_lock\n");
> +	igt_info("  Method: Semaphore sync + CPU pinning \n");
> +	igt_info("  Iterations: %d\n", iterations);
> +	igt_info("  Detection: Both change AND close succeed simultaneously\n\n");
> +
> +	shared.fd = ctx->fd;
> +	kasan_before = check_kernel_traces();
> +
> +	for (i = 0; i < iterations; i++) {
> +		struct drm_mode_create_dumb create = {
> +			.width = 64,
> +			.height = 64,
> +			.bpp = 32,
> +		};
> +		pthread_t tid_change, tid_close;
> +		int ret;
> +
> +		ret = igt_ioctl(ctx->fd, DRM_IOCTL_MODE_CREATE_DUMB, &create);
> +		if (ret < 0) {
> +			/*
> +			 * MODE_CREATE_DUMB may not be supported on all
> +			 * drivers/configs. Fall back to gem_create_bo.
> +			 */
> +			create.handle = gem_create_bo(ctx, BO_SIZE);
> +		}
> +
> +		shared.old_handle = create.handle;
> +		shared.new_handle = 0x4000 + i;
> +		shared.ch.handle = shared.old_handle;
> +		shared.ch.new_handle = shared.new_handle;
> +		shared.cl.handle = shared.old_handle;
> +		shared.change_success = 0;
> +		shared.close_success = 0;
> +
> +		sem_init(&shared.change_sem, 0, 0);
> +		sem_init(&shared.close_sem, 0, 0);
> +
> +		igt_assert_eq(pthread_create(&tid_change, NULL,
> +					     race_change_worker, &shared), 0);
> +		igt_assert_eq(pthread_create(&tid_close, NULL,
> +					     race_close_worker, &shared), 0);
> +
> +		/* Brief delay to let threads reach sem_wait */
> +		usleep(100);
> +
> +		/* Fire both threads as close to simultaneously as possible */
> +		sem_post(&shared.change_sem);
> +		sem_post(&shared.close_sem);
> +
> +		pthread_join(tid_change, NULL);
> +		pthread_join(tid_close, NULL);
> +
> +		sem_destroy(&shared.change_sem);
> +		sem_destroy(&shared.close_sem);
> +
> +		if (shared.change_success && shared.close_success) {
> +			race_wins++;
> +			igt_info("  [%d] *** RACE WIN: both change and close "
> +				 "succeeded (handle %u -> %u) ***\n",
> +				 i, shared.old_handle, shared.new_handle);
> +			/*
> +			 * new_handle now points to a freed object.
> +			 * Do NOT touch it -- just record the event.
> +			 * The dangling handle will be cleaned up on fd close.
> +			 */
> +		} else if (shared.change_success) {
> +			/* Change won, close lost -- normal, clean up new handle */
> +			struct drm_gem_close cleanup = {
> +				.handle = shared.new_handle
> +			};
> +			igt_ioctl(ctx->fd, DRM_IOCTL_GEM_CLOSE, &cleanup);
> +		}
> +		/* If close won and change lost: handle already freed, nothing to do */
> +
> +		if ((i + 1) % 500 == 0)
> +			igt_info("  Progress: %d/%d (race wins: %d)\n",
> +				 i + 1, iterations, race_wins);
> +	}
> +
> +	kasan_after = check_kernel_traces();
> +
> +	igt_info("\n  Results:\n");
> +	igt_info("  Iterations: %d\n", iterations);
> +	igt_info("  Race wins (both succeeded): %d\n", race_wins);
> +	igt_info("  KASAN concurrent access traces: %d\n", kasan_after - kasan_before);
> +
> +	if (race_wins > 0) {
> +		igt_info("\n  *** concurrent access RACE DETECTED ***\n");
> +		igt_info("  %d iterations had both CHANGE_HANDLE and GEM_CLOSE\n"
> +			 "  succeed on the same handle. The new handle now\n"
> +			 "  points to a freed GEM object.\n\n", race_wins);
> +		igt_info("  View KASAN traces:\n");
> +		igt_info("    sudo dmesg | grep -B5 -A40 'concurrent access issues'\n\n");
> +	} else {
> +		igt_info("\n  === NO RACE DETECTED ===\n");
> +		igt_info("  Kernel appears patched. No iteration had both\n");
> +		igt_info("  CHANGE_HANDLE and GEM_CLOSE succeed simultaneously.\n");
> +	}
> +
> +	/*
> +	 * Fail the test if races were detected -- this is a security bug.
> +	 * If KASAN found concurrent access traces, that's definitive proof.
> +	 */
> +	igt_assert_f(race_wins == 0,
> +		     "concurrent access race detected: %d wins in %d iterations\n",
> +		     race_wins, iterations);
> +}
> +
> +/*
> + * ═══════════════════════════════════════════════════════════════════════
> + * EDGE CASE TESTS (requested by Simona Vetter)
> + * ═══════════════════════════════════════════════════════════════════════
> + */
> +
> +/**
> + * test_noop_same_handle - handle == new_handle should be a noop
> + *
> + * When old handle equals new handle, the ioctl should return success
> + * without modifying anything.
> + */

Where is igt_main()? Your series should compile without errors
nor warnings patch after patch.

Regards,
Kamil

> diff --git a/tests/meson.build b/tests/meson.build
> index 9c266569d..76439076d 100644
> --- a/tests/meson.build
> +++ b/tests/meson.build
> @@ -106,6 +106,7 @@ intel_i915_progs = [
>  	'gem_caching',
>  	'gem_create',
>  	'gem_ccs',
> +	'gem_change_handle_race',
>  	'gem_close',
>  	'gem_close_race',
>  	'gem_compute',
> -- 
> 2.54.0
>