• joebarrettaccess

    (@joebarrettaccess)


    AI describing the problem as I couldn't.

    Bug Report: Freemius SDK permanently modifies active_plugins when Plugin Organizer filters the option


    Date of confirmed incident: 2026-08-22 11:43:00 (site local time)

    Symptom: Multiple plugins appear deactivated in wp-admin; files remain on disk; active_plugins in MySQL is shortened.

    Executive summary

    This is not a PHP fatal crash. Freemius’s fs_newest_sdk_plugin_first() runs during normal WordPress bootstrap, calls get_option('active_plugins'), gets a filtered (short) list from Plugin Organizer, then update_option('active_plugins', …) and writes that short list to the database. Plugins that were only hidden for the current URL look permanently deactivated.

    Root cause: Freemius treats a filtered read as the canonical active list and persists it.

    Amplifier: Plugin Organizer MU filters option_active_plugins per URL with no write-protection (Perfmatters MU documents the same Freemius risk and adds a guard; PO does not).

    Environment

    Component Version / notes

    WordPress

    WooCommerce store, ~64 active plugins

    Plugin Organizer

    10.2.5 + MU loader PluginOrganizerMU.class.php

    Freemius SDK carriers on site

    woo-coupon-usage-pro, post-smtp, 3d-viewer (each bundles Freemius)

    Perfmatters MU

    Has pre_update_option_active_plugins write guard; explicitly comments about Freemius

    Object Cache Pro

    Was disabled during testing; issue reproduced without Redis

    Step-by-step: how plugins get “deactivated”

    Step 1 — Plugin Organizer hides plugins for this request only

    Plugin Organizer MU hooks early:

    PluginOrganizerMU.class.php

    Lines 504-504

    add_filter('option_active_plugins', array($PluginOrganizerMU, 'disable_plugins'), 1, 1);

    For a given front-end URL, PO removes plugins from the in-memory list returned by get_option('active_plugins'). Those plugins are not loaded this request, but should still be active in the DB.

    On our site, PO commonly hides ~5 plugins on product/checkout URLs, including:

    woo-order-export-lite

    woocommerce-avatax

    woocommerce-sendinblue-newsletter-subscription

    woocommerce-advanced-bulk-edit

    woocommerce-ajax-filters

    Step 2 — WordPress boots on a normal front-end request

    Any full WP bootstrap can trigger this: homepage /, product pages, checkout, missing static files routed to index.php (e.g. .map 404s), etc.

    Step 3 — Freemius loads during wp-settings.php

    When Woo Coupon Usage Pro loads, Freemius start.php decides the SDK load order needs updating and calls:

    start.php

    Lines 258-260

    if ( ! $fs_active_plugins->newest->in_activation ) {

      // Re-order plugins to load this plugin first.

      fs_newest_sdk_plugin_first();

    (Post SMTP’s copy of the same SDK provides the update_option frame in the backtrace below.)

    Step 4 — Freemius reads filtered list, writes it to DB

    fs-essential-functions.php

    Lines 305-332

    $active_plugins         = get_option( 'active_plugins', array() );

    $updated_active_plugins = array( $newest_sdk_plugin_path );

    // ... reorder loop ...

    if ( $plugin_found ) {

        update_option( 'active_plugins', $updated_active_plugins );

        return true;

    }

    Bug: get_option('active_plugins') returns PO’s filtered list (~57 plugins), not the real DB list (~64). Freemius reorders that short list and update_option persists it. Plugins PO hid are dropped from MySQL permanently.

    Step 5 — Site behavior after write

    wp-admin → Plugins shows plugins as Inactive

    Plugin files still exist

    No PHP fatal in debug.log for this event

    Restore requires manual re-activation or WP-CLI

    Proof — primary log entry (2026-08-22 11:43:00)

    Source: wp-content/uploads/XXXXX/plugin-activation-changes.log (custom MU logger on update_option_active_plugins)

    {

      "time": "2026-08-22 11:43:00",

      "user_id": 0,

      "username": "system",

      "ip": "xx.xx.xx.xx",

      "method": "GET",

      "uri": "/",

      "referer": "/wp-admin/admin.php?page=post-smtp-pro",

      "doing_cron": false,

      "doing_ajax": false,

      "is_cli": false,

      "via": "update_option_active_plugins",

      "old_count": 57,

      "new_count": 59,

      "added_count": 2,

      "removed_count": 0,

      "added": [

        "woocommerce-gateway-amazon-payments-advanced/woocommerce-gateway-amazon-payments-advanced.php",

        "3d-viewer/3d-viewer.php"

      ],

      "removed": [],

      "backtrace": [

        "update_option() wp-content/plugins/post-smtp/freemius/includes/fs-essential-functions.php:332",

        "fs_newest_sdk_plugin_first() wp-content/plugins/woo-coupon-usage-pro/freemius/start.php:260",

        "require_once() wp-content/plugins/woo-coupon-usage-pro/woo-coupon-usage.php:42",

        "wcu_fs() wp-content/plugins/woo-coupon-usage-pro/woo-coupon-usage.php:73",

        "include_once() wp-settings.php:597",

        "require_once() wp-config.php:113",

        "require_once() wp-load.php:50",

        "require_once() wp-blog-header.php:13",

        "require() index.php:17"

      ]

    }

    How to read this entry

    Field Meaning

    user_id: 0 / username: system

    Not wp-admin deactivate; automated write during bootstrap

    uri: /

    Normal homepage request (not Plugins screen)

    old_count: 57

    Filtered count (PO hid ~7 plugins from the read)

    new_count: 59

    Written list still short vs true 64

    removed: []

    Logger compared filtered $old to filtered $new — missed the 5 dropped plugins

    Backtrace

    Freemius fs_newest_sdk_plugin_first() → update_option('active_plugins')

    Proof — database impact (snapshot diff)

    Before incident (/tmp/active-before-product.txt): 64 active plugins

    After incident (wp plugin list --status=active): 59 active plugins

    Plugins removed from DB (present before, missing after):

    woo-order-export-lite

    woocommerce-avatax

    woocommerce-sendinblue-newsletter-subscription

    woocommerce-advanced-bulk-edit

    woocommerce-ajax-filters

    These match Plugin Organizer’s typical per-URL disabled set — not random corruption.

    No PHP fatals during this test window in wp-content/debug.log.

    Proof — same bug on earlier dates (pattern)

    Same mechanism, different URLs — all user_id: 0, no admin action:

    Time  URI Logged change

    2026-08-21 11:47:48

    /wp-content/plugins/post-smtp/.../party.min.js.map

    added: [3d-viewer]

    2026-08-21 12:38:03

    /

    added: [amazon-payments-advanced, 3d-viewer]

    2026-08-21 14:20:48

    /checkout/order-pay/...

    added: [3d-viewer]

    Example (Aug 21 — .map 404 bootstraps full WordPress):

    SH_PLUGIN_CHANGE {"time":"2026-08-21 11:47:48","added":["3d-viewer/3d-viewer.php"],"removed":[],"user_id":0,"username":"system","uri":"/wp-content/plugins/post-smtp/Postman/Wizard/assets/js/party.min.js.map"}

    The .map file does not exist; .htaccess sends the request to index.php → full WP load → Freemius runs.

    Proof — Perfmatters already documents this Freemius behavior

    Perfmatters MU explicitly guards against it:

    perfmatters_mu.php

    Lines 198-212

     * Plugins like Freemius call get_option( 'active_plugins' ) at include time

     * (while our read filter is still active), reorder the list, and update_option()

     * the result. Without this guard, MU-hidden plugins are permanently deactivated.

     */

    function perfmatters_mu_protect_write($value) {

        // ... merges hidden plugins back into the value before save ...

    }

    Plugin Organizer has the read filter but no equivalent write guard.

    Affected parties & recommended fixes

    1. Freemius (primary) — github.com/Freemius/wordpress-sdk

    Issue: fs_newest_sdk_plugin_first() must not call update_option('active_plugins', …) based on a filtered get_option().

    Suggested fix:

    Read unfiltered value (direct $wpdb, or temporarily remove all option_active_plugins filters), or

    Only reorder if count/set matches unfiltered DB, or

    Use pre_update_option_active_plugins to merge back filtered-out plugins (Perfmatters pattern).

    Carriers on our site: Coupon Usage Pro, Post SMTP, 3D Viewer — all use the same SDK function.

    2. Plugin Organizer

    Issue: Per-URL filtering is by design, but combined with Freemius it causes permanent deactivations.

    Suggested fix: Add pre_update_option_active_plugins write protection (restore any plugins PO hid on that request), matching Perfmatters MU.

    3. Plugin vendors using Freemius (Coupon Usage, Post SMTP, bPlugins/3D Viewer)

    Please escalate to Freemius and/or ship a guard until SDK is fixed. Reproduction does not require your plugin’s UI — only SDK bootstrap on a front-end request with PO active.

    What this is NOT

    Not malware or manual admin deactivation

    Not Redis/cache illusion (MySQL active_plugins was verified shortened)

    Not caused by 3D viewer shortcode or .stl files (correlation only)

    Not a PHP crash — silent option write; site may keep running with plugins missing

    Reproduction steps (for vendors)

    WordPress site with Plugin Organizer MU active and per-URL plugin disables configured

    Multiple plugins using Freemius SDK (we have 3+)

    Load any front-end URL that triggers PO filtering (homepage, product, checkout)

    Condition: Freemius decides SDK reorder is needed (fs_newest_sdk_plugin_first() in backtrace)

    Observe active_plugins option count drop in DB; wp-admin shows plugins Inactive

    Verify:

    wp plugin list --status=active --format=count   # drops vs expected

    wp option get active_plugins --format=json      # shorter array in DB

    Hook logger on pre_update_option_active_plugins and log backtrace — expect fs_newest_sdk_plugin_first.

    Workarounds (site operator)

    Disable Plugin Organizer (and MU loader) until fixed

    Keep Perfmatters MU write guard (helps for plugins Perfmatters hides, not PO’s set)

    Add site-wide pre_update_option_active_plugins guard that restores filtered plugins

    Reduce Freemius carriers if possible

    Monitor with plugin-activation-changes.log

    Attachments we can provide on request

    Full plugin-activation-changes.log

    Before/after active plugin name lists

    PO disabled-plugin config for affected URLs

You must be logged in to reply to this topic.