1. Overview
The RootGuardClient.cpp implementation is the bridge between the GTK RootGuard page and privileged RootGuard service/action helpers. It executes helper programs without a shell, parses runtime state and policy-view files, tails the JSONL event log, filters ignored paths, serializes asynchronous requests, and returns consolidated snapshots to the UI.
The file integrates the following components, APIs, or runtime concepts:
- bastionguard-rootguard-service – status/start/stop/reload/restart helper
- bastionguard-rootguard-action – policy, restore, quarantine, and service-unblock helper
- pkexec – authorization boundary for administrative operations
- /var/log/bastionguard/rootguard-events.jsonl – recent event source
- /run/bastionguard-rootguard.policy-view – UI-safe runtime policy projection
- RootGuardEvent – JSON parsing, formatting, and service-path classification
2. Command Execution Helper
run_command() creates a pipe, forks, redirects both stdout and stderr into the pipe, and uses execv() with an argument vector. The parent reads captured output and converts normal exits or signals into a numeric exit code.
Commands are passed as discrete arguments instead of shell text, avoiding shell expansion in this layer.
3. Runtime Status Parsing
parse_status() consumes key/value output from the service helper. It recognizes active, init, state, mode, metadata_action, global_metadata_action, auto_block_services, block_user_home, and pid.
The UI-facing immediateProtection flag is derived rather than read directly: it is true only when mode is enforce and metadata_action is block.
4. Policy View and Ignored Paths
The client reads /run/bastionguard-rootguard.policy-view to obtain ignored exact/recursive paths and the current application-rule lists. Exact ignored entries are compared after lexical normalization; recursive entries use component-aware prefix matching.
This filtering is applied before events are exposed to the page, so generated or explicitly ignored objects do not reappear only because they are present in the historical log.
5. Recent Event Tail
read_recent_events() opens the JSONL log in binary mode and limits reading to the final 2 MiB. When the seek starts in the middle of the file, one potentially partial JSON line is discarded.
Only the newest requested records are retained in a deque; the snapshot requests at most 400 events. Each line is parsed with parse_event_json() and then checked against ignored-path rules.
6. Request Serialization and Refresh Coalescing
The client owns one worker thread. Public methods only enqueue requests. Duplicate pending Refresh requests are coalesced, preventing the one-second UI refresh timer from filling the queue while a slower privileged action is running.
After every non-quit request, the worker executes the action, collects a fresh snapshot, stores it under the mutex, and emits the dispatcher.
7. Service Lifecycle Actions
Start, stop, reload, and restart requests execute the service helper through pkexec:
/usr/bin/pkexec /usr/libexec/bastionguard/bastionguard-rootguard-service <action>
Captured helper output becomes the user-facing operation message. If a failed command prints nothing, the client synthesizes an exit-code diagnostic.
8. Immediate Protection Switching and Rollback
Enabling or disabling immediate protection first calls the action helper with set-protection --state on|off, then reloads the RootGuard service so the edited policy is applied.
If reload fails while enabling protection, the client attempts a compensating set-protection --state off. The result is appended to the operation message, including an explicit warning if the rollback itself fails.
9. Application Rule Persistence
The three rule lists are converted into repeated command-line options for set-application-rules. After successful persistence, RootGuard is restarted so inode identities are rebuilt from the new policy.
bastionguard-rootguard-action set-application-rules
--trusted-application <path> ...
--trusted-executable <path> ...
--denied-executable <path> ...
10. Incident Restore and Removal
Restore requires an actionable path. If the event contains no baseline metadata and refers to a recognized service definition, the client requests unblock-service; otherwise a denied pre-commit event is treated as requiring no metadata restoration.
When baseline metadata is available, restore passes the original mode, UID, GID, device, and inode to the privileged action helper. Removal passes path/device/inode and requests the helper’s quarantine operation.
11. Snapshot Collection
collect_snapshot() combines the service-helper status result, up to 400 recent filtered events, application rules from the runtime policy view, and the operation result. A failed status command with no recognized state is surfaced as unavailable.
12. Runtime and Security Considerations
- No shell command construction: child processes are launched with
execv()argument arrays. - Privilege separation: read-only status/log operations stay unprivileged; state-changing operations cross
pkexec. - Fail-safe enablement: a failed enforcement reload triggers an attempt to restore audit mode.
- Bounded log ingestion: the UI tails a maximum of 2 MiB and keeps a bounded event count.
- GTK thread safety: worker results are delivered through
Glib::Dispatcher.
