The Reject Term Moved to the Top — JavaScript Bug Hunt

Modelled on Cloudflare's outage of 21 June 2022. A change to the network configuration in 19 of its data centres — locations that carry a large share of its…

  • Language: JavaScript
  • Layer: Backend
  • Difficulty: Hard
  • Concepts: Networking, Config
  • Modelled on: Cloudflare · 2022
  • Visible tests: withdrawn reports what a policy would not advertise; normalising a site policy keeps the site advertised
  • Reward: 50 XP for a complete fix

Briefing

Modelled on Cloudflare's outage of 21 June 2022. A change to the network configuration in 19 of its data centres — locations that carry a large share of its traffic — reordered the terms of the BGP policies that decide which IP prefixes are advertised. After the reorder, a term that rejects prefixes came before the terms that accept the sites' own prefixes, so a critical subset of prefixes was withdrawn and those data centres became unreachable until the change was reverted.

This reconstruction's normalise rewrites a policy into a house layout before deployment by grouping all reject terms first — including the catch-all reject that must always be last — and deploy pushes the result with no check on what it stops advertising.

Fix the ordering, and make deploy refuse a policy that would withdraw a critical prefix.

Bug report

BUG-MCP-ORDER · Priority: Critical · Reported by: network engineering

Policies are ordered term lists evaluated by evaluate() (locked): the first matching term decides; unmatched prefixes are rejected.

normalise(terms) -> a NEW array (the input is never modified):

  • every term keeps its position relative to the others, except catch-all terms (matchAll: true), which move to the end, keeping their own relative order

withdrawn(terms, critical) -> the prefixes of critical, in input order, that the policy would not accept.

deploy(terms, critical):

  • normalises the policy
  • if withdrawn(normalised, critical) is non-empty, throws an Error and deploys nothing
  • otherwise returns the normalised policy

Observed: after normalise the catch-all reject was the first term, every site prefix was withdrawn, and deploy pushed it anyway.

Logs

[policy] normalise: 3 reject terms moved ahead of 4 accept terms
[bgp] withdrawing 214 prefixes from 19 sites
[deploy] policy pushed to 19 sites (no pre-deploy checks configured)

The code as shipped

src/bgp/policy.js (editable)

var evaluate = require("./evaluate").evaluate;

// Rewrites a policy into the house layout before it is pushed to the data
// centres. Returns a new array; the input is left alone.
exports.normalise = function (terms) {
  var rejects = terms.filter(function (t) { return t.action === "reject"; });
  var others = terms.filter(function (t) { return t.action !== "reject"; });
  return rejects.concat(others);
};

// The prefixes from critical that this policy would stop advertising.
exports.withdrawn = function (terms, critical) {
  return critical.filter(function (prefix) { return evaluate(terms, prefix) !== "accept"; });
};

// Normalises and returns the policy that will be deployed.
exports.deploy = function (terms, critical) {
  return exports.normalise(terms);
};

Read-only context: src/bgp/evaluate.js.

Open the hunt to edit the files, run the visible tests and submit against the hidden ones. More JavaScript bug hunts.