From cfc95fc36b1e7702482e3f99565d044a0303e8a5 Mon Sep 17 00:00:00 2001 From: Durmus Koyuncu Date: Mon, 6 Jul 2026 20:30:29 +0300 Subject: [PATCH] RDKBWIFI-498: Fix EM subdoc memory leaks in webconfig decode and EM app publishers The EasyMesh AP metrics pipeline leaks on both ends of the webconfig subdoc exchange, growing the consumer of the report (EM agent, via libwifi_webconfig) and OneWifi itself on every reporting interval. Leaks were located with LeakSanitizer and validated fixed on a device (RSS flat over 8 hours). Decode side (libwifi_webconfig): - decode_em_ap_metrics_report_subdoc never freed data->u.encoded.json; per framework convention (see decode_radio_subdoc) the subdoc decoder owns the parsed tree, so the whole cJSON tree of every report leaked. Delete it on the success path and on the two error paths that were missing it. - decode_em_ap_metrics_report_object mallocs per-vap sta_traffic_stats and sta_link_metrics arrays into the decoded params, but nothing on the decode/translate path freed them (webconfig_data_free only frees u.encoded.raw). Free them in webconfig_easymesh_decode after translation, mirroring the existing assoc-map cleanup. EM app publishers (wifi_em.c): - All four publishers (AP metrics report, sta link metrics report, channel scan report, beacon report) leaked the encoded JSON string: webconfig_encode allocates data->u.encoded.raw, the bus publish copies the payload, and the cleanup paths freed the decoded members and `data` but never the encoded string. The beacon report publisher also leaked wb_data itself on the success path. Signed-off-by: Durmus Koyuncu --- source/apps/em/wifi_em.c | 14 +++++++- source/webconfig/wifi_easymesh_translator.c | 33 +++++++++++++++++++ .../wifi_webconfig_em_ap_metrics_report.c | 32 ++++++++++++++++++ 3 files changed, 78 insertions(+), 1 deletion(-) diff --git a/source/apps/em/wifi_em.c b/source/apps/em/wifi_em.c index ee64f6cca..1ab6f41a9 100644 --- a/source/apps/em/wifi_em.c +++ b/source/apps/em/wifi_em.c @@ -466,6 +466,7 @@ static int em_sta_stats_publish(wifi_app_t *app, client_assoc_data_t *stats, int if (rc != bus_error_success) { wifi_util_error_print(WIFI_EM, "%s:%d: bus: bus_event_publish_fn Event failed %d\n", __func__, __LINE__, rc); + free(data->u.encoded.raw); free(data->u.decoded.em_sta_link_metrics_rsp.per_sta_metrics); free(data); return RETURN_ERR; @@ -477,8 +478,11 @@ static int em_sta_stats_publish(wifi_app_t *app, client_assoc_data_t *stats, int } } + free(data->u.encoded.raw); free(data->u.decoded.em_sta_link_metrics_rsp.per_sta_metrics); free(data); + + return RETURN_OK; } static int handle_ready_client_stats(wifi_app_t *app, client_assoc_data_t *stats, size_t stats_num, @@ -896,10 +900,12 @@ static int em_publish_stats_data(channel_scan_response_t *scan_response) if (status != bus_error_success) { wifi_util_error_print(WIFI_EM, "%s:%d: bus: bus_event_publish_fn Event failed %d\n", __func__, __LINE__, status); + free(data->u.encoded.raw); free(data->u.decoded.collect_stats.stats); free(data); return RETURN_ERR; } + free(data->u.encoded.raw); free(data->u.decoded.collect_stats.stats); free(data); @@ -2027,7 +2033,8 @@ static int ap_report_push_cb(em_ap_report_callback_arg_t *args) // Cleanup allocated memory if (data != NULL) { for (int j = 0; j < req_radio_count; j++) { - for (int i = 0; i < radio->vaps.num_vaps && i < MAX_NUM_VAP_PER_RADIO; i++) { + // num_vaps differs per radio; unused entries are NULL + for (int i = 0; i < MAX_NUM_VAP_PER_RADIO; i++) { vap_report = &data->u.decoded.em_ap_metrics_report.radio_reports[j].vap_reports[i]; if (vap_report->sta_link_metrics != NULL) { free(vap_report->sta_link_metrics); @@ -2037,6 +2044,8 @@ static int ap_report_push_cb(em_ap_report_callback_arg_t *args) } } } + // u.encoded.raw is NULL if encode was not reached (data is memset) + free(data->u.encoded.raw); free(data); } @@ -2948,6 +2957,7 @@ static int em_beacon_report_publish(bus_handle_t *handle, void *msg_data) if (rc != bus_error_success) { wifi_util_error_print(WIFI_EM, "%s:%d: bus_event_publish_fn Event failed %d\n", __func__, __LINE__, rc); + free(wb_data->u.encoded.raw); free(wb_data); return RETURN_ERR; } else { @@ -2955,6 +2965,8 @@ static int em_beacon_report_publish(bus_handle_t *handle, void *msg_data) __LINE__, WIFI_EM_BEACON_REPORT); } + free(wb_data->u.encoded.raw); + free(wb_data); return RETURN_OK; } diff --git a/source/webconfig/wifi_easymesh_translator.c b/source/webconfig/wifi_easymesh_translator.c index 74e4774ad..ad8ed6d13 100644 --- a/source/webconfig/wifi_easymesh_translator.c +++ b/source/webconfig/wifi_easymesh_translator.c @@ -94,6 +94,36 @@ unsigned int translate_auth_type_from_easymesh(unsigned int authtype) } } +#ifdef EM_APP +// the ap metrics report decoder mallocs per-vap sta arrays into the decoded +// params (see decode_em_ap_metrics_report_object); webconfig_data_free only +// frees u.encoded.raw, so they must be freed here after translation +static void webconfig_easymesh_free_ap_metrics_report(webconfig_subdoc_data_t *data) +{ + webconfig_subdoc_decoded_data_t *decoded = &data->u.decoded; + int i, j; + + if (data->type != webconfig_subdoc_type_em_ap_metrics_report) { + return; + } + + for (i = 0; i < MAX_NUM_RADIOS; i++) { + for (j = 0; j < MAX_NUM_VAP_PER_RADIO; j++) { + em_vap_metrics_t *vap_report = &decoded->em_ap_metrics_report.radio_reports[i].vap_reports[j]; + + if (vap_report->sta_traffic_stats != NULL) { + free(vap_report->sta_traffic_stats); + vap_report->sta_traffic_stats = NULL; + } + if (vap_report->sta_link_metrics != NULL) { + free(vap_report->sta_link_metrics); + vap_report->sta_link_metrics = NULL; + } + } + } +} +#endif + // webconfig_easymesh_decode() will convert the onewifi structures to easymesh structures webconfig_error_t webconfig_easymesh_decode(webconfig_t *config, const char *str, webconfig_external_easymesh_t *data, @@ -111,6 +141,9 @@ webconfig_error_t webconfig_easymesh_decode(webconfig_t *config, const char *str wifi_util_info_print(WIFI_WEBCONFIG,"%s:%d: Easymesh decode subdoc type %d sucessfully\n", __func__, __LINE__, webconfig_easymesh_data.type); *type = webconfig_easymesh_data.type; //debug_external_protos(&webconfig_easymesh_data, __func__, __LINE__); +#ifdef EM_APP + webconfig_easymesh_free_ap_metrics_report(&webconfig_easymesh_data); +#endif webconfig_data_free(&webconfig_easymesh_data); return webconfig_error_none; } diff --git a/source/webconfig/wifi_webconfig_em_ap_metrics_report.c b/source/webconfig/wifi_webconfig_em_ap_metrics_report.c index 54873198d..5a508ceab 100644 --- a/source/webconfig/wifi_webconfig_em_ap_metrics_report.c +++ b/source/webconfig/wifi_webconfig_em_ap_metrics_report.c @@ -133,6 +133,25 @@ webconfig_error_t encode_em_ap_metrics_report_subdoc(webconfig_t *config, webcon return webconfig_error_none; } +// frees the per-vap arrays allocated by decode_em_ap_metrics_report_object +static void free_em_ap_metrics_report_params(webconfig_subdoc_decoded_data_t *params) +{ + for (int r = 0; r < MAX_NUM_RADIOS; r++) { + for (int v = 0; v < MAX_NUM_VAP_PER_RADIO; v++) { + em_vap_metrics_t *vap_report = ¶ms->em_ap_metrics_report.radio_reports[r].vap_reports[v]; + + if (vap_report->sta_traffic_stats != NULL) { + free(vap_report->sta_traffic_stats); + vap_report->sta_traffic_stats = NULL; + } + if (vap_report->sta_link_metrics != NULL) { + free(vap_report->sta_link_metrics); + vap_report->sta_link_metrics = NULL; + } + } + } +} + webconfig_error_t decode_em_ap_metrics_report_subdoc(webconfig_t *config, webconfig_subdoc_data_t *data) { webconfig_subdoc_decoded_data_t *params; @@ -174,6 +193,14 @@ webconfig_error_t decode_em_ap_metrics_report_subdoc(webconfig_t *config, webcon if (em_ap_report_arr == NULL || !cJSON_IsArray(em_ap_report_arr)) { wifi_util_error_print(WIFI_WEBCONFIG, "%s:%d: Invalid or missing EMAPMetricsReport\n", __func__, __LINE__); + cJSON_Delete(json); + return webconfig_error_decode; + } + + if (cJSON_GetArraySize(em_ap_report_arr) > MAX_NUM_RADIOS) { + wifi_util_error_print(WIFI_WEBCONFIG, "%s:%d: EMAPMetricsReport has %d entries, max is %d\n", + __func__, __LINE__, cJSON_GetArraySize(em_ap_report_arr), MAX_NUM_RADIOS); + cJSON_Delete(json); return webconfig_error_decode; } @@ -182,17 +209,22 @@ webconfig_error_t decode_em_ap_metrics_report_subdoc(webconfig_t *config, webcon if (em_ap_report_obj == NULL) { wifi_util_error_print(WIFI_WEBCONFIG, "%s:%d: Invalid EMAPMetricsReport object at index %d\n", __func__, __LINE__, i); + free_em_ap_metrics_report_params(params); + cJSON_Delete(json); return webconfig_error_decode; } if(decode_em_ap_metrics_report_object(em_ap_report_obj, ¶ms->em_ap_metrics_report.radio_reports[i]) != webconfig_error_none) { wifi_util_error_print(WIFI_WEBCONFIG, "%s:%d: decode_em_ap_metrics_report_object failed\n", __func__, __LINE__); + free_em_ap_metrics_report_params(params); cJSON_Delete(json); return webconfig_error_decode; } } params->em_ap_metrics_report.radio_count = i; + // the subdoc decoder owns u.encoded.json (see decode_radio_subdoc) + cJSON_Delete(json); return webconfig_error_none; } #endif \ No newline at end of file