Преглед на файлове

qcacld-3.0: Add support to parse ROAM_SYNCH_KEY event

ROAM_SYNCH_KEY event will be sent after roaming to a ML AP
with STA advertising standby-links.

WMI_ROAM_SYNCH_KEY_EVENTID TLV format:
-- wmi_roam_ml_key_material_param: PTK (link_id = 0xf)
-- wmi_roam_ml_key_material_param: GTK (link_id = 0)
-- wmi_roam_ml_key_material_param: IGTK (link_id = 0)
-- wmi_roam_ml_key_material_param: BIGTK (link_id = 0)
-- wmi_roam_ml_key_material_param: LTF KEYSEED (link_id = 0xf)
-- wmi_roam_ml_key_material_param: GTK (link_id = 1)
-- wmi_roam_ml_key_material_param: IGTK (link_id = 1)
-- wmi_roam_ml_key_material_param: BIGTK (link_id = 1)
-- wmi_roam_ml_key_material_param: GTK (link_id = 2)
-- wmi_roam_ml_key_material_param: IGTK (link_id = 2)
-- wmi_roam_ml_key_material_param: BIGTK (link_id = 2)

If length of any key exceeds WMI_MAX_KEY_LEN, then multiple
TLVs will be included for that key.

Change-Id: I64238acfbd29bc40df5971f098ad5802360d8777
CRs-Fixed: 3577923
Pragaspathi Thilagaraj преди 1 година
родител
ревизия
0f7d749e5f

+ 31 - 0
components/wmi/inc/wmi_unified_roam_api.h

@@ -24,6 +24,7 @@
 
 #include <wmi_unified_roam_param.h>
 #include "wlan_cm_roam_public_struct.h"
+#include "wlan_crypto_def_i.h"
 
 #ifdef FEATURE_LFR_SUBNET_DETECTION
 /**
@@ -119,6 +120,36 @@ wmi_extract_roam_vendor_control_param_event(wmi_unified_t wmi_handle,
 
 #endif
 
+#if defined(WLAN_FEATURE_ROAM_OFFLOAD) && defined(WLAN_FEATURE_11BE_MLO)
+/**
+ * wmi_extract_roam_synch_key_event() - extract roam synch key event
+ * @wmi_handle: wmi handle
+ * @event: roam synch key event buffer pointer
+ * @len: event len
+ * @keys: destination buffer to copy keys
+ * @num_keys: Number of keys
+ * @mld_addr: MLD address pointer
+ *
+ * Return: QDF_STATUS_SUCCESS on success and QDF_STATUS_E_FAILURE for failure
+ */
+QDF_STATUS
+wmi_extract_roam_synch_key_event(wmi_unified_t wmi_handle, uint8_t *event,
+				 uint32_t len,
+				 struct wlan_crypto_key_entry **keys,
+				 uint8_t *num_keys,
+				 struct qdf_mac_addr *mld_addr);
+#else
+static inline QDF_STATUS
+wmi_extract_roam_synch_key_event(wmi_unified_t wmi_handle, uint8_t *event,
+				 uint32_t len,
+				 struct wlan_crypto_key_entry **keys,
+				 uint8_t *num_keys,
+				 struct qdf_mac_addr *mld_addr)
+{
+	return QDF_STATUS_E_NOSUPPORT;
+}
+#endif
+
 /**
  * wmi_unified_roam_scan_filter_cmd() - send roam scan allowlist,
  *                                      denylist and preferred list

+ 19 - 1
components/wmi/src/wmi_unified_roam_api.c

@@ -1,6 +1,6 @@
 /*
  * Copyright (c) 2013-2021, The Linux Foundation. All rights reserved.
- * Copyright (c) 2021-2022 Qualcomm Innovation Center, Inc. All rights reserved.
+ * Copyright (c) 2021-2023 Qualcomm Innovation Center, Inc. All rights reserved.
  *
  * Permission to use, copy, modify, and/or distribute this software for any
  * purpose with or without fee is hereby granted, provided that the above
@@ -157,6 +157,24 @@ wmi_extract_roam_vendor_control_param_event(wmi_unified_t wmi_handle,
 }
 #endif
 
+#if defined(WLAN_FEATURE_ROAM_OFFLOAD) && defined(WLAN_FEATURE_11BE_MLO)
+QDF_STATUS
+wmi_extract_roam_synch_key_event(wmi_unified_t wmi_handle, uint8_t *event,
+				 uint32_t len,
+				 struct wlan_crypto_key_entry **keys,
+				 uint8_t *num_keys,
+				 struct qdf_mac_addr *mld_addr)
+{
+	if (wmi_handle->ops->extract_roam_synch_key_event)
+		return wmi_handle->ops->extract_roam_synch_key_event(
+							wmi_handle, event,
+							len, keys, num_keys,
+							mld_addr);
+
+	return QDF_STATUS_E_FAILURE;
+}
+#endif
+
 QDF_STATUS wmi_unified_roam_invoke_cmd(wmi_unified_t wmi_handle,
 				       struct roam_invoke_req *roaminvoke)
 {

+ 405 - 0
components/wmi/src/wmi_unified_roam_tlv.c

@@ -27,6 +27,7 @@
 #include "ol_defines.h"
 #include "wlan_cm_roam_api.h"
 #include "wlan_mlme_api.h"
+#include "wlan_crypto_global_api.h"
 
 #define WMI_MAC_TO_PDEV_MAP(x) ((x) + (1))
 #define WMI_PDEV_TO_MAC_MAP(x) ((x) - (1))
@@ -3677,6 +3678,409 @@ wmi_roam_offload_attach_vendor_handoff_tlv(struct wmi_ops *ops)
 }
 #endif
 
+#if defined(WLAN_FEATURE_ROAM_OFFLOAD) && defined(WLAN_FEATURE_11BE_MLO)
+static inline
+enum wlan_crypto_cipher_type wlan_wmi_cipher_to_crypto(uint8_t cipher)
+{
+	switch (cipher) {
+	case WMI_CIPHER_NONE:
+		return WLAN_CRYPTO_CIPHER_NONE;
+	case WMI_CIPHER_WEP:
+		return WLAN_CRYPTO_CIPHER_WEP;
+	case WMI_CIPHER_WAPI:
+		return WLAN_CRYPTO_CIPHER_WAPI_SMS4;
+	case WMI_CIPHER_AES_CCM:
+		return WLAN_CRYPTO_CIPHER_AES_CCM;
+	case WMI_CIPHER_AES_CMAC:
+		return WLAN_CRYPTO_CIPHER_AES_CMAC;
+	case WMI_CIPHER_AES_GMAC:
+		return WLAN_CRYPTO_CIPHER_AES_GMAC;
+	case WMI_CIPHER_AES_GCM:
+		return WLAN_CRYPTO_CIPHER_AES_GCM;
+	default:
+		return 0;
+	}
+}
+#define MLO_PAIRWISE_LINKID 0xF
+/**
+ * wmi_fill_keys_from_tlv  - Fill the destination key buffer from the WMI TLV
+ * @ml_keys: ML Keys TLV pointer
+ * @dst_key: Destination keys
+ * @dst_key_len: Destination keys length
+ * @count: TLV count
+ * @max_num_tlv: Total number of TLVs
+ *
+ * Return: None
+ */
+static void
+wmi_fill_keys_from_tlv(wmi_roam_ml_key_material_param **ml_keys,
+		       uint8_t *dst_key, uint8_t *dst_key_len, uint8_t *count,
+		       uint8_t max_num_tlv)
+{
+	uint8_t rem_key_len, bytes_filled, key_len, total_key_len;
+	uint8_t max_key_len = WLAN_CRYPTO_KEYBUF_SIZE + WLAN_CRYPTO_MICBUF_SIZE;
+
+	*dst_key_len = (*ml_keys)->key_len;
+	if (*dst_key_len > max_key_len)
+		*dst_key_len = max_key_len;
+
+	total_key_len = *dst_key_len;
+	rem_key_len = *dst_key_len;
+
+	while (rem_key_len) {
+		if (!(*ml_keys)) {
+			wmi_err_rl("ml_keys is NULL. rem_key_len:%d",
+				   rem_key_len);
+			return;
+		}
+
+		if (*count >= max_num_tlv) {
+			wmi_debug("Read all TLVs count:%d", *count);
+			return;
+		}
+
+		if (rem_key_len < WMI_MAX_KEY_LEN)
+			key_len = rem_key_len;
+		else
+			key_len = WMI_MAX_KEY_LEN;
+
+		bytes_filled = total_key_len - rem_key_len;
+		qdf_mem_copy(dst_key + bytes_filled, (*ml_keys)->key_buff,
+			     key_len);
+		(*ml_keys)++;
+		(*count)++;
+
+		rem_key_len -= key_len;
+	}
+}
+
+#define WMI_NUM_KEYS_ALLOCATED (WLAN_MAX_ML_BSS_LINKS * 4)
+static QDF_STATUS
+extract_roam_synch_key_event_tlv(wmi_unified_t wmi_handle,
+				 uint8_t *event, uint32_t data_len,
+				 struct wlan_crypto_key_entry **entries,
+				 uint8_t *num_entries,
+				 struct qdf_mac_addr *mld_addr)
+{
+	WMI_ROAM_SYNCH_KEY_EVENTID_param_tlvs *param_buf = NULL;
+	wmi_roam_ml_key_material_param *ml_keys = NULL;
+	struct wlan_crypto_key_entry *key_entry;
+	struct wlan_crypto_keys *all_keys;
+	struct wlan_crypto_key *dst_key, *pairwise;
+	struct wlan_crypto_key *key_alloc_buf[WMI_NUM_KEYS_ALLOCATED];
+	bool flush_keybuf;
+	uint8_t total_num_tlv,  j = 0, k = 0;
+	uint8_t count = 0, total_links = 0, dst_key_count = 0;
+	uint8_t igtk_idx = 0, bigtk_idx = 0;
+	bool slot_found;
+	QDF_STATUS status = QDF_STATUS_SUCCESS;
+
+	param_buf = (WMI_ROAM_SYNCH_KEY_EVENTID_param_tlvs *)event;
+	if (!param_buf) {
+		wmi_err_rl("received null buf from target");
+		return QDF_STATUS_E_INVAL;
+	}
+
+	total_num_tlv = param_buf->num_ml_key_material;
+	ml_keys = (wmi_roam_ml_key_material_param *)param_buf->ml_key_material;
+	if (!ml_keys) {
+		wmi_err_rl("received ml keys param is null");
+		return QDF_STATUS_E_INVAL;
+	}
+
+	*entries = qdf_mem_malloc(WLAN_MAX_ML_BSS_LINKS * sizeof(*key_entry));
+	if (!*entries)
+		return QDF_STATUS_E_NOMEM;
+
+	/*
+	 * Allocate memory for each PTK, GTK, IGTK, BIGTK keys.
+	 * So total WLAN_MAX_ML_BSS_LINKS * 4 keys are needed
+	 */
+	for (k = 0; k < WMI_NUM_KEYS_ALLOCATED; k++) {
+		key_alloc_buf[k] = qdf_mem_malloc(sizeof(*dst_key));
+		if (!key_alloc_buf[k]) {
+			flush_keybuf = true;
+			status = QDF_STATUS_E_NOMEM;
+			goto free_entries;
+		}
+	}
+
+	/*
+	 * key_entry is the master structure that is given directly to the
+	 * crypto module and stored for each link.
+	 * key_entry -> keys ->key filled from dst_key has the PTK & GTK indexed
+	 * with corresponding key index
+	 *
+	 * key_entry -> keys -> iGTK holds the iGTK key
+	 * key_entry -> keys -> BIGTK holds the BIGTK key
+	 */
+	key_entry = *entries;
+
+	/*
+	 * Initialize all the Key Entry structures with invalid Link
+	 * ID to identify empty links allocated and will be freed
+	 * at the end.
+	 */
+	for (j = 0; j < WLAN_MAX_ML_BSS_LINKS; j++)
+		key_entry[j].link_id = MLO_INVALID_LINK_IDX;
+
+	/*
+	 * TLV Format to parse:
+	 * 1. wmi_roam_ml_key_material_param -> For PTK with Link ID = 0xF
+	 * Copy this PTK to all the key entry of all the links.
+	 *
+	 * 2. wmi_roam_ml_key_material_param -> GTK for a valid Link.
+	 * Get available entry, and fill the GTK to that entry
+	 *
+	 * 3. wmi_roam_ml_key_material_param -> IGTK for a valid link
+	 *
+	 * 4. wmi_roam_ml_key_material_param -> BIGTK for a valid link
+	 *
+	 * 5. wmi_roam_ml_key_material_param -> For LTF Keyseed with Link ID =
+	 * 0xF and flags has LTF_USAGE set.
+	 *
+	 * If any of the key length is > WMI_MAX_KEY_LEN, then multiple
+	 * wmi_roam_ml_key_material_param TLVs follow to get the entire key
+	 */
+	while (ml_keys && count < total_num_tlv &&
+	       dst_key_count < WMI_NUM_KEYS_ALLOCATED) {
+		/*
+		 * Track individual keys with key_alloc_buf[dst_key_count] array
+		 * pointer to avoid mem leaks if parsing/validating any of the
+		 * keys fail.
+		 * Freeing the allocated keys it done at the end of this
+		 * function
+		 */
+		dst_key = key_alloc_buf[dst_key_count];
+		wmi_debug("link_id:%d key_ix:%d key_cipher:%d key_len:%d key_flags:%d",
+			  ml_keys->link_id, ml_keys->key_ix,
+			  ml_keys->key_cipher,
+			  ml_keys->key_len, ml_keys->key_flags);
+
+		if (!is_valid_keyix(ml_keys->key_ix)) {
+			wmi_err_rl("invalid key index:%d", ml_keys->key_ix);
+			status = QDF_STATUS_E_INVAL;
+			flush_keybuf = true;
+			goto free_entries;
+		}
+
+		/* Copy pairwise keys to all the entries */
+		if (ml_keys->link_id == MLO_PAIRWISE_LINKID) {
+			WMI_MAC_ADDR_TO_CHAR_ARRAY(&ml_keys->mac_addr,
+						   mld_addr->bytes);
+			if (!ml_keys->key_len) {
+				wmi_err_rl("Received key_len as 0 for tlv:%d",
+					   count);
+				status = QDF_STATUS_E_INVAL;
+				flush_keybuf = true;
+				goto free_entries;
+			}
+
+			if (ml_keys->key_flags & LTF_USAGE) {
+				struct wlan_crypto_ltf_keyseed_data key_seed;
+				uint8_t key_seed_len;
+
+				if (ml_keys->key_len >
+				    WLAN_MAX_SECURE_LTF_KEYSEED_LEN)
+					ml_keys->key_len =
+						WLAN_MAX_SECURE_LTF_KEYSEED_LEN;
+
+				/*
+				 * Filling the keys from multiple TLVs is
+				 * handled by below API and ml_keys ptr gets
+				 * incremented accordingly inside
+				 */
+				wmi_fill_keys_from_tlv(&ml_keys,
+						       key_seed.key_seed,
+						       &key_seed_len, &count,
+						       total_num_tlv);
+				key_seed.key_seed_len = key_seed_len;
+				wmi_debug("ML_KEY: Got LTF keyseed key for MLD: "
+					  QDF_MAC_ADDR_FMT   " key_seed_len:%d",
+					  QDF_MAC_ADDR_REF(mld_addr->bytes),
+					  key_seed.key_seed_len);
+
+				for (j = 0; j < WLAN_MAX_ML_BSS_LINKS; j++)
+					key_entry[j].keys.ltf_key_seed =
+								key_seed;
+
+				continue;
+			}
+
+			dst_key->valid = true;
+			dst_key->keylen = ml_keys->key_len;
+			dst_key->flags = ml_keys->key_flags;
+			dst_key->keyix = ml_keys->key_ix;
+			dst_key->key_type =
+					WLAN_CRYPTO_KEY_TYPE_UNICAST;
+			dst_key->cipher_type =
+				wlan_wmi_cipher_to_crypto(ml_keys->key_cipher);
+			dst_key->keylen = ml_keys->key_len;
+
+			wmi_fill_keys_from_tlv(&ml_keys, dst_key->keyval,
+					       &dst_key->keylen, &count,
+					       total_num_tlv);
+			wmi_err_rl("ML_KEY: Got Pairwise key for MLD: "
+				   QDF_MAC_ADDR_FMT " rem_len:%d",
+				   QDF_MAC_ADDR_REF(mld_addr->bytes),
+				   dst_key->keylen);
+
+			pairwise = dst_key;
+			/*
+			 * Pairwise keys will be sent only once. Copy that for
+			 * all the link entries
+			 */
+			for (j = 0; j < WLAN_MAX_ML_BSS_LINKS; j++) {
+				dst_key = key_alloc_buf[dst_key_count];
+				*dst_key = *pairwise;
+				key_entry[j].keys.key[dst_key->keyix] = dst_key;
+				dst_key_count++;
+			}
+
+			continue;
+		}
+
+		slot_found = false;
+		for (j = 0; j < WLAN_MAX_ML_BSS_LINKS; j++) {
+			if (ml_keys->link_id == MLO_INVALID_LINK_IDX)
+				break;
+
+			if (key_entry[j].link_id == MLO_INVALID_LINK_IDX ||
+			    key_entry[j].link_id == ml_keys->link_id) {
+				slot_found = true;
+				break;
+			}
+		}
+
+		if (!slot_found) {
+			wmi_err_rl("Not able to find a entry for link:%d j=%d",
+				   ml_keys->link_id, j);
+			break;
+		}
+
+		WMI_MAC_ADDR_TO_CHAR_ARRAY(&ml_keys->mac_addr,
+					   dst_key->macaddr);
+		key_entry[j].link_id = ml_keys->link_id;
+		qdf_copy_macaddr((struct qdf_mac_addr *)key_entry[j].mac_addr.raw,
+				 (struct qdf_mac_addr *)dst_key->macaddr);
+		all_keys = &key_entry[j].keys;
+
+		dst_key->valid = true;
+		dst_key->keyix = ml_keys->key_ix;
+		dst_key->cipher_type =
+				wlan_wmi_cipher_to_crypto(ml_keys->key_cipher);
+
+		qdf_mem_copy(dst_key->keyrsc, ml_keys->pn, WMI_MAX_PN_LEN);
+
+		/*
+		 * For LTF keyseed or FILS SHA 384, FILS SHA 512 cases, the key
+		 * size will go beyond WMI_MAX_KEY_LEN(32). So extract first 32
+		 * bytes from 1st TLV and extract the rest of the bytes from
+		 * the following TLVs
+		 */
+		dst_key->keylen = ml_keys->key_len;
+		wmi_fill_keys_from_tlv(&ml_keys, dst_key->keyval,
+				       &dst_key->keylen, &count, total_num_tlv);
+
+		if (is_igtk(dst_key->keyix)) {
+			dst_key->key_type = WLAN_CRYPTO_KEY_TYPE_GROUP;
+
+			igtk_idx = dst_key->keyix - WLAN_CRYPTO_MAXKEYIDX;
+			bigtk_idx = igtk_idx - WLAN_CRYPTO_MAXIGTKKEYIDX;
+
+			wmi_debug("ML_KEY: Slot:%d link_id:%d addr: " QDF_MAC_ADDR_FMT "Key is IGTK key_ix:%d igtk_idx:%d bigtk:%d",
+				  j, key_entry[j].link_id,
+				  QDF_MAC_ADDR_REF(dst_key->macaddr),
+				  dst_key->keyix, igtk_idx, bigtk_idx);
+			all_keys->igtk_key[igtk_idx] = dst_key;
+			all_keys->def_igtk_tx_keyid = igtk_idx;
+
+			bigtk_idx = 0;
+			igtk_idx = 0;
+		} else if (is_bigtk(dst_key->keyix)) {
+			dst_key->key_type = WLAN_CRYPTO_KEY_TYPE_GROUP;
+
+			igtk_idx = dst_key->keyix - WLAN_CRYPTO_MAXKEYIDX;
+			bigtk_idx = igtk_idx - WLAN_CRYPTO_MAXIGTKKEYIDX;
+
+			wmi_debug("ML_KEY: Slot:%d link_id:%d addr: " QDF_MAC_ADDR_FMT "Key is BIGTK key_ix:%d igtk_idx:%d bigtk:%d",
+				  j, key_entry[j].link_id,
+				  QDF_MAC_ADDR_REF(dst_key->macaddr),
+				  dst_key->keyix, igtk_idx, bigtk_idx);
+			all_keys->bigtk_key[bigtk_idx] = dst_key;
+			all_keys->def_bigtk_tx_keyid = bigtk_idx;
+
+			bigtk_idx = 0;
+			igtk_idx = 0;
+		} else if (is_gtk(dst_key->keyix)) {
+			wmi_debug("ML_KEY: Slot:%d link_id:%d addr: " QDF_MAC_ADDR_FMT " Key is GTK key_ix:%d",
+				  j, key_entry[j].link_id,
+				  QDF_MAC_ADDR_REF(dst_key->macaddr),
+				  dst_key->keyix);
+			dst_key->key_type = WLAN_CRYPTO_KEY_TYPE_GROUP;
+			all_keys->key[dst_key->keyix] = dst_key;
+		} else {
+			wmi_debug("Key is Pairwise. Shouldn't reach here");
+			/* Pairwise key */
+			dst_key->key_type = WLAN_CRYPTO_KEY_TYPE_UNICAST;
+			all_keys->key[dst_key->keyix] = dst_key;
+		}
+
+		dst_key_count++;
+	}
+
+	for (j = 0; j < WLAN_MAX_ML_BSS_LINKS; j++) {
+		if (key_entry[j].link_id != MLO_INVALID_LINK_IDX)
+			total_links++;
+	}
+
+	*num_entries = total_links;
+	/* Free the invalid dst_keys allocated */
+	if (!*num_entries)
+		goto free_entries;
+
+	/*
+	 * This is to free the unfilled key_alloc_buf that
+	 * was allocated initially
+	 */
+	flush_keybuf = false;
+
+	wmi_err_rl("ML_KEYS: total_entries filled:%d total_num_tlv:%d dst_key_count:%d",
+		   *num_entries, total_num_tlv, dst_key_count);
+	goto free_keys;
+
+free_entries:
+	qdf_mem_zero(*entries,
+		     WLAN_MAX_ML_BSS_LINKS * sizeof(**entries));
+	qdf_mem_free(*entries);
+
+free_keys:
+	for (k = 0; k < WMI_NUM_KEYS_ALLOCATED; k++) {
+		if (!key_alloc_buf[k])
+			continue;
+
+		if (!flush_keybuf && key_alloc_buf[k]->valid)
+			continue;
+
+		wmi_debug("Free key allocated at idx:%d", k);
+		qdf_mem_zero(key_alloc_buf[k], sizeof(key_alloc_buf[k]));
+		qdf_mem_free(key_alloc_buf[k]);
+	}
+
+	return status;
+}
+
+static void
+wmi_roam_offload_attach_mlo_tlv(struct wmi_ops *ops)
+{
+	ops->extract_roam_synch_key_event = extract_roam_synch_key_event_tlv;
+}
+#else
+static inline void
+wmi_roam_offload_attach_mlo_tlv(struct wmi_ops *ops)
+{}
+#endif
+
 void wmi_roam_offload_attach_tlv(wmi_unified_t wmi_handle)
 {
 	struct wmi_ops *ops = wmi_handle->ops;
@@ -3703,6 +4107,7 @@ void wmi_roam_offload_attach_tlv(wmi_unified_t wmi_handle)
 	ops->send_set_roam_trigger_cmd = send_set_roam_trigger_cmd_tlv;
 	ops->extract_roam_candidate_frame = extract_roam_candidate_frame_tlv;
 	wmi_roam_offload_attach_vendor_handoff_tlv(ops);
+	wmi_roam_offload_attach_mlo_tlv(ops);
 }
 #else
 static inline QDF_STATUS

+ 1 - 1
core/mac/src/pe/lim/lim_process_probe_rsp_frame.c

@@ -337,7 +337,7 @@ lim_process_probe_rsp_frame(struct mac_context *mac_ctx, uint8_t *rx_Packet_info
 		}
 		session_entry->bcnLen =
 			WMA_GET_RX_MPDU_LEN(rx_Packet_info);
-			session_entry->beacon =
+		session_entry->beacon =
 			qdf_mem_malloc(session_entry->bcnLen);
 		if (!session_entry->beacon) {
 			pe_err("No Memory to store beacon");

+ 0 - 1
core/sme/src/csr/csr_api_roam.c

@@ -87,7 +87,6 @@
 #include "wlan_mlo_mgr_sta.h"
 #include "wlan_mlo_mgr_roam.h"
 
-#define RSN_AUTH_KEY_MGMT_SAE           WLAN_RSN_SEL(WLAN_AKM_SAE)
 #define MAX_PWR_FCC_CHAN_12 8
 #define MAX_PWR_FCC_CHAN_13 2
 #define MAX_CB_VALUE_IN_INI (2)