Verifying the drush extension filter
The *.drush.inc backend deny-filter is the kind of change you cannot sign off
from the diff: its whole contract is who is allowed to load a command file, so
the proof has to run as each identity, on a real box, through Drush's real
discovery pipeline. This is the DRUSH-FILTER-TESTING.md runbook, re-cut for a
maintainer who has read the filter source.
The filter lives in the classic Drush 8 fork at
drush-evil-code/includes/boa_extension_filter.inc, wired into three discovery
call sites (command.inc::_drush_add_commandfiles,
annotationcommand_adapter.inc::annotationcommand_adapter_discover,
preflight.inc::_drush_find_commandfiles_drush — the last two filter the array
key, which is the path). It ships as a versioned tarball, so getting a
change onto a box is a _DRUSH_VERSION bump plus a published tarball, not a
branch push — the delivery mechanics and the disposable-VM gate are on
Building & testing BOA changes. This page assumes
the change is already on the box and proves both required properties:
- Protection holds — a backend identity (
aegirmaster, or an Octopus instance useroN) refuses to load a tenant*.drush.inc, closing provision #762138 (tenant code auto-loaded with cross-site authority during a backend task). - Limited-shell CLI is unaffected — a client running Drush as their own
oN.ftpaccount loads their site's contrib commands normally. This is the regression the identity gate exists to prevent: the earlier unconditional filter over-blocked client CLI.
Both properties come straight from the entry point
boa_drush_extension_allowed() (boa_extension_filter.inc:324): kill switch →
backend-identity gate → path-deny → per-instance basename opt-in. The gate is
boa_drush_extension_backend_identity() (:148), keyed on the process's
effective uid resolved through posix_getpwuid() → passwd dir, not
$HOME (BOA backend su without - leaves HOME=/root while euid is oN).
Because the discriminator is the euid, a su-launched test invocation is
faithful to the real session — the client's jailed oN.ftp lshell and a
su -s /bin/bash - oN.ftp land on the same uid and get the same decision.
Placeholder convention
Run everything as root. Substitute real values in every snippet, including inside the here-documents:
<OCT>— the Octopus instance user (e.g.o1).<OCT>.ftp— its limited-shell client user (e.g.o1.ftp).<SITE>— a Drush 8 site alias on that instance (e.g.@mysite.com).
Preconditions
Assert a clean default state — no opt-in or kill-switch control files skewing
the decision. These are the presence-only files
boa_drush_extension_filter_disabled() / _civicrm_enabled() /
_elysia_enabled() consult (boa_extension_filter.inc:185,203,223):
ls -1 /data/conf/drush_extension_filter_disabled.txt \
/data/conf/<OCT>_civicrm.txt /data/conf/<OCT>_elysia_cron.txt 2>/dev/null
Expect no output. If any exist and you want the strict default test, move them
aside for the run — the kill switch short-circuits allowed() to TRUE for
every identity, and either opt-in would let a matching basename through the
backend deny.
Test 1 — direct decision check (definitive, no side effects)
Ask the deployed filter, inside each identity's live Drush process, what it
decides. drush php-script has already sourced the fork, so the filter's
functions are in scope; this bypasses all bootstrap and discovery nuance and is
the ground truth for the decision logic itself.
cat > /tmp/boa_check.php <<'PHP'
<?php
// Run via: drush php-script /tmp/boa_check.php (drush has loaded the filter)
$tenant = '/data/disk/<OCT>/distro/probe/sites/all/drush/evil.drush.inc'; // deny for backend
$boatool = '/data/disk/<OCT>/.drush/sys/provision/provision.drush.inc'; // BOA-managed -> allow for all
drush_print('patched = ' . (function_exists('boa_drush_extension_backend_identity') ? 'yes' : 'NO -- OLD DRUSH STILL ACTIVE'));
drush_print('euid = ' . posix_geteuid());
drush_print('backend_identity = ' . var_export(boa_drush_extension_backend_identity(), true));
drush_print('allowed(tenant) = ' . var_export(boa_drush_extension_allowed($tenant), true));
drush_print('allowed(BOA-tool) = ' . var_export(boa_drush_extension_allowed($boatool), true));
PHP
echo "--- as <OCT> (backend identity) ---"
su - <OCT> -c "drush php-script /tmp/boa_check.php"
echo "--- as <OCT>.ftp (limited shell) ---"
su -s /bin/bash - <OCT>.ftp -c "drush php-script /tmp/boa_check.php"
The two probe paths exercise both arms of boa_drush_extension_path_denied()
(:286): the distro/… tenant path matches the deny pattern
#^/data/disk/[^/]+/(static|distro|platforms)(/|$)#i (:67), while
.drush/sys/provision/… matches the same .drush deny pattern but is rescued
by the .drush/(sys|usr|xts) carve-out (:81) — so allowed(BOA-tool) must be
true for both identities, proving the filter does not over-block BOA's own
extensions.
Expected as <OCT> (backend identity):
patched = yesbackend_identity = trueallowed(tenant) = false— tenant*.drush.incdenied to the backend (#762138 held)allowed(BOA-tool) = true— BOA's own extensions still load (no over-block)
Expected as <OCT>.ftp (limited shell):
patched = yesbackend_identity = false— euid is the.ftpuid; the dotted name fails thestrpos($name,'.') === FALSEtie inboa_drush_extension_octopus_user()(:112)allowed(tenant) = true— the client's own CLI is not filtered (allowed()returns early at the!backend_identity()guard,:328)allowed(BOA-tool) = true
A patched = NO line means that identity is running an un-patched Drush — the
function_exists() probe is the cheap tell that the tarball bump did not land on
that install location. Fix the install before continuing; the rest of the run is
meaningless against old code.
Test 2 — end-to-end through Drush's real discovery pipeline
Test 1 proves the decision function. Test 2 proves the wire-in: that the three discovery call sites actually consult it. Plant one harmless command file in a genuinely tenant-writable path, then watch the same file get denied to the backend and loaded for the client. The file's top-level statement writes a uid-stamped marker only when Drush actually loads it, so the marker — not the command's success — is ground truth.
# Drupal root must sit under /data/disk/<OCT>/{distro,static,platforms}/...
ROOT=$(su - <OCT> -c "drush <SITE> dd" 2>/dev/null); echo "ROOT=$ROOT"
# fallback if 'dd' is unavailable:
# drush <SITE> status --fields=root --format=list
mkdir -p "$ROOT/sites/all/drush"
cat > "$ROOT/sites/all/drush/bprobe.drush.inc" <<'PHP'
<?php
// Harmless BOA filter probe — remove after testing.
@file_put_contents('/tmp/boa_probe_uid_' . posix_geteuid() . '.marker', date('c')."\n", FILE_APPEND);
function bprobe_drush_command() {
return array('bprobe' => array('description' => 'BOA filter probe (harmless).',
'bootstrap' => DRUSH_BOOTSTRAP_DRUSH));
}
function drush_bprobe() { drush_print('BOA-PROBE-RAN uid=' . posix_geteuid()); }
PHP
chown <OCT>:users "$ROOT/sites/all/drush/bprobe.drush.inc"
chmod 644 "$ROOT/sites/all/drush/bprobe.drush.inc"
sites/all/drush/ under a distro/static/platforms root is exactly the
tenant-writable territory the deny patterns anchor on, and the file is
owner-<OCT>, group-users — the ownership a site code contributor can
produce. Note Drush hands the scanner this path relative to the Drupal root
at bootstrap; boa_drush_extension_canonical_path() (:257) realpath()s it to
absolute first, which is what lets the ^/…-anchored patterns match at all.
2a — the backend must NOT see it
rm -f /tmp/boa_probe_uid_*.marker
su - <OCT> -c "drush <SITE> cc drush >/dev/null 2>&1; drush <SITE> bprobe; echo EXIT=\$?"
ls /tmp/boa_probe_uid_$(id -u <OCT>).marker 2>/dev/null && echo "MARKER PRESENT (BAD)" || echo "no marker (GOOD)"
drush <SITE> bprobereports the command is not found (non-zeroEXIT) — the file was filtered out of discovery, sobprobewas never registered.no marker (GOOD)— the backend neither ran nor even loaded the tenant file.
The cc drush first clears Drush's commandfile cache so discovery re-runs
through the filter rather than replaying a cached command list.
2b — the limited shell MUST see it
rm -f /tmp/boa_probe_uid_*.marker
su -s /bin/bash - <OCT>.ftp -c "drush <SITE> cc drush >/dev/null 2>&1; drush <SITE> bprobe; echo EXIT=\$?"
ls /tmp/boa_probe_uid_$(id -u <OCT>.ftp).marker 2>/dev/null && echo "MARKER PRESENT (GOOD)" || echo "no marker (BAD)"
- Prints
BOA-PROBE-RAN uid=<the .ftp uid>(EXIT=0). MARKER PRESENT (GOOD)— the client's own contrib command loads and runs.
This is the reporter's elysia_cron situation reproduced exactly: a contrib
command file the client runs from their own oN.ftp shell, which the earlier
unconditional filter wrongly blocked.
Test 3 — (optional) confirm via a real Aegir task
Tests 1–2 drive su - <OCT> directly. Test 3 proves the actual backend queue
path is protected — the identity Hostmaster/Provision tasks run under — not
just an interactive su. Leave the probe planted from Test 2:
rm -f /tmp/boa_probe_uid_*.marker
# Run a Verify on the site or its platform from the Aegir UI, or:
# su - <OCT> -c "drush @hostmaster hosting-task <SITE> verify -y"
ls /tmp/boa_probe_uid_$(id -u <OCT>).marker 2>/dev/null && echo "BACKEND LOADED IT (BAD)" || echo "backend did not load it (GOOD)"
- The task completes normally.
backend did not load it (GOOD)— the euid under the real task isoN, so the gate denies the tenant file on the queue path too.
Cleanup
rm -f "$ROOT/sites/all/drush/bprobe.drush.inc"
rmdir "$ROOT/sites/all/drush" 2>/dev/null # only if we created it and it is now empty
rm -f /tmp/boa_probe_uid_*.marker /tmp/boa_check.php
su - <OCT> -c "drush <SITE> cc drush >/dev/null 2>&1"
Pass criteria
| # | Property | Signal |
|---|---|---|
| 1 | Protection holds | Test 1 as <OCT>: allowed(tenant)=false; Test 2a: bprobe not found and no marker; Test 3: no backend marker |
| 2 | Limited-shell CLI unaffected | Test 1 as <OCT>.ftp: allowed(tenant)=true; Test 2b: BOA-PROBE-RAN and marker present |
| — | No over-block / patch active | Both identities: patched=yes and allowed(BOA-tool)=true |
A run is definitive only when all three rows hold. Row 1 without row 2 is the over-blocking regression; row 2 without row 1 is the open #762138; either without the last row means you tested the wrong (un-patched) Drush or the carve-out broke.
Related
- Building & testing BOA changes — why the Drush
fork ships as a versioned tarball, the
_DRUSH_VERSION/_DRUSH_EIGHT_VRNbump that makes a box re-fetch it, and the disposable-VM gate this runbook runs inside. - Drush fork internals — the classic Drush 8 fork the filter patches, its discovery pipeline, and the other BOA divergences from stock Drush.
- Variables —
_DRUSH_VERSION,_DRUSH_EIGHT_VRN, and the other pins a test build sets.