Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,21 @@ namespace KeyboardEventHandlers
key_count = std::get<Shortcut>(it->second).Size();
}

const DWORD sourceKey = data->lParam->vkCode;
const bool isKeyUp = (data->wParam == WM_KEYUP || data->wParam == WM_SYSKEYUP);

// If the matching key-down injection was blocked earlier, we passed the
// original key-down through to the foreground app to keep the key alive.
// The corresponding key-up must be passed through as well; otherwise the
// physical key is stranded DOWN (its down reached the app, but its up would
// be swallowed by the remap). Key-down and key-up arrive as separate hook
// events, so this is the cross-invocation counterpart of the key-down
// passthrough handled below.
if (isKeyUp && state.ConsumeSingleKeyRemapInjectionFailed(sourceKey))
{
return 0;
}

std::vector<INPUT> keyEventList;

// Handle remaps to VK_WIN_BOTH
Expand Down Expand Up @@ -169,7 +184,25 @@ namespace KeyboardEventHandlers
}
}

ii.SendVirtualInput(keyEventList);
if (!ii.SendVirtualInput(keyEventList))
{
// Injection was blocked (e.g. by UIPI). Return 0 so the ORIGINAL key is
// passed through instead of being swallowed, leaving no dead key. For a
// key-down, remember that we passed it through so the matching key-up is
// passed through too (handled above), preventing a key stranded DOWN.
if (!isKeyUp)
{
state.SetSingleKeyRemapInjectionFailed(sourceKey, true);
}
return 0;
}

// Injection succeeded; drop any stale passthrough marker for this key so its
// key-up follows the normal (suppressed) path.
if (!isKeyUp)
{
state.SetSingleKeyRemapInjectionFailed(sourceKey, false);
}

if (data->wParam == WM_KEYDOWN || data->wParam == WM_SYSKEYDOWN)
{
Expand Down Expand Up @@ -540,9 +573,12 @@ namespace KeyboardEventHandlers

// Send modifier release events first, then send text directly
// (SendTextInput handles multiline by flushing between chunks)
ii.SendVirtualInput(keyEventList);
if (!ii.SendVirtualInput(keyEventList))
{
return 0;
}
keyEventList.clear();
Helpers::SendTextInput(remapping);
Helpers::SendTextInput(remapping, ii);
}

it->second.isShortcutInvoked = true;
Expand All @@ -554,7 +590,10 @@ namespace KeyboardEventHandlers

Logger::trace(L"ChordKeyboardHandler:keyEventList.size:{}", keyEventList.size());

ii.SendVirtualInput(keyEventList);
if (!ii.SendVirtualInput(keyEventList))
{
return 0;
}
if (activatedApp.has_value())
{
if (remapToKey)
Expand Down Expand Up @@ -693,7 +732,10 @@ namespace KeyboardEventHandlers
state.SetActivatedApp(KeyboardManagerConstants::NoActivatedApp);
}

ii.SendVirtualInput(keyEventList);
if (!ii.SendVirtualInput(keyEventList))
{
return 0;
}
return 1;
}

Expand Down Expand Up @@ -724,11 +766,14 @@ namespace KeyboardEventHandlers
{
auto& remapping = std::get<std::wstring>(it->second.targetShortcut);
ii.SendVirtualInput(keyEventList);
Helpers::SendTextInput(remapping);
Helpers::SendTextInput(remapping, ii);
return 1;
}

ii.SendVirtualInput(keyEventList);
if (!ii.SendVirtualInput(keyEventList))
{
return 0;
}
return 1;
}

Expand Down Expand Up @@ -815,7 +860,10 @@ namespace KeyboardEventHandlers
}
}

ii.SendVirtualInput(keyEventList);
if (!ii.SendVirtualInput(keyEventList))
{
return 0;
}
return 1;
}

Expand Down Expand Up @@ -940,7 +988,10 @@ namespace KeyboardEventHandlers
state.SetActivatedApp(KeyboardManagerConstants::NoActivatedApp);
}

ii.SendVirtualInput(keyEventList);
if (!ii.SendVirtualInput(keyEventList))
{
return 0;
}
return 1;
}
else
Expand Down Expand Up @@ -1009,7 +1060,10 @@ namespace KeyboardEventHandlers
state.SetActivatedApp(KeyboardManagerConstants::NoActivatedApp);
}

ii.SendVirtualInput(keyEventList);
if (!ii.SendVirtualInput(keyEventList))
{
return 0;
}
return 1;
}
else
Expand Down Expand Up @@ -1787,8 +1841,9 @@ namespace KeyboardEventHandlers
return 0;
}

// Only send the text on keydown event
if (data->wParam != WM_KEYDOWN)
// Only send the text on key-down events. WM_SYSKEYDOWN is sent instead of
// WM_KEYDOWN while Alt is held, so accept it too or the remap silently drops.
if (data->wParam != WM_KEYDOWN && data->wParam != WM_SYSKEYDOWN)
{
return 0;
}
Expand All @@ -1799,7 +1854,43 @@ namespace KeyboardEventHandlers
return 0;
}

Helpers::SendTextInput(*remapping);
// Release held modifiers before text injection to prevent Ctrl+text corruption
constexpr int modifierKeys[] = { VK_LCONTROL, VK_RCONTROL, VK_LSHIFT, VK_RSHIFT, VK_LMENU, VK_RMENU, VK_LWIN, VK_RWIN };
std::vector<INPUT> releaseEvents;

// A dummy key event must precede the modifier releases so that releasing a
// held Win (Start Menu) or Alt (menu bar) does not trigger its lone-press
// action when we inject the modifier key-up.
Helpers::SetDummyKeyEvent(releaseEvents, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);

bool anyModifierHeld = false;
for (int vk : modifierKeys)
{
if (ii.GetVirtualKeyState(vk))
{
Helpers::SetKeyEvent(releaseEvents, INPUT_KEYBOARD, static_cast<WORD>(vk), KEYEVENTF_KEYUP, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
anyModifierHeld = true;
}
}

// Only inject the dummy + modifier releases when a modifier was actually held.
if (anyModifierHeld)
{
if (!ii.SendVirtualInput(releaseEvents))
{
return 0;
}
}

Helpers::SendTextInput(*remapping, ii);

// Intentionally do NOT re-press the released modifiers. Once we inject a
// KEYUP for a modifier, GetAsyncKeyState (and therefore GetVirtualKeyState)
// reports it as up, so there is no reliable way to tell whether the user is
// still physically holding the key or has released it. Re-pressing
// unconditionally would risk leaving a modifier stuck down if the user let
// go during injection — the exact failure this change set prevents. Leaving
// the modifier released is always safe: the user taps it again to re-engage.

return 1;
}
Expand Down
17 changes: 17 additions & 0 deletions src/modules/keyboardmanager/KeyboardManagerEngineLibrary/State.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,20 @@ std::wstring State::GetActivatedApp()
{
return activatedAppSpecificShortcutTarget;
}

void State::SetSingleKeyRemapInjectionFailed(const DWORD sourceKey, const bool failed)
{
if (failed)
{
singleKeyRemapInjectionFailedKeys.insert(sourceKey);
}
else
{
singleKeyRemapInjectionFailedKeys.erase(sourceKey);
}
}

bool State::ConsumeSingleKeyRemapInjectionFailed(const DWORD sourceKey)
{
return singleKeyRemapInjectionFailedKeys.erase(sourceKey) > 0;
}
17 changes: 17 additions & 0 deletions src/modules/keyboardmanager/KeyboardManagerEngineLibrary/State.h
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
#pragma once
#include <keyboardmanager/common/MappingConfiguration.h>
#include <unordered_set>

class State : public MappingConfiguration
{
private:
// Stores the activated target application in app-specific shortcut
std::wstring activatedAppSpecificShortcutTarget;

// Source keys whose single-key remap key-down injection was blocked, so the original
// key-down was passed through to the foreground app. The matching key-up must be
// passed through too, otherwise the physical key is stranded DOWN. Only accessed from

Check failure

Code scanning / check-spelling

Forbidden Pattern Error

, otherwise matches a line_forbidden.patterns rule: Should be '; otherwise' or '. Otherwise' - ', [Oo]therwise\\b'
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
// the (serialized) low-level keyboard hook thread.
std::unordered_set<DWORD> singleKeyRemapInjectionFailedKeys;

public:
// Function to get the iterator of a single key remap given the source key. Returns nullopt if it isn't remapped
std::optional<SingleKeyRemapTable::iterator> GetSingleKeyRemap(const DWORD& originalKey);
Expand All @@ -26,4 +33,14 @@

// Gets the activated target application in app-specific shortcut
std::wstring GetActivatedApp();

// Records (failed == true) or clears (failed == false) that the single-key remap
// key-down injection for sourceKey was blocked and the original key-down was passed
// through to the foreground app.
void SetSingleKeyRemapInjectionFailed(const DWORD sourceKey, const bool failed);

// Returns true and clears the marker if sourceKey's single-key remap key-down
// injection was previously blocked, indicating that its key-up should be passed
// through as well.
bool ConsumeSingleKeyRemapInjectionFailed(const DWORD sourceKey);
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,14 @@ void MockedInput::SetHookProc(std::function<intptr_t(LowlevelKeyboardEvent*)> ho
}

// Function to simulate keyboard input - arguments and return value based on SendInput function (https://learn.microsoft.com/windows/win32/api/winuser/nf-winuser-sendinput)
void MockedInput::SendVirtualInput(const std::vector<INPUT>& inputs)
bool MockedInput::SendVirtualInput(const std::vector<INPUT>& inputs)
{
// Simulate an injection failure (e.g. SendInput blocked) when configured.
if (sendVirtualInputShouldFail != nullptr && sendVirtualInputShouldFail(inputs))
{
return false;
}

// Iterate over inputs
for (const INPUT& input : inputs)
{
Expand Down Expand Up @@ -107,6 +113,7 @@ void MockedInput::SendVirtualInput(const std::vector<INPUT>& inputs)
}
}
}
return true;
}

// Function to simulate keyboard hook behavior
Expand All @@ -129,6 +136,12 @@ bool MockedInput::GetVirtualKeyState(int key)
return keyboardState[key];
}

// Function to set the state of a particular key for test setup
void MockedInput::SetKeyboardState(int key, bool state)
{
keyboardState[key] = state;
}

// Function to reset the mocked keyboard state
void MockedInput::ResetKeyboardState()
{
Expand All @@ -142,6 +155,12 @@ void MockedInput::SetSendVirtualInputTestHandler(std::function<bool(LowlevelKeyb
sendVirtualInputCallCondition = condition;
}

// Function to force SendVirtualInput to fail for calls matching a predicate
void MockedInput::SetSendVirtualInputShouldFail(std::function<bool(const std::vector<INPUT>&)> condition)
{
sendVirtualInputShouldFail = condition;
}

// Function to get SendVirtualInput call count
int MockedInput::GetSendVirtualInputCallCount()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ namespace KeyboardManagerInput
int sendVirtualInputCallCount = 0;
std::function<bool(LowlevelKeyboardEvent*)> sendVirtualInputCallCondition;

// Optional predicate; when set and it returns true for a SendVirtualInput
// call, that call fails (returns false) to simulate a SendInput failure.
std::function<bool(const std::vector<INPUT>&)> sendVirtualInputShouldFail;

std::wstring currentProcess;

public:
Expand All @@ -34,20 +38,26 @@ namespace KeyboardManagerInput
void SetHookProc(std::function<intptr_t(LowlevelKeyboardEvent*)> hookProcedure);

// Function to simulate keyboard input
void SendVirtualInput(const std::vector<INPUT>& inputs);
bool SendVirtualInput(const std::vector<INPUT>& inputs);

// Function to simulate keyboard hook behavior
intptr_t MockedKeyboardHook(LowlevelKeyboardEvent* data);

// Function to get the state of a particular key
bool GetVirtualKeyState(int key);

// Function to set the state of a particular key for test setup
void SetKeyboardState(int key, bool state);

// Function to reset the mocked keyboard state
void ResetKeyboardState();

// Function to set SendVirtualInput call count condition
void SetSendVirtualInputTestHandler(std::function<bool(LowlevelKeyboardEvent*)> condition);

// Function to force SendVirtualInput to fail for calls matching a predicate
void SetSendVirtualInputShouldFail(std::function<bool(const std::vector<INPUT>&)> condition);

// Function to get SendVirtualInput call count
int GetSendVirtualInputCallCount();

Expand Down
Loading
Loading