Skip to content

fix reproduced audit bugs#467

Open
BANANASJIM wants to merge 1 commit into
mainfrom
fix/audit-reproduced-bugs
Open

fix reproduced audit bugs#467
BANANASJIM wants to merge 1 commit into
mainfrom
fix/audit-reproduced-bugs

Conversation

@BANANASJIM

@BANANASJIM BANANASJIM commented Jul 4, 2026

Copy link
Copy Markdown
Owner

Summary

  • harden mapping/device config validation for reproduced invalid-target and init-buffer cases
  • preserve timer-staged macro/gesture taps across layer cleanup
  • make touchpad emit state updates commit only after successful writes
  • make config init/edit and immutable uninstall behavior fail safely

Verification

  • zig fmt --check src/ tools/
  • git diff --check
  • targeted Docker regression tests for mapping/device/discovery/edit/init/install/mapper/uinput fixes
  • full Docker test: 1864/1875 tests passed; 11 skipped
  • Docker build produced zig-out/bin/padctl
  • CLI smoke validates reproduced bad configs fail closed
  • pre-push Docker test-tsan: 1874/1885 tests passed; 11 skipped

Summary by CodeRabbit

  • New Features

    • Added stricter configuration validation for device, mapping, and profile names to catch invalid settings earlier.
    • Improved config edit/init flows to prevent accidental overwrites and report validation results more clearly.
  • Bug Fixes

    • Preserved staged tap actions across layer and gesture transitions.
    • Fixed touchpad event retries so failed writes no longer lose release state.
    • Uninstall now cleans up immutable systemd files more completely without removing custom drop-ins.

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR replaces child-process config validation with in-process validation in CLI edit/init flows, adds mapping/device config validation tightening (target resolution, macro steps, layer triggers, init-report bounds, safe mapping names), extends immutable uninstall drop-in cleanup, preserves timer-staged tap bits across mapper layer changes, and fixes touchpad emit state commit ordering.

Changes

CLI Config Commands

Layer / File(s) Summary
In-process edit validation
src/cli/config/edit.zig
Replaces spawned --validate process with direct validate_mod.validateFile() calls via new editorSucceeded/validateEditedFile helpers, printing results or returning error.ValidationFailed/error.EditorFailed; adds unit tests.
Exclusive mapping creation on init
src/cli/config/init.zig
Adds writeNewMappingFile using exclusive file creation; run() now handles error.PathAlreadyExists with guidance messaging; adds a test verifying no overwrite occurs.

Config Validation

Layer / File(s) Summary
Mapping target/macro/layer validation
src/config/mapping.zig
Adds validateTarget, validateMacroSteps, validateRemapSourcesAndTargets helpers wired into validate(); validates layer.trigger as a known ButtonId; adds/updates tests for invalid targets, macro steps, triggers, and layer tap/hold.
Device init-report bounds
src/config/device.zig
Adds MAX_INIT_REPORT_SIZE constant and validates response_prefix (0–255) and report_size (0–64) with new tests.
Safe mapping name lookup
src/config/mapping_discovery.zig
Adds isSafeMappingName() restricting names to alphanumerics/_/-; gates findMapping()/new findMappingFromDirs() on this check; adds traversal-rejection test.
Validation test tolerance
src/tools/validate.zig
Relaxes order-dependent and exact-count assertions in two remap-target error tests.

Install Uninstall Cleanup

Layer / File(s) Summary
Immutable drop-in cleanup
src/cli/install/phase.zig, src/cli/install/tests.zig
Adds removal of the user drop-in file immutable.conf and the user drop-in directory during immutable uninstall; adds a test verifying custom drop-ins are preserved.

Mapper and Touchpad State

Layer / File(s) Summary
Preserve timer-staged taps across layer changes
src/core/mapper.zig, src/test/bugfix_regression_test.zig
Removes clearing of macro_timer_tap_pending/gesture_timer_tap_pending during layer active-change cleanup; adds regression tests confirming staged taps survive layer transitions.
Touchpad emit commit-on-success
src/io/uinput.zig
Stages next_slots/next_btn_touch/next_tracking_id locally in emit() and commits to device state only after a successful write; adds nonblocking pipe test helpers and a retry-preservation regression test.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant EditCLI as edit.zig run()
  participant Editor
  participant Validator as validate_mod

  User->>EditCLI: padctl config edit <name>
  EditCLI->>Editor: spawn editor process
  Editor-->>EditCLI: process term status
  EditCLI->>EditCLI: editorSucceeded(term)
  alt editor exited 0
    EditCLI->>Validator: validateEditedFile(path)
    Validator-->>EditCLI: errors[] or empty
    alt no errors
      EditCLI-->>User: "Validation: OK"
    else errors present
      EditCLI-->>User: print file/message pairs
      EditCLI-->>User: return error.ValidationFailed
    end
  else editor failed
    EditCLI-->>User: return error.EditorFailed
  end
Loading

Possibly related PRs

  • BANANASJIM/padctl#358: Prior PR introduced layer.hold with macro: rejection semantics that this PR's validateTarget-based refactor of layer.tap/layer.hold validation in src/config/mapping.zig directly builds on.
  • BANANASJIM/padctl#462: Both PRs modify handleLayerActiveChanged/layer-change cleanup in src/core/mapper.zig, with this PR preserving macro_timer_tap_pending/gesture_timer_tap_pending in the same cleanup path.
  • BANANASJIM/padctl#232: Both PRs modify uninstall() cleanup logic in src/cli/install/phase.zig, this PR targeting immutable systemd drop-in file/directory removal.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title is related to the changes, but it is too vague to convey the main fix or affected areas. Rename it to mention the primary changes, e.g. "Harden config validation and preserve timer-staged taps".
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/audit-reproduced-bugs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Harden config validation and preserve staged input state across cleanup/retries

🐞 Bug fix 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Validate mapping/device configs more strictly and fail closed on invalid targets/buffers.
• Preserve timer-staged macro/gesture taps across layer cleanup; make touchpad state updates
 transactional.
• Make config init/edit and immutable uninstall behavior safer; expand regression coverage.
Diagram

graph TD
  A["padctl CLI config"] --> B["tools/validate"] --> C["config validators"] --> D["core/Mapper"] --> E["io/uinput Touchpad"]
  A --> F["install/uninstall"] --> G["systemd unit files"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Keep validation as a subprocess invocation of /proc/self/exe
  • ➕ Reuses the exact CLI validate path and output formatting
  • ➕ Keeps config/edit independent from validate module APIs
  • ➖ Harder to reason about failure modes (editor failure vs validate failure)
  • ➖ More overhead and more brittle in constrained environments/tests
2. Centralize all mapping-name safety checks in a shared config/paths utility
  • ➕ Single source of truth for safe mapping identifiers across discovery/install/CLI
  • ➕ Easier to keep install-time and discovery-time naming rules aligned
  • ➖ Requires some refactoring and moving functions/APIs
  • ➖ Potential for churn across multiple call sites

Recommendation: The PR’s approach is sound: it removes the self-exec validation indirection, making edit/init fail safely and deterministically, and it tightens validation at the config layer so invalid targets are rejected early. If mapping name rules are used in more places later, consider extracting isSafeMappingName into a shared naming/path utility to avoid drift.

Files changed (11) +508 / -35

Bug fix (8) +390 / -29
edit.zigValidate edited mapping in-process and only after editor success +56/-16

Validate edited mapping in-process and only after editor success

• Adds in-process validation via tools/validate instead of spawning /proc/self/exe. Skips validation and fails with EditorFailed when the editor exits non-zero, and adds tests for editor exit handling and invalid mappings.

src/cli/config/edit.zig

init.zigFail-safe mapping creation by refusing overwrites +33/-3

Fail-safe mapping creation by refusing overwrites

• Introduces writeNewMappingFile using exclusive create to prevent overwriting existing mappings. Improves error messaging for already-existing profiles and adds a regression test ensuring content is preserved.

src/cli/config/init.zig

phase.zigUninstall removes user immutable drop-in directory +4/-0

Uninstall removes user immutable drop-in directory

• Extends immutable uninstall cleanup to include /etc/systemd/user/padctl.service.d/immutable.conf and attempts to remove the user drop-in directory without deleting unrelated files.

src/cli/install/phase.zig

device.zigValidate device init settings against fixed 64-byte init buffer +89/-1

Validate device init settings against fixed 64-byte init buffer

• Adds MAX_INIT_REPORT_SIZE and validates init response_prefix bytes and init report_size bounds/negatives to prevent invalid writes/casts at runtime. Adds targeted tests for these new validation rules.

src/config/device.zig

mapping.zigReject invalid remap/macro/layer targets and invalid layer triggers +105/-2

Reject invalid remap/macro/layer targets and invalid layer triggers

• Adds validation for remap string targets, macro step targets, and layer tap/hold targets by resolving against known targets and disallowing macro: where needed. Also validates layer.trigger against ButtonId enum and adds multiple tests for invalid cases; updates macro test to use DPadLeft target.

src/config/mapping.zig

mapping_discovery.zigHarden mapping discovery against traversal and unsafe names +35/-0

Harden mapping discovery against traversal and unsafe names

• Adds isSafeMappingName and gates findMapping/findMappingFromDirs to only accept alphanumeric/underscore/hyphen names, rejecting path separators and traversal. Adds a regression test covering ../ and nested paths.

src/config/mapping_discovery.zig

mapper.zigPreserve timer-staged macro/gesture taps across layer cleanup +0/-3

Preserve timer-staged macro/gesture taps across layer cleanup

• Stops clearing macro_timer_tap_pending and gesture_timer_tap_pending during layer-change cleanup so staged taps still apply on subsequent processing.

src/core/mapper.zig

uinput.zigCommit touchpad state only after successful writes; add retry regression test +68/-4

Commit touchpad state only after successful writes; add retry regression test

• Stages next slot/button/tracking-id state locally and commits only after writeExact succeeds, preventing state divergence on partial/failed writes. Adds helpers to fill/drain a nonblocking pipe and a test ensuring a WouldBlock failure preserves release diffs for a later retry.

src/io/uinput.zig

Tests (3) +118 / -6
tests.zigTest immutable user drop-in removal preserves custom drop-ins +41/-0

Test immutable user drop-in removal preserves custom drop-ins

• Adds a regression test that uninstall removes immutable.conf but leaves custom.conf in the same user drop-in directory.

src/cli/install/tests.zig

bugfix_regression_test.zigAdd regression tests for staged taps surviving layer cleanup +71/-0

Add regression tests for staged taps surviving layer cleanup

• Adds two end-to-end mapper tests proving that macro timer-staged and gesture timer-staged gamepad taps still emit correctly after layer active change cleanup.

src/test/bugfix_regression_test.zig

validate.zigMake validation tests robust to multiple reported errors +6/-6

Make validation tests robust to multiple reported errors

• Adjusts tests to tolerate additional validation errors (e.g., due to stricter checks) by searching for a specific expected message and relaxing an exact error count assertion.

src/tools/validate.zig

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Config edit path traversal 🐞 Bug ⛨ Security
Description
padctl config edit constructs the target path by concatenating the user-supplied name into
<mapping_dir>/<name>.toml without enforcing the new safe-name rules, so inputs like "../escape"
can resolve outside the mappings directory. This can lead to editing/validating arbitrary files
(especially impactful when run with elevated privileges).
Code

src/cli/config/edit.zig[R24-25]

    return std.fmt.allocPrint(allocator, "{s}/{s}", .{ dirs[0], filename });
}
Relevance

⭐⭐ Medium

Team rejected similar traversal hardening for SUDO_USER in paths.zig (PR56); no direct evidence for
config edit paths.

PR-#56

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
resolveMappingPath builds filename from the raw name and then concatenates it into a path;
paths.findConfig also concatenates without normalization, so traversal sequences remain effective.
A safe-name policy exists elsewhere (isSafeMappingName) but is not applied here, leaving `config
edit` as a bypass.

src/cli/config/edit.zig[12-25]
src/config/paths.zig[70-99]
src/config/paths.zig[114-125]
src/config/mapping_discovery.zig[12-20]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`src/cli/config/edit.zig` accepts a user-supplied mapping “name” and builds a filesystem path via string concatenation. Because the name is not validated as a safe mapping stem, values containing path separators or traversal segments (e.g. `../...`) can escape the mappings directory.

### Issue Context
The repo now has a shared safe-name policy (`mapping_discovery.isSafeMappingName`) used to prevent traversal and unsafe identifiers. `config edit` should follow the same policy.

### Fix Focus Areas
- src/cli/config/edit.zig[12-25]
- src/cli/config/edit.zig[52-55]
- src/config/mapping_discovery.zig[12-20]

### Suggested fix
- Treat the CLI argument as a *stem*, not a path.
- If the user passes a value ending in `.toml`, strip the suffix to get the stem.
- Reject names that are not `mapping_discovery.isSafeMappingName(stem)` (this also rejects `/`, `..`, etc.).
- Rebuild `filename` as `{stem}.toml` after validation.
- (Optional) If you intentionally want to support editing an arbitrary path, add an explicit flag (e.g. `--path`) and keep the default safe behavior for stems.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Discovery/lookup name mismatch 🐞 Bug ≡ Correctness
Description
discoverMappingsFromDirs returns mapping profile names derived from filenames without applying
isSafeMappingName, but findMapping now refuses unsafe names; code paths that discover a name
then later resolve via findMapping can fail unexpectedly. In particular, supervisor chord-switch
returns p.name from discovery and then fails lookup when that name is deemed unsafe.
Code

src/config/mapping_discovery.zig[R79-91]

/// Find a mapping profile by name. Returns the full path or null. Caller frees.
pub fn findMapping(allocator: std.mem.Allocator, name: []const u8) !?[]const u8 {
+    if (!isSafeMappingName(name)) return null;
    const dirs = try paths.resolveMappingConfigDirs(allocator);
    defer paths.freeConfigDirs(allocator, dirs);
+    return findMappingFromDirs(allocator, name, dirs);
+}

+pub fn findMappingFromDirs(allocator: std.mem.Allocator, name: []const u8, dirs: []const []const u8) !?[]const u8 {
+    if (!isSafeMappingName(name)) return null;
    const filename = try std.fmt.allocPrint(allocator, "{s}.toml", .{name});
    defer allocator.free(filename);
Relevance

⭐⭐⭐ High

Team recently tightened mapping_discovery correctness and drift prevention (PR380) and accepts
aligning shared constraints (PR210).

PR-#380
PR-#210

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Discovery does not apply the safe-name predicate, but lookup does; supervisor chord-switch uses
discovery to return a name and then resolves via findMapping, so an unsafe (but discoverable) stem
can break switching. The CLI list command will also print these names, increasing the chance users
will try to use an unresolvable name.

src/config/mapping_discovery.zig[30-73]
src/config/mapping_discovery.zig[79-93]
src/supervisor.zig[1925-1951]
src/supervisor.zig[1070-1077]
src/cli/list_mappings.zig[21-49]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`mapping_discovery.discoverMappingsFromDirs()` can return profile names that `mapping_discovery.findMapping()` will later reject (because `findMapping()` now enforces `isSafeMappingName`). This creates an internal inconsistency where discovery surfaces a mapping but later lookups (and flows built on them) fail.

### Issue Context
Supervisor chord-switch uses discovery to pick a mapping by `chord_index`, returning `p.name`, and then later resolves that name via `findMapping()`. If a filename stem contains characters outside the safe-name vocabulary (e.g. `.`), chord-switch can select it but then fail to resolve it.

### Fix Focus Areas
- src/config/mapping_discovery.zig[14-20]
- src/config/mapping_discovery.zig[47-73]
- src/config/mapping_discovery.zig[79-93]
- src/supervisor.zig[1925-1951]
- src/supervisor.zig[1070-1077]

### Suggested fix (preferred)
- Apply `isSafeMappingName()` during discovery:
 - In `discoverMappingsFromDirs`, after computing `name = stem`, `continue` unless `isSafeMappingName(name)`.
 - This guarantees that any discovered `.name` is resolvable via `findMapping`.

### Alternative fix
- Keep discovery as-is, but change chord-switch to return `p.path` (absolute path) instead of `p.name`, and pass that into `handleSwitch`; this avoids re-resolving by name.

### Notes
If you intentionally want to support dotted stems (e.g. `foo.v1.toml`), then you should instead relax `isSafeMappingName` to match the supported filename grammar, and ensure install-time identifier rules are updated consistently.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread src/cli/config/edit.zig
Comment on lines 24 to 25
return std.fmt.allocPrint(allocator, "{s}/{s}", .{ dirs[0], filename });
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Config edit path traversal 🐞 Bug ⛨ Security

padctl config edit constructs the target path by concatenating the user-supplied name into
<mapping_dir>/<name>.toml without enforcing the new safe-name rules, so inputs like "../escape"
can resolve outside the mappings directory. This can lead to editing/validating arbitrary files
(especially impactful when run with elevated privileges).
Agent Prompt
### Issue description
`src/cli/config/edit.zig` accepts a user-supplied mapping “name” and builds a filesystem path via string concatenation. Because the name is not validated as a safe mapping stem, values containing path separators or traversal segments (e.g. `../...`) can escape the mappings directory.

### Issue Context
The repo now has a shared safe-name policy (`mapping_discovery.isSafeMappingName`) used to prevent traversal and unsafe identifiers. `config edit` should follow the same policy.

### Fix Focus Areas
- src/cli/config/edit.zig[12-25]
- src/cli/config/edit.zig[52-55]
- src/config/mapping_discovery.zig[12-20]

### Suggested fix
- Treat the CLI argument as a *stem*, not a path.
- If the user passes a value ending in `.toml`, strip the suffix to get the stem.
- Reject names that are not `mapping_discovery.isSafeMappingName(stem)` (this also rejects `/`, `..`, etc.).
- Rebuild `filename` as `{stem}.toml` after validation.
- (Optional) If you intentionally want to support editing an arbitrary path, add an explicit flag (e.g. `--path`) and keep the default safe behavior for stems.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines 79 to 91
/// Find a mapping profile by name. Returns the full path or null. Caller frees.
pub fn findMapping(allocator: std.mem.Allocator, name: []const u8) !?[]const u8 {
if (!isSafeMappingName(name)) return null;
const dirs = try paths.resolveMappingConfigDirs(allocator);
defer paths.freeConfigDirs(allocator, dirs);
return findMappingFromDirs(allocator, name, dirs);
}

pub fn findMappingFromDirs(allocator: std.mem.Allocator, name: []const u8, dirs: []const []const u8) !?[]const u8 {
if (!isSafeMappingName(name)) return null;
const filename = try std.fmt.allocPrint(allocator, "{s}.toml", .{name});
defer allocator.free(filename);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

2. Discovery/lookup name mismatch 🐞 Bug ≡ Correctness

discoverMappingsFromDirs returns mapping profile names derived from filenames without applying
isSafeMappingName, but findMapping now refuses unsafe names; code paths that discover a name
then later resolve via findMapping can fail unexpectedly. In particular, supervisor chord-switch
returns p.name from discovery and then fails lookup when that name is deemed unsafe.
Agent Prompt
### Issue description
`mapping_discovery.discoverMappingsFromDirs()` can return profile names that `mapping_discovery.findMapping()` will later reject (because `findMapping()` now enforces `isSafeMappingName`). This creates an internal inconsistency where discovery surfaces a mapping but later lookups (and flows built on them) fail.

### Issue Context
Supervisor chord-switch uses discovery to pick a mapping by `chord_index`, returning `p.name`, and then later resolves that name via `findMapping()`. If a filename stem contains characters outside the safe-name vocabulary (e.g. `.`), chord-switch can select it but then fail to resolve it.

### Fix Focus Areas
- src/config/mapping_discovery.zig[14-20]
- src/config/mapping_discovery.zig[47-73]
- src/config/mapping_discovery.zig[79-93]
- src/supervisor.zig[1925-1951]
- src/supervisor.zig[1070-1077]

### Suggested fix (preferred)
- Apply `isSafeMappingName()` during discovery:
  - In `discoverMappingsFromDirs`, after computing `name = stem`, `continue` unless `isSafeMappingName(name)`.
  - This guarantees that any discovered `.name` is resolvable via `findMapping`.

### Alternative fix
- Keep discovery as-is, but change chord-switch to return `p.path` (absolute path) instead of `p.name`, and pass that into `handleSwitch`; this avoids re-resolving by name.

### Notes
If you intentionally want to support dotted stems (e.g. `foo.v1.toml`), then you should instead relax `isSafeMappingName` to match the supported filename grammar, and ensure install-time identifier rules are updated consistently.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/cli/install/phase.zig (1)

575-596: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Inconsistent drop-in cleanup: system-scope still wipes custom drop-ins.

The new user-scope cleanup (Lines 593-595) correctly uses deleteDirAbsolute, which only succeeds when the directory is empty — preserving any custom *.conf files a user added alongside immutable.conf. However, the system-scope cleanup a few lines above (Lines 590-592, unchanged) still uses std.fs.deleteTreeAbsolute on /etc/systemd/system/padctl.service.d, which unconditionally deletes the entire directory tree including any custom drop-ins.

Since this cohort's explicit goal is to preserve custom drop-ins on immutable uninstall, a system-wide immutable install (sudo padctl install --immutable, no --user-service) will still lose custom drop-in files that the user-scope path now protects. Consider aligning the system-scope cleanup to also only remove the directory if it's empty (or only when immutable.conf was the sole surviving file), matching the new user-scope behavior.

🛠️ Suggested fix to align system-scope behavior
         const dropin_dir = try std.fmt.allocPrint(allocator, "{s}/etc/systemd/system/padctl.service.d", .{destdir});
         defer allocator.free(dropin_dir);
-        std.fs.deleteTreeAbsolute(dropin_dir) catch {};
+        std.fs.deleteDirAbsolute(dropin_dir) catch {};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/install/phase.zig` around lines 575 - 596, The immutable uninstall
cleanup in `phase.zig` still wipes custom system-scoped drop-ins because the
`deleteTreeAbsolute` call on `padctl.service.d` removes the entire directory
tree. Update the system-scope path in the same cleanup block that handles
`effective_immutable` so it matches the `user_dropin_dir` behavior: use
`std.fs.deleteDirAbsolute` (or equivalent empty-directory-only removal) instead
of recursively deleting the directory. Keep the existing file deletions for
`immutable.conf`, and ensure the cleanup only removes the drop-in directory when
it is empty so custom `*.conf` files are preserved.
src/cli/config/edit.zig (1)

12-25: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

src/cli/config/edit.zig:12-25 — Reject raw name inputs here. resolveMappingPath() concatenates the caller-supplied value into dirs[0]/{filename}, and paths.findConfig() only joins paths. A ../../... name can escape the mapping dirs and make config edit open/validate an arbitrary file. Reuse isSafeMappingName() here or reject path separators/...

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/config/edit.zig` around lines 12 - 25, resolveMappingPath() is
currently accepting raw caller-supplied names and building a path with dirs[0]
and filename, which can let config edit escape the mapping directories. Add
validation in resolveMappingPath() (or call isSafeMappingName() before any path
resolution) to reject path separators, .. segments, and other unsafe names
before using paths.findConfig() or the fallback allocPrint join. Keep the fix
localized to resolveMappingPath() and the existing mapping-name validation
helper.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/test/bugfix_regression_test.zig`:
- Around line 219-256: The regression test in the gesture/macro cleanup scenario
is not hitting the cleanup path at the promotion frame, so it may miss the
stale-bit bug. Update the test around the existing helper setup in
bugfix_regression_test.zig so the layer activation change and staged tap
promotion happen in the same apply() frame that exercises active_changed
cleanup, using the existing helpers.makeMapper, onMacroTimerExpired, and apply
flow to trigger the bug before the fix and verify the tap bit is cleared
correctly afterward.

---

Outside diff comments:
In `@src/cli/config/edit.zig`:
- Around line 12-25: resolveMappingPath() is currently accepting raw
caller-supplied names and building a path with dirs[0] and filename, which can
let config edit escape the mapping directories. Add validation in
resolveMappingPath() (or call isSafeMappingName() before any path resolution) to
reject path separators, .. segments, and other unsafe names before using
paths.findConfig() or the fallback allocPrint join. Keep the fix localized to
resolveMappingPath() and the existing mapping-name validation helper.

In `@src/cli/install/phase.zig`:
- Around line 575-596: The immutable uninstall cleanup in `phase.zig` still
wipes custom system-scoped drop-ins because the `deleteTreeAbsolute` call on
`padctl.service.d` removes the entire directory tree. Update the system-scope
path in the same cleanup block that handles `effective_immutable` so it matches
the `user_dropin_dir` behavior: use `std.fs.deleteDirAbsolute` (or equivalent
empty-directory-only removal) instead of recursively deleting the directory.
Keep the existing file deletions for `immutable.conf`, and ensure the cleanup
only removes the drop-in directory when it is empty so custom `*.conf` files are
preserved.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bcc2a2c0-f191-474b-9981-5d4f42de2f05

📥 Commits

Reviewing files that changed from the base of the PR and between c5282bb and fbad684.

📒 Files selected for processing (11)
  • src/cli/config/edit.zig
  • src/cli/config/init.zig
  • src/cli/install/phase.zig
  • src/cli/install/tests.zig
  • src/config/device.zig
  • src/config/mapping.zig
  • src/config/mapping_discovery.zig
  • src/core/mapper.zig
  • src/io/uinput.zig
  • src/test/bugfix_regression_test.zig
  • src/tools/validate.zig
💤 Files with no reviewable changes (1)
  • src/core/mapper.zig

Comment on lines +219 to +256
test "macro timer-staged gamepad tap survives layer active change cleanup" {
const allocator = testing.allocator;

var ctx = try helpers.makeMapper(
\\[[macro]]
\\name = "delayed_a"
\\steps = [
\\ { delay = 10 },
\\ { tap = "A" },
\\]
\\
\\[[layer]]
\\name = "menu"
\\trigger = "Select"
\\activation = "toggle"
\\
\\[remap]
\\M1 = "macro:delayed_a"
, allocator);
defer ctx.deinit();
var m = &ctx.mapper;

const m1_mask = helpers.btnMask(.M1);
const select_mask = helpers.btnMask(.Select);
const a_mask = helpers.btnMask(.A);

_ = try m.apply(.{ .buttons = m1_mask | select_mask }, 16, 0);
try testing.expectEqual(@as(usize, 1), m.active_macros.items.len);

_ = m.onMacroTimerExpired(11 * std.time.ns_per_ms);
try testing.expect((m.macro_timer_tap_pending & a_mask) != 0);

const ev = try m.apply(.{ .buttons = m1_mask }, 16, 12 * std.time.ns_per_ms);
try testing.expect((ev.gamepad.buttons & a_mask) != 0);

const release = try m.apply(.{ .buttons = m1_mask }, 16, 13 * std.time.ns_per_ms);
try testing.expectEqual(@as(u64, 0), release.gamepad.buttons & a_mask);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect where staged tap bits are cleared and how toggle layers report active_changed.
rg -nP 'macro_timer_tap_pending|gesture_timer_tap_pending' src/core/mapper.zig
echo '--- handleLayerActiveChanged ---'
ast-grep outline src/core/mapper.zig --match handleLayerActiveChanged --view expanded
echo '--- toggle activation / active_changed on release ---'
rg -nP 'active_changed|toggle' src/core/layer.zig -C2

Repository: BANANASJIM/padctl

Length of output: 15400


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- mapper apply / staged tap promotion ---'
sed -n '360,430p' src/core/mapper.zig

echo '--- layer trigger processing around toggle semantics ---'
sed -n '80,150p' src/core/layer.zig

echo '--- regression tests around the macro/gesture cases ---'
sed -n '219,288p' src/test/bugfix_regression_test.zig

echo '--- any mutation/audit tests for staged tap clearing ---'
rg -n 'macro_timer_tap_pending|gesture_timer_tap_pending|active_changed|tap_pending' src/test src/core -C2

Repository: BANANASJIM/padctl

Length of output: 29762


Rework the gesture regression to hit cleanup on the promotion frame
src/test/bugfix_regression_test.zig:258-288 The macro case already exercises the release-edge toggle and should fail pre-fix; the gesture case still toggles the layer at t=10, so the staged tap is promoted later with no concurrent active_changed and won’t catch the stale-bit clearing bug.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/test/bugfix_regression_test.zig` around lines 219 - 256, The regression
test in the gesture/macro cleanup scenario is not hitting the cleanup path at
the promotion frame, so it may miss the stale-bit bug. Update the test around
the existing helper setup in bugfix_regression_test.zig so the layer activation
change and staged tap promotion happen in the same apply() frame that exercises
active_changed cleanup, using the existing helpers.makeMapper,
onMacroTimerExpired, and apply flow to trigger the bug before the fix and verify
the tap bit is cleared correctly afterward.

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.

1 participant