Sql-server – Passing NULL xml value to a certain CLR stored function crashes it when called in a certain way

crashsql-clrsql-server-2008xml

Consider a CLR stored procedure that accepts an XML parameter and calls another stored procedure in the same schema:

[SqlFunction(DataAccess = DataAccessKind.Read,
     IsDeterministic = false, IsPrecise = true,
     SystemDataAccess = SystemDataAccessKind.Read)]
public static SqlBoolean TestTest(SqlXml Data)
{
    using (var c = new SqlConnection("context connection=true"))
    {
        c.Open();

        using (var cmd = new SqlCommand(@"select case when exists(select 0 from [testing].WillBeCalledByCLR(@d)) then 1 else 0 end;", c))
        {
            var p = cmd.Parameters.Add("@d", SqlDbType.Xml);

            p.Direction = ParameterDirection.Input;
            if (Data.IsNull)
                { p.Value = DBNull.Value; }
            else
                { p.Value = Data; }  // Or Data.Value

            return (bool)cmd.ExecuteScalar();
        }
    }
}

This CLR function is visible in SQL as:

create function [testing].[TestTest] ( @data xml )
returns bit
WITH CALLED ON NULL INPUT
AS
EXTERNAL NAME [Test].[Test.Test].[TestTest]

The stored function it calls is this:

create function testing.WillBeCalledByCLR (@x xml = null)
returns table
as
return (
  select 1 as one, 2 as two, 'three' as three
);

In this setup, if I call the CLR function like this:

if testing.TestTest(null) = 1
begin
  select 'Meaningful actions';
end;

or like this:

declare @res bit = testing.TestTest(null);

or like this:

declare @res bit;
set @res = testing.TestTest(null);

or like this:

declare @res bit;
select @res = testing.TestTest(null);

then I get:

Msg 2905, Level 25, State 1, Line 6

Msg 0, Level 20, State 0, Line 0
A severe error occurred on the current command. The results, if any, should be discarded.

But if I call it like this:

select testing.TestTest(null);

or like this:

declare @res bit;
set @res = (select testing.TestTest(null));

I get a proper value back (e.g. 1).

If, instead of null, I pass an empty string '', the function is successfully called in all cases.

Why? Did I do something wrong in my CLR function?

Microsoft SQL Server 2008 (SP2) – 10.0.4000.0 (X64)
Sep 16 2010 19:43:16
Copyright (c) 1988-2008 Microsoft Corporation
Standard Edition (64-bit) on Windows NT 6.0 (Build 6002: Service Pack 2) (VM)

Best Answer

Overall, no, you did not do anything wrong with your .NET code. There is a minor technical issue, but we will get to that in a moment.

I was able to reproduce this (both error and non-error scenarios) in SQL Server 2008 R2 RTM. At first, in the "non-error" scenario, I was getting another error for:

specified cast is invalid

This error was a result of the query passing back an INT and the C# code attempting to cast it to a bool. I changed the query to be:

... then CONVERT(BIT, 1) else CONVERT(BIT, 0) end ...

That forces the datatype of the value to be returned to be something that can be cast into bool. This was the "minor error" that I was referring to, and I consider it "minor" and not really an error because I suspect your code originally attempted to cast into int and returned a SqlInt32 since you claim that passing in an empty string allowed it to return successfully. There is no way this would ever return successfully, even with an empty string instead of NULL for the input value, if your code was attempting to cast to bool and return SqlBoolean. And yes, I could have changed the cast to be (int) and the return type to be SqlInt32, but this was less of a structural change.

I tested several types of changes: removing the execution, removing the @d parameter and hard-coding the null into the query but still having the SqlParameter defined, removing the @d parameter and the SqlParameter, etc. What it finally came down to is this: merely the existence of a SqlParameter of a LOB type (i.e. XML, VARCHAR(MAX), NVARCHAR(MAX), VARBINARY(MAX) ), even if unused in the query, caused the "severe error" when passing in a NULL. However, if any of those MAX types were changed to be their maximum non-MAX lengths (i.e. 8000, 4000, and 8000 respectively), then there was no error when passing in a NULL. Hence the following reduced code should be enough to reproduce the error when passing in a NULL to the SQLCLR scalar function:

using (var cmd = new SqlCommand(@"SELECT 1;", c))
{
    var p = cmd.Parameters.Add("@d", SqlDbType.NVarChar, (int)SqlMetaData.Max);

    cmd.ExecuteScalar();
}

Now, I was not able to reproduce the error with the original code in SQL Server 2012 SP3 or SQL Server 2014 SP1 :-). Therefore, I suspect that this was an issue with CLR 2.0 and/or the .NET Framework versions associated with CLR 2.0 (i.e. "2.0", "3.0", and "3.5"). OR, another aspect to consider is that it with this failing ( declare @res bit; set @res = testing.TestTest(null); ) while this works ( declare @res bit; set @res = (select testing.TestTest(null)); ) that could point to a problem with the Query Optimizer and not with the CLR version. Or maybe it is a combination of the two. Or perhaps it is Descartes's pesky Evil Demon. Either way. ( ** please see comment at the end )

If you are able to move up to SQL Server 2012 or newer, then the problem should "magically" disappear. BUT, if stuck on SQL Server 2008 / 2008 R2, I was able to work around the issue by declaring the parameter to be a non-MAX type if the input is NULL, which should be fine since there won't be any risk of truncation when passing through a NULL. And, in the case of the input parameter datatype of the T-SQL object being called being XML, it still works to define the parameter datatype in the .NET code as NVARCHAR(4000) as it will implicitly convert that into XML when called. The following code worked for me in SQL Server 2008 R2 when passing in a NULL:

SqlParameter _XmlData;

if (Data.IsNull)
{
    // Parameter can't be a LOB type if input is NULL
    _XmlData = new SqlParameter("@d", SqlDbType.NVarChar, 4000);
    _XmlData.Value = DBNull.Value;
}
else
{
    _XmlData = new SqlParameter("@d", SqlDbType.Xml);
    _XmlData.Value = Data.Value;
}

cmd.Parameters.Add(_XmlData);

** @MartinSmith left this illuminating comment:

Specifically the error seems to be "Access violation reading location 0x0000000000000000" inside sqlservr.exe!CClrXvarProxy::EnsureBufferInit so looks like a null pointer bug but googling those method names doesn't return any KB article.