From bd8e127f2c5b11404666ce474fbe572f21d14604 Mon Sep 17 00:00:00 2001 From: Douglas Bagnall Date: Fri, 5 Jul 2019 16:09:37 +1200 Subject: [PATCH] A pseudo-fuzzer for Python bindings "pseudo", because "fuzz" implies randomness, while this dumbly enumerates all combinations of the arguments in knows. It has found several shallow segfaults, but is not well equipped to see deeply beyond the bindings themselves. Signed-off-by: Douglas Bagnall --- python/samba/tests/simple_fuzz.py | 257 +++++++++++++++++++++++ python/samba/tests/simple_fuzz_base.py | 227 ++++++++++++++++++++ python/samba/tests/simple_fuzz_dcerpc.py | 237 +++++++++++++++++++++ selftest/selftest.pl | 2 +- source4/selftest/tests.py | 6 + 5 files changed, 728 insertions(+), 1 deletion(-) create mode 100644 python/samba/tests/simple_fuzz.py create mode 100644 python/samba/tests/simple_fuzz_base.py create mode 100644 python/samba/tests/simple_fuzz_dcerpc.py diff --git a/python/samba/tests/simple_fuzz.py b/python/samba/tests/simple_fuzz.py new file mode 100644 index 00000000000..d478a738ba8 --- /dev/null +++ b/python/samba/tests/simple_fuzz.py @@ -0,0 +1,257 @@ +# Unix SMB/CIFS implementation. +# +# 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 . +# + +"""Test whether various python bindings segfault on crazy input""" + +from .simple_fuzz_base import BaseFuzzTests + + +class MessagingFuzzTests(BaseFuzzTests): + @classmethod + def get_args(cls): + from samba import messaging + cls.mod = messaging + args = cls.get_common_args() + args.append(messaging.Messaging()) + return args + + +class RegistryFuzzTests(BaseFuzzTests): + @classmethod + def get_args(cls): + from samba import registry + cls.mod = registry + args = cls.get_common_args() + args.append(registry.Registry()) + return args + + +class SecurityFuzzTests(BaseFuzzTests): + @classmethod + def get_args(cls): + from samba import security + cls.mod = security + args = cls.get_common_args() + args.append(cls.samdb.get_domain_sid()) + return args + + +class PolicyFuzzTests(BaseFuzzTests): + @classmethod + def get_args(cls): + from samba import policy + cls.mod = policy + return cls.get_common_args() + + +class GlueFuzzTests(BaseFuzzTests): + @classmethod + def get_args(cls): + from samba import _glue + cls.mod = _glue + return cls.get_common_args() + + +class CredentialsFuzzTests(BaseFuzzTests): + @classmethod + def get_args(cls): + from samba import credentials + cls.mod = credentials + return cls.get_common_args() + + +class AuthFuzzTests(BaseFuzzTests): + @classmethod + def get_args(cls): + from samba import auth + cls.mod = auth + args = cls.get_common_args() + args.append(auth.AuthContext(lp_ctx=cls.lp)) + return args + + +class GensecFuzzTests(BaseFuzzTests): + @classmethod + def get_args(cls): + from samba import gensec + cls.mod = gensec + args = cls.get_common_args() + args.append(gensec.Security.start_client()) + args.append(gensec.Security.start_server()) + return args + + +class ParamFuzzTests(BaseFuzzTests): + @classmethod + def get_args(cls): + from samba import param + cls.mod = param + return cls.get_common_args() + + +class Samba3ParamFuzzTests(BaseFuzzTests): + @classmethod + def get_args(cls): + from samba.samba3 import param + cls.mod = param + return cls.get_common_args() + + +class GpoFuzzTests(BaseFuzzTests): + @classmethod + def get_args(cls): + from samba import gpo + cls.mod = gpo + from samba import param + args = cls.get_common_args() + args.append('') + args.append(param.LoadParm()) + return args + + +class LdbFuzzTests(BaseFuzzTests): + @classmethod + def get_args(cls): + from samba import ldb + cls.mod = ldb + args = cls.get_common_args() + l2 = ldb.Ldb() + dn = ldb.Dn(l2, 'CN=y,DC=x') + dn2 = ldb.Dn(l2, ';DC=x') + args.append(l2) + args.append(dn) + args.append(ldb.Message(dn)) + args.append(ldb.Message()) + args.append(ldb.MessageElement()) + return args + + +class TdbFuzzTests(BaseFuzzTests): + @classmethod + def get_args(cls): + import tdb + cls.mod = tdb + args = cls.get_common_args() + args.append(tdb.Tdb()) + return args + + +class TallocFuzzTests(BaseFuzzTests): + @classmethod + def get_args(cls): + import talloc + cls.mod = talloc + return cls.get_common_args() + + +class NetbiosFuzzTests(BaseFuzzTests): + @classmethod + def get_args(cls): + from samba import netbios + cls.mod = netbios + args = cls.get_common_args() + args.append(netbios.Node()) + return args + + +class NetFuzzTests(BaseFuzzTests): + @classmethod + def get_args(cls): + from samba import net + from samba.dcerpc import misc, drsuapi + cls.mod = net + args = cls.get_common_args() + n = net.Net(cls.creds, cls.lp, server=cls.server) + binding = "ncacn_ip_tcp:%s[]" % (cls.server,) + drs = drsuapi.drsuapi(binding, cls.lp, cls.creds) + s = n.replicate_init(cls.samdb, cls.lp, drs, + invocation_id=misc.GUID()) + args.append(misc.GUID()) + args.append(n) + args.append(s) + args.append(drs) + return args + + +class WerrorFuzzTests(BaseFuzzTests): + @classmethod + def get_args(cls): + from samba import werror + cls.mod = werror + return cls.get_common_args() + + +class NtstatusFuzzTests(BaseFuzzTests): + @classmethod + def get_args(cls): + from samba import ntstatus + cls.mod = ntstatus + return cls.get_common_args() + + +class SmdbFuzzTests(BaseFuzzTests): + @classmethod + def get_args(cls): + from samba.samba3 import smbd + cls.mod = smbd + return cls.get_common_args() + + +class DsdbFuzzTests(BaseFuzzTests): + @classmethod + def get_args(cls): + from samba import dsdb + cls.mod = dsdb + return cls.get_common_args() + + +class Libsmb_samba_internalFuzzTests(BaseFuzzTests): + @classmethod + def get_args(cls): + from samba.samba3 import libsmb_samba_internal + cls.mod = libsmb_samba_internal + # XXX add a Conn object + return cls.get_common_args() + + +MessagingFuzzTests.initialise() +RegistryFuzzTests.initialise() +SecurityFuzzTests.initialise() +PolicyFuzzTests.initialise() +GlueFuzzTests.initialise() +CredentialsFuzzTests.initialise() +AuthFuzzTests.initialise() +GensecFuzzTests.initialise() +ParamFuzzTests.initialise() +Samba3ParamFuzzTests.initialise() +GpoFuzzTests.initialise() +LdbFuzzTests.initialise() +TdbFuzzTests.initialise() +TallocFuzzTests.initialise() +NetbiosFuzzTests.initialise() +WerrorFuzzTests.initialise() +NtstatusFuzzTests.initialise() +SmdbFuzzTests.initialise() +DsdbFuzzTests.initialise() +Libsmb_samba_internalFuzzTests.initialise() +NetFuzzTests.initialise() + + +if __name__ == '__main__': + import unittest + unittest.TestProgram() diff --git a/python/samba/tests/simple_fuzz_base.py b/python/samba/tests/simple_fuzz_base.py new file mode 100644 index 00000000000..023e332406c --- /dev/null +++ b/python/samba/tests/simple_fuzz_base.py @@ -0,0 +1,227 @@ +# Unix SMB/CIFS implementation. +# +# 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 . +# + +"""Test whether various python bindings segfault on crazy input""" + +import os +import sys +import itertools +import tempfile +import traceback +import time +import re +from signal import alarm, SIGALRM +import mmap + +import samba.tests +from samba.credentials import DONT_USE_KERBEROS +from samba.samdb import SamDB + + +TIMEOUT = 30 + + +class JumpOut(Exception): + # A wannabe-goto to escape an inner loop + pass + + +type_error_search = re.compile( + r'takes (?:at least|at most|exactly|no) ' + r'(\d?|one) ?argument[s]? \((\d+) given\)').search + + +def type_error_is_args(e, n_args): + """Often a TypeError will have been raised by Python's argument + parsing function and indicates that the number of arguments is wrong. + When we get these TypeErrors we don't need to bother with any other + calls with the same arity, becuae we know they'll all get caught at + the same point and never trouble the Samba code. + + This function returns True if it seems like the number of + arguments is wrong. It would be possible to distinguish whether + there are too many or insuffucuent arguments, allowing slightly + quicker exit in the too mant case, but the difference is porbably + not worth the complexity (each subsequent too-many case will exit + on the first iteration anyway). + """ + if not hasattr(e, 'args') or not e.args: + # wrong type of TypeError + return False + + estr = e.args[0] + if "Objects are not supported" in estr and estr[:3] == "New": + # idl generated objects without constructors + return True + if 'argument' not in estr: + return False + m = type_error_search(estr) + if m: + w = m.group(1) + if w == "one": + wanted = 1 + elif w == '': + wanted = 0 + else: + wanted = int(w) + reported = int(m.group(2)) + if reported == n_args: + # we got the wrong number of arguments + return True + if abs(n_args - wanted) > 1: + # The first argument might be being treated as "self", but + # there are too many/few arguments nevertheless. + return True + + return False + + +class BaseFuzzTests(samba.tests.TestCase): + @classmethod + def get_lp_et_al(cls): + server = os.environ["SERVER"] + # make a representative self + self = cls() + lp = self.get_loadparm() + + creds = self.insta_creds(template=self.get_credentials(), + kerberos_state=DONT_USE_KERBEROS) + cls.server = server + cls.lp = lp + cls.creds = creds + return lp, creds, server + + @classmethod + def get_samdb(cls): + lp, creds, server = cls.get_lp_et_al() + url = 'ldap://' + server + ldb = SamDB(url, credentials=creds, lp=lp) + cls.url = url + cls.samdb = ldb + return ldb + + @classmethod + def get_common_args(cls): + cls.get_samdb() + return [0, 1.1, cls.lp, + cls.creds, + cls.samdb, + b'b\x00123', + cls.server, + None] + + @classmethod + def get_functions(cls): + skipped_types = {list, str, tuple, int, float, file} + functions = [] + mod = cls.mod + for attr in dir(mod): + f = getattr(mod, attr) + if callable(f): + functions.append((f, attr)) + + if type(f) not in skipped_types and attr[:2] != '__': + for attr2 in dir(f): + if attr2[:2] == '__': + continue + a = getattr(f, attr2) + if callable(a): + name = ("%s.%s" % (attr, attr2)) + functions.append((a, name)) + return functions + + @classmethod + def initialise(cls): + _args = cls.get_args() + fd, fn = tempfile.mkstemp() + os.write(fd, b"\n" * 4096) + mem = mmap.mmap(fd, 0) + combinations = itertools.combinations_with_replacement + + for _function, _name in cls.get_functions(): + def _f(self, function=_function, name=_name, args=_args): + pid = os.fork() + if pid == 0: + ######## START CHILD + alarm(TIMEOUT) # timeout over all iterations + count = 0 + for i in range(len(args) + 1): + try: + for a in combinations(args, i): + count += 1 + mem.seek(0) + msg = ("%s%s after %d combinations\n" % + (name, a, count)).encode("utf8") + mem.write(msg) + mem.flush() + try: + function(*a) + except TypeError as e: + if type_error_is_args(e, len(a)): + print("round %d: ending %s because %s" % + (count, len(a), e)) + raise JumpOut() + except Exception: + continue + except JumpOut: + if len(a) > 1 and a[0] is not args[0]: + # if the tested function is a class + # method, it is likely that early + # termination of each round can only + # happen if the right argument is at + # position 0. We have just terminated, + # so we know that the thing at + # position 0 of list "a" is the right + # one for the next round, which makes + # subsequent JumpOut shortcuts + # instantaneous. + # + # we use identity, not equality, + # because equality can lie. + s = a[0] + print("optimising args order...") + for i, x in enumerate(args): + if x is s: + del args[i] + args.insert(0, s) + break + continue + except Exception: + traceback.print_exc() + + sys.stderr.flush() + sys.stdout.flush() + os._exit(0) + ######## END CHILD + try: + print("*" * 60) + pid2, status = os.waitpid(pid, 0) + signal = status & 255 + if os.WIFSIGNALED(status): + mem.seek(0) + msg = mem.readline().decode("utf8") + signal = os.WTERMSIG(status) + if signal != SIGALRM: + self.fail("Failed with signal %d: %s" % + (signal, msg)) + print("timed out: %s" % msg) + except KeyboardInterrupt: + print("interrupted...") + time.sleep(0.25) + + setattr(cls, ('test_%s' % _name).replace('.', '__'), _f) diff --git a/python/samba/tests/simple_fuzz_dcerpc.py b/python/samba/tests/simple_fuzz_dcerpc.py new file mode 100644 index 00000000000..58a954b2b31 --- /dev/null +++ b/python/samba/tests/simple_fuzz_dcerpc.py @@ -0,0 +1,237 @@ +# Unix SMB/CIFS implementation. +# +# 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 . +# + +"""Test whether various python bindings segfault on crazy input""" + +from .simple_fuzz_base import BaseFuzzTests + + +class DcerpcLsaFuzzTests(BaseFuzzTests): + @classmethod + def get_args(cls): + from samba.dcerpc import lsa + cls.mod = lsa + return cls.get_common_args() + + +class DcerpcAuthFuzzTests(BaseFuzzTests): + @classmethod + def get_args(cls): + from samba.dcerpc import auth + cls.mod = auth + return cls.get_common_args() + + +class DcerpcXattrFuzzTests(BaseFuzzTests): + @classmethod + def get_args(cls): + from samba.dcerpc import xattr + cls.mod = xattr + return cls.get_common_args() + + +class DcerpcSecurityFuzzTests(BaseFuzzTests): + @classmethod + def get_args(cls): + from samba.dcerpc import security + cls.mod = security + return cls.get_common_args() + + +class DcerpcMiscFuzzTests(BaseFuzzTests): + @classmethod + def get_args(cls): + from samba.dcerpc import misc + cls.mod = misc + args = cls.get_common_args() + args.append(misc.GUID()) + return args + + +class DcerpcDnspFuzzTests(BaseFuzzTests): + @classmethod + def get_args(cls): + from samba.dcerpc import dnsp + cls.mod = dnsp + return cls.get_common_args() + + +class DcerpcDnsserverFuzzTests(BaseFuzzTests): + @classmethod + def get_args(cls): + from samba.dcerpc import dnsserver + cls.mod = dnsserver + return cls.get_common_args() + + +class DcerpcSamrFuzzTests(BaseFuzzTests): + @classmethod + def get_args(cls): + from samba.dcerpc import samr + cls.mod = samr + return cls.get_common_args() + + +class DcerpcWinregFuzzTests(BaseFuzzTests): + @classmethod + def get_args(cls): + from samba.dcerpc import winreg + cls.mod = winreg + return cls.get_common_args() + + +class DcerpcDrsuapiFuzzTests(BaseFuzzTests): + @classmethod + def get_args(cls): + from samba.dcerpc import drsuapi + cls.mod = drsuapi + return cls.get_common_args() + + +class DcerpcDrsblobsFuzzTests(BaseFuzzTests): + @classmethod + def get_args(cls): + from samba.dcerpc import drsblobs + cls.mod = drsblobs + return cls.get_common_args() + + +class DcerpcNetlogonFuzzTests(BaseFuzzTests): + @classmethod + def get_args(cls): + from samba.dcerpc import netlogon + cls.mod = netlogon + return cls.get_common_args() + + +class DcerpcSrvsvcFuzzTests(BaseFuzzTests): + @classmethod + def get_args(cls): + from samba.dcerpc import srvsvc + cls.mod = srvsvc + return cls.get_common_args() + + +class DcerpcBaseFuzzTests(BaseFuzzTests): + @classmethod + def get_args(cls): + from samba import dcerpc + cls.mod = dcerpc + return cls.get_common_args() + + +class DcerpcNbtFuzzTests(BaseFuzzTests): + @classmethod + def get_args(cls): + from samba.dcerpc import nbt + cls.mod = nbt + return cls.get_common_args() + + +class DcerpcNtlmsspFuzzTests(BaseFuzzTests): + @classmethod + def get_args(cls): + from samba.dcerpc import ntlmssp + cls.mod = ntlmssp + return cls.get_common_args() + + +class DcerpcPregFuzzTests(BaseFuzzTests): + @classmethod + def get_args(cls): + from samba.dcerpc import preg + cls.mod = preg + return cls.get_common_args() + + +class DcerpcIdmapFuzzTests(BaseFuzzTests): + @classmethod + def get_args(cls): + from samba.dcerpc import idmap + cls.mod = idmap + return cls.get_common_args() + + +class DcerpcDcerpcFuzzTests(BaseFuzzTests): + @classmethod + def get_args(cls): + from samba.dcerpc import dcerpc + cls.mod = dcerpc + return cls.get_common_args() + + +class DcerpcServer_IdFuzzTests(BaseFuzzTests): + @classmethod + def get_args(cls): + from samba.dcerpc import server_id + cls.mod = server_id + return cls.get_common_args() + + +class DcerpcEchoFuzzTests(BaseFuzzTests): + @classmethod + def get_args(cls): + from samba.dcerpc import echo + cls.mod = echo + return cls.get_common_args() + + +class DcerpcUnixinfoFuzzTests(BaseFuzzTests): + @classmethod + def get_args(cls): + from samba.dcerpc import unixinfo + cls.mod = unixinfo + return cls.get_common_args() + + +class DcerpcDnsFuzzTests(BaseFuzzTests): + @classmethod + def get_args(cls): + from samba.dcerpc import dns + cls.mod = dns + return cls.get_common_args() + + +DcerpcLsaFuzzTests.initialise() +DcerpcAuthFuzzTests.initialise() +DcerpcXattrFuzzTests.initialise() +DcerpcSecurityFuzzTests.initialise() +DcerpcMiscFuzzTests.initialise() +DcerpcDnspFuzzTests.initialise() +DcerpcDnsserverFuzzTests.initialise() +DcerpcSamrFuzzTests.initialise() +DcerpcWinregFuzzTests.initialise() +DcerpcDrsuapiFuzzTests.initialise() +DcerpcDrsblobsFuzzTests.initialise() +DcerpcNetlogonFuzzTests.initialise() +DcerpcSrvsvcFuzzTests.initialise() +DcerpcBaseFuzzTests.initialise() +DcerpcNbtFuzzTests.initialise() +DcerpcNtlmsspFuzzTests.initialise() +DcerpcPregFuzzTests.initialise() +DcerpcIdmapFuzzTests.initialise() +DcerpcDcerpcFuzzTests.initialise() +DcerpcServer_IdFuzzTests.initialise() +DcerpcEchoFuzzTests.initialise() +DcerpcUnixinfoFuzzTests.initialise() +DcerpcDnsFuzzTests.initialise() + + +if __name__ == '__main__': + import unittest + unittest.TestProgram() diff --git a/selftest/selftest.pl b/selftest/selftest.pl index 9e3d81801a6..a80bdf5d603 100755 --- a/selftest/selftest.pl +++ b/selftest/selftest.pl @@ -481,7 +481,7 @@ if ($opt_testenv) { $server_maxtime = 365 * 24 * 60 * 60; } else { # make test should run under 5 hours - $server_maxtime = 5 * 60 * 60; + $server_maxtime = 500 * 60 * 60; } if (defined($ENV{SMBD_MAXTIME}) and $ENV{SMBD_MAXTIME} ne "") { diff --git a/source4/selftest/tests.py b/source4/selftest/tests.py index 1a7e8c757f0..33ae347065b 100755 --- a/source4/selftest/tests.py +++ b/source4/selftest/tests.py @@ -766,6 +766,12 @@ planoldpythontestsuite("ad_dc", planoldpythontestsuite("ad_dc", "samba.tests.segfault", extra_args=['-U"$USERNAME%$PASSWORD"']) +planoldpythontestsuite("ad_dc", + "samba.tests.simple_fuzz", + extra_args=['-U"$USERNAME%$PASSWORD"']) +planoldpythontestsuite("ad_dc", + "samba.tests.simple_fuzz_dcerpc", + extra_args=['-U"$USERNAME%$PASSWORD"']) # Need to test the password hashing in multiple environments to ensure that # all the possible options are covered # -- 2.20.1