X-Git-Url: https://git.exim.org/exim.git/blobdiff_plain/da47dd4d092ba35e4f8ff055d79693cc1266c816..a85c067ba6c6940512cf57ec213277a370d87e70:/src/src/auths/gsasl_exim.c diff --git a/src/src/auths/gsasl_exim.c b/src/src/auths/gsasl_exim.c index 708957f04..aac9c84e6 100644 --- a/src/src/auths/gsasl_exim.c +++ b/src/src/auths/gsasl_exim.c @@ -2,9 +2,10 @@ * Exim - an Internet mail transport agent * *************************************************/ -/* Copyright (c) The Exim Maintainers 2019-2020 */ +/* Copyright (c) The Exim Maintainers 2019 - 2022 */ /* Copyright (c) University of Cambridge 1995 - 2018 */ /* See the file NOTICE for conditions of use and distribution. */ +/* SPDX-License-Identifier: GPL-2.0-only */ /* Copyright (c) Twitter Inc 2012 Author: Phil Pennock */ @@ -27,7 +28,6 @@ sense in all contexts. For some, we can do checks at init time. */ #include "../exim.h" -#define CHANNELBIND_HACK #ifndef AUTH_GSASL /* dummy function to satisfy compilers when we link in an "empty" file. */ @@ -40,14 +40,40 @@ static void dummy(int x) { dummy2(x-1); } #include "gsasl_exim.h" -#if GSASL_VERSION_MINOR >= 9 +#if GSASL_VERSION_MAJOR == 2 + # define EXIM_GSASL_HAVE_SCRAM_SHA_256 +# define EXIM_GSASL_SCRAM_S_KEY +# if GSASL_VERSION_MINOR >= 1 +# define EXIM_GSASL_HAVE_EXPORTER +# elif GSASL_VERSION_PATCH >= 1 +# define EXIM_GSASL_HAVE_EXPORTER +# endif -# if GSASL_VERSION_PATCH >= 1 +#elif GSASL_VERSION_MAJOR == 1 +# if GSASL_VERSION_MINOR >= 10 +# define EXIM_GSASL_HAVE_SCRAM_SHA_256 # define EXIM_GSASL_SCRAM_S_KEY + +# elif GSASL_VERSION_MINOR == 9 +# define EXIM_GSASL_HAVE_SCRAM_SHA_256 + +# if GSASL_VERSION_PATCH >= 1 +# define EXIM_GSASL_SCRAM_S_KEY +# endif +# if GSASL_VERSION_PATCH < 2 +# define CHANNELBIND_HACK +# endif + +# else +# define CHANNELBIND_HACK # endif #endif +/* Convenience for testing strings */ + +#define STREQIC(Foo, Bar) (strcmpic((Foo), (Bar)) == 0) + /* Authenticator-specific options. */ /* I did have server_*_condition options for various mechanisms, but since @@ -99,7 +125,7 @@ void auth_gsasl_init(auth_instance *ablock) {} int auth_gsasl_server(auth_instance *ablock, uschar *data) {return 0;} int auth_gsasl_client(auth_instance *ablock, void * sx, int timeout, uschar *buffer, int buffsize) {return 0;} -void auth_gsasl_version_report(FILE *f) {} +gstring * auth_gsasl_version_report(gstring * g) {return NULL;} void auth_gsasl_macros(void) @@ -191,15 +217,21 @@ if (!gsasl_client_support_p(gsasl_ctx, CCS ob->server_mech)) "GNU SASL does not support mechanism \"%s\"", ablock->name, ob->server_mech); -ablock->server = TRUE; - -if ( !ablock->server_condition - && ( streqic(ob->server_mech, US"EXTERNAL") - || streqic(ob->server_mech, US"ANONYMOUS") - || streqic(ob->server_mech, US"PLAIN") - || streqic(ob->server_mech, US"LOGIN") - ) ) +if (ablock->server_condition) + ablock->server = TRUE; +else if( ob->server_mech + && !STREQIC(ob->server_mech, US"EXTERNAL") + && !STREQIC(ob->server_mech, US"ANONYMOUS") + && !STREQIC(ob->server_mech, US"PLAIN") + && !STREQIC(ob->server_mech, US"LOGIN") + ) { + /* At present, for mechanisms we don't panic on absence of server_condition; + need to figure out the most generically correct approach to deciding when + it's critical and when it isn't. Eg, for simple validation (PLAIN mechanism, + etc) it clearly is critical. + */ + ablock->server = FALSE; HDEBUG(D_auth) debug_printf("%s authenticator: " "Need server_condition for %s mechanism\n", @@ -210,7 +242,7 @@ if ( !ablock->server_condition which properties will be needed. */ if ( !ob->server_realm - && streqic(ob->server_mech, US"DIGEST-MD5")) + && STREQIC(ob->server_mech, US"DIGEST-MD5")) { ablock->server = FALSE; HDEBUG(D_auth) debug_printf("%s authenticator: " @@ -218,12 +250,6 @@ if ( !ob->server_realm ablock->name, ob->server_mech); } -/* At present, for mechanisms we don't panic on absence of server_condition; -need to figure out the most generically correct approach to deciding when -it's critical and when it isn't. Eg, for simple validation (PLAIN mechanism, -etc) it clearly is critical. -*/ - ablock->client = ob->client_username && ob->client_password; } @@ -245,7 +271,7 @@ if (!cb_state) if (prop == GSASL_CB_TLS_UNIQUE) { uschar * s; - if ((s = gsasl_callback_hook_get(ctx))) + if ((s = gsasl_callback_hook_get(ctx))) /* Gross hack for early lib vers */ { HDEBUG(D_auth) debug_printf("GSASL_CB_TLS_UNIQUE from ctx hook\n"); gsasl_property_set(sctx, GSASL_CB_TLS_UNIQUE, CS s); @@ -299,48 +325,56 @@ gsasl_prop_code_to_name(Gsasl_property prop) { switch (prop) { - case GSASL_AUTHID: return US"AUTHID"; - case GSASL_AUTHZID: return US"AUTHZID"; - case GSASL_PASSWORD: return US"PASSWORD"; - case GSASL_ANONYMOUS_TOKEN: return US"ANONYMOUS_TOKEN"; - case GSASL_SERVICE: return US"SERVICE"; - case GSASL_HOSTNAME: return US"HOSTNAME"; - case GSASL_GSSAPI_DISPLAY_NAME: return US"GSSAPI_DISPLAY_NAME"; - case GSASL_PASSCODE: return US"PASSCODE"; - case GSASL_SUGGESTED_PIN: return US"SUGGESTED_PIN"; - case GSASL_PIN: return US"PIN"; - case GSASL_REALM: return US"REALM"; - case GSASL_DIGEST_MD5_HASHED_PASSWORD: return US"DIGEST_MD5_HASHED_PASSWORD"; - case GSASL_QOPS: return US"QOPS"; - case GSASL_QOP: return US"QOP"; - case GSASL_SCRAM_ITER: return US"SCRAM_ITER"; - case GSASL_SCRAM_SALT: return US"SCRAM_SALT"; - case GSASL_SCRAM_SALTED_PASSWORD: return US"SCRAM_SALTED_PASSWORD"; + case GSASL_AUTHID: return US"AUTHID"; + case GSASL_AUTHZID: return US"AUTHZID"; + case GSASL_PASSWORD: return US"PASSWORD"; + case GSASL_ANONYMOUS_TOKEN: return US"ANONYMOUS_TOKEN"; + case GSASL_SERVICE: return US"SERVICE"; + case GSASL_HOSTNAME: return US"HOSTNAME"; + case GSASL_GSSAPI_DISPLAY_NAME: return US"GSSAPI_DISPLAY_NAME"; + case GSASL_PASSCODE: return US"PASSCODE"; + case GSASL_SUGGESTED_PIN: return US"SUGGESTED_PIN"; + case GSASL_PIN: return US"PIN"; + case GSASL_REALM: return US"REALM"; + case GSASL_DIGEST_MD5_HASHED_PASSWORD: return US"DIGEST_MD5_HASHED_PASSWORD"; + case GSASL_QOPS: return US"QOPS"; + case GSASL_QOP: return US"QOP"; + case GSASL_SCRAM_ITER: return US"SCRAM_ITER"; + case GSASL_SCRAM_SALT: return US"SCRAM_SALT"; + case GSASL_SCRAM_SALTED_PASSWORD: return US"SCRAM_SALTED_PASSWORD"; #ifdef EXIM_GSASL_SCRAM_S_KEY - case GSASL_SCRAM_STOREDKEY: return US"SCRAM_STOREDKEY"; - case GSASL_SCRAM_SERVERKEY: return US"SCRAM_SERVERKEY"; + case GSASL_SCRAM_STOREDKEY: return US"SCRAM_STOREDKEY"; + case GSASL_SCRAM_SERVERKEY: return US"SCRAM_SERVERKEY"; #endif - case GSASL_CB_TLS_UNIQUE: return US"CB_TLS_UNIQUE"; - case GSASL_SAML20_IDP_IDENTIFIER: return US"SAML20_IDP_IDENTIFIER"; - case GSASL_SAML20_REDIRECT_URL: return US"SAML20_REDIRECT_URL"; - case GSASL_OPENID20_REDIRECT_URL: return US"OPENID20_REDIRECT_URL"; - case GSASL_OPENID20_OUTCOME_DATA: return US"OPENID20_OUTCOME_DATA"; - case GSASL_SAML20_AUTHENTICATE_IN_BROWSER: return US"SAML20_AUTHENTICATE_IN_BROWSER"; - case GSASL_OPENID20_AUTHENTICATE_IN_BROWSER: return US"OPENID20_AUTHENTICATE_IN_BROWSER"; -#ifdef EXIM_GSASL_SCRAM_S_KEY - case GSASL_SCRAM_CLIENTKEY: return US"SCRAM_CLIENTKEY"; +#ifdef EXIM_GSASL_HAVE_EXPORTER /* v. 2.1.0 */ + case GSASL_CB_TLS_EXPORTER: return US"CB_TLS_EXPORTER"; #endif - case GSASL_VALIDATE_SIMPLE: return US"VALIDATE_SIMPLE"; - case GSASL_VALIDATE_EXTERNAL: return US"VALIDATE_EXTERNAL"; - case GSASL_VALIDATE_ANONYMOUS: return US"VALIDATE_ANONYMOUS"; - case GSASL_VALIDATE_GSSAPI: return US"VALIDATE_GSSAPI"; - case GSASL_VALIDATE_SECURID: return US"VALIDATE_SECURID"; - case GSASL_VALIDATE_SAML20: return US"VALIDATE_SAML20"; - case GSASL_VALIDATE_OPENID20: return US"VALIDATE_OPENID20"; + case GSASL_CB_TLS_UNIQUE: return US"CB_TLS_UNIQUE"; + case GSASL_SAML20_IDP_IDENTIFIER: return US"SAML20_IDP_IDENTIFIER"; + case GSASL_SAML20_REDIRECT_URL: return US"SAML20_REDIRECT_URL"; + case GSASL_OPENID20_REDIRECT_URL: return US"OPENID20_REDIRECT_URL"; + case GSASL_OPENID20_OUTCOME_DATA: return US"OPENID20_OUTCOME_DATA"; + case GSASL_SAML20_AUTHENTICATE_IN_BROWSER: return US"SAML20_AUTHENTICATE_IN_BROWSER"; + case GSASL_OPENID20_AUTHENTICATE_IN_BROWSER: return US"OPENID20_AUTHENTICATE_IN_BROWSER"; + case GSASL_VALIDATE_SIMPLE: return US"VALIDATE_SIMPLE"; + case GSASL_VALIDATE_EXTERNAL: return US"VALIDATE_EXTERNAL"; + case GSASL_VALIDATE_ANONYMOUS: return US"VALIDATE_ANONYMOUS"; + case GSASL_VALIDATE_GSSAPI: return US"VALIDATE_GSSAPI"; + case GSASL_VALIDATE_SECURID: return US"VALIDATE_SECURID"; + case GSASL_VALIDATE_SAML20: return US"VALIDATE_SAML20"; + case GSASL_VALIDATE_OPENID20: return US"VALIDATE_OPENID20"; } return CUS string_sprintf("(unknown prop: %d)", (int)prop); } +static void +preload_prop(Gsasl_session * sctx, Gsasl_property propcode, const uschar * val) +{ +DEBUG(D_auth) debug_printf("preloading prop %s val %s\n", + gsasl_prop_code_to_name(propcode), val); +gsasl_property_set(sctx, propcode, CCS val); +} + /************************************************* * Server entry point * *************************************************/ @@ -348,12 +382,12 @@ return CUS string_sprintf("(unknown prop: %d)", (int)prop); /* For interface, see auths/README */ int -auth_gsasl_server(auth_instance *ablock, uschar *initial_data) +auth_gsasl_server(auth_instance * ablock, uschar * initial_data) { -char *tmps; -char *to_send, *received; -Gsasl_session *sctx = NULL; -auth_gsasl_options_block *ob = +uschar * tmps; +char * to_send, * received; +Gsasl_session * sctx = NULL; +auth_gsasl_options_block * ob = (auth_gsasl_options_block *)(ablock->options_block); struct callback_exim_state cb_state; int rc, auth_result, exim_error, exim_error_override; @@ -374,9 +408,9 @@ if (tls_in.channelbinding && ob->server_channelbinding) } # endif # ifdef CHANNELBIND_HACK -/* This is a gross hack to get around the library a) requiring that -c-b was already set, at the _start() call, and b) caching a b64'd -version of the binding then which it never updates. */ +/* This is a gross hack to get around the library before 1.9.2 +a) requiring that c-b was already set, at the _start() call, and +b) caching a b64'd version of the binding then which it never updates. */ gsasl_callback_hook_set(gsasl_ctx, tls_in.channelbinding); # endif @@ -396,18 +430,18 @@ cb_state.ablock = ablock; cb_state.currently = CURRENTLY_SERVER; gsasl_session_hook_set(sctx, &cb_state); -tmps = CS expand_string(ob->server_service); -gsasl_property_set(sctx, GSASL_SERVICE, tmps); -tmps = CS expand_string(ob->server_hostname); -gsasl_property_set(sctx, GSASL_HOSTNAME, tmps); +tmps = expand_string(ob->server_service); +preload_prop(sctx, GSASL_SERVICE, tmps); +tmps = expand_string(ob->server_hostname); +preload_prop(sctx, GSASL_HOSTNAME, tmps); if (ob->server_realm) { - tmps = CS expand_string(ob->server_realm); + tmps = expand_string(ob->server_realm); if (tmps && *tmps) - gsasl_property_set(sctx, GSASL_REALM, tmps); + preload_prop(sctx, GSASL_REALM, tmps); } /* We don't support protection layers. */ -gsasl_property_set(sctx, GSASL_QOPS, "qop-auth"); +preload_prop(sctx, GSASL_QOPS, US "qop-auth"); #ifndef DISABLE_TLS if (tls_in.channelbinding) @@ -429,13 +463,24 @@ if (tls_in.channelbinding) would then result in mechanism name changes on a library update, we have little choice but to default it off and let the admin choose to enable it. *sigh* + + Earlier library versions need this set early, during the _start() call, + so we had to misuse gsasl_callback_hook_set/get() as a data transfer + mech for the callback done at that time to get the bind-data. More recently + the callback is done (if needed) during the first gsasl_stop(). We know + the bind-data here so can set it (and should not get a callback). */ if (ob->server_channelbinding) { HDEBUG(D_auth) debug_printf("Auth %s: Enabling channel-binding\n", ablock->name); # ifndef CHANNELBIND_HACK - gsasl_property_set(sctx, GSASL_CB_TLS_UNIQUE, CCS tls_in.channelbinding); + preload_prop(sctx, +# ifdef EXIM_GSASL_HAVE_EXPORTER + tls_in.channelbind_exporter ? GSASL_CB_TLS_EXPORTER : +# endif + GSASL_CB_TLS_UNIQUE, + tls_in.channelbinding); # endif } else @@ -570,14 +615,19 @@ return GSASL_AUTHENTICATION_ERROR; } +/* Set the "next" $auth[n] and increment expand_nmax */ + static void set_exim_authvar_from_prop(Gsasl_session * sctx, Gsasl_property prop) { uschar * propval = US gsasl_property_fast(sctx, prop); int i = expand_nmax, j = i + 1; propval = propval ? string_copy(propval) : US""; -auth_vars[i] = expand_nstring[j] = propval; +HDEBUG(D_auth) debug_printf("auth[%d] <= %s'%s'\n", + j, gsasl_prop_code_to_name(prop), propval); +expand_nstring[j] = propval; expand_nlength[j] = Ustrlen(propval); +if (i < AUTH_VARS) auth_vars[i] = propval; expand_nmax = j; } @@ -621,10 +671,10 @@ static int server_callback(Gsasl *ctx, Gsasl_session *sctx, Gsasl_property prop, auth_instance *ablock) { -char *tmps; -uschar *s, *propval; +char * tmps; +uschar * s; int cbrc = GSASL_NO_CALLBACK; -auth_gsasl_options_block *ob = +auth_gsasl_options_block * ob = (auth_gsasl_options_block *)(ablock->options_block); HDEBUG(D_auth) debug_printf("GNU SASL callback %s for %s/%s as server\n", @@ -740,7 +790,7 @@ switch (prop) for memory wiping, so expanding strings will leave stuff laying around. But no need to compound the problem, so get rid of the one we can. */ - memset(tmps, '\0', strlen(tmps)); + if (US tmps != s) memset(tmps, '\0', strlen(tmps)); cbrc = GSASL_OK; break; @@ -765,7 +815,6 @@ set_client_prop(Gsasl_session * sctx, Gsasl_property prop, uschar * val, unsigned flags, uschar * buffer, int buffsize) { uschar * s; -int rc; if (!val) return !!(flags & PROP_OPTIONAL); if (!(s = expand_string(val)) || !(flags & PROP_OPTIONAL) && !*s) @@ -791,13 +840,13 @@ return TRUE; int auth_gsasl_client( - auth_instance *ablock, /* authenticator block */ + auth_instance * ablock, /* authenticator block */ void * sx, /* connection */ int timeout, /* command timeout */ - uschar *buffer, /* buffer for reading response */ + uschar * buffer, /* buffer for reading response */ int buffsize) /* size of buffer */ { -auth_gsasl_options_block *ob = +auth_gsasl_options_block * ob = (auth_gsasl_options_block *)(ablock->options_block); Gsasl_session * sctx = NULL; struct callback_exim_state cb_state; @@ -816,16 +865,17 @@ if (tls_out.channelbinding && ob->client_channelbinding) { # ifndef DISABLE_TLS_RESUME if (!tls_out.ext_master_secret && tls_out.resumption == RESUME_USED) - { /* per RFC 7677 section 4 */ + { /* Per RFC 7677 section 4. See also RFC 7627, "Triple Handshake" + vulnerability, and https://www.mitls.org/pages/attacks/3SHAKE */ string_format(buffer, buffsize, "%s", "channel binding not usable on resumed TLS without extended-master-secret"); return FAIL; } # endif # ifdef CHANNELBIND_HACK - /* This is a gross hack to get around the library a) requiring that - c-b was already set, at the _start() call, and b) caching a b64'd - version of the binding then which it never updates. */ + /* This is a gross hack to get around the library before 1.9.2 + a) requiring that c-b was already set, at the _start() call, and + b) caching a b64'd version of the binding then which it never updates. */ gsasl_callback_hook_set(gsasl_ctx, tls_out.channelbinding); # endif @@ -846,10 +896,7 @@ gsasl_session_hook_set(sctx, &cb_state); /* Set properties */ -if ( !set_client_prop(sctx, GSASL_SCRAM_SALTED_PASSWORD, ob->client_spassword, - 0, buffer, buffsize) - && - !set_client_prop(sctx, GSASL_PASSWORD, ob->client_password, +if ( !set_client_prop(sctx, GSASL_PASSWORD, ob->client_password, 0, buffer, buffsize) || !set_client_prop(sctx, GSASL_AUTHID, ob->client_username, 0, buffer, buffsize) @@ -865,7 +912,12 @@ if (tls_out.channelbinding) HDEBUG(D_auth) debug_printf("Auth %s: Enabling channel-binding\n", ablock->name); # ifndef CHANNELBIND_HACK - gsasl_property_set(sctx, GSASL_CB_TLS_UNIQUE, CCS tls_out.channelbinding); + preload_prop(sctx, +# ifdef EXIM_GSASL_HAVE_EXPORTER + tls_out.channelbind_exporter ? GSASL_CB_TLS_EXPORTER : +# endif + GSASL_CB_TLS_UNIQUE, + tls_out.channelbinding); # endif } else @@ -879,24 +931,27 @@ if (tls_out.channelbinding) for(s = NULL; ;) { uschar * outstr; - BOOL fail; + BOOL fail = TRUE; rc = gsasl_step64(sctx, CS s, CSS &outstr); - fail = initial - ? smtp_write_command(sx, SCMD_FLUSH, - outstr ? "AUTH %s %s\r\n" : "AUTH %s\r\n", - ablock->public_name, outstr) <= 0 - : outstr - ? smtp_write_command(sx, SCMD_FLUSH, "%s\r\n", outstr) <= 0 - : FALSE; - if (outstr && *outstr) free(outstr); - if (fail) + if (rc == GSASL_NEEDS_MORE || rc == GSASL_OK) { - yield = FAIL_SEND; - goto done; + fail = initial + ? smtp_write_command(sx, SCMD_FLUSH, + outstr ? "AUTH %s %s\r\n" : "AUTH %s\r\n", + ablock->public_name, outstr) <= 0 + : outstr + ? smtp_write_command(sx, SCMD_FLUSH, "%s\r\n", outstr) <= 0 + : FALSE; + free(outstr); + if (fail) + { + yield = FAIL_SEND; + goto done; + } + initial = FALSE; } - initial = FALSE; if (rc != GSASL_NEEDS_MORE) { @@ -927,10 +982,13 @@ for(s = NULL; ;) } done: -HDEBUG(D_auth) +if (yield == OK) { - const uschar * s = CUS gsasl_property_fast(sctx, GSASL_SCRAM_SALTED_PASSWORD); - if (s) debug_printf(" - SaltedPassword: '%s'\n", s); + expand_nmax = 0; + set_exim_authvar_from_prop(sctx, GSASL_AUTHID); + set_exim_authvar_from_prop(sctx, GSASL_SCRAM_ITER); + set_exim_authvar_from_prop(sctx, GSASL_SCRAM_SALT); + set_exim_authvar_from_prop(sctx, GSASL_SCRAM_SALTED_PASSWORD); } gsasl_finish(sctx); @@ -944,11 +1002,40 @@ HDEBUG(D_auth) debug_printf("GNU SASL callback %s for %s/%s as client\n", gsasl_prop_code_to_name(prop), ablock->name, ablock->public_name); switch (prop) { - case GSASL_CB_TLS_UNIQUE: - HDEBUG(D_auth) - debug_printf(" filling in\n"); +#ifdef EXIM_GSASL_HAVE_EXPORTER + case GSASL_CB_TLS_EXPORTER: /* Should never get called for this, as pre-set */ + if (!tls_out.channelbind_exporter) break; + HDEBUG(D_auth) debug_printf(" filling in\n"); + gsasl_property_set(sctx, GSASL_CB_TLS_EXPORTER, CCS tls_out.channelbinding); + return GSASL_OK; +#endif + case GSASL_CB_TLS_UNIQUE: /* Should never get called for this, as pre-set */ +#ifdef EXIM_GSASL_HAVE_EXPORTER + if (tls_out.channelbind_exporter) break; +#endif + HDEBUG(D_auth) debug_printf(" filling in\n"); gsasl_property_set(sctx, GSASL_CB_TLS_UNIQUE, CCS tls_out.channelbinding); + return GSASL_OK; + case GSASL_SCRAM_SALTED_PASSWORD: + { + uschar * client_spassword = + ((auth_gsasl_options_block *) ablock->options_block)->client_spassword; + uschar dummy[4]; + HDEBUG(D_auth) if (!client_spassword) + debug_printf(" client_spassword option unset\n"); + if (client_spassword) + { + expand_nmax = 0; + set_exim_authvar_from_prop(sctx, GSASL_AUTHID); + set_exim_authvar_from_prop(sctx, GSASL_SCRAM_ITER); + set_exim_authvar_from_prop(sctx, GSASL_SCRAM_SALT); + set_client_prop(sctx, GSASL_SCRAM_SALTED_PASSWORD, client_spassword, + 0, dummy, sizeof(dummy)); + for (int i = 0; i < AUTH_VARS; i++) auth_vars[i] = NULL; + expand_nmax = 0; + } break; + } default: HDEBUG(D_auth) debug_printf(" not providing one\n"); @@ -961,14 +1048,12 @@ return GSASL_NO_CALLBACK; * Diagnostic API * *************************************************/ -void -auth_gsasl_version_report(FILE *f) +gstring * +auth_gsasl_version_report(gstring * g) { -const char *runtime; -runtime = gsasl_check_version(NULL); -fprintf(f, "Library version: GNU SASL: Compile: %s\n" - " Runtime: %s\n", - GSASL_VERSION, runtime); +return string_fmt_append(g, "Library version: GNU SASL: Compile: %s\n" + " Runtime: %s\n", + GSASL_VERSION, gsasl_check_version(NULL)); }