diff --git a/gdb/doc/gdb.texinfo b/gdb/doc/gdb.texinfo
index ceb69669ea60..cacbdede50db 100644
--- a/gdb/doc/gdb.texinfo
+++ b/gdb/doc/gdb.texinfo
@@ -51095,6 +51095,12 @@ gdb-index} (@pxref{Index Files}).  The index section is
 DWARF-specific; some knowledge of DWARF is assumed in this
 description.
 
+Note that the @code{.gdb_index} format does not support describing
+skeletonless type units, that is, type units in @file{.dwo} files that
+don't have a corresponding skeleton in the main file.  @value{GDBN}
+will refuse to generate a @code{.gdb_index} index for such executables.
+Consider using the @code{.debug_names} format instead.
+
 The mapped index file format is designed to be directly
 @code{mmap}able on any architecture.  In most cases, a datum is
 represented using a little-endian 32-bit integer value, called an
diff --git a/gdb/dwarf2/index-write.c b/gdb/dwarf2/index-write.c
index 3a70787355cc..0c8474b3e4ed 100644
--- a/gdb/dwarf2/index-write.c
+++ b/gdb/dwarf2/index-write.c
@@ -633,6 +633,18 @@ write_address_map (const addrmap *addrmap, data_buf &addr_vec,
 		       addrmap_index_data.previous_cu_index);
 }
 
+/* Return true if TU is a foreign type unit, that is a type unit defined in a
+   .dwo file without a corresponding skeleton in the main file.  */
+
+static bool
+is_foreign_tu (const signatured_type *tu)
+{
+  /* If a type unit has a skeleton, then `tu->section ()` will be the section
+     of the skeleton, in the main file.  If it's foreign, it will point to the
+     section in the .dwo file.  */
+  return endswith (tu->section ()->get_name (), ".dwo");
+}
+
 /* DWARF-5 .debug_names builder.  */
 class debug_names
 {
@@ -1375,6 +1387,16 @@ write_gdbindex (dwarf2_per_bfd *per_bfd, cooked_index *table,
   cu_index_htab.reserve (per_bfd->all_units.size ());
 
   unit_lists units = get_unit_lists (*per_bfd);
+
+  /* .gdb_index doesn't have a way to describe skeletonless type units, the way
+     that DWARF 5's .debug_names does with "foreign type units".  If the
+     executable has such skeletonless type units, refuse to produce an index,
+     instead of producing a bogus one.  */
+  for (const signatured_type *tu : units.type)
+    if (is_foreign_tu (tu))
+      error (_("Found foreign (skeletonless) type unit, unable to produce "
+	       ".gdb_index.  Consider using .debug_names instead."));
+
   int counter = 0;
 
   /* Write comp units.  */
diff --git a/gdb/testsuite/gdb.dwarf2/gdb-index-skeletonless-tu.c b/gdb/testsuite/gdb.dwarf2/gdb-index-skeletonless-tu.c
new file mode 100644
index 000000000000..c86a4322de82
--- /dev/null
+++ b/gdb/testsuite/gdb.dwarf2/gdb-index-skeletonless-tu.c
@@ -0,0 +1,23 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+   Copyright 2026 Free Software Foundation, Inc.
+
+   This program is free software; you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 3 of the License, or
+   (at your option) any later version.
+
+   This program 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 General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see
+   <http://www.gnu.org/licenses/>.  */
+
+int
+main (int argc, char **argv)
+{
+  return 0;
+}
diff --git a/gdb/testsuite/gdb.dwarf2/gdb-index-skeletonless-tu.exp b/gdb/testsuite/gdb.dwarf2/gdb-index-skeletonless-tu.exp
new file mode 100644
index 000000000000..31a45e224cad
--- /dev/null
+++ b/gdb/testsuite/gdb.dwarf2/gdb-index-skeletonless-tu.exp
@@ -0,0 +1,103 @@
+# Copyright 2026 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+# Test that GDB refuses to produce a .gdb_index when skeletonless type units
+# are present.  A skeletonless type unit is a type unit in a .dwo file that
+# doesn't have a corresponding skeleton in the main file.  The .gdb_index
+# format cannot represent these, so GDB must refuse to produce one rather than
+# produce a bogus index.
+
+load_lib dwarf.exp
+
+# This test can only be run on targets which support DWARF-2 and use gas.
+require dwarf2_support
+
+# Can't produce an index with readnow.
+require !readnow
+
+standard_testfile .c -dw.S
+
+set asm_file [standard_output_file $srcfile2]
+
+Dwarf::assemble $asm_file {
+    # In the main file: a skeleton CU pointing to the .dwo file.
+    cu {
+	version 5
+	dwo_id 0xF00D
+    } {
+	compile_unit {
+	    DW_AT_dwo_name ${::gdb_test_file_name}-dw.dwo DW_FORM_strp
+	} {}
+    }
+
+    # In the .dwo file: a type unit (skeletonless, no corresponding skeleton
+    # in main file).
+    tu {
+	fission 1
+	version 5
+    } 0xCAFE "the_type" {
+	type_unit {} {
+	    the_type: base_type {
+		DW_AT_byte_size 4 DW_FORM_sdata
+		DW_AT_encoding  @DW_ATE_signed
+		DW_AT_name      int
+	    }
+	}
+    }
+
+    # In the .dwo file: the split compile unit.
+    cu {
+	fission 1
+	version 5
+	dwo_id 0xF00D
+    } {
+	compile_unit {} {
+	    DW_TAG_variable {
+		DW_AT_name global_var
+		DW_AT_type 0xCAFE DW_FORM_ref_sig8
+		DW_AT_location {
+		    DW_OP_const1u 12
+		    DW_OP_stack_value
+		} SPECIAL_expr
+	    }
+	}
+    }
+}
+
+set obj [standard_output_file "${testfile}-dw.o"]
+if {[build_executable_and_dwo_files "$testfile.exp" "${binfile}" {} \
+	 [list $asm_file {nodebug split-dwo} $obj] \
+	 [list $srcfile  {nodebug}]]} {
+    return
+}
+
+clean_restart ${testfile}
+
+# Sanity check, verify that the executable works correctly.
+gdb_test "print global_var" " = 12"
+
+# Verify that saving a .gdb_index index fails.
+set output_dir [standard_output_file ""]
+gdb_test "save gdb-index ${output_dir}" \
+    "Found foreign \\(skeletonless\\) type unit, unable to produce \\.gdb_index\\.  Consider using \\.debug_names instead\\." \
+    "save gdb-index fails"
+
+# Verify that saving a .debug_names index works.
+gdb_test_no_output "save gdb-index -dwarf-5 ${output_dir}" \
+    "save gdb-index -dwarf-5 succeeds"
+
+# Verify that the .debug_names file was created.
+set debug_names_file "${output_dir}/${testfile}.debug_names"
+gdb_assert {[file exists $debug_names_file]} ".debug_names file exists"
diff --git a/gdbsupport/common-utils.h b/gdbsupport/common-utils.h
index fb4b8ea28ced..de83a715ac45 100644
--- a/gdbsupport/common-utils.h
+++ b/gdbsupport/common-utils.h
@@ -115,6 +115,18 @@ startswith (const char *str, const std::string_view &prefix)
   return strncmp (str, prefix.data (), prefix.length ()) == 0;
 }
 
+/* Return true if the end of STR matches PATTERN, false otherwise.
+
+   This can be replaced with std::string_view::ends_with when we require
+   C++20.  */
+
+static inline bool
+endswith (std::string_view str, std::string_view pattern)
+{
+  return (str.length () >= pattern.length ()
+	  && str.substr (str.length () - pattern.length ()) == pattern);
+}
+
 /* Return true if the strings are equal.  */
 
 static inline bool
