Skip to content

Fix crash, UB, and correctness defects found in full-codebase review - #10

Open
OleksandrAWS wants to merge 2 commits into
rundax:masterfrom
OleksandrAWS:fix/code-review-findings
Open

Fix crash, UB, and correctness defects found in full-codebase review#10
OleksandrAWS wants to merge 2 commits into
rundax:masterfrom
OleksandrAWS:fix/code-review-findings

Conversation

@OleksandrAWS

Copy link
Copy Markdown

Summary

A full expert review of the codebase (Core input pipeline, Dictionary, UI/Utils/App) surfaced one likely crasher, several undefined-behavior sites, and a set of correctness/robustness defects. This PR fixes all confirmed findings.

Note: this branch is stacked on #9 (Excluded Apps settings UI) and includes its commit. If #9 merges first, this PR will show only the fixes.

Crash / undefined behavior

  • Pasteboard restore crash (likely crasher): performSelectionCorrection snapshotted pasteboard.pasteboardItems, called clearContents(), then wrote the same items back 150 ms later. Items read from a pasteboard are invalidated by clearing; writing them back raises an ObjC exception on the main thread, and the user's clipboard was permanently lost. Now item data is copied into fresh NSPasteboardItems before clearing.
  • withUnsafeBytes pointer escapes (UB) ×3: KeyCodeMapping, KeyboardMonitor.translatedCharacter, InputSourceManager.translatedCharacter all returned the UCKeyboardLayout pointer out of the closure and called UCKeyTranslate afterwards. The calls now happen inside the closure.
  • BloomFilter public inits could trap (modulo-by-zero on bitCount: 0, out-of-bounds on undersized bit arrays). Now clamped/padded.

Correctness

  • TIS APIs off the main thread: apply()/undo() called TISSelectInputSource on the correction queue; TIS is main-thread-only. Now hopped to main (the selection path already did this).
  • Forward delete corrupted the buffer: keycode 117 fell through to character classification as U+F728 (PUA), desyncing the correction delete count from on-screen text. Now classified as navigation; F17–F19 added to function keys.
  • CapsLock revert double-fire: flagsChanged arrives on both press and release; the second event found no undo state and fell through to converting the current selection. Now edge-detected on the alpha-shift bit flip.
  • Hotkey on "A" silently rebound: key code 0 (letter A) was conflated with "unset" and read back as Space/CapsLock. Now detected via object(forKey:).
  • Mixed-script Latin range included [ \ ] ^ _ (0x41–0x7A); fixed to A–Z/a–z in bothScriptAnalyzerandLayoutDetector`.
  • Stale-context event drops now mark the buffer invalid-until-boundary so a partial word can't be corrected with a wrong delete count.
  • NFC normalization for dictionaries: bloom hashing and partition lookup operate on raw UTF-8 bytes; NFD input (e.g. text with decomposed й/ї/ё) produced false negatives — "no false negatives" is the invariant the whole pipeline leans on. Words are now NFC-normalized at compile time and at query time.
  • Irregular contractions ("can't", "won't", "shan't", …) no longer flagged invalid.

Resource management / UI

  • Hotkey recorder: NSEvent monitor now stopped in deinit (closing Settings mid-recording leaked a monitor that swallowed keystrokes app-wide); non-CapsLock flagsChanged events pass through.
  • Excluded-apps open panel: non-blocking begin instead of app-modal runModal() (which stole frontmost-app focus from the capture pipeline); stale list selections pruned on reload.
  • AppFilter: lock-guarded; persists user deltas instead of a frozen effective set, so future default-blacklist additions reach existing users (legacy key migrated and kept in sync for downgrades).
  • SettingsWindowController: willClose observers no longer accumulate per open/close cycle.
  • Status bar menu: autoenablesItems = false so explicit isEnabled writes take effect; removed a doubled separator.
  • Removed dead CorrectionContext.swift (unreferenced duplicate of LayoutDetector's private logic).

Known issues deliberately not addressed here

  • The 150 ms clipboard-restore timer can still race very slow paste targets (inherent to the synthetic-paste approach; needs an event-driven redesign).
  • InputSourceManager.switchTo trusts a cached source ID (deliberate perf tradeoff after the TISCopyCurrentKeyboardInputSource lag fix).
  • Unbounded permission polling loops and launchAtLogin/SMAppService status reconciliation.

Test plan

  • swift build clean
  • TestRunner: 116/116 passed (incl. synthetic UK↔EN coverage and dictionary perf)
  • InputPipelineTestRunner: 842/842 passed (incl. 100,000-event stress, tap-reset recovery, secure-focus fail-closed)
  • App smoke-launch: starts cleanly, event tap active, no crash
  • Manual QA of CapsLock revert single-fire and forward-delete behavior recommended on real hardware

Settings previously had no way to view or manage the correction
blacklist beyond the "Enable/Disable in Current App" menu-bar item,
which only affected the frontmost app.

- Add an "Excluded Apps" list to SettingsView backed by AppFilter,
  with icons/names resolved via NSWorkspace and remove via a minus
  button (multi-select supported).
- Add app via a menu: pick from currently running apps (a sheet
  listing NSWorkspace.shared.runningApplications, excluding apps
  already excluded and SwitchFix itself), or browse the filesystem
  via NSOpenPanel (defaults to /Applications, but any folder works).
- Resize the Settings window to fit the new section.
Crash / undefined behavior:
- TextCorrector: snapshot pasteboard item data into fresh NSPasteboardItems
  before clearContents(); items read from a pasteboard are invalidated by
  clearing and writing them back throws an ObjC exception (clipboard was
  also permanently lost on selection corrections).
- KeyCodeMapping, KeyboardMonitor, InputSourceManager: keep the
  UCKeyboardLayout pointer inside withUnsafeBytes; returning it out of the
  closure and calling UCKeyTranslate afterwards is undefined behavior.
- BloomFilter: clamp bitCount/hashCount to >= 1 and pad undersized bit
  arrays (public inits could previously trap on modulo-by-zero or index
  out of bounds).

Correctness:
- TextCorrector: hop TISSelectInputSource calls in apply()/undo() to the
  main thread (TIS APIs are main-thread-only; these ran on the correction
  queue), and refuse to build events for empty replacement text (a
  zero-length unicode key event types a literal "a" in many apps).
- KeyboardMonitor: classify forward delete (117) and F17-F19 (64/79/80)
  as navigation/function keys; forward delete previously entered the word
  buffer as U+F728, corrupting correction delete counts.
- KeyboardMonitor: fire the CapsLock revert hotkey only when the
  alpha-shift bit actually flips, so one press cannot trigger revert and
  then fall through to converting the current selection.
- PreferencesManager: distinguish key code 0 ("A") from "unset" via
  object(forKey:); recording a hotkey on the letter A silently rebound it
  to the default (Space / CapsLock).
- ScriptAnalyzer, LayoutDetector: fix the Latin range in mixed-script
  detection (0x41-0x7A included "[ \ ] ^ _ `"; now A-Z / a-z).
- InputEngine/InputStateMachine: after dropping an event with a stale
  capture context, mark the buffer invalid until the next boundary so a
  partial word can't be "corrected" with a wrong delete count.
- Dictionary: normalize words to NFC in compile_dictionary.swift and in
  DictionaryLoader lookups; bloom hashing and partition lookup operate on
  raw UTF-8 bytes, so NFD input previously produced false negatives.
- WordValidator: accept irregular English contractions ("can't", "won't",
  "shan't", ...) whose base does not survive suffix stripping.

Resource management / UI:
- SettingsView: stop the hotkey recorder's NSEvent monitor in deinit
  (closing Settings mid-recording leaked a monitor that swallowed
  keystrokes app-wide) and pass through non-CapsLock flagsChanged events;
  use a non-blocking NSOpenPanel.begin instead of runModal(); prune stale
  list selections on reload.
- AppFilter: guard state with a lock (the singleton is reachable from
  multiple threads) and persist user deltas instead of the frozen
  effective set, so future default-blacklist additions reach existing
  users; legacy key is migrated and kept in sync.
- SettingsWindowController: remove the willClose observer for dead
  windows (registrations accumulated per open/close cycle).
- StatusBarController: disable menu auto-enablement so explicit
  isEnabled writes take effect; drop a doubled separator.
- Remove dead CorrectionContext.swift (unreferenced duplicate of
  LayoutDetector's private suppression logic).

Verified: swift build clean; TestRunner 116/116, InputPipelineTestRunner
842/842 (incl. 100k-event stress); app smoke-launches with active tap.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants