fix: rDelete fataled on musl (GLOB_BRACE) - broke ALL deletion and plugin removal

Reported as "deleting terminal just cycles in the ui". Root cause is much larger than the
plugin: it was a platform bug that broke deletion everywhere.

CAUSE
  traits/helpers.php::rDelete() did:
      glob($target . "{*,.[!.]*,..?*}", GLOB_BRACE|GLOB_MARK);
  GLOB_BRACE is a GLIBC extension. Alpine is musl, where the constant is undefined,
  and referencing an undefined constant is a FATAL in PHP 8:
      PHP Fatal error: Uncaught Error: Undefined constant "GLOB_BRACE"
      in traits/helpers.php:138
      #0 components/market/class.market.php(178): Common::rDelete()
      #1 components/market/controller.php(46): Market->remove()
  The fatal kills the request before Common::send(200), so the front end never gets a
  settle response and the UI spins forever. A hang, not an error - which is why it
  presented as "cycles in the ui".

BLAST RADIUS (measured, two call sites)
  components/market/class.market.php:178   removing a plugin from the market
  components/filetree/class.filetree.php:59  DELETING ANY FILE OR FOLDER IN THE EDITOR
  The second is core functionality, so this was never a plugin bug - installing the
  Terminal plugin is just what made it visible.

FIX
  patches/helpers-rDelete-GLOB_BRACE.diff - expand the brace pattern by hand when
  GLOB_BRACE is undefined, keeping upstream behaviour where it is defined. Verified:
  nested dirs + dotfiles + single files all delete correctly now.
  Upstream has NOT fixed this (checked main) so it cannot be resolved by updating.

PLUGIN REMOVED
  Once deletion worked, removal via the market succeeded and the plugin is now gone:
    {"text":"Plugin successfully removed.","status":200}
  That is the right end state - a browser shell was the largest attack surface, and
  macros already provide command execution. Removed through the market's own code path
  so the plugin list, addons cache and bundle stayed consistent.

TESTS (now 42 passed, 1 failed - the failure is branch protection, a genuine open item)
  + a GLOB_BRACE regression guard that fails loudly if the patch is lost to an update
  + the identity guard is INVERTED: FR-11 required an EMPTY identity so the agent could
    not impersonate the operator, but the operator has since asked for it to be set, so
    the test asserting emptiness was asserting the opposite of the current design. It
    now requires the identity to be present and to NOT look agent-generated.

docs/known-issues.md: new section 1 documents the GLOB_BRACE fault in full, including the
honest note that this is the real cost of choosing Alpine over a glibc base image.
This commit is contained in:
2026-09-29 16:28:44 +10:00
parent 0bdd15b31b
commit c4a4de1da7
4 changed files with 161 additions and 180 deletions
+22
View File
@@ -0,0 +1,22 @@
--- /var/www/atheos/traits/helpers.php.orig
+++ /var/www/atheos/traits/helpers.php
@@ -135,7 +135,18 @@
if (is_dir($target)) {
- $files = glob($target . "{*,.[!.]*,..?*}", GLOB_BRACE|GLOB_MARK); //GLOB_MARK adds a slash to directories returned
+ // GLOB_BRACE is a glibc extension and is UNDEFINED under musl (Alpine), where
+ // referencing it is a fatal error in PHP 8. Expand the brace pattern by hand
+ // so this works on both libcs.
+ if (defined("GLOB_BRACE")) {
+ $files = glob($target . "{*,.[!.]*,..?*}", GLOB_BRACE | GLOB_MARK);
+ } else {
+ $files = array_merge(
+ glob($target . "/*", GLOB_MARK) ?: [],
+ glob($target . "/.[!.]*", GLOB_MARK) ?: [],
+ glob($target . "/..?*", GLOB_MARK) ?: []
+ );
+ }
// $files = glob($target . "/*");
foreach ($files as $file) {