linux-nfs.vger.kernel.org archive mirror
 help / color / mirror / Atom feed
* [PATCH 0/3] nfs-utils: gssd support for KRB5_AP_ERR_BAD_INTEGRITY
@ 2023-08-29 20:18 Olga Kornievskaia
  2023-08-29 20:18 ` [PATCH 1/1] libtirpc: gss-api: expose gss major/minor error in authgss_refresh() Olga Kornievskaia
                   ` (3 more replies)
  0 siblings, 4 replies; 6+ messages in thread
From: Olga Kornievskaia @ 2023-08-29 20:18 UTC (permalink / raw)
  To: steved; +Cc: linux-nfs

From: Olga Kornievskaia <kolga@netapp.com>

Together with libtirpc patch this series attempts to provide
support for handling KRB5_AP_ERR_BAD_INTEGRITY.

Such error can be returned by the server when it has changed
its key material and the client is still using the service
ticket that was issues prior to the change.

Upon calling authgss_create_default() and receiving a NULL
context, we can inspect the returned structure to see
if gss major/minor error code was set. If the client
determines that it received KRB5_AP_ERR_BAD_INTEGRITY error,
it will proceed to handle it based on what type of credentials
were used for context establishement. If machine credentials
were used, the client can call into a routine and force
credential renewal. If user credentials were used, the client
needs to remove the existing service ticket and then retry
the request.

Olga Kornievskaia (3):
  gssd: enable forcing cred renewal using the keytab
  gssd: handle KRB5_AP_ERR_BAD_INTEGRITY for machine credentials
  gssd: handle KRB5_AP_ERR_BAD_INTEGRITY for user credentials

 utils/gssd/gssd_proc.c | 20 ++++++++++++--
 utils/gssd/krb5_util.c | 62 ++++++++++++++++++++++++++++++++++++------
 utils/gssd/krb5_util.h |  4 ++-
 3 files changed, 75 insertions(+), 11 deletions(-)

-- 
2.39.1


^ permalink raw reply	[flat|nested] 6+ messages in thread

* [PATCH 1/1] libtirpc: gss-api: expose gss major/minor error in authgss_refresh()
  2023-08-29 20:18 [PATCH 0/3] nfs-utils: gssd support for KRB5_AP_ERR_BAD_INTEGRITY Olga Kornievskaia
@ 2023-08-29 20:18 ` Olga Kornievskaia
  2023-08-29 20:19 ` [PATCH 1/3] nfs-utils: gssd: enable forcing cred renewal using the keytab Olga Kornievskaia
                   ` (2 subsequent siblings)
  3 siblings, 0 replies; 6+ messages in thread
From: Olga Kornievskaia @ 2023-08-29 20:18 UTC (permalink / raw)
  To: steved; +Cc: linux-nfs

From: Olga Kornievskaia <kolga@netapp.com>

When the client calls into the libtirpc to establish security
context, the errors that occurred are squashed. Instead, extend
authgss_refresh to propagate back the gss major/minor error
codes to the caller.

Signed-off-by: Olga Kornievskaia <kolga@netapp.com>
---
 src/auth_gss.c       | 17 +++++++++--------
 tirpc/rpc/auth_gss.h |  2 ++
 2 files changed, 11 insertions(+), 8 deletions(-)

diff --git a/src/auth_gss.c b/src/auth_gss.c
index e317664..fa96acd 100644
--- a/src/auth_gss.c
+++ b/src/auth_gss.c
@@ -54,7 +54,7 @@
 
 static void	authgss_nextverf(AUTH *);
 static bool_t	authgss_marshal(AUTH *, XDR *);
-static bool_t	authgss_refresh(AUTH *, void *);
+static bool_t	authgss_refresh(AUTH *, rpc_gss_options_ret_t *);
 static bool_t	authgss_validate(AUTH *, struct opaque_auth *);
 static void	authgss_destroy(AUTH *);
 static void	authgss_destroy_context(AUTH *);
@@ -184,6 +184,7 @@ authgss_create(CLIENT *clnt, gss_name_t name, struct rpc_gss_sec *sec)
 	AUTH			*auth, *save_auth;
 	struct rpc_gss_data	*gd;
 	OM_uint32		min_stat = 0;
+	rpc_gss_options_ret_t	ret;
 
 	gss_log_debug("in authgss_create()");
 
@@ -229,8 +230,12 @@ authgss_create(CLIENT *clnt, gss_name_t name, struct rpc_gss_sec *sec)
 	save_auth = clnt->cl_auth;
 	clnt->cl_auth = auth;
 
-	if (!authgss_refresh(auth, NULL))
+	memset(&ret, 0, sizeof(rpc_gss_options_ret_t));
+	if (!authgss_refresh(auth, &ret)) {
 		auth = NULL;
+		sec->major_status = ret.major_status;
+		sec->minor_status = ret.minor_status;
+	}
 	else
 		authgss_auth_get(auth); /* Reference for caller */
 
@@ -265,7 +270,6 @@ authgss_create_default(CLIENT *clnt, char *service, struct rpc_gss_sec *sec)
 	}
 
 	auth = authgss_create(clnt, name, sec);
-
 	if (name != GSS_C_NO_NAME) {
 		LIBTIRPC_DEBUG(3, ("authgss_create_default: freeing name %p", name));
  		gss_release_name(&min_stat, &name);
@@ -619,12 +623,9 @@ _rpc_gss_refresh(AUTH *auth, rpc_gss_options_ret_t *options_ret)
 }
 
 static bool_t
-authgss_refresh(AUTH *auth, void *dummy)
+authgss_refresh(AUTH *auth, rpc_gss_options_ret_t *ret)
 {
-	rpc_gss_options_ret_t ret;
-
-	memset(&ret, 0, sizeof(ret));
-	return _rpc_gss_refresh(auth, &ret);
+	return _rpc_gss_refresh(auth, ret);
 }
 
 bool_t
diff --git a/tirpc/rpc/auth_gss.h b/tirpc/rpc/auth_gss.h
index f2af6e9..a530d42 100644
--- a/tirpc/rpc/auth_gss.h
+++ b/tirpc/rpc/auth_gss.h
@@ -64,6 +64,8 @@ struct rpc_gss_sec {
 	rpc_gss_svc_t	svc;		/* service */
 	gss_cred_id_t	cred;		/* cred handle */
 	u_int		req_flags;	/* req flags for init_sec_context */
+	int		major_status;
+	int		minor_status;
 };
 
 /* Private data required for kernel implementation */
-- 
2.39.1


^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [PATCH 1/3] nfs-utils: gssd: enable forcing cred renewal using the keytab
  2023-08-29 20:18 [PATCH 0/3] nfs-utils: gssd support for KRB5_AP_ERR_BAD_INTEGRITY Olga Kornievskaia
  2023-08-29 20:18 ` [PATCH 1/1] libtirpc: gss-api: expose gss major/minor error in authgss_refresh() Olga Kornievskaia
@ 2023-08-29 20:19 ` Olga Kornievskaia
  2023-08-29 20:19 ` [PATCH 2/3] nfs-utils: gssd: handle KRB5_AP_ERR_BAD_INTEGRITY for machine credentials Olga Kornievskaia
  2023-08-29 20:19 ` [PATCH 3/3] nfs-utils: gssd: handle KRB5_AP_ERR_BAD_INTEGRITY for user credentials Olga Kornievskaia
  3 siblings, 0 replies; 6+ messages in thread
From: Olga Kornievskaia @ 2023-08-29 20:19 UTC (permalink / raw)
  To: steved; +Cc: linux-nfs

From: Olga Kornievskaia <kolga@netapp.com>

Add a new function parameter "force_renewal" that callers could
set to force service ticket renewal even if one exists already.

This is needed in preparation for handling
KRB5_AP_ERR_BAD_INTEGRITY when service's keytab changes while
the client holds valid service ticket in the cache.

Signed-off-by: Olga Kornievskaia <kolga@netapp.com>
---
 utils/gssd/gssd_proc.c |  2 +-
 utils/gssd/krb5_util.c | 20 ++++++++++++--------
 utils/gssd/krb5_util.h |  3 ++-
 3 files changed, 15 insertions(+), 10 deletions(-)

diff --git a/utils/gssd/gssd_proc.c b/utils/gssd/gssd_proc.c
index ae568f15..4fb6b72d 100644
--- a/utils/gssd/gssd_proc.c
+++ b/utils/gssd/gssd_proc.c
@@ -571,7 +571,7 @@ krb5_use_machine_creds(struct clnt_info *clp, uid_t uid,
 
 	do {
 		gssd_refresh_krb5_machine_credential(clp->servername,
-						     service, srchost);
+						     service, srchost, 0);
 	/*
 	 * Get a list of credential cache names and try each
 	 * of them until one works or we've tried them all
diff --git a/utils/gssd/krb5_util.c b/utils/gssd/krb5_util.c
index e3f270e9..f6ce1fec 100644
--- a/utils/gssd/krb5_util.c
+++ b/utils/gssd/krb5_util.c
@@ -165,7 +165,7 @@ static int select_krb5_ccache(const struct dirent *d);
 static int gssd_find_existing_krb5_ccache(uid_t uid, char *dirname,
 		const char **cctype, struct dirent **d);
 static int gssd_get_single_krb5_cred(krb5_context context,
-		krb5_keytab kt, struct gssd_k5_kt_princ *ple);
+		krb5_keytab kt, struct gssd_k5_kt_princ *ple, int force_renew);
 static int query_krb5_ccache(const char* cred_cache, char **ret_princname,
 		char **ret_realm);
 
@@ -391,7 +391,8 @@ gssd_check_if_cc_exists(struct gssd_k5_kt_princ *ple)
 static int
 gssd_get_single_krb5_cred(krb5_context context,
 			  krb5_keytab kt,
-			  struct gssd_k5_kt_princ *ple)
+			  struct gssd_k5_kt_princ *ple,
+			  int force_renew)
 {
 #ifdef HAVE_KRB5_GET_INIT_CREDS_OPT_SET_ADDRESSLESS
 	krb5_get_init_creds_opt *init_opts = NULL;
@@ -421,7 +422,7 @@ gssd_get_single_krb5_cred(krb5_context context,
 	 */
 	now += 300;
 	pthread_mutex_lock(&ple_lock);
-	if (ple->ccname && ple->endtime > now && !nocache) {
+	if (ple->ccname && ple->endtime > now && !nocache && !force_renew) {
 		printerr(3, "%s(0x%lx): Credentials in CC '%s' are good until %s",
 			 __func__, tid, ple->ccname, ctime((time_t *)&ple->endtime));
 		code = 0;
@@ -1155,7 +1156,8 @@ err_cache:
 static int
 gssd_refresh_krb5_machine_credential_internal(char *hostname,
 				     struct gssd_k5_kt_princ *ple,
-				     char *service, char *srchost)
+				     char *service, char *srchost,
+				     int force_renew)
 {
 	krb5_error_code code = 0;
 	krb5_context context;
@@ -1221,7 +1223,7 @@ gssd_refresh_krb5_machine_credential_internal(char *hostname,
 			goto out_free_kt;
 		}
 	}
-	retval = gssd_get_single_krb5_cred(context, kt, ple);
+	retval = gssd_get_single_krb5_cred(context, kt, ple, force_renew);
 out_free_kt:
 	krb5_kt_close(context, kt);
 out_free_context:
@@ -1344,7 +1346,7 @@ gssd_get_krb5_machine_cred_list(char ***list)
 		pthread_mutex_unlock(&ple_lock);
 		/* Make sure cred is up-to-date before returning it */
 		retval = gssd_refresh_krb5_machine_credential_internal(NULL, ple,
-								       NULL, NULL);
+								       NULL, NULL, 0);
 		pthread_mutex_lock(&ple_lock);
 		if (gssd_k5_kt_princ_list == NULL) {
 			/* Looks like we did shutdown... abort */
@@ -1456,10 +1458,12 @@ gssd_destroy_krb5_principals(int destroy_machine_creds)
  */
 int
 gssd_refresh_krb5_machine_credential(char *hostname,
-				     char *service, char *srchost)
+				     char *service, char *srchost,
+				     int force_renew)
 {
     return gssd_refresh_krb5_machine_credential_internal(hostname, NULL,
-							 service, srchost);
+							 service, srchost,
+							 force_renew);
 }
 
 /*
diff --git a/utils/gssd/krb5_util.h b/utils/gssd/krb5_util.h
index 2415205a..62c91a0e 100644
--- a/utils/gssd/krb5_util.h
+++ b/utils/gssd/krb5_util.h
@@ -16,7 +16,8 @@ int  gssd_get_krb5_machine_cred_list(char ***list);
 void gssd_free_krb5_machine_cred_list(char **list);
 void gssd_destroy_krb5_principals(int destroy_machine_creds);
 int  gssd_refresh_krb5_machine_credential(char *hostname,
-					  char *service, char *srchost);
+					  char *service, char *srchost,
+					  int force_renew);
 char *gssd_k5_err_msg(krb5_context context, krb5_error_code code);
 void gssd_k5_get_default_realm(char **def_realm);
 
-- 
2.39.1


^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [PATCH 2/3] nfs-utils: gssd: handle KRB5_AP_ERR_BAD_INTEGRITY for machine credentials
  2023-08-29 20:18 [PATCH 0/3] nfs-utils: gssd support for KRB5_AP_ERR_BAD_INTEGRITY Olga Kornievskaia
  2023-08-29 20:18 ` [PATCH 1/1] libtirpc: gss-api: expose gss major/minor error in authgss_refresh() Olga Kornievskaia
  2023-08-29 20:19 ` [PATCH 1/3] nfs-utils: gssd: enable forcing cred renewal using the keytab Olga Kornievskaia
@ 2023-08-29 20:19 ` Olga Kornievskaia
  2023-08-29 20:19 ` [PATCH 3/3] nfs-utils: gssd: handle KRB5_AP_ERR_BAD_INTEGRITY for user credentials Olga Kornievskaia
  3 siblings, 0 replies; 6+ messages in thread
From: Olga Kornievskaia @ 2023-08-29 20:19 UTC (permalink / raw)
  To: steved; +Cc: linux-nfs

From: Olga Kornievskaia <kolga@netapp.com>

During context establishment, when the client received
KRB5_AP_ERR_BAD_INTEGRITY error, it might be due to the server
updating its key material. To handle such error, get a new
service ticket and re-try the AP_REQ.

Signed-off-by: Olga Kornievskaia <kolga@netapp.com>
---
 utils/gssd/gssd_proc.c | 16 +++++++++++++++-
 1 file changed, 15 insertions(+), 1 deletion(-)

diff --git a/utils/gssd/gssd_proc.c b/utils/gssd/gssd_proc.c
index 4fb6b72d..e5cc1d98 100644
--- a/utils/gssd/gssd_proc.c
+++ b/utils/gssd/gssd_proc.c
@@ -412,13 +412,27 @@ create_auth_rpc_client(struct clnt_info *clp,
 		tid, tgtname);
 	auth = authgss_create_default(rpc_clnt, tgtname, &sec);
 	if (!auth) {
+		if (sec.minor_status == KRB5KRB_AP_ERR_BAD_INTEGRITY) {
+			printerr(2, "WARNING: server=%s failed context "
+				 "creation with KRB5_AP_ERR_BAD_INTEGRITY\n",
+				 clp->servername);
+			if (cred == GSS_C_NO_CREDENTIAL)
+				retval = gssd_refresh_krb5_machine_credential(clp->servername,
+					"*", NULL, 1);
+			if (!retval) {
+				auth = authgss_create_default(rpc_clnt, tgtname,
+						&sec);
+				if (auth)
+					goto success;
+			}
+		}
 		/* Our caller should print appropriate message */
 		printerr(2, "WARNING: Failed to create krb5 context for "
 			    "user with uid %d for server %s\n",
 			 uid, tgtname);
 		goto out_fail;
 	}
-
+success:
 	/* Success !!! */
 	rpc_clnt->cl_auth = auth;
 	*clnt_return = rpc_clnt;
-- 
2.39.1


^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [PATCH 3/3] nfs-utils: gssd: handle KRB5_AP_ERR_BAD_INTEGRITY for user credentials
  2023-08-29 20:18 [PATCH 0/3] nfs-utils: gssd support for KRB5_AP_ERR_BAD_INTEGRITY Olga Kornievskaia
                   ` (2 preceding siblings ...)
  2023-08-29 20:19 ` [PATCH 2/3] nfs-utils: gssd: handle KRB5_AP_ERR_BAD_INTEGRITY for machine credentials Olga Kornievskaia
@ 2023-08-29 20:19 ` Olga Kornievskaia
  3 siblings, 0 replies; 6+ messages in thread
From: Olga Kornievskaia @ 2023-08-29 20:19 UTC (permalink / raw)
  To: steved; +Cc: linux-nfs

From: Olga Kornievskaia <kolga@netapp.com>

Unlike the machine credential case, we can't throw away the ticket
cache and use the keytab to renew the credentials. Instead, we
need to remove the service ticket for the server that returned
KRB5_AP_ERR_BAD_INTEGRITY and try again.

Signed-off-by: Olga Kornievskaia <kolga@netapp.com>
---
 utils/gssd/gssd_proc.c |  2 ++
 utils/gssd/krb5_util.c | 42 ++++++++++++++++++++++++++++++++++++++++++
 utils/gssd/krb5_util.h |  1 +
 3 files changed, 45 insertions(+)

diff --git a/utils/gssd/gssd_proc.c b/utils/gssd/gssd_proc.c
index e5cc1d98..a96647df 100644
--- a/utils/gssd/gssd_proc.c
+++ b/utils/gssd/gssd_proc.c
@@ -419,6 +419,8 @@ create_auth_rpc_client(struct clnt_info *clp,
 			if (cred == GSS_C_NO_CREDENTIAL)
 				retval = gssd_refresh_krb5_machine_credential(clp->servername,
 					"*", NULL, 1);
+			else
+				retval = gssd_k5_remove_bad_service_cred(clp->servername);
 			if (!retval) {
 				auth = authgss_create_default(rpc_clnt, tgtname,
 						&sec);
diff --git a/utils/gssd/krb5_util.c b/utils/gssd/krb5_util.c
index f6ce1fec..6f66ef4f 100644
--- a/utils/gssd/krb5_util.c
+++ b/utils/gssd/krb5_util.c
@@ -1553,6 +1553,48 @@ gssd_acquire_user_cred(gss_cred_id_t *gss_cred)
 	return ret;
 }
 
+/* Removed a service ticket for nfs/<name> from the ticket cache
+ */
+int
+gssd_k5_remove_bad_service_cred(char *name)
+{
+        krb5_creds in_creds, out_creds;
+        krb5_error_code ret;
+        krb5_context context;
+        krb5_ccache cache;
+        krb5_principal principal;
+        int retflags = KRB5_TC_MATCH_SRV_NAMEONLY;
+        char srvname[1024];
+
+        ret = krb5_init_context(&context);
+        if (ret)
+                goto out_cred;
+        ret = krb5_cc_default(context, &cache);
+        if (ret)
+                goto out_free_context;
+        ret = krb5_cc_get_principal(context, cache, &principal);
+        if (ret)
+                goto out_close_cache;
+        memset(&in_creds, 0, sizeof(in_creds));
+        in_creds.client = principal;
+        sprintf(srvname, "nfs/%s", name);
+        ret = krb5_parse_name(context, srvname, &in_creds.server);
+        if (ret)
+                goto out_free_principal;
+        ret = krb5_cc_retrieve_cred(context, cache, retflags, &in_creds, &out_creds);
+        if (ret)
+                goto out_free_principal;
+        ret = krb5_cc_remove_cred(context, cache, 0, &out_creds);
+out_free_principal:
+        krb5_free_principal(context, principal);
+out_close_cache:
+        krb5_cc_close(context, cache);
+out_free_context:
+        krb5_free_context(context);
+out_cred:
+        return ret;
+}
+
 #ifdef HAVE_SET_ALLOWABLE_ENCTYPES
 /*
  * this routine obtains a credentials handle via gss_acquire_cred()
diff --git a/utils/gssd/krb5_util.h b/utils/gssd/krb5_util.h
index 62c91a0e..7ef87018 100644
--- a/utils/gssd/krb5_util.h
+++ b/utils/gssd/krb5_util.h
@@ -22,6 +22,7 @@ char *gssd_k5_err_msg(krb5_context context, krb5_error_code code);
 void gssd_k5_get_default_realm(char **def_realm);
 
 int gssd_acquire_user_cred(gss_cred_id_t *gss_cred);
+int gssd_k5_remove_bad_service_cred(char *srvname);
 
 #ifdef HAVE_SET_ALLOWABLE_ENCTYPES
 extern int limit_to_legacy_enctypes;
-- 
2.39.1


^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [PATCH 2/3] nfs-utils: gssd: handle KRB5_AP_ERR_BAD_INTEGRITY for machine credentials
  2023-10-04 17:32 [PATCH v2 0/3] nfs-utils: gssd support for KRB5_AP_ERR_BAD_INTEGRITY Olga Kornievskaia
@ 2023-10-04 17:32 ` Olga Kornievskaia
  0 siblings, 0 replies; 6+ messages in thread
From: Olga Kornievskaia @ 2023-10-04 17:32 UTC (permalink / raw)
  To: steved; +Cc: linux-nfs

From: Olga Kornievskaia <kolga@netapp.com>

During context establishment, when the client received
KRB5_AP_ERR_BAD_INTEGRITY error, it might be due to the server
updating its key material. To handle such error, get a new
service ticket and re-try the AP_REQ.

Signed-off-by: Olga Kornievskaia <kolga@netapp.com>
---
 utils/gssd/gssd_proc.c | 16 +++++++++++++++-
 1 file changed, 15 insertions(+), 1 deletion(-)

diff --git a/utils/gssd/gssd_proc.c b/utils/gssd/gssd_proc.c
index 4fb6b72d..e5cc1d98 100644
--- a/utils/gssd/gssd_proc.c
+++ b/utils/gssd/gssd_proc.c
@@ -412,13 +412,27 @@ create_auth_rpc_client(struct clnt_info *clp,
 		tid, tgtname);
 	auth = authgss_create_default(rpc_clnt, tgtname, &sec);
 	if (!auth) {
+		if (sec.minor_status == KRB5KRB_AP_ERR_BAD_INTEGRITY) {
+			printerr(2, "WARNING: server=%s failed context "
+				 "creation with KRB5_AP_ERR_BAD_INTEGRITY\n",
+				 clp->servername);
+			if (cred == GSS_C_NO_CREDENTIAL)
+				retval = gssd_refresh_krb5_machine_credential(clp->servername,
+					"*", NULL, 1);
+			if (!retval) {
+				auth = authgss_create_default(rpc_clnt, tgtname,
+						&sec);
+				if (auth)
+					goto success;
+			}
+		}
 		/* Our caller should print appropriate message */
 		printerr(2, "WARNING: Failed to create krb5 context for "
 			    "user with uid %d for server %s\n",
 			 uid, tgtname);
 		goto out_fail;
 	}
-
+success:
 	/* Success !!! */
 	rpc_clnt->cl_auth = auth;
 	*clnt_return = rpc_clnt;
-- 
2.39.1


^ permalink raw reply related	[flat|nested] 6+ messages in thread

end of thread, other threads:[~2023-10-04 17:32 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2023-08-29 20:18 [PATCH 0/3] nfs-utils: gssd support for KRB5_AP_ERR_BAD_INTEGRITY Olga Kornievskaia
2023-08-29 20:18 ` [PATCH 1/1] libtirpc: gss-api: expose gss major/minor error in authgss_refresh() Olga Kornievskaia
2023-08-29 20:19 ` [PATCH 1/3] nfs-utils: gssd: enable forcing cred renewal using the keytab Olga Kornievskaia
2023-08-29 20:19 ` [PATCH 2/3] nfs-utils: gssd: handle KRB5_AP_ERR_BAD_INTEGRITY for machine credentials Olga Kornievskaia
2023-08-29 20:19 ` [PATCH 3/3] nfs-utils: gssd: handle KRB5_AP_ERR_BAD_INTEGRITY for user credentials Olga Kornievskaia
2023-10-04 17:32 [PATCH v2 0/3] nfs-utils: gssd support for KRB5_AP_ERR_BAD_INTEGRITY Olga Kornievskaia
2023-10-04 17:32 ` [PATCH 2/3] nfs-utils: gssd: handle KRB5_AP_ERR_BAD_INTEGRITY for machine credentials Olga Kornievskaia

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).