#2171·kanata

Bug: Simultaneous/"stacked" `press-vkey`s results in unexpected behavior (including crashes)

Author: brandonbeaumiaCreated Aug 30, 2026Updated Aug 30, 2026
Labelsbug

Requirements

Describe the bug

Simultaneous/"stacked" press-vkeys where there is no release-vkey associated results in a lot of unexpected behavior.

Take this example:

(defvirtualkeys testkey (multi
        (macro S-9 t S-0)
        (macro-repeat-release-cancel 10 a 1000 b 1000 c 1000 spc 1000)
    )
)

(defalias
    hold (on-press press-vkey testkey)
    release (on-press release-vkey testkey)
)

The appropriate behavior if you tap your @hold alias is "abc " repeatedly until you @release it, which works. A second @hold results in a single additional concurrent macro that does not repeat, outputting "(t)abc abc a(t)abbcc abc abc(t)a bc abc abc abc". The additional macro runs simultaneously but has different behavior from the first press as it stops after one cycle.

Simultaneous presses where vkey emits a cmd action will emit the action properly until reaching approximately 70 presses. At this point, any key tapped before the release of another will be dropped (effectively 1-key rollover) despite the debug log showing all the appropriate IO events.

While simultaneous presses of a cmd action work until press ~70, the release fails much sooner. At 17 "stacked" vkeys, performing a release-vkey action succeeds and the key is released. At 18 "stacked" vkeys, performing a release-vkey action crashes.

For a press-vkey where the vkey outputs a layer-while-held, the crash happens on the 14th press (no release).

Relevant kanata config

(defcfg
    danger-enable-cmd yes
    process-unmapped-keys yes
)
(defsrc)

(defvirtualkeys
    testmacro (multi
        (macro S-9 t S-0)
        (macro-repeat-release-cancel 10 a 1000 b 1000 c 1000 spc 1000))
    testcmd (cmd echo "pressed")
    testlayer (layer-while-held dummylayer)
)

(deflayermap base
    [ (on-press press-vkey testmacro)
    ] (on-press release-vkey testmacro)
    - (on-press press-vkey testcmd)
    = (on-press release-vkey testcmd)
    pgup (on-press press-vkey testlayer)
    pgdn (on-press release-vkey testlayer)
)

(deflayermap dummylayer
    pgup (on-press press-vkey testlayer)
    pgdn (on-press release-vkey testlayer)
)

To Reproduce

(With above config:) Press [ twice to see concurrent macro issue. Press - ~70-80 times and then try typing a capital letter or very quickly for dropped input. Press - 18 times, then = for crash. Press pgup 14 times for crash.

Expected behavior

I know these are "virtual" but I would still expect similar behavior to a real keyboard where the state of a key is either off or on, and "changing" it from on to on is equivalent to a nop. A key having potential states of on, on on, on on on (etc) is surprising and difficult to reason about, especially if there are timers involved.

Is the best solution to align vkey behavior with a mechanical switch? If the switch is already pressed, do nothing. This shortcircuits all of these issues, of which I doubt anyone is relying upon because the behavior is pretty unpredictable and I could not find other open issues like it. If someone's intention is for multiple physical keys to set the same vkey "flag" for e.g. switching upon, this works fine as the flag remains unchanged. If someone's intention is for a vkey press to perform an action on every press regardless of its state, this is covered by multiing that action directly rather than using a vkey. I don't see a situation in which stacking the same held vkey is beneficial, especially enough to try solving these issues individually rather than in one fell swoop.

Kanata version

kanata 1.12.0

Debug logs

Performing a release-vkey on a cmd action after 18 stacks:

thread '<unnamed>' (2288758) panicked at keyberon/src/layout.rs:386:29:
overflowed custom action release queue
stack backtrace:
   0:     0x55af0252a6a9 - <<std[52919eca6bce4da3]::sys::backtrace::BacktraceLock>::print::DisplayBacktrace as core[18c8dd30382e7099]::fmt::Display>::fmt
   1:     0x55af02464e54 - core[18c8dd30382e7099]::fmt::write
   2:     0x55af02529d41 - <std[52919eca6bce4da3]::sys::stdio::unix::Stderr as std[52919eca6bce4da3]::io::Write>::write_fmt
   3:     0x55af02529a48 - std[52919eca6bce4da3]::panicking::panic_with_hook
   4:     0x55af0254f598 - std[52919eca6bce4da3]::panicking::panic_handler::{closure#0}
   5:     0x55af0254f519 - std[52919eca6bce4da3]::sys::backtrace::__rust_end_short_backtrace::<std[52919eca6bce4da3]::panicking::panic_handler::{closure#0}, !>
   6:     0x55af0254fa0c - __rustc[8068f81614cfe5c]::rust_begin_unwind
   7:     0x55af02464ceb - core[18c8dd30382e7099]::panicking::panic_fmt
   8:     0x55af024d3b7b - kanata_keyberon::layout::State<T>::release::h8731106ab16e09ac
   9:     0x55af024d9200 - heapless::vec::Vec<T,_>::retain::{{closure}}::ha9c70fe88dfab357
  10:     0x55af024d6b79 - kanata_keyberon::layout::Layout<_,_,T>::dequeue::h5317011d83557e39
  11:     0x55af024ecf2f - kanata_state_machine::kanata::Kanata::handle_keystate_changes::h94fa1530df3981be
  12:     0x55af024e907c - kanata_state_machine::kanata::Kanata::tick_states::h27e4e3f1b66e80d0
  13:     0x55af024e8884 - kanata_state_machine::kanata::Kanata::handle_time_ticks::h89e4e48c4ba8dd9a
  14:     0x55af024e71cc - std::sys::backtrace::__rust_begin_short_backtrace::hef40d65a44e59a52
  15:     0x55af024e6c33 - core::ops::function::FnOnce::call_once{{vtable.shim}}::hb0830d6121135088
  16:     0x55af02550da8 - <std[52919eca6bce4da3]::sys::thread::unix::Thread>::new::thread_start
  17:     0x7f3913460c19 - start_thread
  18:     0x7f39134e45cc - __clone3
  19:                0x0 - <unknown>




Pressing 14 of a vkey that emits layer-while-held:

thread '<unnamed>' (2332079) panicked at /home/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/heapless-0.7.17/src/vec.rs:935:30:
Vec::from_iter overflow
stack backtrace:
   0:     0x55b3460ad6a9 - <<std[52919eca6bce4da3]::sys::backtrace::BacktraceLock>::print::DisplayBacktrace as core[18c8dd30382e7099]::fmt::Display>::fmt
   1:     0x55b345fe7e54 - core[18c8dd30382e7099]::fmt::write
   2:     0x55b3460acd41 - <std[52919eca6bce4da3]::sys::stdio::unix::Stderr as std[52919eca6bce4da3]::io::Write>::write_fmt
   3:     0x55b3460aca48 - std[52919eca6bce4da3]::panicking::panic_with_hook
   4:     0x55b3460d2567 - std[52919eca6bce4da3]::panicking::panic_handler::{closure#0}
   5:     0x55b3460d2519 - std[52919eca6bce4da3]::sys::backtrace::__rust_end_short_backtrace::<std[52919eca6bce4da3]::panicking::panic_handler::{closure#0}, !>
   6:     0x55b3460d2a0c - __rustc[8068f81614cfe5c]::rust_begin_unwind
   7:     0x55b345fe7ceb - core[18c8dd30382e7099]::panicking::panic_fmt
   8:     0x55b345fec543 - core[18c8dd30382e7099]::option::expect_failed
   9:     0x55b34605a7f5 - kanata_keyberon::layout::Layout<_,_,T>::trans_resolution_layer_order::h49da2a070b12b40a
  10:     0x55b3460599c7 - kanata_keyberon::layout::Layout<_,_,T>::dequeue::h5317011d83557e39
  11:     0x55b34606ff2f - kanata_state_machine::kanata::Kanata::handle_keystate_changes::h94fa1530df3981be
  12:     0x55b34606c07c - kanata_state_machine::kanata::Kanata::tick_states::h27e4e3f1b66e80d0
  13:     0x55b34606b884 - kanata_state_machine::kanata::Kanata::handle_time_ticks::h89e4e48c4ba8dd9apressed
  14:     0x55b34606a8fa - std::sys::backtrace::__rust_begin_short_backtrace::hef40d65a44e59a52
  15:     0x55b346069c33 - core::ops::function::FnOnce::call_once{{vtable.shim}}::hb0830d6121135088
  16:     0x55b3460d3da8 - <std[52919eca6bce4da3]::sys::thread::unix::Thread>::new::thread_start
  17:     0x7f898c144c19 - start_thread
  18:     0x7f898c1c85cc - __clone3
  19:                0x0 - <unknown>

Operating system and I/O mechanism

Linux

Additional context

No response