diff --git a/iconv/Makefile b/iconv/Makefile
index 69555cbe579..02980457b38 100644
--- a/iconv/Makefile
+++ b/iconv/Makefile
@@ -78,8 +78,9 @@ cpp-srcs-left := $(iconv_prog-modules) $(iconvconfig-modules)
 lib := iconvprogs
 include $(patsubst %,$(..)libof-iterator.mk,$(cpp-srcs-left))
 
+tests-container += tst-iconvconfig-cache
+
 ifeq ($(run-built-tests),yes)
-xtests-special += $(objpfx)test-iconvconfig.out
 tests-special += \
 	$(objpfx)tst-iconv_prog-buffer-large.out \
 	$(objpfx)tst-iconv_prog-buffer-tiny.out \
@@ -129,16 +130,6 @@ $(inst_bindir)/iconv: $(objpfx)iconv_prog $(+force)
 $(objpfx)iconv_prog: $(iconv_prog-modules:%=$(objpfx)%.o)
 $(objpfx)iconvconfig: $(iconvconfig-modules:%=$(objpfx)%.o)
 
-$(objpfx)test-iconvconfig.out: $(objpfx)iconvconfig
-	(set -e; \
-	 tmp=$(objpfx)gconv-modules.cache.$$$$; \
-	 rm -f $$tmp; \
-	 $(run-program-prefix) $(objpfx)iconvconfig \
-		--output=$$tmp --nostdlib $(inst_gconvdir); \
-	 cmp $$tmp $(inst_gconvdir)/gconv-modules.cache; \
-	 rm -f $$tmp) > $@; \
-	$(evaluate-test)
-
 $(objpfx)tst-iconv_prog.out: tst-iconv_prog.sh $(objpfx)iconv_prog \
   $(gen-locales)
 	$(BASH) $< $(common-objdir) '$(test-wrapper-env)' \
diff --git a/iconv/tst-iconvconfig-cache.c b/iconv/tst-iconvconfig-cache.c
new file mode 100644
index 00000000000..e0d87e6654e
--- /dev/null
+++ b/iconv/tst-iconvconfig-cache.c
@@ -0,0 +1,192 @@
+/* Test that iconvconfig produces a valid, reproducible module cache.
+   Copyright (C) 2026 Free Software Foundation, Inc.
+   This file is part of the GNU C Library.
+
+   The GNU C Library is free software; you can redistribute it and/or
+   modify it under the terms of the GNU Lesser General Public
+   License as published by the Free Software Foundation; either
+   version 2.1 of the License, or (at your option) any later version.
+
+   The GNU C Library is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   Lesser General Public License for more details.
+
+   You should have received a copy of the GNU Lesser General Public
+   License along with the GNU C Library; if not, see
+   <https://www.gnu.org/licenses/>.  */
+
+#include <fcntl.h>
+#include <iconv.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <support/capture_subprocess.h>
+#include <support/check.h>
+#include <support/support.h>
+#include <support/temp_file.h>
+#include <support/xunistd.h>
+
+#define GCONV_CACHE_MAGIC 0x20010324
+
+static char *iconvconfig;
+static char *gconvdir;
+
+static void
+run_iconvconfig (const char *out)
+{
+  char *arg_output = xasprintf ("--output=%s", out);
+  char *const argv[] =
+    {
+      iconvconfig, arg_output, (char *) "--nostdlib", gconvdir, NULL
+    };
+  struct support_capture_subprocess proc
+    = support_capture_subprogram (iconvconfig, argv, NULL);
+  support_capture_subprocess_check (&proc, "iconvconfig", 0, sc_allow_none);
+  support_capture_subprocess_free (&proc);
+  free (arg_output);
+}
+
+struct cache_file_t
+{
+  char *data;
+  size_t length;
+};
+
+static struct cache_file_t
+read_cache_file (const char *path)
+{
+  struct stat64 st;
+  xstat64 (path, &st);
+  char *buffer = xmalloc (st.st_size);
+  int fd = xopen (path, O_RDONLY, 0);
+  xread (fd, buffer, st.st_size);
+  xclose (fd);
+  return (struct cache_file_t) { buffer, st.st_size };
+}
+
+static void
+free_cache_file (struct cache_file_t *cache_file)
+{
+  free (cache_file->data);
+}
+
+static void
+write_cache_file (const char *path, const char *data, size_t length)
+{
+  int fd = xopen (path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
+  xwrite (fd, data, length);
+  xclose (fd);
+}
+
+static void
+move_cache_file (const char *from, const char *to)
+{
+  if (access (from, F_OK) != 0)
+    FAIL_EXIT1 ("access(%s): %m", from);
+  if (rename (from, to) != 0)
+    FAIL_EXIT1 ("rename(%s, %s): %m", from, to);
+}
+
+static void
+check_valid_cache (const struct cache_file_t *cache_file)
+{
+  TEST_VERIFY_EXIT (cache_file->length >= sizeof (uint32_t));
+  uint32_t magic;
+  memcpy (&magic, cache_file->data, sizeof (magic));
+  TEST_COMPARE (magic, GCONV_CACHE_MAGIC);
+}
+
+/* Install the generated cache CAHCE_FILE at the canonical gconv cache path
+   and confirm that the runtime loads it and resolves a module-backed
+   conversion.  The gconv-modules text configuration is moved aside, so the
+   conversion can only succeed if it is satisfied from the cache.  A corrupt
+   cache would make the loader fall back to the missing text configuration
+   and the conversion would fail.  */
+static void
+check_loadable_cache (const struct cache_file_t *cache_file)
+{
+  char *cache = xasprintf ("%s/gconv-modules.cache", gconvdir);
+  char *conf = xasprintf ("%s/gconv-modules", gconvdir);
+  char *confd = xasprintf ("%s/gconv-modules.d", gconvdir);
+  char *conf_bak = xasprintf ("%s.disabled", conf);
+  char *confd_bak = xasprintf ("%s.disabled", confd);
+
+  write_cache_file (cache, cache_file->data, cache_file->length);
+
+  /* The runtime bypasses the cache when GCONV_PATH is set.  */
+  unsetenv ("GCONV_PATH");
+
+  move_cache_file (conf, conf_bak);
+  move_cache_file (confd, confd_bak);
+
+  /* Force a module resolution, which here can only come from the cache.  */
+  iconv_t cd = iconv_open ("EUC-JP", "UTF-8");
+  TEST_VERIFY (cd != (iconv_t) -1);
+
+  {
+    /* ASCII is a subset of EUC-JP, so 'A' converts to itself.  */
+    char in[] = "A";
+    char out[8];
+    char *inp = in;
+    char *outp = out;
+    size_t inleft = 1;
+    size_t outleft = sizeof (out);
+    TEST_VERIFY (iconv (cd, &inp, &inleft, &outp, &outleft) != (size_t) -1);
+    TEST_COMPARE (outp - out, 1);
+    TEST_COMPARE (out[0], 'A');
+    TEST_COMPARE (iconv_close (cd), 0);
+  }
+
+  /* Restore the configuration for any later use of the container.  */
+  TEST_COMPARE (rename (conf_bak, conf), 0);
+  TEST_COMPARE (rename (confd_bak, confd), 0);
+
+  free (cache);
+  free (conf);
+  free (confd);
+  free (conf_bak);
+  free (confd_bak);
+}
+
+static int
+do_test (void)
+{
+  iconvconfig = xasprintf ("%s/iconvconfig", support_sbindir_prefix);
+  gconvdir = xasprintf ("%s/gconv", support_libdir_prefix);
+
+  char *cache1;
+  xclose (create_temp_file ("iconvconfig-cache1-", &cache1));
+  char *cache2;
+  xclose (create_temp_file ("iconvconfig-cache2-", &cache2));
+
+  run_iconvconfig (cache1);
+  run_iconvconfig (cache2);
+
+  struct cache_file_t cache_file1 = read_cache_file (cache1);
+  struct cache_file_t cache_file2 = read_cache_file (cache2);
+
+  /* Check if the cache generation is deterministic.  */
+  TEST_VERIFY (cache_file1.length > 0);
+  TEST_COMPARE (cache_file1.length, cache_file2.length);
+  TEST_COMPARE_BLOB (cache_file1.data, cache_file1.length,
+		     cache_file1.data, cache_file1.length);
+
+  /* And well-formed.  */
+  check_valid_cache (&cache_file1);
+
+  /* And the runtime must be able to load it and convert through it.  */
+  check_loadable_cache (&cache_file2);
+
+  free_cache_file (&cache_file1);
+  free_cache_file (&cache_file2);
+  free (iconvconfig);
+  free (gconvdir);
+
+  return 0;
+}
+
+#include <support/test-driver.c>
diff --git a/iconv/tst-iconvconfig-cache.root/postclean.req b/iconv/tst-iconvconfig-cache.root/postclean.req
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/iconv/tst-iconvconfig-cache.root/preclean.req b/iconv/tst-iconvconfig-cache.root/preclean.req
new file mode 100644
index 00000000000..e69de29bb2d
