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 -s /bin/bash - <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 -s /bin/bash - <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 -s /bin/bash - <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 Ægir task
Tests 1–2 drive su -s /bin/bash - <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 -s /bin/bash - <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.
Test 4 — Drupal 11 contributed command files stay on disk and never load for the backend
BOA no longer deletes contributed *.drush.inc files from codebases (the sweep
that removed them on Drupal 11 platforms at lock time and in the platform
permission helper is retired), so this test proves the property that replaced
it: a command file inside an ENABLED contributed module's directory on a locked
Drupal 11 composer codebase is found by Drush's recursive scan, denied for the
backend, and the two paths that used to delete it now leave it in place. Use a
site on a locked Drupal 11 composer codebase (modules/contrib exists only in
that layout; a built-in platform keeps contrib under modules/o_contrib_eleven)
and pick a contributed module that is enabled on it (token is a safe choice on
most distributions). @<PLATFORM> below is the platform's context name, listed
by ls /data/disk/<OCT>/.drush/platform_*.alias.drushrc.php. The probe has its
own name so it can coexist with the Test 2 probe. The permission helper is run
the way the fix_permissions task runs it, by the Octopus user through sudo; it
refuses any other caller. Test 2b already covers the limited-shell identity, so
this test has no <OCT>.ftp step.
For the maintainer: the deny decision here is taken in _drush_add_commandfiles()
(command.inc:1641 at scan time, :1666 before the load), which is what makes a
file that stays on disk harmless to the backend.
ROOT=$(su -s /bin/bash - <OCT> -c "drush <SITE> dd" 2>/dev/null) # or: status --fields=root --format=list
MOD="$ROOT/modules/contrib/token" # any ENABLED contrib module dir
cat > "$MOD/cprobe.drush.inc" <<'PHP'
<?php
@file_put_contents('/tmp/boa_probe_uid_' . posix_geteuid() . '.marker', date('c') . "\n", FILE_APPEND);
function cprobe_drush_command() {
return array('cprobe' => array('description' => 'BOA probe command', 'bootstrap' => DRUSH_BOOTSTRAP_DRUSH));
}
function drush_cprobe() { drush_print('BOA-PROBE-RAN'); }
PHP
chown <OCT>:users "$MOD/cprobe.drush.inc"; chmod 0644 "$MOD/cprobe.drush.inc"
rm -f /tmp/boa_probe_uid_*.marker
# 4a — backend: full site bootstrap after a cache clear; the probe must not appear or run
su -s /bin/bash - <OCT> -c "drush <SITE> cc drush >/dev/null 2>&1; drush <SITE> help 2>&1" | grep -c '^ *cprobe' # expect 0
ls /tmp/boa_probe_uid_$(id -u <OCT>).marker 2>/dev/null && echo "BACKEND LOADED IT (BAD)" || echo "backend did not load it (GOOD)"
# 4b — the two paths that used to delete the file now leave it in place
su -s /bin/bash - <OCT> -c "drush @<PLATFORM> provision-dunlock && drush @<PLATFORM> provision-dlock" >/dev/null 2>&1; echo "lock cycle exit=$?"
su -s /bin/bash - <OCT> -c "sudo --non-interactive /usr/local/bin/fix-drupal-platform-permissions.sh --root=$ROOT" >/dev/null 2>&1; echo "helper exit=$?"
ls -la "$MOD/cprobe.drush.inc" # still present
ls /tmp/boa_probe_uid_$(id -u <OCT>).marker 2>/dev/null && echo "BACKEND LOADED IT (BAD)" || echo "backend did not load it (GOOD)"
- 4a:
0andbackend did not load it (GOOD) - 4b: both exit codes
0, the probe file still present, andbackend did not load it (GOOD)again
A real contributed command file written for Drush 12 behaves the same way for
the backend. For the unfiltered <OCT>.ftp identity it loads, and on a platform
whose enabled modules ship such a file every Drush 8 command in the limited
shell, cc drush included, ends at command discovery with the file's own
exception. That is expected and is why clients use the site-local vdrush on
Drupal 8+.
Cleanup
rm -f "$ROOT/sites/all/drush/bprobe.drush.inc"
rm -f "$ROOT/modules/contrib/token/cprobe.drush.inc" # Test 4 probe, if planted
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 -s /bin/bash - <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 |
| 3 | Files stay on disk, never load for the backend | Test 4a: 0 and no backend marker; Test 4b: lock cycle and helper exit 0, probe still present, no backend marker |
| — | No over-block / patch active | Both identities: patched=yes and allowed(BOA-tool)=true |
A run is definitive only when all four 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.