diff options
author | Douglas Bagnall <douglas.bagnall@catalyst.net.nz> | 2019-10-31 16:28:28 +1300 |
---|---|---|
committer | Andrew Bartlett <abartlet@samba.org> | 2019-12-10 07:50:28 +0000 |
commit | 7b265830ad6796dbbe721f7abfd62a19c2185b65 (patch) | |
tree | 86dea325c0e73ef5765b6768a1a8bfade35602d2 | |
parent | ef5d79e24ba8aec226419e594de0cf91c24d7fc4 (diff) | |
download | samba-7b265830ad6796dbbe721f7abfd62a19c2185b65.tar.gz |
lib/fuzzing: add fuzz_ndr_X
This NDR fuzzer links with each "interface" in the IDL files to
create avsingle binary. This tries to matches what the fuzzing
engines desire.
It started as a copy of ndrdump but very little of that remains
in place.
The fancy build rules try to avoid needing a lof of boilerplate
in the wscript_build files and ensure new fuzzers are generated
and run when new IDL is added automatically.
Signed-off-by: Douglas Bagnall <douglas.bagnall@catalyst.net.nz>
Signed-off-by: Andrew Bartlett <abartlet@samba.org>
Pair-programmed-by: Andrew Bartlett <abartlet@samba.org>
Reviewed-by: Andrew Bartlett <abartlet@samba.org>
-rw-r--r-- | buildtools/wafsamba/samba_pidl.py | 12 | ||||
-rw-r--r-- | buildtools/wafsamba/wscript | 2 | ||||
-rw-r--r-- | lib/fuzzing/fuzz_ndr_X.c | 291 | ||||
-rw-r--r-- | lib/fuzzing/wscript_build | 80 | ||||
-rw-r--r-- | librpc/idl/wscript_build | 72 | ||||
-rw-r--r-- | pidl/lib/Parse/Pidl/Samba4/NDR/Parser.pm | 25 | ||||
-rw-r--r-- | wscript_build | 6 |
7 files changed, 471 insertions, 17 deletions
diff --git a/buildtools/wafsamba/samba_pidl.py b/buildtools/wafsamba/samba_pidl.py index a34c871d183..8785563e5e6 100644 --- a/buildtools/wafsamba/samba_pidl.py +++ b/buildtools/wafsamba/samba_pidl.py @@ -108,14 +108,22 @@ def SAMBA_PIDL(bld, pname, source, t.more_includes = '#' + bld.path.path_from(bld.srcnode) Build.BuildContext.SAMBA_PIDL = SAMBA_PIDL - def SAMBA_PIDL_LIST(bld, name, source, options='', output_dir='.', - generate_tables=True): + generate_tables=True, + generate_fuzzers=True): '''A wrapper for building a set of IDL files''' for p in TO_LIST(source): bld.SAMBA_PIDL(name, p, options=options, output_dir=output_dir, generate_tables=generate_tables) + + # Some IDL files don't exactly match between name and + # "interface" so we need a way to skip those, while other IDL + # files have the table generation skipped entirely, on which + # the fuzzers rely + if generate_tables and generate_fuzzers: + interface = p[0:-4] # strip off the .idl suffix + bld.SAMBA_NDR_FUZZ(interface) Build.BuildContext.SAMBA_PIDL_LIST = SAMBA_PIDL_LIST diff --git a/buildtools/wafsamba/wscript b/buildtools/wafsamba/wscript index b9f2f495617..764e357cc87 100644 --- a/buildtools/wafsamba/wscript +++ b/buildtools/wafsamba/wscript @@ -605,7 +605,7 @@ struct foo bar = { .y = 'X', .x = 1 }; conf.env.enable_libfuzzer = Options.options.enable_libfuzzer if conf.env.enable_libfuzzer: - conf.DEFINE('ENABLE_LIBFUZZER', 1) + conf.DEFINE('FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION', 1) conf.env.FUZZ_TARGET_LDFLAGS = Options.options.FUZZ_TARGET_LDFLAGS conf.load('clang_compilation_database') diff --git a/lib/fuzzing/fuzz_ndr_X.c b/lib/fuzzing/fuzz_ndr_X.c new file mode 100644 index 00000000000..8c9e5721739 --- /dev/null +++ b/lib/fuzzing/fuzz_ndr_X.c @@ -0,0 +1,291 @@ +/* + Unix SMB/CIFS implementation. + SMB torture tester + Copyright (C) Andrew Tridgell 2003 + Copyright (C) Jelmer Vernooij 2006 + Copyright (C) Andrew Bartlett 2019 + Copyright (C) Catalyst.NET Ltd 2019 + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "includes.h" +#include "system/filesys.h" +#include "system/locale.h" +#include "librpc/ndr/libndr.h" +#include "librpc/gen_ndr/ndr_dcerpc.h" +#include "util/byteorder.h" +#include "fuzzing/fuzzing.h" + +extern const struct ndr_interface_table FUZZ_PIPE_TABLE; + +#define FLAG_NDR64 4 + +enum { + TYPE_STRUCT = 0, + TYPE_IN, + TYPE_OUT +}; + +/* + * header design (little endian): + * + * struct { + * uint16_t flags; + * uint16_t function_or_struct_no; + * }; + */ + +/* + * We want an even number here to ensure 4-byte alignment later + * not just for efficieny but because the fuzzers are known to guess + * that numbers will be 4-byte aligned + */ +#define HEADER_SIZE 4 + +#define INVALID_FLAGS (~(FLAG_NDR64 | 3)) + +static const struct ndr_interface_call *find_function( + const struct ndr_interface_table *p, + unsigned int function_no) +{ + if (function_no >= p->num_calls) { + return NULL; + } + return &p->calls[function_no]; +} + +/* + * Get a public structure by number and return it as if it were + * a function. + */ +static const struct ndr_interface_call *find_struct( + const struct ndr_interface_table *p, + unsigned int struct_no, + struct ndr_interface_call *out_buffer) +{ + const struct ndr_interface_public_struct *s = NULL; + + if (struct_no >= p->num_public_structs) { + return NULL; + } + + s = &p->public_structs[struct_no]; + + *out_buffer = (struct ndr_interface_call) { + .name = s->name, + .struct_size = s->struct_size, + .ndr_pull = s->ndr_pull, + .ndr_push = s->ndr_push, + .ndr_print = s->ndr_print + }; + return out_buffer; +} + + +static NTSTATUS pull_chunks(struct ndr_pull *ndr_pull, + const struct ndr_interface_call_pipes *pipes) +{ + enum ndr_err_code ndr_err; + uint32_t i; + + for (i=0; i < pipes->num_pipes; i++) { + while (true) { + void *saved_mem_ctx; + uint32_t *count; + void *c; + + c = talloc_zero_size(ndr_pull, pipes->pipes[i].chunk_struct_size); + if (c == NULL) { + return NT_STATUS_NO_MEMORY; + } + /* + * Note: the first struct member is always + * 'uint32_t count;' + */ + count = (uint32_t *)c; + + saved_mem_ctx = ndr_pull->current_mem_ctx; + ndr_pull->current_mem_ctx = c; + ndr_err = pipes->pipes[i].ndr_pull(ndr_pull, NDR_SCALARS, c); + ndr_pull->current_mem_ctx = saved_mem_ctx; + + if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { + talloc_free(c); + return ndr_map_error2ntstatus(ndr_err); + } + if (*count == 0) { + talloc_free(c); + break; + } + talloc_free(c); + } + } + + return NT_STATUS_OK; +} + +static void ndr_print_nothing(struct ndr_print *ndr, const char *format, ...) +{ + /* + * This is here so that we walk the tree but don't output anything. + * This helps find buggy ndr_print routines + */ + + /* + * TODO: consider calling snprinf() to find strings without NULL + * terminators (for example) + */ +} + + +int LLVMFuzzerTestOneInput(uint8_t *data, size_t size) { + uint8_t type; + int pull_push_print_flags; + uint16_t fuzz_packet_flags, function; + TALLOC_CTX *mem_ctx = NULL; + uint32_t ndr_flags = 0; + struct ndr_push *ndr_push; + enum ndr_err_code ndr_err; + struct ndr_interface_call f_buffer; + const struct ndr_interface_call *f = NULL; + NTSTATUS status; + + if (size < HEADER_SIZE) { + /* + * the first few bytes decide what is being fuzzed -- + * if they aren't all there we do nothing. + */ + return 0; + } + + fuzz_packet_flags = SVAL(data, 0); + if (fuzz_packet_flags & INVALID_FLAGS) { + return 0; + } + + function = SVAL(data, 2); + + type = fuzz_packet_flags & 3; + + switch (type) { + case TYPE_STRUCT: + pull_push_print_flags = NDR_SCALARS|NDR_BUFFERS; + f = find_struct(&FUZZ_PIPE_TABLE, function, &f_buffer); + break; + case TYPE_IN: + pull_push_print_flags = NDR_IN; + f = find_function(&FUZZ_PIPE_TABLE, function); + break; + case TYPE_OUT: + pull_push_print_flags = NDR_OUT; + f = find_function(&FUZZ_PIPE_TABLE, function); + break; + default: + return 0; + } + + if (f == NULL) { + return 0; + } + if (fuzz_packet_flags & FLAG_NDR64) { + ndr_flags |= LIBNDR_FLAG_NDR64; + } + + mem_ctx = talloc_init("ndrfuzz"); + + { + /* + * f->struct_size is well-controlled, it is essentially + * defined in the IDL + */ + uint8_t st[f->struct_size]; + + DATA_BLOB blob = data_blob_const(data + HEADER_SIZE, + size - HEADER_SIZE); + struct ndr_pull *ndr_pull = ndr_pull_init_blob(&blob, + mem_ctx); + + if (ndr_pull == NULL) { + perror("ndr_pull_init_blob"); + TALLOC_FREE(mem_ctx); + return 0; + } + ndr_pull->flags |= LIBNDR_FLAG_REF_ALLOC; + + if (type == TYPE_OUT) { + status = pull_chunks(ndr_pull, + &f->out_pipes); + if (!NT_STATUS_IS_OK(status)) { + TALLOC_FREE(mem_ctx); + return 0; + } + } + + ndr_err = f->ndr_pull(ndr_pull, + pull_push_print_flags, + st); + if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { + TALLOC_FREE(mem_ctx); + return 0; + } + + if (type == TYPE_IN) { + status = pull_chunks(ndr_pull, + &f->in_pipes); + if (!NT_STATUS_IS_OK(status)) { + TALLOC_FREE(mem_ctx); + return 0; + } + } + + ndr_push = ndr_push_init_ctx(mem_ctx); + if (ndr_push == NULL) { + TALLOC_FREE(mem_ctx); + return 0; + } + + ndr_push->flags |= ndr_flags; + + /* + * Now push what was pulled, just in case we generated an + * invalid structure in memory, this should notice + */ + ndr_err = f->ndr_push(ndr_push, + pull_push_print_flags, + st); + if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { + TALLOC_FREE(mem_ctx); + return 0; + } + + { + struct ndr_print *ndr_print = talloc_zero(mem_ctx, struct ndr_print); + ndr_print->print = ndr_print_nothing; + ndr_print->depth = 1; + + /* + * Finally print (to nowhere) the structure, this may also + * notice invalid memory + */ + f->ndr_print(ndr_print, + f->name, + pull_push_print_flags, + st); + } + } + TALLOC_FREE(mem_ctx); + + return 0; +} diff --git a/lib/fuzzing/wscript_build b/lib/fuzzing/wscript_build index 386145c43b2..25cdcd323bf 100644 --- a/lib/fuzzing/wscript_build +++ b/lib/fuzzing/wscript_build @@ -1,5 +1,7 @@ #!/usr/bin/env python +from waflib import Build + bld.SAMBA_SUBSYSTEM('fuzzing', source='fuzzing.c', deps='talloc', @@ -40,3 +42,81 @@ bld.SAMBA_BINARY('fuzz_ldb_parse_tree', source='fuzz_ldb_parse_tree.c', deps='fuzzing ldb', fuzzer=True) + +def SAMBA_NDR_FUZZ(bld, interface): + name = "fuzz_ndr_%s" % (interface.lower()) + fuzz_dir = os.path.join(bld.env.srcdir, 'lib/fuzzing') + fuzz_reldir = os.path.relpath(fuzz_dir, bld.path.abspath()) + fuzz_src = os.path.join(fuzz_reldir, 'fuzz_ndr_X.c') + fuzz_named_src = os.path.join(fuzz_reldir, + 'fuzz_ndr_%s.c' % interface.lower()) + + # Work around an issue that WAF is invoked from up to 3 different + # directories so doesn't create a unique name for the multiple .o + # files like it would if called from just one place. + bld.SAMBA_GENERATOR(fuzz_named_src, + source=fuzz_src, + target=fuzz_named_src, + rule='cp ${SRC} ${TGT}') + + bld.SAMBA_BINARY(name, source=fuzz_named_src, + cflags = "-D FUZZ_PIPE_TABLE=ndr_table_%s" % interface, + deps = "ndr-table NDR_DCERPC", + install=False, + fuzzer=True) + +Build.BuildContext.SAMBA_NDR_FUZZ = SAMBA_NDR_FUZZ + +# fuzz_ndr_X is generated from the list if IDL fed to PIDL +# however there are exceptions to the normal pattern +bld.SAMBA_NDR_FUZZ('IOXIDResolver') # oxidresolver.idl +bld.SAMBA_NDR_FUZZ('IRemoteActivation') # remact.idl +bld.SAMBA_NDR_FUZZ('iremotewinspool') # winspool.idl +bld.SAMBA_NDR_FUZZ('FileServerVssAgent') # fsvrp.idl +bld.SAMBA_NDR_FUZZ('lsarpc') # lsa.idl +bld.SAMBA_NDR_FUZZ('netdfs') # dfs.idl +bld.SAMBA_NDR_FUZZ('nfs4acl_interface') # nfs4acl.idl +bld.SAMBA_NDR_FUZZ('ObjectRpcBaseTypes') # orpc.idl +bld.SAMBA_NDR_FUZZ('rpcecho') # echo.idl + +# quota.idl +bld.SAMBA_NDR_FUZZ('file_quota') +bld.SAMBA_NDR_FUZZ('smb2_query_quota') +bld.SAMBA_NDR_FUZZ('smb1_nt_transact_query_quota') + +# ioctl.idl +bld.SAMBA_NDR_FUZZ('copychunk') +bld.SAMBA_NDR_FUZZ('compression') +bld.SAMBA_NDR_FUZZ('netinterface') +bld.SAMBA_NDR_FUZZ('sparse') +bld.SAMBA_NDR_FUZZ('resiliency') +bld.SAMBA_NDR_FUZZ('trim') + +# Skipped: dsbackup (all todo) + +# WMI tables +bld.SAMBA_NDR_FUZZ('IWbemClassObject') +bld.SAMBA_NDR_FUZZ('IWbemServices') +bld.SAMBA_NDR_FUZZ('IEnumWbemClassObject') +bld.SAMBA_NDR_FUZZ('IWbemContext') +bld.SAMBA_NDR_FUZZ('IWbemLevel1Login') +bld.SAMBA_NDR_FUZZ('IWbemWCOSmartEnum') +bld.SAMBA_NDR_FUZZ('IWbemFetchSmartEnum') +bld.SAMBA_NDR_FUZZ('IWbemCallResult') +bld.SAMBA_NDR_FUZZ('IWbemObjectSink') + +# DCOM tables +bld.SAMBA_NDR_FUZZ('dcom_Unknown') +bld.SAMBA_NDR_FUZZ('IUnknown') +bld.SAMBA_NDR_FUZZ('IClassFactory') +bld.SAMBA_NDR_FUZZ('IRemUnknown') +bld.SAMBA_NDR_FUZZ('IClassActivator') +bld.SAMBA_NDR_FUZZ('ISCMLocalActivator') +bld.SAMBA_NDR_FUZZ('IMachineLocalActivator') +bld.SAMBA_NDR_FUZZ('ILocalObjectExporter') +bld.SAMBA_NDR_FUZZ('ISystemActivator') +bld.SAMBA_NDR_FUZZ('IRemUnknown2') +bld.SAMBA_NDR_FUZZ('IDispatch') +bld.SAMBA_NDR_FUZZ('IMarshal') +bld.SAMBA_NDR_FUZZ('ICoffeeMachine') +bld.SAMBA_NDR_FUZZ('IStream') diff --git a/librpc/idl/wscript_build b/librpc/idl/wscript_build index c9b19c4aac4..5dda944ca71 100644 --- a/librpc/idl/wscript_build +++ b/librpc/idl/wscript_build @@ -5,16 +5,26 @@ bld.SAMBA_PIDL_LIST('PIDL', eventlog.idl browser.idl dssetup.idl frsapi.idl spoolss.idl - dnsserver.idl echo.idl lsa.idl + dnsserver.idl samr.idl srvsvc.idl winreg.idl mgmt.idl netlogon.idl svcctl.idl wkssvc.idl eventlog6.idl backupkey.idl - fsrvp.idl witness.idl clusapi.idl - mdssvc.idl - winspool.idl''', + witness.idl clusapi.idl + mdssvc.idl''', options='--header --ndr-parser --samba3-ndr-server --server --client --python', output_dir='../gen_ndr') +# The interface names here are not the same as the IDL name, so the +# auto-genration of the fuzzer fails to link +bld.SAMBA_PIDL_LIST('PIDL', + '''echo.idl + fsrvp.idl + lsa.idl + winspool.idl''', + options='--header --ndr-parser --samba3-ndr-server --server --client --python', + output_dir='../gen_ndr', + generate_fuzzers=False) + # Services that we only have a client for bld.SAMBA_PIDL_LIST('PIDL', '''atsvc.idl''', @@ -23,26 +33,42 @@ bld.SAMBA_PIDL_LIST('PIDL', # Services that we only have a server in the source3 style bld.SAMBA_PIDL_LIST('PIDL', - '''dfs.idl initshutdown.idl ntsvcs.idl''', + '''initshutdown.idl ntsvcs.idl''', options='--header --ndr-parser --client --python --samba3-ndr-server', output_dir='../gen_ndr') -# Services that we only have a server in the source4 style +# The interface names here are not the same as the IDL name, so the +# auto-genration of the fuzzer fails to link +bld.SAMBA_PIDL_LIST('PIDL', + '''dfs.idl''', + options='--header --ndr-parser --client --python --samba3-ndr-server', + output_dir='../gen_ndr', + generate_fuzzers=False) + +# Services that we only have a server in the source4 style. + bld.SAMBA_PIDL_LIST('PIDL', '''unixinfo.idl''', options='--header --ndr-parser --client --python --server', output_dir='../gen_ndr') # DCOM stuff + +# The interface names here are not the same as the IDL name, so the +# auto-genration of the fuzzer fails to link bld.SAMBA_PIDL_LIST('PIDL', '''oxidresolver.idl remact.idl''', options='--header --ndr-parser --client', - output_dir='../gen_ndr') + output_dir='../gen_ndr', + generate_fuzzers=False) +# The interface names here are not the same as the IDL name, so the +# auto-genration of the fuzzer fails to link bld.SAMBA_PIDL_LIST('PIDL', 'wmi.idl dcom.idl', options='--header --ndr-parser --server --client --dcom-proxy --com-header', - output_dir='../gen_ndr') + output_dir='../gen_ndr', + generate_fuzzers=False) # DCE/RPC protocols which Samba does not implement a client or server # for @@ -50,13 +76,11 @@ bld.SAMBA_PIDL_LIST('PIDL', bld.SAMBA_PIDL_LIST('PIDL', ''' audiosrv.idl - dsbackup.idl efs.idl frstrans.idl frsrpc.idl keysvc.idl msgsvc.idl - orpc.idl policyagent.idl rot.idl scerpc.idl @@ -67,6 +91,18 @@ bld.SAMBA_PIDL_LIST('PIDL', options='--header --ndr-parser', output_dir='../gen_ndr') +# The interface names here are not the same as the IDL name, so the +# auto-genration of the fuzzer fails to link + +bld.SAMBA_PIDL_LIST('PIDL', + ''' + dsbackup.idl + orpc.idl + ''', + options='--header --ndr-parser', + output_dir='../gen_ndr', + generate_fuzzers=False) + # Non-DCE/RPC protocols encoded in IDL for Samba or helper IDLs for # DCE/RPC protocols (eg defining constands or structures but not # functions) @@ -78,14 +114,11 @@ bld.SAMBA_PIDL_LIST('PIDL', file_id.idl fscc.idl fsrvp_state.idl - ioctl.idl named_pipe_auth.idl negoex.idl - nfs4acl.idl notify.idl ntprinting.idl printcap.idl - quota.idl rap.idl schannel.idl smb2_lease_struct.idl @@ -93,6 +126,19 @@ bld.SAMBA_PIDL_LIST('PIDL', options='--header --ndr-parser', output_dir='../gen_ndr') +# The interface names here are not the same as the IDL name, so the +# auto-genration of the fuzzer fails to link + +bld.SAMBA_PIDL_LIST('PIDL', + ''' + ioctl.idl + nfs4acl.idl + quota.idl + ''', + options='--header --ndr-parser', + output_dir='../gen_ndr', + generate_fuzzers=False) + # Non-DCE/RPC protocls with Python bindings # (for structures or constants) diff --git a/pidl/lib/Parse/Pidl/Samba4/NDR/Parser.pm b/pidl/lib/Parse/Pidl/Samba4/NDR/Parser.pm index 94428ec2037..91b5f942994 100644 --- a/pidl/lib/Parse/Pidl/Samba4/NDR/Parser.pm +++ b/pidl/lib/Parse/Pidl/Samba4/NDR/Parser.pm @@ -2604,6 +2604,31 @@ sub ParseFunctionPull($$) $self->pidl("if (flags & NDR_OUT) {"); $self->indent; + $self->pidl("#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION"); + + # This for fuzzers of ndr_pull where the out elements refer to + # in elements in size_is or length_is. + # + # Not actually very harmful but also not useful outsie a fuzzer + foreach my $e (@{$fn->{ELEMENTS}}) { + next unless (grep(/in/, @{$e->{DIRECTION}})); + next unless ($e->{LEVELS}[0]->{TYPE} eq "POINTER" and + $e->{LEVELS}[0]->{POINTER_TYPE} eq "ref"); + next if (($e->{LEVELS}[1]->{TYPE} eq "DATA") and + ($e->{LEVELS}[1]->{DATA_TYPE} eq "string")); + next if ($e->{LEVELS}[1]->{TYPE} eq "PIPE"); + next if ($e->{LEVELS}[1]->{TYPE} eq "ARRAY"); + + $self->pidl("if (r->in.$e->{NAME} == NULL) {"); + $self->indent; + $self->pidl("NDR_PULL_ALLOC($ndr, r->in.$e->{NAME});"); + $self->pidl("NDR_ZERO_STRUCTP(r->in.$e->{NAME});"); + $self->deindent; + $self->pidl("}"); + } + + $self->pidl("#endif /* FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION */"); + $env = GenerateFunctionOutEnv($fn); foreach my $e (@{$fn->{ELEMENTS}}) { next unless grep(/out/, @{$e->{DIRECTION}}); diff --git a/wscript_build b/wscript_build index f9e033c0dab..b2e32987acb 100644 --- a/wscript_build +++ b/wscript_build @@ -38,6 +38,11 @@ bld.CONFIGURE_FILE('docs-xml/build/DTD/samba.build.version', DOC_VERSION=bld.env.DOC_VERSION) bld.RECURSE('docs-xml') +# This needs to be earlier than anything containing IDL +# That in turn allows the build rules for fuzz_ndr_X to be +# near the code +bld.RECURSE('lib/fuzzing') + bld.RECURSE('lib/replace') bld.RECURSE('lib/socket') bld.RECURSE('lib/talloc') @@ -150,7 +155,6 @@ bld.RECURSE('dfs_server') bld.RECURSE('file_server') bld.RECURSE('lib/krb5_wrap') bld.RECURSE('packaging') -bld.RECURSE('lib/fuzzing') bld.RECURSE('testsuite/headers') |