Plugin loading and plugin-lifetime follow-ups from #1131/#1133/#1134/#1135

Author: mishamyteCreated Sep 8, 2026Updated Sep 8, 2026
Labelsarea/subghzarea/uiarea/systemtype/enhancement

Follow-ups surfaced while reviewing #1131, #1133, #1134 and #1135. None of them is a regression from those PRs — they are pre-existing issues those PRs walked past, plus two pieces of consistency work that only became possible once #1135 landed. Each item is independent; they are collected here rather than as six issues because they are all small and all came out of the same review pass.

All line numbers are against dev @ 83fbee38f, and each claim below was verified against the code.

Plugin loading

  • plugin_manager_load_all_prefixed() reports success for a directory it could not open. lib/flipper_application/plugins/plugin_manager.c:129-132result is initialised to PluginManagerErrorNone and the storage_dir_open() failure path just breaks without touching it, so a missing or unreadable folder returns "no error". Contrast the read-failure path at :158-168, which does set PluginManagerErrorLoaderError. Effect: with apps_data/subghz/plugins missing, subghz_device_registry_init() gets PluginManagerErrorNone and only notices via plugin_count == 0, which it downgrades to a warning — while the two Sub-GHz feature plugins each raise a full error screen for the same root cause. Same condition, two very different levels of honesty. (From #1133.)

  • subghz_device_registry_init() does not check plugin_manager_get_ep() for NULL. lib/subghz/devices/registry.c:44-45 stores the result straight into items[], and plugin_manager_get_ep() returns lib_descr->entry_point unchecked. A .fal that matches on app id and API version but carries a null entry point puts NULL in the array, and subghz_device_registry_get_by_name() then dereferences subghz_device_registry->items[i]->name at registry.c:68 — a null deref on every Sub-GHz start, with the offending file never named. #1134 added exactly this guard to subghz_feature_plugin_load(); it was not carried across to the other consumer of the same directory.

Plugin memory outliving its image

  • The Frequency Analyzer hands plugin .rodata to the async notification queue. applications/main/subghz/plugins/frequency_analyzer/subghz_frequency_analyzer_plugin.c:9 declares sequence_saved as static const NotificationSequence — so it lives in the .fal — and :67 passes it to notification_message(), which stores the pointer and returns. The sequence contains message_delay_100, so it is still being walked after the call. Backing out of the analyzer inside that window unmaps the image under the notification service. Same invariant class as the byte_input header that #1134 had to take back on unload: nothing the plugin owns may outlive the unmap, including anything handed to an async service. (From #1131.)

  • byte_input and text_input store their header by reference while submenu copies its labels. applications/services/gui/modules/byte_input.c:868-872 and text_input.c:650-653 both do model->header = text;, and the draw callbacks dereference it unguarded. submenu_add_item() copies into a FuriString instead. Three workarounds for this already exist in the tree: applications/system/js_app/modules/js_gui/byte_input.c keeps a dedicated FuriString alive purely to hold the pointer valid, subghz_scene_signal_settings.c passes furi_string_get_cstr() of a scene-owned string, and #1134 clears the header on plugin unload. Fix is to own the string in the model — no signature change, so no API bump, at the cost of one small allocation per instance and a few tens of bytes of flash. This is a GUI service used by every app and every external FAP, so it wants its own PR and its own testing.

Consistency, unblocked by #1135

  • cli_subghz sources are still swept into the Sub-GHz app's own glob. applications/main/subghz/application.fam:33 declares sources=["subghz_cli.c", "helpers/subghz_chat.c"] for the CLI plugin, while the app above it globs *.c* over the same folder — so both files are compiled into the firmware and into cli_subghz.fal. NFC excludes exactly this with !cli, infrared with !infrared_cli.c. It costs no flash today (--gc-sections collects the unreferenced copy, and there is no symbol collision), so this is consistency rather than a saving — but it is the last counterexample sitting in the very manifest #1135 edited, and it only became fixable because #1135 made built-in app exclusions work.

  • applications/system/menu_styles is the last directory in the shape #1135 fixed. Eight FlipperAppType.PLUGIN entries, requires=["loader"], no app in the folder, parent living in applications/services/loader — sitting in an APPDIRS scan root as a sibling of real system apps. By #1135's standard they belong at applications/services/loader/plugins/menu_styles/, which needs !plugins on the built-in loader and is possible for the first time now. Decide deliberately rather than by symmetry: the style sources are fork-only Momentum ports, applications/system/ is where this fork keeps fork-only code, and applications/services/loader/ is heavily OFW-shared — every fork-only file added there is rebase surface forever. That is a legitimate reason to leave them where they are; it just deserves to be written down instead of looking like an oversight.

Note, not an action item

#1135 made a folder's source exclusions apply to every built-in app sharing that folder (they have to — otherwise a sibling's default *.c* re-globs what was excluded). applications/debug/unit_tests is the one place where that could bite: delay_test deliberately compiles tests/common/*.c and tests/rpc/*.c into the firmware while 30 plugin entries build the same tree into .fals. Anyone reaching for the now-working !tests on unit_tests would silently strip delay_test's sources and break its link. There is a comment to that effect in scripts/fbt/appmanifest.py; flagging it here too.

Source: DarkFlippers/unleashed-firmware