Tk Source Code

View Ticket
Login
Ticket UUID: 33b3af1ceac681ad0f7756f9495de42bbdadb925
Title: macOS 27 beta: modal dialog opened synchronously from a menu command becomes unresponsive
Type: Bug Version: 8.6.17 and 9
Submitter: bdubzkc Created on: 2026-07-21 13:04:34
Subsystem: 11. Aqua Menus Assigned To: marc_culler
Priority: 5 Medium Severity: Critical
Status: Closed Last Modified: 2026-10-16 22:19:42
Resolution: Fixed Closed By: chrstphrchvz
    Closed on: 2026-10-16 22:19:42
Description:

On macOS 27 beta, opening a modal dialog synchronously from a Tk menu command causes the dialog and application to become unresponsive.

The dialog appears, but it does not accept mouse clicks or keyboard input. The application must be force-quit.

Deferring the identical dialog with after idle avoids the problem.

Environment

  • Affected OS: macOS 27.0 Developer Beta 4 (also occured in Beta 3)
  • Affected build: 26A5378n
  • Affected Tk versions: Tk 8.6 and Tk 9.0
  • Architecture: Apple silicon
  • Not reproduced on: macOS 25 or macOS 26

Steps to reproduce

  1. Save the attached reproduction script.
  2. Run it with Tk 8.6 or Tk 9.0.
  3. Select Test > Direct (hangs).
  4. Observe that the modal window appears but does not respond to clicks or typing.
  5. Force-quit and restart Wish.
  6. Select Test > Deferred (works).
  7. Observe that the identical dialog remains responsive.

Actual result

When openModal is called directly by the menu command, .modal is displayed but does not process mouse or keyboard input.

Wish must be force-quit.

Expected result

tkwait window should continue processing events, allowing the user to interact with and close the modal dialog even when it was opened by a menu command.

Workaround

Deferring the dialog until the next idle iteration works correctly:

proc deferred {} {
    after idle {openModal deferred}
}

Version information

The reproduction works correctly on macOS 25 and macOS 26. Both the direct and deferred paths remain responsive on those releases.

The failure has been reproduced on macOS 27 beta build 26A5378n with Tk 8.6 and Tk 9.0. My suspicion that all of apple's changes to the menu bar for macOS 27 (see all of the issues with BarTender and other menu bar management tools) broke something, but I'm struggling to find documentation exactly on what they've done and am not too familiar with the Tk codebase to be much help.

User Comments: chrstphrchvz added on 2026-10-16 22:19:42:

Can [1bc92655] please be backported to 8.6/9.0? Thanks


marc_culler (claiming to be Marc Culler) added on 2026-10-16 13:27:48:
I think this is sufficient, so I will close the ticket.

chrstphrchvz added on 2026-10-13 03:45:47:

There are also the options to document the behavior (e.g. "If the main interpreter is deleted, tkAboutDialog will not be called"); or, match the existing fallback behavior in -[TKApplication orderFromStandardAboutPanel:], which is to show the standard about dialog (the following works, but might not be ideal):

--- macosx/tkMacOSXMenus.c
+++ macosx/tkMacOSXMenus.c
@@ -23,7 +23,12 @@ static void ShowAboutIdleTask(void *clientData) {
     int code = Tcl_EvalEx(interp, "tkAboutDialog", TCL_INDEX_NONE,
 			  TCL_EVAL_GLOBAL);
     if (code != TCL_OK) {
-	Tcl_BackgroundException(interp, code);
+	// Equivalent to how -[TKApplication orderFrontStandardAboutPanel:]
+	// falls back to calling [super orderFrontStandardAboutPanel:NSApp].
+	SEL selector = @selector(orderFrontStandardAboutPanel:);
+	Method method = class_getInstanceMethod([NSApplication class], selector);
+	IMP imp = method_getImplementation(method);
+	((void (*)(id, SEL, id))imp)(NSApp, selector, NSApp);
     }
     Tcl_ResetResult(interp);
     Tcl_Release(interp);

chrstphrchvz added on 2026-10-13 02:48:10:

I think even developers could find that error message confusing, whether or not their program uses eval/bgerror/etc. Several Tkinter users had encountered [310c74ec], for example, and it seemed not uncommon for such issues to prompt them to believe (or be told) that their code was responsible for the error, but then not figure out exactly why.


marc_culler (claiming to be Marc Culler) added on 2026-10-11 16:16:16:
I think I would lean towards emitting a message.

In a real app, it is hard for me to imagine how a user would be in the
situation where an "About" dialog is being requested from a deleted
interpreter.  A user might open the dialog to check the version of
the software or on the first run to find out whatever information
is available.  But that is not likely to involve a deleted interpreter.

So the message would be aimed at developers, and they deserve to
get a message. I don't know what is wrong with "attempt to call eval
in deleted interpreter"

chrstphrchvz added on 2026-10-11 04:51:02:

Thanks. Preserving _eventInterp is indeed one option, although it might be desirable to also check Tcl_InterpDeleted() in showAboutIdleTask() to prevent an "attempt to call eval in deleted interpreter" error. Another option would be to use Tcl_CallWhenDeleted(). But with either approach, I am not sure which would be better: letting tkAboutDialog silently fail to run, or emitting some message explaining the situation.


marc_culler (claiming to be Marc Culler) added on 2026-10-11 03:53:35:
Thank you.  Of course you may.

I did not see any problem when running that script, but I added the
preserve call anyway.  I released _eventInterp in the idle task itself.
I assume that is what you had in mind.

I just pushed that change to the tip of main.

chrstphrchvz added on 2026-10-11 00:36:24:

Reopening this if I may. The current fix passes _eventInterp to Tcl_DoWhenIdle() without preserving it, which leaves open the possibility of a use-after-free if _eventInterp is deleted before showAboutIdleTask() is called. The following tclsh script triggers the problem, at least after [8e32cb7e] and [1ab7a5f2] are fixed first:

interp create i
interp eval i package require Tk
interp eval i proc tkAboutDialog x {{puts unimplemented}}
interp eval i ::tk::mac::standardAboutPanel
interp delete i
update idletasks


marc_culler (claiming to be Marc Culler) added on 2026-10-05 15:50:45:
I have merged bug-33b3af1cea into 9.1, 9.0 and 8.6.  I am closing this
ticket now.

marc_culler (claiming to be Marc Culler) added on 2026-10-05 02:54:49:
Here is my current best guess.  People claim that the menubar display
has been completely changed in macOS 27 and now uses a private undocumented
framework named MenuBarClientCore.  Evidently that framework changed the
behavior of the runloop which is started when a menu is opened. In macOS 26
a runloop is started in NSEventTrackingRunLoopMode which terminates when
a menuitem is selected.  The action for the menuitem is then called in Tk's
default state, which has a nil runloop mode.

My experiments indicate that in macOS 27 a runloop is first started in
NSDefaultRunLoopMode which does unknown things before starting a runloop
in NSEventTrackingRunLoopMode.  When that runloop ends the action for the
selected item gets run before the parent default runloop stops.  Apparently
the default runloop does not stop until the menuitem action returns.  But
Tk does not attempt to process any events from the AppKit event queue unless
the runloop mode is nil (and possibly it would not be allowed to do that
if it did attempt to).  Therefore, if user input, such as pressing the
OK button in a dialog window, must be processed before the command returns
this means that the command can never return and the default runloop will
never stop and Tk will never be able to process a user event again.  The
idle task workaround allows the menuitem action to return immediately,
which allows the outer default runloop to end and Tk is then able to
return to processing user events.

I can see that when either the NSMenuDidBeginTrackingNotification or the
NSMenuDidEndTrackingNotification is received by Tk the current runloop
mode is NSDefaultRunLoopMode in macOS 27 but is nil in macOS 26.

Also, the Tcl function TclpWaitForEvent, which starts a runloop to process
one event from any event source, tracks the runloop nesting level.  I can
see that in macOS 26 the nesting level never exceeds 2, but in macOS 27
it reaches 3.

This data is at least consistent with my guess of what is happening.

Given all this, I think the idle task workaround is the most reasonable
way to deal with this.  (We are lucky that you noticed that it works!)
This has been implemented in the bug-33b3af1cea branch and it was
verified that it allowed the Python IDLE.app (which launches dialogs
from menu item callbacks) to work again.

My plan is to merge the bugfix branch into 9.0 and 9.1, and perhaps also
into 8.6, although I would prefer that people not use 8.6 on macOS 27
and later.

marc_culler (claiming to be Marc Culler) added on 2026-09-30 20:16:16:
Yes, I think it is a blocker for 9.0.5.

I have pushed a bugfix branch bug-33b3af1cea which uses the workaround
suggested here.  All menu actions are run as idle tasks instead of being
called directtly.

I am not comfortable with this workaround because I cannot explain why it works.  However, as far as I can tell, it does work.

I suspect the answer to why it works may be buried in the Tcl notifier for
macosx.  If that is correct, changing the Tcl notifier might be a better
fix.

jan.nijtmans added on 2026-09-30 15:57:19:

This looks like a blocker for Tk 9.0.5.

+1


marc_culler (claiming to be Marc Culler) added on 2026-09-29 16:08:05:

This is a copy of the comment I just uploaded to the Python github site about this issue: Issue 1580530

I have at least identified what changed between macOS 26 and 27. Of course, with Apple, we are dealing with a black box. So I can only report symptoms, not underlying causes.

The background is:

  • Tk uses Apple's event loop as an event source, removing events from Apple's queue, translating them into XEvents, and adding them to the Tcl event queue.
  • Tk never calls CFRunLoopRun, and therefore assumes that the current runloop mode will be nil unless Apple is using the queue to run a temporary runloop, for example one with mode NSEventTrackingRunLoopMode (used when a menu is open) or NSModalPanelRunLoopMode (used when a file dialog is running). The function TkMacOSXEventsSetupProc, which is used to check whether there are any events relevant to Tk on the Apple queue, returns immediately if the runloop mode is not nil. The logic is that Tk should not remove events from the queue while Apple is using it.

The behavior which changed between macOS 26 and 27 is:

  • In macOS 26, the current runloop mode is nil before and after Apple runs the runloop with mode NSEventTrackingRunLoopMode which handles events while a menu is open.
  • In macOS 27, the current runloop mode is NSDefaultRunLoopMode before and after the NSEventTrackingRunLoopMode run, This means that Apple is wrapping the menu runloop inside of a generic runloop. We are seeing that the generic runloop does not terminate for an unknown period of time.

Tk receives the notifications NSMenuDidBeginTrackingNotification and NSMenuDidEndTrackingNotification. So the information above is found by printing the runloop mode in the handlers for those events. Since Tk does not process any events when the runloop mode is not nil, the consequence of the runloop mode being set to NSDefaultRunLoopMode when a menu closes is that Tk will hang until the runloop mode returns to nil. It is not clear how to force the runloop mode to return to nil. But a nil runloop mode is the indicator that the Apple runloop is idle (as it should be if CFRunLoopRun has never been called), and the fact that it sometimes gets set to nil when Tk enters its idle state seems encouraging.


marc_culler (claiming to be Marc Culler) added on 2026-07-22 01:47:18:
Correction!  When I said "thread" with respect to menus and file dialogs I
was not correct.  I should have said "Runloop Mode".  All of the events are
processed by the main thread, but menus and file dialog events are handled
in different runloop modes.  For menus, the runloop mode is called
NSEventTrackingRunLoopMode and for file dialogs it is called NSModalPanelRunLoopMode.

So this is probably caused by an Apple bug which broke the transition from NSEventTrackingRunLoopMode to NSModalPanelRunLoopMode when that transition occurs by calling NSOpenPanel or NSSavePanel from a menu action.

If the bug persists after the OS release Tk can probably work around it
the same way - by calling NSOpenPanel and NSSavePanel from an idle task.

We have to wait and see if they fix it.

marc_culler (claiming to be Marc Culler) added on 2026-07-21 23:28:37:
I am glad there is a simple workaround, but I would like to know what the
cause of the hang is, and what changed in macOS 27 to cause this.

Some things I know are:

* Opening a menu launches a separate thread which handles *all* mouse and
  key events belonging to the Tk application.  Tk does not receive any of
  these events while a menu is open.

* In order to make Tk timer tasks get processed while a menu is open (for
  example, to keep the pendulum swinging in the pendulum demo while a menu
  is open) Tk launches its own separate thread when a menu is opened.  The
  Tk thread processes Timer tasks only.

* Opening a file dialog also causes Apple to create a separate thread which
  manages all mouse and key events targeting the dialog while hiding all of
  those events from Tk.

One thing to keep in mind is that this may be an Apple bug which may get
fixed before macOS 27 is released.  However, it may also be an intentional
change.  The fact that it can be worked around by just delaying the launch
of the file dialog suggests a race condition has been created which can
somehow prevent Apple from launching the thread to manage the file dialog.
It is hard to imagine that Apple would intentionally create a race condition
like that.  So I am leaning towards the idea that this is an Apple bug.

The race condition theory may be wrong, however. I believe that when a menu
item is selected which runs a Tk command, the Tk command may actually get run in
the thread which controls the menu.  So the issue may be whether a file dialog
thread can be launched by a thread which is not the main thread.  Maybe that
used to be possible but is not any longer, due to some change in macOS 27.
This alternate theory is consistent with this report, because the workaround
changes the menu action from "launch file dialog" to "add command to launch
file dialog to the idle queue".  The idle command would then be run by the
main thread instead of by the menu management thread, so the file dialog thread
would be launched by the main thread with the proposed workaround.

Attachments: