Bug 2054320: Post bug.modify events on dependency changes - #2666
Bug 2054320: Post bug.modify events on dependency changes#2666justdave wants to merge 8 commits into
Conversation
b129fcc to
267aa35
Compare
|
force-push was backing out the last commit, that was meant for a different branch. |
There was a problem hiding this comment.
Pull request overview
Adds indirect Push events for bugs affected by dependency or regression relationship edits.
Changes:
- Generates counterpart
bug.modifyevents. - Adds
indirect_changemetadata to event payloads.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
dklawren
left a comment
There was a problem hiding this comment.
High-effort review of extensions/Push/Extension.pm (new counterpart bug-modify event logic). 4 finders → 9 candidates → 7 verifiers → 6 findings survived (0 refuted). Ranked most-severe first.
🔴 Correctness
- indirect_change.source_bug_id bypasses Webhook redaction — private-bug-id leak
extensions/Push/Extension.pm:382 · CONFIRMED
indirect_change (carrying source_bug_id) is attached to the counterpart event but is not stripped by the Webhook connector's private-modify redaction, unlike event.changes which are deleted for private targets.
▎ Leak: Private source bug A gains/loses a dependency on public bug B. The counterpart modify event for B carries indirect_change.source_bug_id = A. Webhook.pm:166 only deletes event.changes when the target is private — but B is public, so no redaction runs. _owner_can_see only checks visibility of B (the target), never the referenced source_bug_id A. A webhook owner who can see public B but not private A still receives A's bug id.
- Related bugs serialized in full with no visibility check
extensions/Push/Extension.pm:185 (also :188) · PLAUSIBLE
The no-leak invariant deliberately enforced in _object_modified (lines 108–143, which sends $old_bug "so we don't leak any security sensitive information") is not re-established on the new counterpart path. Related bugs are loaded via Bugzilla::Bug->new($related_bug_id) and pushed with full serialized state, with no check that the actor or consumer may see them.
▎ Leak: A user links a public bug to a security-restricted related bug. _emit_counterpart_bug_modify_events loads the restricted bug and calls _push_object('modify', $related_bug, ...), serializing its contents. A connector without its own owner-visibility filtering (unlike Webhook's _owner_can_see) emits an event exposing the confidential bug's fields.
- Create-path counterpart events use a fresh change_set_id() — correlation broken
extensions/Push/Extension.pm:474 (also :475) · CONFIRMED
In bug_end_of_create, counterpart modify events get a fresh change_set_id() that differs from the new bug's own create event, so the two halves of one user action can't be correlated. The update path correctly shares one change_set.
▎ Failure: User files bug A that blocks existing bug B. _object_created pushes A's create event with change_set X (line 92); _emit_counterpart_bug_modify_events calls change_set_id() again (line 474) → change_set Y. A consumer grouping by change_set sees A's create and B's modify under two unrelated sets and can't tie them together.
- Synchronous per-related-bug loads inside the open transaction
extensions/Push/Extension.pm:188 · CONFIRMED
Each related bug is loaded via Bugzilla::Bug->new and pushed synchronously in a loop while the create/update DB transaction is still open, scaling linearly with the number of changed dependencies.
▎ Failure: A tracking/meta bug blocking hundreds of bugs is created or bulk-edited. bug_end_of_create/_object_modified performs hundreds of Bugzilla::Bug->new round-trips and inserts a push message for each — all inside the still-open transaction — causing slow submission, prolonged row/table locking, and potential request timeouts.
🟡 Cleanup
- _counterpart_bug_changes recomputes a diff it was already given
extensions/Push/Extension.pm:218 · CONFIRMED
$changes->{$field} is already [removed_ids, added_ids] (Bugzilla/Bug.pm:1076). Rebuilding added/removed via set-membership diff (%all_ids, $in_old, $in_new, next if $in_old == $in_new) is dead machinery that only ever reproduces the split it was handed — obscuring that the inputs are deltas, not snapshots.
- Dead ARRAY-ref branch in _parse_related_bug_ids
extensions/Push/Extension.pm:243 · CONFIRMED
Both callers pass comma-joined strings: the modify path gets string values from the changes hash, and the create path explicitly does join(', ', @$related_ids) at line 467. ref $raw_value eq 'ARRAY' is never true — unreachable code, and the create path needlessly stringifies an arrayref only to re-split it.
Suggested priority: #1 and #2 are data-leak vectors and should block. #3 breaks consumer event correlation. #4 is a scaling/locking risk worth addressing (consider deferring related-bug pushes outside the transaction, e.g. via the JobQueue). #5/#6 are safe simplifications.
|
|
The test failures look like something server-side was broken (s3 was down?) when the tests ran, and not the tests themselves actually failing.
|
|
3 things in the changeset just pushed to this PR:
|
There was a problem hiding this comment.
Correctness
- Autovivification bug (Webhook.pm:170) — the exists $payload->{event}->{indirect_change}->{source_bug_id} check autovivifies an empty indirect_change hash into every non-indirect webhook payload, adding a spurious "indirect_change":{} field and triggering per-send uninitialized-value warnings.
- Metadata leak (Webhook.pm:248) — after redaction blanks the changes, an empty-changes bug.modify counterpart event is still delivered, signalling to the owner that an invisible private bug altered relationships.
Cleanup
- Redundant/disagreeing redaction (Extension.pm:194) — source_bug_id is redacted both globally at emit time and per-owner in Webhook.pm; the two layers conflict and over-redact for consumers legitimately entitled to see the private source bug.
- Unconditional relationship SELECTs (Extension.pm:478) — bug_end_of_create reads all four relationship accessors (4 DB round-trips) on every bug creation before checking connector interest, slowing the create hot path.
I knew this one when I posted it... but at the point it gets redacted, the decision has already been made that "there's a webhook getting delivered" and I can't stop it from going out, only make changes to it. Let me think about this one. Might be able to hook it earlier in the process, or get the dispatcher to look for a "please abort" flag or something. |
For this one, I discovered that bug_before_create actually gives us the relationships without an extra DB hit, which bug_end_of_create has no way to get them without hitting the database. So I hooked bug_before_create and stashed them so we could fetch them out again during bug_end_of_create when we actually need them without hitting the DB. This does raise the question though of what if someone else changes those relationships in an intervening hook before we get that far? Seems unlikely, but surely possible. Do we need to revert this and live with the database hit for data correctness? There were merge conflicts, so I had to do a merge commit, so for review purposes you can ignore all the stuff that you co-authored. :-) I split the four review comments into four separate commits so you can look at them individually if you want. |
|
My merge commit apparently pulled in some things it shouldn't have ... the Changed Files view is showing a bunch of things I swear I didn't touch ... rebase/force-push coming shortly to clean out unrelated/horked changes. |
…creation in additon to updates
e926b89 to
7c30751
Compare
|
OK, looks clean now. The last four commits are the ones dealing with the last set of review comments. |
| my $value = $params->{$field}; | ||
| next unless ref $value eq 'ARRAY' && @$value; | ||
| $relation_values{$field} = [@$value]; |
| # Do not emit counterpart payloads for private target bugs because | ||
| # connectors may not uniformly apply per-consumer visibility checks. | ||
| next unless is_public($related_bug); |
This updates the Push extension to generate a bug.modify event for any bug touched by a relationship change (blocked, dependson, regresses, regressed_by), and adds an indirect_change hash to the top-level of the object which contains metadata about the source of the change (which bug was edited that caused it)