TransferService: harden OBEX incoming-file handling (7 audit findings) - #3320
Open
geraldo-netto wants to merge 44 commits into
Open
TransferService: harden OBEX incoming-file handling (7 audit findings)#3320geraldo-netto wants to merge 44 commits into
geraldo-netto wants to merge 44 commits into
Conversation
Add AGENTS.md (canonical agent behavior rules) and CLAUDE.md pointer. Populate TODO.md with project-wide rescan findings, one table per AGENTS.md review category (security, STRIDE, data governance, watchdog, state machine, composition, dependency, extensibility, legacy, configuration, platform, data structure, vectorization, robustness, ui/ux, documentation, plus existing performance/concurrency/SOLID tables). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
wire-1 (DHCP renew routed to wrong proxy) and wire-2 (AppletDhcpClientService never instantiated) fixed on branch fix/wire-dhcp-proxy. wire-3 recorded as a deliberately-rejected false positive: AppletStatusIconService is a required signal-only proxy in Tray.py. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SpeedCalc log-bounding + zero-elapsed guard shipped on branch perf/small-optimizations. Park the ManagerProgressbar cleanup and GtkAnimation timer items: both are GTK-app-bound / architectural, not low-risk and not unit-testable headless. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
NetConf iptables arg handling + IPv4 validation, Rfcomm ps-output parsing, and PPPConnection APN validation shipped on branch security/input-validation. Removes sec-1..sec-4 and the duplicate stride-3 row. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
DhcpClient client-state guards + poll fix, mechanism Network handler-key validation, NetConf cleanup next() default, and Rfcomm open error handling shipped on branch reliability/crash-guards. Removes rel-1, rel-4, rel-6, rel-8, rel-10 and the duplicate stride-2 row. Remaining rel items are the PPPConnection/Services paths that need a live GLib loop. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the flat category-name list with a deduplicated Category definitions subsection: each review category carries a concise definition and, where relevant, the framework to cite per finding (STRIDE, OWASP ASVS, Laws of UX). Scoped to this project's domain (D-Bus, polkit, network plugins, GTK, gettext, meson/autotools CI). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add 33 findings across previously-empty review categories: dependability, distributed systems, time & scheduling correctness, memory and cpu management, system design, CLI / option integrity, product engineering, design thinking, and test / fuzz coverage; plus rel-13 (NetConf clean_up StopIteration). Deduped against existing findings; dropped false/already- fixed candidates (DhcpClient timeout no-op, blueman-report nonexistent, overlaps with perf-/arch-). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Remove findings whose fix exists on a dedicated branch or already landed on main, to stop double-tracking: - perf-2/6/7, scale-4, dup-3 -> fix/manager-device-list-perf (GetAll, single power-level timer, cached uuids, batched set) - conc-1/2, sysd-1 -> fix/bluez-base-concurrency-guards (Cancellable plumbing, WeakValueDictionary instance cache + destroy cleanup) - rel-2/3/5/7, sm-1 -> fix/ppp-crash-guards (init attrs, guarded cleanup, source removal) - rel-13 -> reliability/crash-guards (next(...,None)) - ds-1 -> already on main (SpeedCalc deque, blueman-project#3289) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Each finding's description starts with file:line, so sorting every TODO.md table by description clusters same-file items for batch fixing. Add the rule to AGENTS.md and sort all existing tables accordingly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
comp-1 (InstanceRegistry extraction), perf-4 (cache-first Base.get), cache-1 (explicit freshness/stale), obs-13 (log cached fallback) are implemented on the perf/bluez-base-property-cache branch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
rob-4 (track/cancel discovery progress timer), perf-3 (single-pass clear), and vec-3 (release row references before clearing) are implemented on the perf/devicelist-clear-discovery branch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment only the non-obvious (why/constraints/edge cases); delete redundant comments rather than write them. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Exact powers of 1024 fell through to the GB branch because both band edges used strict `<`, so format_bytes(1024) returned (9.5e-07, "GB") instead of (1.0, "KB"); 1 MiB and 1 GiB were mislabelled the same way. Drop the lower-bound comparison and rely on the cascading upper bounds so each boundary lands in its own unit. Add test/test_functions.py covering the 1024/1048576/1073741824 boundaries plus zero, sub-KB, mid-band, and a huge value. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
adapter_path_to_name parses a D-Bus object path with a greedy `re.search(r".*(hci[0-9]*)", path)` and had no tests. Add cases pinning the current contract: normal paths, None/empty -> None, no-hci -> None, case sensitivity, trailing device segments, zero-digit "hci", greedy last-occurrence selection, and embedded matches. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…zz-5, data-3)
parse_os_release was nested inside log_system_info and split each line
with `line.split("=")`, so a valid quoted value containing "=" (e.g.
PRETTY_NAME="Name=Variant") raised ValueError and was dropped from the
logged system info.
Promote it to module scope, parse with str.partition("=") so only the
first "=" separates key from value, and skip blank lines explicitly.
Add tests for basic keys, a value containing "=", unquoted values,
comment/blank lines, lines without "=", and a missing file.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
have() hand-rolled a PATH scan with a hardcoded ":/sbin:/usr/sbin" suffix and checked os.access(path, os.EX_OK) -- os.EX_OK is 0, i.e. F_OK, so it only confirmed existence, not executability. Delegate the lookup to shutil.which, which honours the executable bit, and append the sbin directories to the search path only when they are not already present. Add tests covering found/not-found, sbin-dir augmentation, and de-duplication. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
create_logger unconditionally constructed SysLogHandler(address="/dev/log"), which raises on platforms and minimal containers without that socket, taking down the whole process at logger setup. Guard the handler construction and, on OSError, log a warning and keep the basicConfig stderr handler instead. Add tests for the available, unavailable, and syslog-disabled paths. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
--loglevel had no choices and no help, so a typo like `--loglevel verbose` silently coerced to WARNING across all entry points, leaving users with quieter logs than intended and no error. Add case-insensitive choices (debug/info/warning/error/critical) via type=str.lower plus help text, so argparse rejects unknown values clearly. Existing consumers compare args.LEVEL.upper(), which is unaffected. Add tests for default, lowercasing, rejection, help/choices metadata, the syslog flag, and disabling loglevel. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "applet needs to be running" and "Failed to enable bluetooth" messages went to stdout via print(), bypassing the logging configuration and leaving no record in syslog/journald. Route both through logging.error (with exc_info on the DBusProxyFailed path, replacing the redundant logging.exception). Add tests for the missing-applet exit path and the no-PowerManager early return. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
set_proc_title unconditionally loaded libc.so.6 and called prctl(PR_SET_NAME), both Linux/glibc specific. On other platforms the LoadLibrary or prctl lookup raises and crashes process startup. Return early as a no-op on non-Linux, wrap the libc/prctl access in try/except returning -1 on failure, and document the behaviour in the docstring. Add tests for the non-Linux no-op, the Linux prctl path, and the libc-unavailable failure. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
launch() accepted only a full command-line string, so callers embedded
options directly in cmd (e.g. Notes.py built
"blueman-sendto --delete --device={addr}"), making argument boundaries
depend on GLib command-line parsing rather than an argv contract.
Add an optional args iterable: when provided, the program token and each
argument are shell-quoted individually via GLib.shell_quote, so spaces,
quotes, and shell metacharacters can never cross argument boundaries.
The legacy string form still works when args is omitted (deprecated).
Migrate the Notes.py send-note call site to the argv form.
Add tests for the legacy form, argv quoting, metacharacter
neutralization, the launch result, and path-to-GFile conversion.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
doc-1 framed these as dead helpers to "document or remove", but they are live: every entry point in apps/*.in imports and calls them (the audit missed the .in sources). Document them instead of removing, noting their role and the syslog fallback / shared CLI surface. set_proc_title was already documented alongside the leg-5 platform guard. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
set_proc_title, create_logger, and create_parser were flagged as unused with "no production callers", but the audit only scanned *.py and missed apps/*.in: all three are imported and called by every blueman binary. Deleting them would break startup of every executable. Move them from "unused functions" to "Audit picks deliberately rejected" with the evidence and pointers to the real fixes made instead (cli-1, plat-8, leg-5, doc-1). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Both deprecated APIs (Gtk.Dialog.run and Gtk.ImageMenuItem) still function under GTK3. leg-1 is a synchronous startup gate whose async rewrite ripples into every entry point and needs a live main loop; leg-2's create_menuitem is a 20-call-site chokepoint whose replacement changes menu-item child structure. Neither can reach genuine coverage headless or be validated without running the GUI, matching the existing parked-item rationale (perf-12/perf-14). Park for the GTK4 migration. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Remove the 14 NetConf.py findings addressed on the fix/netconf-hardening branch (PR to upstream): obs-2, obs-5, plat-3, plat-9, plat-4, cfg-3, depend-2, depend-1, mem-3, sm-7, wd-7, dist-2, dist-1, dist-4. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Remove the eight Sendto.py findings addressed on the fix/sendto-hardening branch (PR to upstream): time-2, rob-7, vec-1, dup-7, obs-8, prodeng-1, prodeng-2, ux-1. Park leg-3/ux-6 (async-dialog conversion) with rationale alongside the other parked GTK deprecation items. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ec-1) The fallback "incoming files directory does not exist" notification interpolated the user-configured `shared-path` and the default fallback path into Pango `<b>%s</b>` markup without escaping. A path containing markup or entities could alter the notification body, and daemons that render body markup could misinterpret it. Escape both interpolated paths with `html.escape` before formatting. Add regression tests covering `<`/`>`, `&`, and quote characters. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The reset action on the invalid-share-path notification used a bare English string, so it never localized. Wrap it in `_()`; the file is already listed in `po/POTFILES.in`, so the string is now extracted. Add a test that the label is routed through gettext. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 60s timeout that revokes a one-shot OPP authorization read `self._pending_transfer['address']` when it fired, not when it was scheduled. An overlapping push or cleared pending state could revoke the wrong device or trip the assertion. Capture the accepted address as a default argument bound at schedule time and revoke it idempotently. Switch `_allowed_devices` to a set so removal is `discard`-style and a double fire cannot raise. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A single `_pending_transfer` slot held the in-flight authorization, so a second incoming push arriving before the user answered the first overwrote the state the first notification's callback later read — the wrong file could be accepted or the action could fail. Key pending records by `transfer_path` in a dict and bind each notification callback to its own immutable record (captured as a default argument). The callback pops only its own record on accept/reject, so overlapping requests stay independent. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Collision handling prefixed only a second-resolution timestamp and then moved without rechecking the timestamped destination, so two same-named transfers completing in the same second could collide and overwrite/fail. Add `reserve_destination`, which walks `name`, `timestamp_name`, `timestamp_1_name`, ... and reserves the first free candidate with an O_EXCL create, then move the source onto the reservation. Clean up the reserved placeholder if the move fails. Covered by same-second and fuzz tests over odd filenames. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Agent took the whole `BluemanApplet` only to reach `parent.Manager.get_adapter`/`find_device` for the pushing device's name and trust state, coupling the D-Bus agent to the applet object graph. Introduce a `DeviceResolver` callable `(source, address) -> (name, trusted)`; the plugin supplies one bound to its `parent.Manager` and the Agent depends only on that. Drops the `BluemanApplet` import and makes authorization unit-testable with a plain resolver stub. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add focused unit tests for the incoming-transfer paths that previously had none: overlapping authorization requests, allowed-device expiry, filename collisions, failed final moves, transfer counters, session summaries, auto-accept, agent control, and share-path resolution. They exercise the helpers extracted for data-1/sm-8/dec-1 with mocked Transfer, Session, and Notification. Module statement coverage is now 83% (from 51%). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Remove AGENTS.md, CLAUDE.md, and TODO.md; they are project-rescan-todo scaffolding, not part of the TransferService changes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Document that a resolver may raise and that callers treat a raised resolver as an untrusted/unknown device — a behavioral contract the type alias alone does not convey. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up review of TransferService.py surfaced four issues beyond the original rescan items: A. reliability — _on_session_removed reported the silent/normal transfer counts but never reset them, so every session after the first double-counted background transfers. Reset both counters once the summary has consumed them. B. correctness — `_handlerids` was a class-level mutable list shared by every instance; _on_dbus_name_appeared mutated the class list before any instance copy existed. Initialize it per-instance in on_load. D. robustness — _make_share_path built `Path(GLib.get_user_special_dir( DOWNLOAD))` unconditionally; when XDG returns no download dir this raised TypeError on Path(None) before the `~` fallback could run. Build default_path only when XDG yields a value so the existing fallback applies. Also collapse a redundant `elif not success` to `else` (C). Tests bring module coverage to 100% (50 cases), including agent lifecycle, D-Bus name appeared/vanished, on_load/on_unload, and every _make_share_path branch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace logging.error(..., exc_info=True) with logging.exception(...) at the three exception handlers (applet proxy failure, set_proc_title, socket creation). Equivalent output; clearer intent and satisfies the static-analysis rule. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
test_resolver_changed truncated the resolver file then drained only already-queued GLib events. The Gio.FileMonitor CHANGED event is delivered asynchronously and was often not queued yet, so the assertion saw the changed signal as never emitted. Block the main loop until the signal fires, bounded by a 5s timeout backstop. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Summary
Hardens
blueman/plugins/applet/TransferService.py— the OBEX incoming-file(OPP) plugin. Started from 7
TODO.mdrescan findings scoped to this file; afollow-up review surfaced 4 more. 11 findings total, one commit per finding,
each with tests. Module statement coverage is now 100% (50 unit + fuzz cases).
No behavioral change for the happy path; every change either closes a security/
correctness gap or makes existing intent actually hold.
Findings
sec-1 — Pango markup injection in the invalid-dir notification (security)
shared-pathand the fallback path into<b>%s</b>markup without escaping.<,&, quotes) could alter the notification body; daemons that render body markup could misinterpret it.html.escapeboth interpolated paths before formatting. Regression tests with<b>,&, and quote characters.i18n-3 — Untranslated action label (i18n)
_()(file already inpo/POTFILES.in, so it's extracted). Test asserts the label routes through gettext.rel-10 — Wrong device revoked by removal timeout (reliability)
self._pending_transfer['address']when it fired, not when scheduled.assert._allowed_devicesis now asetrevoked withdiscardso a double-fire can't raise. Tests cover correct revoke, idempotency, and overlap.sm-8 — Single pending slot clobbered by overlapping pushes (scalability/concurrency)
_pending_transferslot held the in-flight authorization, so a second push arriving before the user answered overwrote the state the first notification's callback read.transfer_pathin a dict; bind each notification callback to its own immutable record (default-arg capture); the callback pops only its own record. Test drives two overlapping requests and asserts independence.data-1 — Same-second filename collision can overwrite (data integrity)
reserve_destinationwalksname,timestamp_name,timestamp_1_name, … and reserves the first free candidate with anO_EXCLcreate, then moves the source onto the reservation; the placeholder is cleaned up if the move fails. Same-second and fuzz tests over odd filenames (spaces, unicode, dotfiles, long names).dec-1 — Agent coupled to the whole applet (decoupling/SOLID)
Agenttook the entireBluemanAppletonly to reachparent.Manager.get_adapter/find_devicefor the device name + trust state.DeviceResolvercallable(source, address) -> (name, trusted); the plugin supplies one bound to itsparent.Manager, and the Agent depends only on that. Drops theBluemanAppletimport. The resolver's raise→untrusted-fallback contract is documented on the type alias. Authorization is now testable with a 1-line stub.test-3 — No focused tests for authorization/completion (test coverage)
A — Transfer counters never reset (reliability) [follow-up]
_on_session_removedreported_silent_transfers/_normal_transfersbut never zeroed them.B — Class-level mutable
_handlerids(correctness) [follow-up]_handlerids = []was a class attribute shared by all instances;_on_dbus_name_appearedappended to the class list before any instance copy existed.self._handlerids = []per-instance inon_load. Test asserts the instance list is distinct from the class attribute.C — Redundant
elif not success(code clarity) [follow-up]successis a bool;if success / elif not successcollapsed toif/else.D —
Path(None)crash when XDG has no download dir (robustness) [follow-up]_make_share_pathbuiltPath(GLib.get_user_special_dir(DOWNLOAD))unconditionally; when XDG returns no dir this raisedTypeErroronPath(None)before the existing~fallback could run — making that fallback dead code.default_pathonly when XDG yields a value, so the~fallback applies. Test patches XDG toNone.Unrelated CI fix — flaky
test_resolver_changedAdding this PR's tests pushed the suite to 392 tests and shifted timing enough to expose a pre-existing flake in
test/main/test_dns_server_provider.py(not part of the OPP hardening). CI failed on it fortest (3.12)andtest (3.14):_test_changedtruncated the watched resolver file, then drained only already-queued GLib events (while context.pending(): context.iteration()). TheGio.FileMonitorCHANGEDevent is delivered asynchronously — it was frequently not queued yet when the drain ran, so thechangedsignal looked like it never fired. The systemd-resolved variant emits synchronously over D-Bus, so only the file-monitor variant raced.changedsignal actually fires (context.iteration(may_block=True)), bounded by a 5s timeout backstop so a genuine miss still fails fast instead of hanging. Deterministic for both the file-monitor and D-Bus variants.Kept as a standalone commit so the OPP hardening history stays per-finding.
Testing
pycodestyle+pyflakesclean;mypy --strictclean on the moduleBLUEMAN_SOURCE=1 python3 -m unittest test.plugins.applet.test_transfer_service🤖 Generated with Claude Code