Nvgpu_kmem_cache_create() leaks a /sys/kernel/slab/nvgpu-cache-* sysfs entry on every create/destroy cycle (SLUB merge-alias limitation)

Jetson platform: Jetson Orin NX 16GB (bug is in generic nvgpu allocator code, not board-specific —
see version-scope section below for confirmation across the whole product line)
L4T / JetPack (originally found on): R36.3.0 / JetPack 6.0 GA
Kernel: 5.15.136-tegra
Confirmed also present, unpatched, in: JetPack 6.2 (L4T R36.4.3), JetPack 7.0 (L4T R38.2.0, Thor/Blackwell
generation), JetPack 7.2 (L4T R39.2.0) — see “Version scope” section below.

Summary

nvgpu_kmem_cache_create() (nvgpu/drivers/gpu/nvgpu/os/linux/kmem.c) creates a dynamically-named
kmem_cache per call using kmem_cache_create(name, size, size, 0, NULL) — flags=0, no constructor. A
cache created this way is mergeable: SLUB silently folds it into an already-existing same-sized cache
and only creates a sysfs alias symlink (/sys/kernel/slab/nvgpu-cache-<ptr>-<size>-<id>) for the
caller’s unique name, rather than a real standalone kmem_cache.

kmem_cache_destroy() on a merged/aliased cache correctly drops the shared cache’s refcount — the
underlying memory really is reused correctly, no error or warning is printed — but it never removes the
sysfs alias symlink
it created. This is a long-standing Linux SLUB behavior gap (aliasing via
sysfs_slab_alias() has no corresponding “unalias” cleanup path in shutdown_cache()), not a bug in
nvgpu’s own allocator logic.

Symptom / real-world impact

Every nvgpu_kmem_cache_create() / nvgpu_kmem_cache_destroy() cycle permanently leaks one dangling
sysfs symlink + kobject (kernfs_node/dentry/inode bookkeeping — real, unreclaimable kernel memory).
In practice this is triggered by anything that briefly opens and closes a GPU/CUDA context — we hit this
via the NVIDIA-provided Jetson ffmpeg package (repo.download.nvidia.com/jetson/ffmpeg, which links
libnvbufsurftransform.so/libnvbufsurface.so): every single ffmpeg/ffprobe invocation —
including a bare ffmpeg -version with no encode/decode at all — creates two nvgpu_buddy_allocator
instances (a “gk20a_as_N” / “gk20a_as_N-sys” pair) at GPU-context setup, each backed by an
nvgpu_kmem_cache for struct nvgpu_buddy (128 bytes). Both create and destroy for these allocators
fire correctly on process exit — but the sysfs alias entries are never cleaned up regardless.

On an appliance that invokes ffmpeg/ffprobe frequently (e.g. our case: ~95 invocations/min for video
thumbnail/frame extraction), this accumulates roughly 4 leaked nvgpu-cache-* entries per invocation,
~134 MB of unreclaimable (“SUnreclaim”) memory per day, leading to OOM after roughly 50 days of uptime.
(We were already aware of, and had separately mitigated, this specific ffmpeg package via a userspace
workaround — replacing it with a stock, non-NVIDIA ffmpeg build — before tracing the leak down to this
kernel-side root cause. The kernel-side fix below eliminates the leak regardless of which userspace
component creates/destroys the GPU context, since the bug is generic to any nvgpu_kmem_cache_create()
caller, not specific to ffmpeg.)

Reproduction

# baseline
ls /sys/kernel/slab/ | grep -c '^nvgpu-cache-'

# trigger 100 GPU-context create/destroy cycles (no decode/encode needed)
for i in $(seq 1 100); do ffmpeg -version >/dev/null 2>&1; done

# recount
ls /sys/kernel/slab/ | grep -c '^nvgpu-cache-'

Observed on our system: baseline 51 → after 100 invocations 451 (delta 400, i.e. exactly 4 per
invocation). This is stable/reproducible; also reproduced at 200x and 1000x invocation counts, and with a
real hardware-accelerated video decode workload (not just process startup), all showing the same ~4
leaked entries per GPU-context create/destroy cycle.

Root cause detail

Every nvgpu-cache-* entry we inspected under /sys/kernel/slab/ was a symlink, not a directory,
all pointing at the same underlying SLUB-merged cache:

lrwxrwxrwx ... nvgpu-cache-0x00000000dd65b75c-128-1  -> :0000128
lrwxrwxrwx ... nvgpu-cache-0x00000000dd65b75c-128-10 -> :0000128

Confirmed by instrumenting nvgpu_buddy_allocator_init()/nvgpu_buddy_allocator_destroy() (and the
page/bitmap allocator equivalents) with temporary pr_info() markers: for every ffmpeg invocation, both
_init and _destroy fire correctly for all 4 allocator instances created — the create/destroy pairing
in the allocator layer is not broken. The leak is entirely explained by the SLUB alias-symlink-never-
removed behavior described above.

Verified this mechanism directly against mm/slub.c / mm/slab_common.c in the Jetson Linux 5.15.136-tegra
kernel source (kernel_src.tbz2):

// mm/slub.c
static int sysfs_slab_alias(struct kmem_cache *s, const char *name)
{
	if (slab_state == FULL) {
		/* If we have a leftover link then remove it. */
		sysfs_remove_link(&slab_kset->kobj, name);
		return sysfs_create_link(&slab_kset->kobj, &s->kobj, name);
	}
	...
}

// mm/slab_common.c
void kmem_cache_destroy(struct kmem_cache *s)
{
	...
	s->refcount--;
	if (s->refcount)
		goto out_unlock;        /* <-- always taken for a widely-shared merged cache */
	err = shutdown_cache(s);    /* <-- never reached in the merged case */
	...
}

sysfs_remove_link(&slab_kset->kobj, name) for our alias is only ever invoked the next time
sysfs_slab_alias() happens to be called again with the exact same name string — which never happens
here, since nvgpu_kmem_cache_create()'s name embeds a monotonically-incrementing atomic counter
(atomic_inc_return(&kmem_cache_id)), so no two calls ever produce the same name. The alias is therefore
permanent. This is a direct, verifiable code-level explanation, not an inference from behavior alone.

Version scope — confirmed present in JetPack 6.2, 7.0, and 7.2 too, unpatched

Pulled the public GPL sources for JetPack 6.2 (L4T R36.4.3), JetPack 7.0 (L4T R38.2.0 — the new Jetson
Thor/Blackwell generation, kernel 6.8, SBSA/Ubuntu 24.04) and JetPack 7.2 (L4T R39.2.0), and diffed both
nvgpu_kmem_cache_create() (os/linux/kmem.c) and nvgpu_page_allocator_destroy()
(common/mm/allocators/page_allocator.c, a second, related cleanup gap noticed along the way — see patch)
against the R36.3.0 version this report is based on.

Both are byte-for-byte unchanged (bug still present) in all three later releases, including the
brand-new Thor-generation source tree (nvgpu was split into its own kernel_nvgpu_src.tbz2 archive there,
but the function bodies are identical apart from a cosmetic cache-name format change unrelated to this
bug). This is not specific to R36.3.0, this AI Key unit, or the Orin NX — it affects the entire nvgpu
allocator subsystem across at least the R36.x and R38.x/R39.x L4T generations, i.e. essentially the whole
current Jetson product line.

Public source URLs used for the comparison (all verified reachable):

  • JetPack 6.2: https://developer.download.nvidia.com/embedded/L4T/r36_Release_v4.3/sources/public_sources.tbz2
  • JetPack 7.0: https://developer.download.nvidia.com/embedded/L4T/r38_Release_v2.0/sources/public_sources.tbz2
  • JetPack 7.2: https://developer.download.nvidia.com/embedded/L4T/r39_Release_v2.0/sources/public_sources.tbz2

Fix

Pass a non-NULL (no-op) constructor to kmem_cache_create(). SLUB’s find_mergeable() unconditionally
excludes any cache with a non-NULL ctor from merging — this is checked before any size/alignment/flags
criteria — so every nvgpu_kmem_cache_create() call now creates a genuine, standalone cache whose sysfs
entry is actually removed by kmem_cache_destroy().

--- a/drivers/gpu/nvgpu/os/linux/kmem.c
+++ b/drivers/gpu/nvgpu/os/linux/kmem.c
@@ -613,6 +613,21 @@
 }
 #endif /* CONFIG_NVGPU_TRACK_MEM_USAGE */
 
+/*
+ * A plain kmem_cache_create() with no ctor and no anti-merge flags is
+ * mergeable: SLUB silently folds it into an existing same-sized cache and
+ * only creates a sysfs alias symlink for our unique name. kmem_cache_destroy()
+ * on a merged cache never removes that alias symlink (a long-standing SLUB
+ * limitation), so every create/destroy cycle permanently leaks one
+ * /sys/kernel/slab/nvgpu-cache-* entry even though the underlying memory is
+ * reused correctly. A non-NULL ctor unconditionally disables merging, so the
+ * cache we create here is a real, standalone cache whose sysfs entry is
+ * actually removed on destroy.
+ */
+static void nvgpu_kmem_cache_ctor(void *obj)
+{
+}
+
 struct nvgpu_kmem_cache *nvgpu_kmem_cache_create(struct gk20a *g, size_t size)
 {
 	struct nvgpu_kmem_cache *cache =
@@ -627,7 +642,7 @@
 		 "nvgpu-cache-0x%p-%d-%d", g, (int)size,
 		 atomic_inc_return(&kmem_cache_id));
 	cache->cache = kmem_cache_create(cache->name,
-					 size, size, 0, NULL);
+					 size, size, 0, nvgpu_kmem_cache_ctor);
 	if (!cache->cache) {
 		nvgpu_kfree(g, cache);
 		return NULL;

(Full patch file with commit-message-style header attached below.)

Verification

Built against the R36.3.0 public OOT module sources (kernel_oot_modules_src.tbz2), deployed on-device
by replacing /usr/lib/modules/5.15.136-tegra/updates/nvgpu.ko and rebooting (this module cannot be
safely hot-reloaded live — unrelated pre-existing issue, its .fini/exit handling isn’t designed for
runtime unload/reload — a plain replace-on-disk + reboot is required).

Test Before fix After fix
100x ffmpeg -version delta = 400 delta = 0
200x ffmpeg -version — delta = 0
1000x ffmpeg -version — delta = 0
20x real HW video decode (h264_nvv4l2dec) — delta = 0

No functional regressions observed: all GPU-dependent services (CUDA-based inference workload, camera
capture, hardware video decode) continued operating normally after the fix, across multiple reboots and
several hours of production load.

Would appreciate this being picked up for inclusion in a future L4T release across all affected branches
(R36.x, R38.x, R39.x — see version scope above), since the underlying nvgpu_kmem_cache_create()/
_destroy() pattern is used broadly across the nvgpu allocator code (buddy/page/bitmap allocators all use
it) and confirmed unpatched on the current Thor-generation source line too.


Hi,

I think there are some points to clarify before doing further discussions

  1. None of the release version you mentioned is the latest one for that branch. For example, rel-36.5 has other fix than rel-36.4.3. Are you sure the issue is still present on rel-36.5?
    Also, same to rel-39.2.

  2. nvgpu is not in use on Thor anymore. It has been moved to openRM. Thus, the issue might only affect Orin.

Hi Wayne, thanks — both points are fair. I re-checked against the newest published sources on each
branch before answering.

1. Is it still present on the latest of each branch?

Yes, in the sources. I pulled the newest public_sources.tbz2 currently published for each branch:

  • .../L4T/r36_Release_v5.0/sources/public_sources.tbz2 (36.5.0)
  • .../L4T/r36_Release_v5.2/sources/public_sources.tbz2 (36.5.2 — newest r36 I can find; v5.1 and v6.0 are 404)
  • .../L4T/r39_Release_v2.1/sources/public_sources.tbz2 (newest r39 I can find; v3.0 is 404)

nvgpu/drivers/gpu/nvgpu/os/linux/kmem.c, r36.5.0 and r36.5.2 (line 629, and the two files are
byte-identical — md5 c5808e91cfa0951f32835642b6195feb):

	(void) snprintf(cache->name, sizeof(cache->name),
		 "nvgpu-cache-0x%p-%d-%d", g, (int)size,
		 atomic_inc_return(&kmem_cache_id));
	cache->cache = kmem_cache_create(cache->name,
					 size, size, 0, NULL);

r39.2.1 (kernel_nvgpu_src.tbz2, drivers/gpu/nvgpu/os/linux/kmem.c:628) — the name format lost the
%p, but the create call is the same:

	(void) snprintf(cache->name, sizeof(cache->name),
		 "nvgpu-cache-%d-%d", (int)size,
		 atomic_inc_return(&kmem_cache_id));
	cache->cache = kmem_cache_create(cache->name,
					 size, size, 0, NULL);

So on every one of these releases the cache is created with a unique name, flags = 0 and ctor = NULL,
which is exactly the mergeable case, and nvgpu_kmem_cache_destroy() is still a plain
kmem_cache_destroy().

The leak itself is kernel-side and release-independent — it does not depend on any nvgpu fix in 36.4.3 vs
36.5. In mm/slab_common.c (6.8.12, from the r39.2.1 kernel_src.tbz2):

void kmem_cache_destroy(struct kmem_cache *s)
{
	...
	s->refcount--;
	if (s->refcount)
		goto out_unlock;      /* merged cache: return, alias symlink stays */

and the alias was created in mm/slub.c:sysfs_slab_alias() via sysfs_create_link(), with nothing on the
destroy path removing it. Same in 5.15 on the r36 branch. So each create/destroy cycle permanently leaves
one /sys/kernel/slab/nvgpu-cache-* symlink + kobject behind; the object memory itself is fine.

To be precise about what I have and have not tested: the runtime reproduction and the fix validation
were done on real hardware running 36.3.0 (Orin NX 16GB). For 36.4.3 / 36.5.0 / 36.5.2 / 39.2.1 my claim is
source-level only — I have not re-run the runtime test on a 36.5.x image. Given the code above is
unchanged I would expect identical behaviour, but I did not want to overstate it. The check on 36.5.x is
one command on any Orin:

before=$(ls /sys/kernel/slab/ | grep -c '^nvgpu-cache-')
for i in $(seq 100); do <any CUDA process, e.g. ffmpeg -version from the nvidia build>; done
after=$(ls /sys/kernel/slab/ | grep -c '^nvgpu-cache-')
echo "$before -> $after"     # on 36.3.0: 51 -> 451, i.e. +4 per process, never reclaimed
ls -la /sys/kernel/slab/ | grep nvgpu-cache | head   # all symlinks -> :0000128

If it would help, I can flash a 36.5.2 image on a spare Orin NX and post the before/after numbers.

2. Thor / openRM

Understood, and I’ll take your word on the runtime side — I have no Thor measurement, my claim there was
purely “the same code is in the tarball”, which I should have stated more carefully.

For what it’s worth, the r39.2.1 public sources do still ship kernel_nvgpu_src.tbz2, and the OOT build
still has nvgpu in the default target (Makefile:52: modules: hwpm nvidia-oot nvgpu ...), so nvgpu.ko
still gets built on that branch. But the DT match table in os/linux/module.c:871 only lists
nvidia,gv11b and nvidia,ga10b, no T264/GB10B compatible, which is consistent with what you describe —
Thor’s GPU coming up on openRM (nvidia_unified_gpu_display_driver_source.tbz2) and nvgpu.ko simply never
binding.

Could you confirm that nvgpu.ko is not loaded at all on a stock Thor image? If so, please scope this
report to Orin (r36.x), plus any r39 configuration that still loads nvgpu.ko.

Suggested fix

Unchanged from the original post, with one addition now that the r39 branch is on 6.8:

  • All branches (5.15 and 6.8): pass a non-NULL no-op ctor to kmem_cache_create() — s->ctor is
    checked in slab_unmergeable() before any other criterion, so the cache is never merged and its sysfs
    entry is really removed on destroy. This is what I validated on 36.3.0 (delta went 400 → 0 over 100x,
    200x and 1000x runs, plus 20x real h264_nvv4l2dec decode cycles).
  • Kernels >= 6.5 (i.e. the r39 branch): SLAB_NO_MERGE (include/linux/slab.h:124 in 6.8.12) is the
    cleaner expression of the same intent: kmem_cache_create(name, size, size, SLAB_NO_MERGE, NULL).
  • Best fix, if you are open to it: don’t create a uniquely-named kmem_cache per allocator instance at
    all. There are only 4 call sites (buddy_allocator.c, page_allocator.c x2, bitmap_allocator.c) and
    the caches are per-size, so one shared cache per size (or plain nvgpu_kzalloc()) would remove the
    unbounded-sysfs-name growth entirely rather than working around SLUB merging.

Just want to confirm so above steps could observe the issue you reported?

Yes, those are exactly the steps — with one caveat that matters, otherwise you may see delta 0 and
conclude it doesn’t reproduce.

The ffmpeg binary has to be the NVIDIA one (ffmpeg_4.4.2-nvidia, from
repo.download.nvidia.com/jetson/ffmpeg), i.e. the one that links libnvbufsurftransform.so and hence
libcuda. On a stock JetPack image that is what apt install ffmpeg gives you, so the loop works as
written. But a distro/stock-Ubuntu ffmpeg creates no CUDA context at all and gives delta 0. Worth
checking first:

ldd $(command -v ffmpeg) | grep -c nvbufsurftransform   # must be >= 1

(On our own units the NVIDIA ffmpeg had been replaced with the stock Ubuntu build precisely to avoid this
leak, so for the measurement we ran the NVIDIA binary out of an extracted .deb with LD_LIBRARY_PATH
pointing at its libs.)

Nothing here is ffmpeg- or codec-specific — no decode/encode is needed, -version is enough, because the
GPU context is set up eagerly by libcuda at library load (visible under strace as /dev/nvmap open +
libnvrm_host1x.so + tegra-drm DRM ioctls). ffmpeg is just what our production workload happens to call
~95x/min. To be precise about what we actually measured: ffmpeg -version and real
h264_nvv4l2dec decode runs; any other CUDA-context-creating process should behave the same, but we
didn’t measure one.

What we see on Orin NX 16GB / JetPack 6.0 GA (L4T 36.3.0), stock nvgpu.ko:

51 -> 451     # 100 iterations, exactly +4 per invocation, deterministic
23 -> 23      # same test, with the ctor fix applied (also 200x and 1000x: delta 0)

Two extra things that make the diagnosis unambiguous, if you want them in the same run:

ls -la /sys/kernel/slab/ | grep nvgpu-cache | head    # every entry is a *symlink* -> :0000128
grep SUnreclaim /proc/meminfo                         # before/after: this is the memory cost

The -> :0000128 is the whole point: the caches are being merged into the shared 128-byte SLUB cache, so
what accumulates is the sysfs alias symlink + kobject, not the objects. The count never drops — it is not
reclaimed at any point, it only resets on reboot.

One cheap confirmation experiment, if it’s convenient on your side: boot the same image with
slab_nomerge on the kernel command line and re-run the loop. The growth should disappear, since each
cache then becomes a real standalone cache whose sysfs entry is removed by kmem_cache_destroy(). That
isolates the cause to the merge/alias path rather than anything in nvgpu’s own create/destroy pairing —
we instrumented nvgpu_buddy_allocator_{init,destroy} and confirmed every create does get a matching
destroy.

Also worth noting for repro: run it on an otherwise idle unit, and take the “after” count once the loop
has finished, so no live contexts are counted.

Hi,
On Jetson platforms, we use NVDEC in hardware decoding. Please install the package and give it try:

Accelerated Decode with ffmpeg — NVIDIA Jetson Linux Developer Guide

There are critical fixes in Jetpack 6.2.2 r36.5 and 6.2.3 r36.5.2. Would suggest use the later version.

Thanks Wayne — but I think the report is being read as a decode/ffmpeg usage question, and it isn’t one.
Let me try to restate it more clearly, since that’s on me.

We are not asking how to do hardware decode. NVDEC works fine on our unit and we use it: after finding
that the generic h264_v4l2m2m name doesn’t apply on Jetson, we switched to NVIDIA’s decoder
(ffmpeg -c:v h264_nvv4l2dec) and it decodes correctly through /dev/v4l2-nvdec
(NvMMLiteOpen : Block : BlockType = 261, full-rate). No issue there.

ffmpeg is only the trigger we happened to notice the leak with, not the subject of the report. No
decode is involved at all — plain ffmpeg -version reproduces it, because libcuda sets up the Tegra GPU
context eagerly at library load. Any process that creates a CUDA context walks the same path. The report
is about what nvgpu does in that path:

  • nvgpu_kmem_cache_create() creates a kmem_cache with a unique name, flags = 0, ctor = NULL
  • SLUB therefore merges it and only creates a sysfs alias symlink for the requested name
  • kmem_cache_destroy() on a merged cache decrements the refcount and returns — the alias symlink and its
    kobject are never removed
  • so every GPU-context create/destroy cycle permanently leaks one /sys/kernel/slab/nvgpu-cache-* entry
    (4 per process in our case), which is unreclaimable kernel memory. In our deployment that is ~134 MB of
    SUnreclaim per day.

The object memory is fine; nvgpu’s own create/destroy pairing is fine (we instrumented
nvgpu_buddy_allocator_{init,destroy} to confirm it). It is purely the sysfs alias that accumulates.

On upgrading to r36.5 / r36.5.2: I did check those before replying, which is why I don’t think it
helps here. I can’t speak to the other fixes in 6.2.2 / 6.2.3, but the specific code is unchanged:

  • nvgpu/drivers/gpu/nvgpu/os/linux/kmem.c is byte-identical between the r36.5.0 and r36.5.2 public
    sources (md5 c5808e91cfa0951f32835642b6195feb), line 629 in both:
    kmem_cache_create(cache->name, size, size, 0, NULL)
  • the same call is present in the r39.2.1 sources (kmem.c:628)
  • nvgpu_kmem_cache_destroy() is still a plain kmem_cache_destroy() in all of them
  • and on the kernel side, 6.8.12 (shipped in r39.2.1) still has no alias removal on the destroy path
    (mm/slab_common.c:475, mm/slub.c:sysfs_slab_alias()), so this isn’t a 5.15-only artefact either

So moving to r36.5.2 would not fix this particular leak. If I’ve missed a change in 6.2.2/6.2.3 that does
address it, please point me at it and I’ll gladly retest.

For completeness on our side: we are on JetPack 6.0 (R36.3.0) on a shipping product and can’t move BSP
casually — but that is secondary here, because the defect is present in the latest published sources too.
We also already have a userspace mitigation deployed fleet-wide, so we are not blocked. We’re reporting it
upstream so other Jetson users don’t have to find it the hard way.

What we’re actually asking:

  1. Can you confirm the behaviour is acknowledged in the current code (r36.5.2 / r39.2.1)?
  2. Is a fix planned for a future r36.x? The one-line options are: pass a non-NULL no-op ctor (works on
    5.15 and 6.8, s->ctor is checked in slab_unmergeable()), or SLAB_NO_MERGE on kernels >= 6.5, or —
    cleanest — stop creating a uniquely-named kmem_cache per allocator instance at all (only 4 call sites:
    buddy_allocator.c, page_allocator.c x2, bitmap_allocator.c).
  3. If it will not be fixed, that’s a fine answer too — just say so and we’ll keep carrying the patch
    locally.

We validated the ctor fix on R36.3.0 hardware: nvgpu-cache-* delta went from +400 to 0 over 100x, 200x
and 1000x invocation runs plus 20x real h264_nvv4l2dec decode cycles, with no regressions observed. Happy
to test any candidate patch you’d like on our hardware.

Hi,
We would suggest upgrade to Jetpack 6.2.2 r36.5 or 6.2.3 r36.5.2. If you have to stay on r36.3, please apply the fixes:

Making sure you're not a bot!
[NvGPU] slab-out-of-bounds in nvgpu_gr_config_init

Making sure you're not a bot!
[gpu/host1x-fence] Significant kernel memory leak when using Argus camera

Making sure you're not a bot!

Jetson Linux Release 36.4.4 | NVIDIA Developer
Additional Files
cuda_driver_36.4.4.tbz2

Thanks Wayne — the host1x-fence link is genuinely useful, I appreciate it.

On [gpu/host1x-fence] (t/325399/4): that is a separate leak we had also hit and independently
patched, and your fix is identical to ours — dma_fence_put(fence); added before the success return 0;
in dev_file_ioctl_fence_extract(). Good to have it confirmed as the official fix; we’ll align on it.
(For the record, we see that path exercised on a unit with no CSI camera at all — it is reached via
CUDA/GPU work submission, our host1x_pollfd fds come from a llama.cpp inference server — so the fix
matters beyond the Argus use case.)

On [NvGPU] slab-out-of-bounds (t/329611/17): that one doesn’t apply to this report. It modifies
drivers/gpu/nvgpu/common/gr/gr_config.c (the gpc_tpc_physical_id_map allocation and a max_gpc_count
bounds check). The leak I reported is in drivers/gpu/nvgpu/os/linux/kmem.c, which that patch does not
touch. Same for cuda_driver_36.4.4.tbz2 — the behaviour here is kernel-side, in how the cache is created
and destroyed, so a userspace CUDA driver update can’t change it.

On upgrading to r36.5 / r36.5.2: I did diff those before my previous reply — os/linux/kmem.c is
byte-identical between the r36.5.0 and r36.5.2 public sources (md5 c5808e91cfa0951f32835642b6195feb),
and line 629 in both is still:

	cache->cache = kmem_cache_create(cache->name, size, size, 0, NULL);

flags = 0 and ctor = NULL is precisely the mergeable case, so SLUB still folds the cache into
:0000128 and still leaves the /sys/kernel/slab/nvgpu-cache-* alias symlink behind on destroy. The same
call is in r39.2.1 (kmem.c:628). So upgrading does fix the two issues you linked, but not this one — it
is still present in the newest sources published on either branch.

To keep this simple, my question is just:

  1. Is there a patch for this specific issue (the nvgpu_kmem_cache_create() / SLUB alias leak) that I
    have not found? If so I’ll gladly test it and close the thread.
  2. If there isn’t: can it be filed for a future r36.x? The minimal change is passing a non-NULL no-op ctor
    to kmem_cache_create() (s->ctor is checked in slab_unmergeable(), so the cache is never merged and
    its sysfs entry is genuinely removed); SLAB_NO_MERGE is the equivalent on kernels >= 6.5.
  3. If it’s decided as won’t-fix, that’s an acceptable answer — we’ll carry the patch locally, and the
    thread will at least document it for others.

We validated the ctor change on R36.3.0: nvgpu-cache-* growth went from +4 per CUDA process (+400 over
100 invocations) to exactly 0, across 100x / 200x / 1000x runs and 20x real h264_nvv4l2dec decode
cycles, with no regressions observed. Happy to test a candidate patch on our hardware if you’d prefer your
own version.

Hi,
All known leaks on Orin are fixed in Jetpack 6.2.3 r36.5.2. If you still observe it on the version, please share the test application and one-by-one steps to replicate the leak on developer kit. We will set up developer kit and check.

Reproduced on a stock Jetson Orin NX developer kit with JetPack 6.2.3 (L4T r36.5.2)

Thanks for the pointer to 6.2.3 — we got hold of an Orin NX EVB, flashed stock, and the leak is still
present, at exactly the same rate as on r36.3.0. Full step-by-step below so you can run the same thing on
your devkit.

Environment (untouched stock image, nothing of ours installed)

$ uname -r
5.15.199-tegra

$ head -1 /etc/nv_tegra_release
# R36 (release), REVISION: 5.2, GCID: 46426093, BOARD: generic, EABI: aarch64, DATE: Thu Jul 16 18:56:22 UTC 2026

$ cat /proc/device-tree/model
NVIDIA Jetson Orin NX Engineering Reference Developer Kit

$ grep TNSPEC /etc/nv_boot_control.conf
TNSPEC 3767-301-0000-G.1-1-1-jetson-orin-nano-devkit-

$ dpkg -l | grep nvidia-l4t-kernel-oot-modules
ii  nvidia-l4t-kernel-oot-modules  5.15.199-tegra-36.5.2-20260716114719

$ modinfo -n nvgpu
/lib/modules/5.15.199-tegra/updates/nvgpu.ko          # stock module, not rebuilt

$ grep -c slab_nomerge /proc/cmdline
0                                                     # slab merging enabled (default)

Reproduction A — 40 lines of C, no ffmpeg, no CUDA toolkit

This is the one I’d suggest running: it has no dependencies beyond gcc, does not touch the system, and
shows that the trigger is simply creating and destroying a GPU context. libcuda.so.1 is dlopen()ed, so
no -lcuda and no stub library are needed.

nvgpu_cache_leak_repro.c:

/* gcc -O0 -o nvgpu_cache_leak_repro nvgpu_cache_leak_repro.c -ldl */
#include <dlfcn.h>
#include <stdio.h>

typedef int (*cuInit_t)(unsigned int);
typedef int (*cuDeviceGet_t)(int *, int);
typedef int (*cuDevicePrimaryCtxRetain_t)(void **, int);
typedef int (*cuDevicePrimaryCtxRelease_t)(int);

int main(void)
{
	void *h = dlopen("libcuda.so.1", RTLD_NOW);
	if (!h) { fprintf(stderr, "dlopen libcuda.so.1: %s\n", dlerror()); return 1; }

	cuInit_t cuInit = dlsym(h, "cuInit");
	cuDeviceGet_t cuDeviceGet = dlsym(h, "cuDeviceGet");
	cuDevicePrimaryCtxRetain_t retain = dlsym(h, "cuDevicePrimaryCtxRetain");
	cuDevicePrimaryCtxRelease_t release = dlsym(h, "cuDevicePrimaryCtxRelease");
	if (!cuInit || !cuDeviceGet || !retain || !release) { fprintf(stderr, "dlsym failed\n"); return 1; }

	int rc, dev = 0;
	void *ctx = NULL;

	if ((rc = cuInit(0)) != 0)            { fprintf(stderr, "cuInit: %d\n", rc); return 1; }
	if ((rc = cuDeviceGet(&dev, 0)) != 0) { fprintf(stderr, "cuDeviceGet: %d\n", rc); return 1; }
	if ((rc = retain(&ctx, dev)) != 0)    { fprintf(stderr, "cuDevicePrimaryCtxRetain: %d\n", rc); return 1; }
	if ((rc = release(dev)) != 0)         { fprintf(stderr, "cuDevicePrimaryCtxRelease: %d\n", rc); return 1; }

	return 0;
}

Steps:

gcc -O0 -o nvgpu_cache_leak_repro nvgpu_cache_leak_repro.c -ldl
./nvgpu_cache_leak_repro; echo "exit=$?"            # prints nothing, exit=0

B=$(ls /sys/kernel/slab/ | grep -c '^nvgpu-cache-')
for i in $(seq 1 100); do ./nvgpu_cache_leak_repro >/dev/null 2>&1; done
A=$(ls /sys/kernel/slab/ | grep -c '^nvgpu-cache-')
echo "$B -> $A  (delta $((A-B)))"

Result on the EVB above:

27 -> 227  (delta 200)          # +2 per process

Reproduction B — NVIDIA’s own ffmpeg build, -version only (no decode)

This is how we originally hit it in production. Note -version alone is enough: no decode, no encode, no
camera. The GPU context is created by libcuda at library load.

The r36.5 ffmpeg pool is empty, so we took the package from NVIDIA’s r36.4 pool and extracted it to
/tmp rather than installing it
, to leave the system ffmpeg alone:

cd /tmp
wget https://repo.download.nvidia.com/jetson/ffmpeg/pool/main/f/ffmpeg/ffmpeg_4.4.2-nvidia_arm64.deb
mkdir -p ffnv && dpkg-deb --fsys-tarfile ffmpeg_4.4.2-nvidia_arm64.deb | tar -xC ffnv

# two Ubuntu runtime deps this binary needs, also just downloaded and unpacked, not installed
apt-get download libchromaprint1 libsrt1.4-gnutls
dpkg-deb --fsys-tarfile libchromaprint1_*.deb    | tar -xC ffnv
dpkg-deb --fsys-tarfile libsrt1.4-gnutls_*.deb   | tar -xC ffnv
ln -sf libsrt-gnutls.so.1.4.4 ffnv/usr/lib/aarch64-linux-gnu/libsrt.so.1.4

export LD_LIBRARY_PATH=/tmp/ffnv/usr/lib/aarch64-linux-gnu

# confirm this really is the NVIDIA build (the stock Ubuntu one will NOT reproduce)
ldd /tmp/ffnv/usr/bin/ffmpeg | grep nvbufsurftransform
#   libnvbufsurftransform.so.1.0.0 => /usr/lib/aarch64-linux-gnu/nvidia/libnvbufsurftransform.so.1.0.0

Then:

B=$(ls /sys/kernel/slab/ | grep -c '^nvgpu-cache-')
for i in $(seq 1 100); do /tmp/ffnv/usr/bin/ffmpeg -version >/dev/null 2>&1; done
A=$(ls /sys/kernel/slab/ | grep -c '^nvgpu-cache-')
echo "$B -> $A  (delta $((A-B)))"

Results on the EVB above:

100 iterations:  235 ->  635   (delta 400)     # +4 per invocation
200 iterations:  635 -> 1435   (delta 800)     # +4 per invocation, perfectly linear

The count went from 25 at boot to 1435 during a few minutes of testing, and only resets on reboot.


Evidence that this is the SLUB merge/alias path

Every leaked entry is a symlink into a shared merged cache — and it is not only the 128-byte cache,
64-byte caches merge and leak identically:

$ ls -la /sys/kernel/slab/ | grep nvgpu-cache- | tail -2
lrwxrwxrwx 1 root root 0 ... nvgpu-cache-0x00000000bf41cd11-128-999 -> :0000128
lrwxrwxrwx 1 root root 0 ... nvgpu-cache-0x00000000bf41cd11-64-7    -> :0000064

They are never reclaimed:

$ ls /sys/kernel/slab/ | grep -c '^nvgpu-cache-'            # 635
$ sync; echo 3 | sudo tee /proc/sys/vm/drop_caches; sleep 20
$ ls /sys/kernel/slab/ | grep -c '^nvgpu-cache-'            # 635

A cross-check that isolates the mechanism: boot the same devkit with slab_nomerge and re-run. The growth
disappears, because each cache then becomes a real standalone cache whose sysfs entry is removed on
destroy.

(On an otherwise idle box the per-run SUnreclaim delta is small and noisy — the deterministic signal is
the alias count. The memory cost becomes visible under sustained load: in our deployment, at ~95
invocations/min, this accumulates ~134 MB of SUnreclaim per day.)


Root cause

nvgpu/drivers/gpu/nvgpu/os/linux/kmem.c:629 — unchanged in r36.5.2:

	cache->cache = kmem_cache_create(cache->name, size, size, 0, NULL);

With flags = 0 and ctor = NULL the cache is mergeable. SLUB folds it into the existing same-size cache
and only creates a sysfs alias symlink for the unique name
(mm/slub.c:sysfs_slab_alias() → sysfs_create_link()). kmem_cache_destroy() on a merged cache
decrements the refcount and returns (mm/slab_common.c), so the alias symlink and its kobject are never
removed. The objects themselves are freed correctly; what accumulates is sysfs bookkeeping.

nvgpu’s own pairing is correct — we instrumented nvgpu_buddy_allocator_{init,destroy} and every create
has a matching destroy.

For completeness, these files are byte-identical across r36.3.0, r36.5.0 and r36.5.2 public sources,
which is consistent with the identical measurements on 36.3.0 and 36.5.2:

file (nvgpu/drivers/gpu/nvgpu/) md5 (same in 36.3.0 / 36.5.0 / 36.5.2)
os/linux/kmem.c c5808e91cfa0951f32835642b6195feb
common/mm/allocators/buddy_allocator.c b3c6102a236de4fb9adf596b46cdafbe
common/mm/allocators/bitmap_allocator.c cbabac80d5bcad1a5cc878d4fa804422
common/mm/allocators/page_allocator.c 197d2207c9cd35c2ec14733ef8d4f104

Fix

Any of these works; the first is what we validated:

  1. Pass a non-NULL no-op ctor to kmem_cache_create(). s->ctor is checked in slab_unmergeable()
    before any other criterion, so the cache is never merged and its sysfs entry is genuinely removed on
    destroy. Works on 5.15 and 6.8.
  2. SLAB_NO_MERGE (include/linux/slab.h, kernels >= 6.5) — same intent, cleaner, available on the
    r39 branch.
  3. Best: stop creating a uniquely-named kmem_cache per allocator instance at all. There are only four
    call sites (buddy_allocator.c, page_allocator.c x2, bitmap_allocator.c) and the caches are
    per-size, so one shared cache per size would remove the unbounded sysfs-name growth entirely.

Validation of option 1 on r36.3.0 hardware: nvgpu-cache-* delta went from +400 to 0 over 100x, 200x
and 1000x invocation runs, plus 20x real h264_nvv4l2dec decode cycles, with no regressions observed over
several days.

Happy to run any candidate patch you have on either unit (r36.3.0 or the 6.2.3 EVB) and report back.

Hi,

Thanks for reporting this.

Confirmed that we are seeing the same behavior (reproduce A) on Orin NX + r36.5.2.
We are testing your recommended fix internally and discussing it with our internal team.

Will keep you updated.
Thanks.

Hi,

Thanks for reporting this issue.
Please find below for our fix:
r36.5.2 : a8144e4.diff (10.1 KB)
r39.2.1 : 027b11f.diff (5.8 KB)

Thanks.

Hi all,

I have tested and reproduced this exact behavior on an Orin Nano 8GB running JetPack 6.2 (L4T R36.4.3).

As described, during sequential GPU open/close cycles (e.g. repeated FFmpeg / repro invocations), /sys/kernel/slab/nvgpu-cache-* symlinks continuously accumulate and link to :0000128, which leads to significant sysfs/dentry leakage over prolonged operation.

I tested appending slab_nomerge to the kernel bootargs in /boot/extlinux/extlinux.conf, and observed that the leak completely disappeared (delta = 0 across tests), as SLUB merging is bypassed globally and kmem_cache_destroy() properly cleans up the entries.

Could the NVIDIA team clarify:

  1. Are there any hidden side effects, stability concerns, or performance penalties with keeping slab_nomerge active in production (e.g. on multimedia pipelines, GStreamer, V4L2/MIPI camera capture, or general memory overhead)?
  2. Or is it strongly recommended to rebuild nvgpu.ko with the dummy constructor patch instead?
-					 size, size, 0, NULL);
+					 size, size, 0, nvgpu_kmem_cache_ctor);

Thanks!

Hi,

Could you try our fix mentioned above?

Thanks.