Plugin loading and plugin-lifetime follow-ups from #1131/#1133/#1134/#1135
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-132—resultis initialised toPluginManagerErrorNoneand thestorage_dir_open()failure path justbreaks without touching it, so a missing or unreadable folder returns "no error". Contrast the read-failure path at:158-168, which does setPluginManagerErrorLoaderError. Effect: withapps_data/subghz/pluginsmissing,subghz_device_registry_init()getsPluginManagerErrorNoneand only notices viaplugin_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 checkplugin_manager_get_ep()for NULL.lib/subghz/devices/registry.c:44-45stores the result straight intoitems[], andplugin_manager_get_ep()returnslib_descr->entry_pointunchecked. A.falthat matches on app id and API version but carries a null entry point puts NULL in the array, andsubghz_device_registry_get_by_name()then dereferencessubghz_device_registry->items[i]->nameatregistry.c:68— a null deref on every Sub-GHz start, with the offending file never named. #1134 added exactly this guard tosubghz_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
.rodatato the async notification queue.applications/main/subghz/plugins/frequency_analyzer/subghz_frequency_analyzer_plugin.c:9declaressequence_savedasstatic const NotificationSequence— so it lives in the.fal— and:67passes it tonotification_message(), which stores the pointer and returns. The sequence containsmessage_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 thebyte_inputheader 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_inputandtext_inputstore their header by reference whilesubmenucopies its labels.applications/services/gui/modules/byte_input.c:868-872andtext_input.c:650-653both domodel->header = text;, and the draw callbacks dereference it unguarded.submenu_add_item()copies into aFuriStringinstead. Three workarounds for this already exist in the tree:applications/system/js_app/modules/js_gui/byte_input.ckeeps a dedicatedFuriStringalive purely to hold the pointer valid,subghz_scene_signal_settings.cpassesfuri_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_subghzsources are still swept into the Sub-GHz app's own glob.applications/main/subghz/application.fam:33declaressources=["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 intocli_subghz.fal. NFC excludes exactly this with!cli, infrared with!infrared_cli.c. It costs no flash today (--gc-sectionscollects 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_stylesis the last directory in the shape #1135 fixed. EightFlipperAppType.PLUGINentries,requires=["loader"], no app in the folder, parent living inapplications/services/loader— sitting in an APPDIRS scan root as a sibling of real system apps. By #1135's standard they belong atapplications/services/loader/plugins/menu_styles/, which needs!pluginson the built-inloaderand 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, andapplications/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