diff --git a/Rakefile b/Rakefile index 497179a..f87780b 100644 --- a/Rakefile +++ b/Rakefile @@ -130,6 +130,7 @@ file check("client") => build/mbox.o build/mirror.o build/status.o + build/sockutil.o build/util.o} do |t| gcc_link t.name, t.prerequisites + [LIBCHECK] end @@ -165,6 +166,7 @@ file check("serve") => build/acl.o build/mbox.o build/ioutil.o + build/sockutil.o build/util.o} do |t| gcc_link t.name, t.prerequisites + [LIBCHECK] end @@ -185,6 +187,7 @@ file check("readwrite") => build/nbdtypes.o build/mbox.o build/ioutil.o + build/sockutil.o build/util.o} do |t| gcc_link t.name, t.prerequisites + [LIBCHECK] end @@ -194,6 +197,7 @@ file check("flexnbd") => %w{build/tests/check_flexnbd.o build/flexnbd.o build/ioutil.o + build/sockutil.o build/util.o build/control.o build/mbox.o diff --git a/src/ioutil.c b/src/ioutil.c index c693be9..51cd7a1 100644 --- a/src/ioutil.c +++ b/src/ioutil.c @@ -146,7 +146,10 @@ int sendfileloop(int out_fd, int in_fd, off64_t *offset, size_t count) ssize_t result = sendfile64(out_fd, in_fd, offset, count-sent); debug("sendfile64(out_fd=%d, in_fd=%d, offset=%p, count-sent=%ld) = %ld", out_fd, in_fd, offset, count-sent, result); - if (result == -1) { return -1; } + if (result == -1) { + debug( "%s (%i) calling sendfile64()", strerror(errno), errno ); + return -1; + } sent += result; debug("sent=%ld, count=%ld", sent, count); } diff --git a/src/mode.h b/src/mode.h index 4b96b4a..2c9d1fc 100644 --- a/src/mode.h +++ b/src/mode.h @@ -12,10 +12,8 @@ void mode(char* mode, int argc, char **argv); #define OPT_HELP "help" #define OPT_ADDR "addr" -#define OPT_REBIND_ADDR "rebind-addr" #define OPT_BIND "bind" #define OPT_PORT "port" -#define OPT_REBIND_PORT "rebind-port" #define OPT_FILE "file" #define OPT_SOCK "sock" #define OPT_FROM "from" @@ -44,9 +42,7 @@ void mode(char* mode, int argc, char **argv); #define GETOPT_DENY GETOPT_FLAG( OPT_DENY, 'd' ) #define GETOPT_ADDR GETOPT_ARG( OPT_ADDR, 'l' ) -#define GETOPT_REBIND_ADDR GETOPT_ARG( OPT_REBIND_ADDR, 'L') #define GETOPT_PORT GETOPT_ARG( OPT_PORT, 'p' ) -#define GETOPT_REBIND_PORT GETOPT_ARG( OPT_REBIND_PORT, 'P') #define GETOPT_FILE GETOPT_ARG( OPT_FILE, 'f' ) #define GETOPT_SOCK GETOPT_ARG( OPT_SOCK, 's' ) #define GETOPT_FROM GETOPT_ARG( OPT_FROM, 'F' ) @@ -86,3 +82,4 @@ void mode(char* mode, int argc, char **argv); char * help_help_text; #endif + diff --git a/src/nbdtypes.c b/src/nbdtypes.c index 51b57f9..8403e73 100644 --- a/src/nbdtypes.c +++ b/src/nbdtypes.c @@ -55,3 +55,4 @@ void nbd_h2r_reply( struct nbd_reply * from, struct nbd_reply_raw * to ) to->error = be32toh( from->error ); memcpy( to->handle, from->handle, 8 ); } + diff --git a/src/nbdtypes.h b/src/nbdtypes.h index 36d94b8..ea82699 100644 --- a/src/nbdtypes.h +++ b/src/nbdtypes.h @@ -3,13 +3,19 @@ /* http://linux.derkeiler.com/Mailing-Lists/Kernel/2003-09/2332.html */ -#define INIT_PASSWD "NBDMAGIC" -#define INIT_MAGIC 0x0000420281861253 -#define REQUEST_MAGIC 0x25609513 -#define REPLY_MAGIC 0x67446698 -#define REQUEST_READ 0 -#define REQUEST_WRITE 1 -#define REQUEST_DISCONNECT 2 +#define INIT_PASSWD "NBDMAGIC" +#define INIT_MAGIC 0x0000420281861253 +#define REQUEST_MAGIC 0x25609513 +#define REPLY_MAGIC 0x67446698 +#define REQUEST_READ 0 +#define REQUEST_WRITE 1 +#define REQUEST_DISCONNECT 2 + +/* 1MiB is the de-facto standard for maximum size of header + data */ +#define NBD_MAX_SIZE ( 1024 * 1024 ) + +#define NBD_REQUEST_SIZE ( sizeof( struct nbd_request_raw ) ) +#define NBD_REPLY_SIZE ( sizeof( struct nbd_reply_raw ) ) #include #include @@ -51,7 +57,7 @@ struct nbd_init { struct nbd_request { uint32_t magic; - uint32_t type; /* == READ || == WRITE */ + uint32_t type; /* == READ || == WRITE || == DISCONNECT */ char handle[8]; uint64_t from; uint32_t len; @@ -63,7 +69,6 @@ struct nbd_reply { char handle[8]; /* handle you got from request */ }; - void nbd_r2h_init( struct nbd_init_raw * from, struct nbd_init * to ); void nbd_r2h_request( struct nbd_request_raw *from, struct nbd_request * to ); void nbd_r2h_reply( struct nbd_reply_raw * from, struct nbd_reply * to ); diff --git a/src/readwrite.c b/src/readwrite.c index 8759602..0f20f24 100644 --- a/src/readwrite.c +++ b/src/readwrite.c @@ -56,6 +56,25 @@ fail: return 0; } +int socket_nbd_write_hello(int fd, off64_t out_size) +{ + struct nbd_init init; + struct nbd_init_raw init_raw; + + memcpy(&init.passwd, INIT_PASSWD, 8); + init.magic = INIT_MAGIC; + init.size = out_size; + + memset( &init_raw, 0, sizeof( init_raw ) ); // ensure reserved is 0s + nbd_h2r_init(&init, &init_raw); + + if ( 0 > writeloop( fd, &init_raw, sizeof( init_raw ) ) ) { + warn( SHOW_ERRNO( "failed to write hello to socket" ) ); + return 0; + } + return 1; +} + void fill_request(struct nbd_request *request, int type, off64_t from, int len) { request->magic = htobe32(REQUEST_MAGIC); diff --git a/src/readwrite.h b/src/readwrite.h index bb3029c..e58c398 100644 --- a/src/readwrite.h +++ b/src/readwrite.h @@ -7,6 +7,7 @@ int socket_connect(struct sockaddr* to, struct sockaddr* from); int socket_nbd_read_hello(int fd, off64_t * size); +int socket_nbd_write_hello(int fd, off64_t size); void socket_nbd_read(int fd, off64_t from, int len, int out_fd, void* out_buf, int timeout_secs); void socket_nbd_write(int fd, off64_t from, int len, int out_fd, void* out_buf, int timeout_secs); int socket_nbd_disconnect( int fd ); diff --git a/src/serve.c b/src/serve.c index c738150..9438061 100644 --- a/src/serve.c +++ b/src/serve.c @@ -2,6 +2,7 @@ #include "client.h" #include "nbdtypes.h" #include "ioutil.h" +#include "sockutil.h" #include "util.h" #include "bitset.h" #include "control.h" @@ -20,22 +21,6 @@ #include #include -static inline void* sockaddr_address_data(struct sockaddr* sockaddr) -{ - NULLCHECK( sockaddr ); - - struct sockaddr_in* in = (struct sockaddr_in*) sockaddr; - struct sockaddr_in6* in6 = (struct sockaddr_in6*) sockaddr; - - if (sockaddr->sa_family == AF_INET) { - return &in->sin_addr; - } - if (sockaddr->sa_family == AF_INET6) { - return &in6->sin6_addr; - } - return NULL; -} - struct server * server_create ( struct flexnbd * flexnbd, char* s_ip_address, @@ -231,82 +216,15 @@ int server_port( struct server * server ) } -/* Try to bind to our serving socket, retrying until it works or gives a - * fatal error. */ -void serve_bind( struct server * serve ) -{ - int bind_result; - - char s_address[64]; - memset( s_address, 0, 64 ); - strcpy( s_address, "???" ); - inet_ntop( serve->bind_to.generic.sa_family, - sockaddr_address_data( &serve->bind_to.generic), - s_address, 64 ); - - do { - bind_result = bind( - serve->server_fd, - &serve->bind_to.generic, - sizeof(serve->bind_to)); - - if ( 0 == bind_result ) { - info( "Bound to %s port %d", - s_address, - ntohs(serve->bind_to.v4.sin_port)); - break; - } - else { - - warn( "Couldn't bind to %s port %d: %s", - s_address, - ntohs(serve->bind_to.v4.sin_port), - strerror( errno ) ); - - switch (errno){ - /* bind() can give us EACCES, - * EADDRINUSE, EADDRNOTAVAIL, EBADF, - * EINVAL or ENOTSOCK. - * - * Any of these other than EACCES, - * EADDRINUSE or EADDRNOTAVAIL signify - * that there's a logic error somewhere. - * - * EADDRINUSE is fatal: if there's - * something already where we want to be - * listening, we have no guarantees that - * any clients will cope with it. - */ - case EACCES: - case EADDRNOTAVAIL: - debug("retrying"); - sleep(1); - continue; - case EADDRINUSE: - fatal( "%s port %d in use, giving up.", - s_address, - ntohs(serve->bind_to.v4.sin_port)); - default: - fatal( "Giving up" ); - } - } - } while ( 1 ); -} - - - /** Prepares a listening socket for the NBD server, binding etc. */ void serve_open_server_socket(struct server* params) { NULLCHECK( params ); - int optval=1; - - params->server_fd= socket(params->bind_to.generic.sa_family == AF_INET ? + params->server_fd = socket(params->bind_to.generic.sa_family == AF_INET ? PF_INET : PF_INET6, SOCK_STREAM, 0); - FATAL_IF_NEGATIVE(params->server_fd, - "Couldn't create server socket"); + FATAL_IF_NEGATIVE( params->server_fd, "Couldn't create server socket" ); /* We need SO_REUSEADDR so that when we switch from listening to * serving we don't have to change address if we don't want to. @@ -317,8 +235,7 @@ void serve_open_server_socket(struct server* params) * we barf. */ FATAL_IF_NEGATIVE( - setsockopt(params->server_fd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)), - "Couldn't set SO_REUSEADDR" + sock_set_reuseaddr( params->server_fd, 1 ), "Couldn't set SO_REUSEADDR" ); /* TCP_NODELAY makes everything not be slow. If we can't set @@ -326,14 +243,16 @@ void serve_open_server_socket(struct server* params) * understand. */ FATAL_IF_NEGATIVE( - setsockopt(params->server_fd, IPPROTO_TCP, TCP_NODELAY, &optval, sizeof(optval)), - "Couldn't set TCP_NODELAY" + sock_set_tcp_nodelay( params->server_fd, 1 ), "Couldn't set TCP_NODELAY" ); /* If we can't bind, presumably that's because someone else is * squatting on our ip/port combo, or the ip isn't yet * configured. Ideally we want to retry this. */ - serve_bind(params); + FATAL_UNLESS_ZERO( + sock_try_bind( params->server_fd, ¶ms->bind_to.generic ), + SHOW_ERRNO( "Failed to bind() socket" ) + ); FATAL_IF_NEGATIVE( listen(params->server_fd, params->tcp_backlog), @@ -354,17 +273,13 @@ int tryjoin_client_thread( struct client_tbl_entry *entry, int (*joinfunc)(pthre int join_errno; if (entry->thread != 0) { - char s_client_address[64]; + char s_client_address[128]; - memset(s_client_address, 0, 64); - strcpy(s_client_address, "???"); - inet_ntop( entry->address.generic.sa_family, - sockaddr_address_data(&entry->address.generic), - s_client_address, - 64 ); + sockaddr_address_string( &entry->address.generic, &s_client_address[0], 128 ); debug( "%s(%p,...)", joinfunc == pthread_join ? "joining" : "tryjoining", entry->thread ); join_errno = joinfunc(entry->thread, &status); + /* join_errno can legitimately be ESRCH if the thread is * already dead, but the client still needs tidying up. */ if (join_errno != 0 && !entry->client->stopped ) { @@ -483,9 +398,11 @@ int server_should_accept_client( NULLCHECK( client_address ); NULLCHECK( s_client_address ); - if (inet_ntop(client_address->generic.sa_family, - sockaddr_address_data(&client_address->generic), - s_client_address, s_client_address_len ) == NULL) { + const char* result = sockaddr_address_string( + &client_address->generic, s_client_address, s_client_address_len + ); + + if ( NULL == result ) { warn( "Rejecting client %s: Bad client_address", s_client_address ); return 0; } diff --git a/src/sockutil.c b/src/sockutil.c new file mode 100644 index 0000000..1adcbb5 --- /dev/null +++ b/src/sockutil.c @@ -0,0 +1,104 @@ +#include "sockutil.h" +#include "util.h" + +size_t sockaddr_size(const struct sockaddr* sa) +{ + size_t ret = 0; + + switch( sa->sa_family ) { + case AF_INET: + ret = sizeof( struct sockaddr_in ); + break; + case AF_INET6: + ret = sizeof( struct sockaddr_in6 ); + break; + } + + return ret; +} + +const char* sockaddr_address_string(const struct sockaddr* sa, char* dest, size_t len) +{ + NULLCHECK( sa ); + NULLCHECK( dest ); + + struct sockaddr_in* in = ( struct sockaddr_in* ) sa; + struct sockaddr_in6* in6 = ( struct sockaddr_in6* ) sa; + + unsigned short real_port = ntohs( in->sin_port ); // common to in and in6 + size_t size; + const char* ret = NULL; + + memset( dest, 0, len ); + + if ( sa->sa_family == AF_INET ) { + ret = inet_ntop( AF_INET, &in->sin_addr, dest, len ); + } else if ( sa->sa_family == AF_INET6 ) { + ret = inet_ntop( AF_INET6, &in6->sin6_addr, dest, len ); + } else { + strncpy( dest, "???", len ); + } + + if ( NULL != ret && real_port > 0 ) { + size = strlen( dest ); + snprintf( dest + size, len - size, " port %d", real_port ); + } + + return ret; +} + +int sock_set_reuseaddr(int fd, int optval) +{ + return setsockopt( fd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval) ); +} + +/* Set the tcp_nodelay option */ +int sock_set_tcp_nodelay(int fd, int optval) +{ + return setsockopt( fd, IPPROTO_TCP, TCP_NODELAY, &optval, sizeof(optval) ); +} + +int sock_try_bind(int fd, const struct sockaddr* sa) +{ + int bind_result; + char s_address[256]; + + sockaddr_address_string( sa, &s_address[0], 256 ); + + do { + bind_result = bind( fd, sa, sockaddr_size( sa ) ); + if ( 0 == bind_result ) { + info( "Bound to %s", s_address ); + break; + } + else { + warn( SHOW_ERRNO( "Couldn't bind to %s", s_address ) ); + + switch ( errno ) { + /* bind() can give us EACCES, EADDRINUSE, EADDRNOTAVAIL, EBADF, + * EINVAL, ENOTSOCK, EFAULT, ELOOP, ENAMETOOLONG, ENOENT, + * ENOMEM, ENOTDIR, EROFS + * + * Any of these other than EADDRINUSE & EADDRNOTAVAIL signify + * that there's a logic error somewhere. + * + * EADDRINUSE is fatal: if there's something already where we + * want to be listening, we have no guarantees that any clients + * will cope with it. + */ + case EADDRNOTAVAIL: + debug( "retrying" ); + sleep( 1 ); + continue; + case EADDRINUSE: + warn( "%s in use, giving up.", s_address ); + break; + default: + warn( "giving up" ); + } + } + } while ( 1 ); + + return bind_result; +} + diff --git a/src/sockutil.h b/src/sockutil.h new file mode 100644 index 0000000..e60fc91 --- /dev/null +++ b/src/sockutil.h @@ -0,0 +1,32 @@ +#ifndef SOCKUTIL_H + +#define SOCKUTIL_H + +#include +#include +#include +#include +#include + +/* Returns the size of the sockaddr, or 0 on error */ +size_t sockaddr_size(const struct sockaddr* sa); + +/* Convert a sockaddr into an address. Like inet_ntop, it returns dest if + * successful, NULL otherwise. In the latter case, dest will contain "???" + */ +const char* sockaddr_address_string(const struct sockaddr* sa, char* dest, size_t len); + +/* Set the SOL_REUSEADDR otion */ +int sock_set_reuseaddr(int fd, int optval); + +/* Set the tcp_nodelay option */ +int sock_set_tcp_nodelay(int fd, int optval); + +/* TODO: Set the tcp_cork option */ +// int sock_set_cork(int fd, int optval); + +/* Attempt to bind the fd to the sockaddr, retrying common transient failures */ +int sock_try_bind(int fd, const struct sockaddr* sa); + +#endif + diff --git a/src/util.h b/src/util.h index 15181a5..2c8af69 100644 --- a/src/util.h +++ b/src/util.h @@ -148,6 +148,7 @@ void mylog(int line_level, const char* format, ...); #define NULLCHECK(value) FATAL_IF_NULL(value, "BUG: " #value " is null") +#define SHOW_ERRNO( msg, ... ) msg ": %s (%i)", ##__VA_ARGS__, ( errno == 0 ? "EOF" : strerror(errno) ), errno #endif diff --git a/tests/acceptance/custom.supp b/tests/acceptance/custom.supp new file mode 100644 index 0000000..faf8d91 --- /dev/null +++ b/tests/acceptance/custom.supp @@ -0,0 +1,13 @@ +{ + avoid_glibc_bug_do_lookup + Memcheck:Addr8 + fun:do_lookup_x + obj:* + fun:_dl_lookup_symbol_x +} +{ + avoid_glibc_bug_check_match + Memcheck:Addr8 + fun:check_match.12149 +} + diff --git a/tests/acceptance/environment.rb b/tests/acceptance/environment.rb index e1efb0c..bbbfadf 100644 --- a/tests/acceptance/environment.rb +++ b/tests/acceptance/environment.rb @@ -5,7 +5,7 @@ require 'file_writer' class Environment attr_reader( :blocksize, :filename1, :filename2, :ip, - :port1, :port2, :nbd1, :nbd2, :file1, :file2, :rebind_port1 ) + :port1, :port2, :nbd1, :nbd2, :file1, :file2 ) def initialize @blocksize = 1024 @@ -14,9 +14,7 @@ class Environment @ip = "127.0.0.1" @available_ports = [*40000..41000] - listening_ports @port1 = @available_ports.shift - @rebind_port1 = @available_ports.shift @port2 = @available_ports.shift - @rebind_port2 = @available_ports.shift @nbd1 = FlexNBD::FlexNBD.new("../../build/flexnbd", @ip, @port1) @nbd2 = FlexNBD::FlexNBD.new("../../build/flexnbd", @ip, @port2) diff --git a/tests/acceptance/fakes/source/close_after_entrust_reply.rb b/tests/acceptance/fakes/source/close_after_entrust_reply.rb index f519c2c..c5858c9 100755 --- a/tests/acceptance/fakes/source/close_after_entrust_reply.rb +++ b/tests/acceptance/fakes/source/close_after_entrust_reply.rb @@ -8,7 +8,7 @@ require 'flexnbd/fake_source' include FlexNBD -addr, port, srv_pid, rebind_addr, rebind_port = *ARGV +addr, port, srv_pid = *ARGV client = FakeSource.new( addr, port, "Timed out connecting" ) client.read_hello @@ -30,3 +30,4 @@ rescue Timeout::Error end exit(0) + diff --git a/tests/acceptance/flexnbd.rb b/tests/acceptance/flexnbd.rb index e66ae75..d018144 100644 --- a/tests/acceptance/flexnbd.rb +++ b/tests/acceptance/flexnbd.rb @@ -21,7 +21,7 @@ class ValgrindExecutor attr_reader :pid def run( cmd ) - @pid = fork do exec "valgrind --track-origins=yes #{cmd}" end + @pid = fork do exec "valgrind --track-origins=yes --suppressions=custom.supp #{cmd}" end end end # class ValgrindExecutor @@ -131,7 +131,7 @@ class ValgrindKillingExecutor def run( cmd ) @io_r, io_w = IO.pipe - @pid = fork do exec( "valgrind --xml=yes --xml-fd=#{io_w.fileno} " + cmd ) end + @pid = fork do exec( "valgrind --suppressions=custom.supp --xml=yes --xml-fd=#{io_w.fileno} " + cmd ) end launch_watch_thread( @pid, @io_r ) @pid end @@ -521,3 +521,4 @@ module FlexNBD end end + diff --git a/tests/unit/check_sockutil.c b/tests/unit/check_sockutil.c new file mode 100644 index 0000000..ab55701 --- /dev/null +++ b/tests/unit/check_sockutil.c @@ -0,0 +1,109 @@ +#include "sockutil.h" + +#include + +START_TEST( test_sockaddr_address_string_af_inet_converts_to_string ) +{ + struct sockaddr sa; + struct sockaddr_in* v4 = (struct sockaddr_in*) &sa; + char testbuf[128]; + const char* result; + + v4->sin_family = AF_INET; + v4->sin_port = htons( 4777 ); + ck_assert_int_eq( 1, inet_pton( AF_INET, "192.168.0.1", &v4->sin_addr )); + + result = sockaddr_address_string( &sa, &testbuf[0], 128 ); + ck_assert( result != NULL ); + + ck_assert_str_eq( "192.168.0.1 port 4777", testbuf ); +} +END_TEST + + +START_TEST( test_sockaddr_address_string_af_inet6_converts_to_string ) +{ + struct sockaddr_in6 v6_raw; + struct sockaddr_in6* v6 = &v6_raw; + struct sockaddr* sa = (struct sockaddr*) &v6_raw; + + char testbuf[128]; + const char* result; + + v6->sin6_family = AF_INET6; + v6->sin6_port = htons( 4777 ); + ck_assert_int_eq( 1, inet_pton( AF_INET6, "fe80::1", &v6->sin6_addr )); + + result = sockaddr_address_string( sa, &testbuf[0], 128 ); + ck_assert( result != NULL ); + + ck_assert_str_eq( "fe80::1 port 4777", testbuf ); +} +END_TEST + +/* We don't know what it is, so we just call it "???" and return NULL */ +START_TEST( test_sockaddr_address_string_af_unspec_is_failure ) +{ + struct sockaddr sa; + struct sockaddr_in* v4 = (struct sockaddr_in*) &sa; + char testbuf[128]; + const char* result; + + v4->sin_family = AF_UNSPEC; + v4->sin_port = htons( 4777 ); + ck_assert_int_eq( 1, inet_pton( AF_INET, "192.168.0.1", &v4->sin_addr )); + + result = sockaddr_address_string( &sa, &testbuf[0], 128 ); + ck_assert( result == NULL ); + + ck_assert_str_eq( "???", testbuf ); +} +END_TEST + +/* This is a complete failure to parse, rather than a partial failure */ +START_TEST( test_sockaddr_address_string_doesnt_overflow_short_buffer ) +{ + struct sockaddr sa; + struct sockaddr_in* v4 = (struct sockaddr_in*) &sa; + char testbuf[128]; + const char* result; + + v4->sin_family = AF_INET; + v4->sin_port = htons( 4777 ); + ck_assert_int_eq( 1, inet_pton( AF_INET, "192.168.0.1", &v4->sin_addr )); + + result = sockaddr_address_string( &sa, &testbuf[0], 4 ); + ck_assert( result == NULL ); + + ck_assert_str_eq( "", testbuf ); + +} +END_TEST + +Suite *sockutil_suite(void) +{ + Suite *s = suite_create("sockutil"); + + TCase *tc_sockaddr_address_string = tcase_create("sockaddr_address_string"); + + tcase_add_test(tc_sockaddr_address_string, test_sockaddr_address_string_af_inet_converts_to_string); + tcase_add_test(tc_sockaddr_address_string, test_sockaddr_address_string_af_inet6_converts_to_string); + tcase_add_test(tc_sockaddr_address_string, test_sockaddr_address_string_af_unspec_is_failure); + tcase_add_test(tc_sockaddr_address_string, test_sockaddr_address_string_doesnt_overflow_short_buffer); + suite_add_tcase(s, tc_sockaddr_address_string); + + return s; +} + +int main(void) +{ + int number_failed; + + Suite *s = sockutil_suite(); + SRunner *sr = srunner_create(s); + srunner_run_all(sr, CK_NORMAL); + number_failed = srunner_ntests_failed(sr); + srunner_free(sr); + return (number_failed == 0) ? 0 : 1; +} +