Intel-XE Archive on lore.kernel.org
 help / color / mirror / Atom feed
From: sashiko-bot@kernel.org
To: "Thomas Hellström" <thomas.hellstrom@linux.intel.com>,
	intel-xe@lists.freedesktop.org
Cc: dri-devel@lists.freedesktop.org
Subject: Re: [PATCH 3/4] drm/ttm, drm/xe: Introduce TTM_TT_FLAG_PARTIAL for partially backed-up tts
Date: Wed, 04 Nov 2026 20:47:09 +0000	[thread overview]
Message-ID: <sashiko-outbox-198028@kernel.org> (raw)
In-Reply-To: <20261104201331.2462-4-thomas.hellstrom@linux.intel.com>

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] drm/ttm: DMA mapping leak in ttm_pool_backup() on -EBUSY after restore abort
- [High] drm/ttm: ttm_tt_backup() leaves tt populated if backup fails at first page
- [Medium] drm/xe: stale remaining_pages and shrinker counts on failed ttm_tt_restore()
--

commit fc282ba9ab604d69dff7ad175cd51a0231a6c0b3
Author: Thomas Hellström <thomas.hellstrom@linux.intel.com>

drm/ttm, drm/xe: Introduce TTM_TT_FLAG_PARTIAL for partially backed-up tts

This commit introduces TTM_TT_FLAG_PARTIAL and ttm_tt::remaining_pages to
track partially backed-up ttm_tt objects so they stay on the normal LRU
and remain eligible for shrinker reclaim. It also adds
ttm_pool_restore_abort() to clean up interrupted restores before backup
and updates xe shrinker page accounting to track resident pages.

> diff --git a/drivers/gpu/drm/ttm/ttm_pool.c b/drivers/gpu/drm/ttm/ttm_pool.c
> index 1bf37023fed66..8f05257e4f542 100644
> --- a/drivers/gpu/drm/ttm/ttm_pool.c
> +++ b/drivers/gpu/drm/ttm/ttm_pool.c

[ ... ]

> @@ -1039,6 +1057,91 @@ void ttm_pool_drop_backed_up(struct ttm_tt *tt)
>  	ttm_pool_free_range(NULL, tt, ttm_cached, start_page, tt->num_pages);
>  }
>  
> +/**
> + * ttm_pool_restore_abort() - Abort an ongoing restore of a partial tt
> + * @tt: The struct ttm_tt with an active restore.

[ ... ]

> +static void ttm_pool_restore_abort(struct ttm_tt *tt)
> +{
> +	struct ttm_pool_tt_restore *restore = tt->restore;
> +	pgoff_t i, nr, mid;

[ ... ]

> +	/*
> +	 * Don't DMA-unmap committed restored pages here; their DMA
> +	 * addresses are in tt->dma_address[] and the backup path
> +	 * will unmap them.
> +	 */
> +
> +	/* Convert non-cached committed pages to write-back. */
> +	mid = restore->snapshot_alloc.caching_divide - tt->pages;
> +#ifdef CONFIG_X86
> +	if (mid > 0 && tt->caching != ttm_cached)
> +		set_pages_array_wb(tt->pages, mid);
> +#endif
> +
> +	kfree(restore);
> +	tt->restore = NULL;
> +
> +	for (i = 0, nr = 0; i < tt->num_pages; i++) {
> +		struct page *p = tt->pages[i];
> +
> +		if (p && !ttm_backup_page_ptr_is_handle(p))
> +			nr++;
> +	}
> +	tt->remaining_pages = nr;
> +}
> +
>  /**
>   * ttm_pool_backup() - Back up or purge a struct ttm_tt
>   * @pool: The pool used when allocating the struct ttm_tt.
> @@ -1071,10 +1174,13 @@ long ttm_pool_backup(struct ttm_pool *pool, struct ttm_tt *tt,
>  	pgoff_t shrunken = 0;
>  	pgoff_t i, j, num_pages, npages;
>  	pgoff_t nr_backed;
> +	bool partial = ttm_tt_is_partial(tt);
>  
>  	if (WARN_ON(ttm_tt_is_backed_up(tt)))
>  		return -EINVAL;
>  
> +	ttm_pool_restore_abort(tt);
> +
>  	if ((!ttm_backup_bytes_avail() && !flags->purge) ||
>  	    ttm_pool_uses_dma_alloc(pool) || ttm_tt_is_backed_up(tt))
>  		return -EBUSY;

[Severity: High]
Can this leak DMA mappings for committed restored pages when
ttm_pool_backup() returns -EBUSY right after ttm_pool_restore_abort(tt)?

When a restore of a backed-up or partial ttm_tt (with tt->dma_address !=
NULL) fails after committing one or more pages via
ttm_pool_allocated_page_commit(), tt->pages[0 .. restore->alloced_pages - 1]
are populated and DMA-mapped in tt->dma_address[], and tt is marked
TTM_TT_FLAG_PARTIAL with tt->restore non-NULL.

When ttm_pool_backup() is later called by the shrinker, it calls
ttm_pool_restore_abort(tt), which intentionally leaves committed restored
pages DMA-mapped in tt->dma_address[] for the backup loop to unmap, and
frees tt->restore (setting tt->restore = NULL).

If ttm_pool_backup() then takes the early -EBUSY return (for example, when
swap space is exhausted so !ttm_backup_bytes_avail() && !flags->purge), the
DMA-unmap loop below is skipped. That leaves a partial tt with
tt->restore == NULL while tt->pages[0 .. alloced_pages - 1] remain
DMA-mapped:

- On teardown, ttm_tt_fini() -> ttm_pool_drop_backed_up() sees
  tt->restore == NULL and calls:

    ttm_pool_free_range(NULL, tt, ttm_cached, 0, tt->num_pages);

  Because pool is NULL, ttm_pool_unmap_and_free() skips ttm_pool_unmap()
  and frees the still-DMA-mapped pages back to the page allocator.

- On a subsequent restore, ttm_pool_restore_and_alloc() starts a fresh
  restore at index 0, and ttm_pool_restore_commit() frees the retained
  resident pages via __free_pages_gpu_account(p, 0, false) without
  DMA-unmapping them and overwrites tt->dma_address[].

> diff --git a/drivers/gpu/drm/ttm/ttm_tt.c b/drivers/gpu/drm/ttm/ttm_tt.c
> index b645a18181843..2b441bf41d927 100644
> --- a/drivers/gpu/drm/ttm/ttm_tt.c
> +++ b/drivers/gpu/drm/ttm/ttm_tt.c

[ ... ]

> @@ -289,7 +289,18 @@ long ttm_tt_backup(struct ttm_device *bdev, struct ttm_tt *tt,
>  	ret = ttm_pool_backup(&bdev->pool, tt, &flags);
>  	if (ret > 0) {
>  		tt->page_flags &= ~TTM_TT_FLAG_PRIV_POPULATED;
> -		tt->page_flags |= TTM_TT_FLAG_BACKED_UP;
> +		if (ret >= tt->remaining_pages)
> +			tt->remaining_pages = 0;
> +		else
> +			tt->remaining_pages -= ret;
> +
> +		if (tt->remaining_pages) {
> +			tt->page_flags &= ~TTM_TT_FLAG_BACKED_UP;
> +			tt->page_flags |= TTM_TT_FLAG_PARTIAL;
> +		} else {
> +			tt->page_flags &= ~TTM_TT_FLAG_PARTIAL;
> +			tt->page_flags |= TTM_TT_FLAG_BACKED_UP;
> +		}
>  	}
>  
>  	return ret;

[Severity: High]
What happens if ttm_pool_backup() fails on the very first page after it has
already converted the pages to write-back and unmapped tt->dma_address[]?

In ttm_pool_backup(), for a populated tt (!partial), all pages are first
converted to write-back via set_pages_array_wb() (when tt->caching !=
ttm_cached), and all entries in tt->dma_address[] are DMA-unmapped via
ttm_pool_unmap() and zeroed via memset() before the folio backup loop
starts.

If ttm_backup_backup_folio() fails on the first page (i == 0) with
nr_backed == 0 (or fault injection truncates num_pages mid-compound on the
first compound page), ttm_pool_backup() breaks out of the loop with
shrunken == 0 and returns a negative error code (or 0).

Because ttm_tt_backup() only clears TTM_TT_FLAG_PRIV_POPULATED and sets
TTM_TT_FLAG_PARTIAL when ret > 0, the tt remains marked
TTM_TT_FLAG_PRIV_POPULATED (and not TTM_TT_FLAG_PARTIAL) even though its
DMA mappings have been unmapped and zeroed and its pages have been
converted to write-back:

- Subsequent ttm_bo_populate() / ttm_tt_populate() calls see
  ttm_tt_is_populated(tt) == true and return 0 without restoring DMA
  mappings or caching.
- A subsequent ttm_pool_backup() call sees !partial and calls
  set_pages_array_wb() a second time on pages that are already WB.
- Subsequent ttm_tt_unpopulate() -> ttm_pool_free() calls ttm_pool_unmap()
  on zeroed DMA addresses and returns the WB pages into the WC/UC pool via
  ttm_pool_type_give().

> @@ -300,10 +311,20 @@ int ttm_tt_restore(struct ttm_device *bdev, struct ttm_tt *tt,
>  {
>  	int ret = ttm_pool_restore_and_alloc(&bdev->pool, tt, ctx);
>  
> -	if (ret)
> +	if (ret) {
> +		/*
> +		 * Let the shrinker reclaim pages made resident by the
> +		 * failed restore. remaining_pages is recounted on backup.
> +		 */
> +		if (ttm_tt_is_backed_up(tt) &&
> +		    ttm_pool_restore_has_resident(tt)) {
> +			tt->page_flags &= ~TTM_TT_FLAG_BACKED_UP;
> +			tt->page_flags |= TTM_TT_FLAG_PARTIAL;
> +		}
>  		return ret;
> +	}
>  
> -	tt->page_flags &= ~TTM_TT_FLAG_BACKED_UP;
> +	tt->page_flags &= ~(TTM_TT_FLAG_BACKED_UP | TTM_TT_FLAG_PARTIAL);
>  
>  	return 0;
>  }

[ ... ]

> diff --git a/drivers/gpu/drm/xe/xe_bo.c b/drivers/gpu/drm/xe/xe_bo.c
> index 2afee1a6de668..6907b683dee07 100644
> --- a/drivers/gpu/drm/xe/xe_bo.c
> +++ b/drivers/gpu/drm/xe/xe_bo.c

[ ... ]

> @@ -676,20 +696,35 @@ static int xe_ttm_tt_populate(struct ttm_device *ttm_dev, struct ttm_tt *tt,
>  	    !(tt->page_flags & TTM_TT_FLAG_EXTERNAL_MAPPABLE))
>  		return 0;
>  
> -	if (ttm_tt_is_backed_up(tt) && !xe_tt->purgeable) {
> +	/*
> +	 * A purgeable backed-up tt was purged and holds neither pages nor
> +	 * handles. Partial tts are never purgeable: madvise only marks
> +	 * populated tts purgeable, and the shrinker purges those fully.
> +	 */
> +	if ((ttm_tt_is_backed_up(tt) || ttm_tt_is_partial(tt)) &&
> +	    !xe_tt->purgeable) {
> +		pgoff_t remaining = ttm_tt_remaining_pages(tt);
> +		long backed_up_pages = tt->num_pages - remaining;
> +
>  		err = ttm_tt_restore(ttm_dev, tt, ctx);
> +		if (!err) {
> +			xe_tt->purgeable = false;
> +			xe_ttm_tt_account_add(ttm_to_xe_device(ttm_dev), tt,
> +					      backed_up_pages, !remaining);
> +			update_global_total_pages(ttm_dev, backed_up_pages);
> +		}
>  	} else {

[ ... ]

> @@ -1478,7 +1522,8 @@ long xe_bo_shrink(struct ttm_operation_ctx *ctx, struct ttm_buffer_object *bo,
>  	if (needs_rpm && !xe_pm_runtime_get_if_active(xe))
>  		goto out_unref;
>  
> -	*scanned += tt->num_pages;
> +	old_remaining = ttm_tt_remaining_pages(tt);
> +	*scanned += old_remaining;
>  	lret = ttm_bo_shrink(ctx, bo, (struct ttm_bo_shrink_flags)
>  			     {.purge = false,
>  			      .writeback = flags.writeback,
> @@ -1486,10 +1531,7 @@ long xe_bo_shrink(struct ttm_operation_ctx *ctx, struct ttm_buffer_object *bo,
>  	if (needs_rpm)
>  		xe_pm_runtime_put(xe);
>  
> -	if (lret > 0) {
> -		xe_ttm_tt_account_subtract(xe, tt, tt->num_pages, 1);
> -		update_global_total_pages(bo->bdev, -(long)tt->num_pages);
> -	}
> +	xe_ttm_tt_account_shrunk(bo->bdev, tt, old_remaining);

[Severity: Medium]
When ttm_tt_restore() fails after making pages resident, doesn't leaving
tt->remaining_pages and the xe shrinker counters unupdated break shrinker
accounting and scanning?

When a fully backed-up tt (with tt->remaining_pages == 0 and 0 pages
accounted in xe->mem.shrinker) fails in ttm_tt_restore() after committing
or restoring pages, ttm_tt_restore() transitions it to TTM_TT_FLAG_PARTIAL,
but tt->remaining_pages stays 0 until ttm_pool_restore_abort() runs during
backup, and xe_ttm_tt_populate() skips xe_ttm_tt_account_add() because
err != 0:

1. Because the newly resident pages were never added via
   xe_ttm_tt_account_add(), xe_shrinker_count() does not see them and can
   return SHRINK_EMPTY if no other BOs are populated.
2. When xe_bo_shrink() (or xe_bo_shrink_purge()) runs on this BO, it reads
   old_remaining = ttm_tt_remaining_pages(tt) (which is still 0) and does
   *scanned += old_remaining before calling ttm_bo_shrink() ->
   ttm_tt_backup() -> ttm_pool_backup() -> ttm_pool_restore_abort(). In
   xe_shrinker_scan(), if nr_scanned remains 0 while freed > 0, it will
   return SHRINK_STOP instead of freed:

   drivers/gpu/drm/xe/xe_shrinker.c:xe_shrinker_scan() {
       ...
       return nr_scanned ? freed : SHRINK_STOP;
   }
3. If the BO is destroyed via xe_ttm_tt_destroy() after a failed restore
   without an intervening backup attempt, or if a backup attempt in
   xe_bo_shrink() aborts the restore and increases tt->remaining_pages from
   0 to N (say, when only part of those N pages are backed up),
   xe_ttm_tt_account_shrunk() and xe_ttm_tt_destroy() will subtract pages
   that were never added in xe_ttm_tt_populate(), underflowing the shrinker
   counters.

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261104201331.2462-1-thomas.hellstrom@linux.intel.com?part=3

  reply	other threads:[~2026-11-04 20:47 UTC|newest]

Thread overview: 9+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-11-04 20:13 [PATCH 0/4] drm/ttm, drm/xe: Support partially backed-up ttm_tts Thomas Hellström
2026-11-04 20:13 ` [PATCH 1/4] drm/ttm: Don't mark backed-up shmem folios accessed Thomas Hellström
2026-11-04 20:13 ` [PATCH 2/4] drm/xe/bo: Pass page and tt counts to the shrinker account helpers Thomas Hellström
2026-11-04 20:13 ` [PATCH 3/4] drm/ttm, drm/xe: Introduce TTM_TT_FLAG_PARTIAL for partially backed-up tts Thomas Hellström
2026-11-04 20:47   ` sashiko-bot [this message]
2026-11-04 20:13 ` [PATCH 4/4] drm/ttm, drm/xe: Gate reserve-dipping backup retries on may_writeback Thomas Hellström
2026-11-04 20:41   ` sashiko-bot
2026-11-04 20:23 ` ✓ CI.KUnit: success for drm/ttm, drm/xe: Support partially backed-up ttm_tts Patchwork
2026-11-04 20:57 ` ✓ Xe.CI.BAT: " Patchwork

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=sashiko-outbox-198028@kernel.org \
    --to=sashiko-bot@kernel.org \
    --cc=dri-devel@lists.freedesktop.org \
    --cc=intel-xe@lists.freedesktop.org \
    --cc=sashiko-reviews@lists.linux.dev \
    --cc=thomas.hellstrom@linux.intel.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox