diff --git a/gdb/frame.c b/gdb/frame.c
index 782ca33abd8..a84cca5b81c 100644
--- a/gdb/frame.c
+++ b/gdb/frame.c
@@ -2232,6 +2232,63 @@ frame_register_unwind_location (const frame_info_ptr &initial_this_frame,
     }
 }
 
+/* When checking if a frame is an entry frame, there are two different
+   entry frames to consider.  This enum is used to choose between them.  */
+
+enum class entry_address_type
+{
+  /* The executable's entry frame.  This is the first frame within the main
+     executable.  */
+  executable,
+
+  /* The inferior's entry frame.  This is the first frame within the
+     inferior, this can be outside the main executable, e.g. for a
+     dynamically linked executable, this will be the first frame in the
+     dynamic linker.  */
+  inferior,
+};
+
+/* Test whether THIS_FRAME is inside the TYPE process entry point
+   function.  */
+
+static bool
+inside_entry_func (const frame_info_ptr &this_frame,
+		   entry_address_type type)
+{
+  const program_space::entry_point_info &ep_info
+    = current_program_space->get_entry_point_info ();
+
+  CORE_ADDR frame_func_addr;
+  if (!get_frame_func_if_available (this_frame, &frame_func_addr))
+    return false;
+
+  switch (type)
+    {
+    case entry_address_type::executable:
+      return ep_info.exec_entry_address () == frame_func_addr;
+
+    case entry_address_type::inferior:
+      return ep_info.inferior_entry_address () == frame_func_addr;
+    }
+
+  gdb_assert_not_reached ("unknown entry address type: %d", ((int) type));
+}
+
+/* Debug routine to print a NULL frame being returned.  */
+
+static void
+frame_debug_got_null_frame (const frame_info_ptr &this_frame,
+			    const char *reason)
+{
+  if (frame_debug)
+    {
+      if (this_frame != NULL)
+	frame_debug_printf ("this_frame=%d -> %s", this_frame->level, reason);
+      else
+	frame_debug_printf ("this_frame=nullptr -> %s", reason);
+    }
+}
+
 /* Get the previous raw frame, and check that it is not identical to
    same other frame frame already in the chain.  If it is, there is
    most likely a stack cycle, so we discard it, and mark THIS_FRAME as
@@ -2538,6 +2595,17 @@ get_prev_frame_always_1 (const frame_info_ptr &this_frame)
 frame_info_ptr
 get_prev_frame_always (const frame_info_ptr &this_frame)
 {
+  if (this_frame->level >= 0
+      && get_frame_type (this_frame) == NORMAL_FRAME
+      && !user_set_backtrace_options.backtrace_past_entry
+      && inside_entry_func (this_frame, entry_address_type::inferior))
+    {
+      this_frame->prev_p = true;
+      this_frame->stop_reason = UNWIND_OUTERMOST;
+      frame_debug_got_null_frame (this_frame, "inside inferior entry func");
+      return nullptr;
+    }
+
   frame_info_ptr prev_frame = NULL;
 
   try
@@ -2626,21 +2694,6 @@ get_prev_frame_raw (const frame_info_ptr &this_frame)
   return frame_info_ptr (prev_frame);
 }
 
-/* Debug routine to print a NULL frame being returned.  */
-
-static void
-frame_debug_got_null_frame (const frame_info_ptr &this_frame,
-			    const char *reason)
-{
-  if (frame_debug)
-    {
-      if (this_frame != NULL)
-	frame_debug_printf ("this_frame=%d -> %s", this_frame->level, reason);
-      else
-	frame_debug_printf ("this_frame=nullptr -> %s", reason);
-    }
-}
-
 /* Is this (non-sentinel) frame in the "main"() function?  */
 
 static bool
@@ -2689,19 +2742,6 @@ inside_main_func (const frame_info_ptr &this_frame)
   return sym_addr == get_frame_func (this_frame);
 }
 
-/* Test whether THIS_FRAME is inside the process entry point function.  */
-
-static bool
-inside_entry_func (const frame_info_ptr &this_frame)
-{
-  const program_space::entry_point_info &ep_info
-    = current_program_space->get_entry_point_info ();
-
-  CORE_ADDR frame_func_addr = get_frame_func (this_frame);
-  return (ep_info.exec_entry_address () == frame_func_addr
-	  || ep_info.inferior_entry_address () == frame_func_addr);
-}
-
 /* Return a structure containing various interesting information about
    the frame that called THIS_FRAME.  Returns NULL if there is either
    no such frame or the frame fails any of a set of target-independent
@@ -2783,11 +2823,10 @@ get_prev_frame (const frame_info_ptr &this_frame)
   if (this_frame->level >= 0
       && get_frame_type (this_frame) == NORMAL_FRAME
       && !user_set_backtrace_options.backtrace_past_entry
-      && frame_pc.has_value ()
-      && inside_entry_func (this_frame))
+      && inside_entry_func (this_frame, entry_address_type::executable))
     {
-      frame_debug_got_null_frame (this_frame, "inside entry func");
-      return NULL;
+      frame_debug_got_null_frame (this_frame, "inside executable entry func");
+      return nullptr;
     }
 
   /* Assume that the only way to get a zero PC is through something
diff --git a/gdb/testsuite/gdb.base/until-in-entry-frame.c b/gdb/testsuite/gdb.base/until-in-entry-frame.c
new file mode 100644
index 00000000000..9b08ea9e1d9
--- /dev/null
+++ b/gdb/testsuite/gdb.base/until-in-entry-frame.c
@@ -0,0 +1,22 @@
+/* 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 (void)
+{
+  return 0;
+}
diff --git a/gdb/testsuite/gdb.base/until-in-entry-frame.exp b/gdb/testsuite/gdb.base/until-in-entry-frame.exp
new file mode 100644
index 00000000000..3da8bc7222a
--- /dev/null
+++ b/gdb/testsuite/gdb.base/until-in-entry-frame.exp
@@ -0,0 +1,166 @@
+# 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/>.
+
+# Check that 'until' in the outermost frame works as expected.
+
+require !use_gdb_stub
+
+standard_testfile
+
+if { [build_executable "failed to build" $testfile $srcfile] } {
+    return
+}
+
+set testfile_static ${testfile}-static
+if { [build_executable "failed to build" $testfile_static $srcfile \
+	  { debug additional_flags=-static } ] } {
+    return
+}
+
+# Use 'frame' to get the name of the current function.  Returns the
+# name of the current function, or the empty string if the current
+# function name cannot be established.
+proc current_function_name { testname } {
+    set func_name ""
+    gdb_test_multiple "frame" $testname {
+	-re -wrap "#0\\s+(?:$::hex in )?(\[^( \]+) \\(.*" {
+	    set func_name $expect_out(1,string)
+	}
+
+	-re -wrap "#0\\s+\[^\r\n\]*\\s+in (\[^( \]+) \\(.*" {
+	    set func_name $expect_out(1,string)
+	}
+    }
+
+    return $func_name
+}
+
+# Start the inferior with 'starti', then use 'until' within the
+# outermost frame.  This is the real outermost frame, not 'main'.
+#
+# PAST_ENTRY should be 'on' or 'off' and is used in a 'set backtrace
+# past-entry ...' command.
+proc run_test { use_stepi past_entry testfile } {
+    clean_restart $testfile
+
+    if { [gdb_starti_cmd] < 0 } {
+	untested starti
+	return
+    }
+
+    # Consume up to the first prompt after 'starti' command.
+    gdb_test "" "" "starti"
+
+    set func_name [current_function_name \
+		       "function name for first function"]
+
+    gdb_test_no_output "set backtrace past-entry $past_entry"
+
+    # On x86-64 GDB does get a valid frame-id for the very first
+    # instruction, but from the second instruction onwards it gets
+    # outer_frame_id.  As a result an 'until' setup at the first
+    # instruction doesn't match later on within the same function as
+    # the frame-ids no longer match.
+    #
+    # Try to work around this by issuing a 'stepi' to move to the
+    # second instruction.
+    #
+    # This is only a heuristic though.  On different architectures GDB
+    # might have a valid frame-id for multiple instructions within the
+    # outer frame, or might not have outer_frame_id for all
+    # instructions.
+    #
+    # Also, we need to consider that the very first instruction might
+    # be a control flow instruction, so using stepi could send the
+    # inferior to another function.  We try to spot this case and
+    # perform an early return if that happens.
+    if { $use_stepi } {
+	gdb_test "stepi"
+	set func_name_after [current_function_name \
+				 "function name after stepi"]
+	if { $func_name ne $func_name_after } {
+	    unsupported "stepi moved to another function"
+	    return
+	}
+    }
+
+    # If the 'until' doesn't trigger then stop in 'main' as a backup.
+    gdb_breakpoint main
+
+    # Find an address to use in the 'until' command.
+    set until_address ""
+    set capture_address false
+    gdb_test_multiple "disassemble" "find address for until" -lbl {
+	-re "\r\n=> $::hex \[^\r\n\]+(?=\r\n)" {
+	    set capture_address true
+	    exp_continue
+	}
+
+	-re "\r\n\\s+($::hex) \[^\r\n\]+(?=\r\n)" {
+	    if { $capture_address } {
+		set until_address $expect_out(1,string)
+		set capture_address false
+	    }
+
+	    exp_continue
+	}
+
+	-re "\r\n$::gdb_prompt $" {
+	    set found_address [expr { $until_address ne "" }]
+	    if { !$found_address } {
+		unsupported "$gdb_test_name (no instruction found)"
+		return
+	    }
+	    pass $gdb_test_name
+	}
+    }
+
+    # Send the 'until' command and then wait for GDB to stop.  See the
+    # notes above about the 'stepi' for why we sometimes expect to see
+    # GDB reach 'main' here.
+    send_gdb "until *${until_address}\n"
+    gdb_test_multiple "" "after until" {
+	-re -wrap "Breakpoint $::decimal, (?:\[^\r\n\]+ in )?main \\(\\).*" {
+	    kfail "gdb/34245" "$gdb_test_name (skipped until breakpoint)"
+	}
+
+	-re -wrap "\r\n(?:$::hex in )?$func_name \\(\\).*" {
+	    pass $gdb_test_name
+	}
+
+	-re -wrap "Warning:\r\nCannot insert breakpoint 0\\.\r\nCannot access memory at address $::hex.*Command aborted\\." {
+	    # With PAST_ENTRY 'on', GDB discovers some invalid frames
+	    # past the entry frame based on whatever happens to be in
+	    # the registers when the entry function is called.  These
+	    # invalid frames are often non-writable, so GDB will fail
+	    # to insert the breakpoint when the inferior resumes.
+	    #
+	    # We still count this as a pass though; the user should
+	    # only turn on 'backtrace past-entry' when they know that
+	    # is needed, so failure here is totally expected.
+	    gdb_assert { $past_entry eq "on" } "$gdb_test_name (caller breakpoint failed)"
+	}
+    }
+}
+
+foreach_with_prefix past_entry { off on } {
+    foreach_with_prefix use_stepi { true false } {
+	run_test $use_stepi $past_entry $testfile
+
+	with_test_prefix "static" {
+	    run_test $use_stepi $past_entry $testfile_static
+	}
+    }
+}
