My SQL Server 2008 R2 CLR works sometimes, but not others, but why?

1

I have a CLR DLL (called clr.dll) which we've previously loaded and used. I'm using it to load a large number of XML files (for which we have no XSD so we can't use the fiddly XML processor in SSIS as this requires an XSD). I'm loading it into a database also called CLR. For info the CLR function returns the XML file as a table by having one row per node. The CLR also has a few test functions

Previously this has worked and now it produces errors. The errors appear to occur intermittently, and occur even if the only thing executing on the server is a call to the CLR. I have a few test functions that use virtually no resource and they also don't work. i.e. it's not a resource problem.

The CLR is loaded with PERMISSION_SET = EXTERNAL_ACCESS. The database is set with TRUSTWORTHY ON. It appears that in SQL Server 2008 R2 we can't load the CLR using hashcode and my dll exceeds the 8000 chars that HASHBYTES supports anyway (suggested by other articles).

Within the CLR the functions belong to a class named UserDefinedFunctions.

This is a cut down version of the script I'm using to setup the CLR. The database has correct permissions. In summary it: (1) drops all functions in the CLR database, (2) drops and reconnects the CLR and (3) creates the functions to link to the assembly and sets the permissions on them appropriately.

USE CLR
GO

-- drop all functions
DECLARE @FunctionName NVARCHAR(400)
DECLARE @SQL NVARCHAR(MAX)

WHILE EXISTS    (SELECT *
                FROM    sysobjects
                WHERE   xtype IN ('FS', 'FT'))
BEGIN
    SET @FunctionName = (SELECT TOP 1 name
                        FROM    sysobjects
                        WHERE   xtype IN ('FS', 'FT'))

    SET @SQL = 'DROP FUNCTION dbo.' + @FunctionName

    EXEC sp_executesql @SQL
END 
GO

-- belt and braces, this should be the case anyway
ALTER DATABASE CLR SET TRUSTWORTHY ON;
GO

-- drops and load the assembly, first time
IF EXISTS   (SELECT *
            FROM    sys.assemblies
            WHERE   name = 'CLR')
    DROP ASSEMBLY CLR
GO

CREATE ASSEMBLY CLR from 'e:\clr\clr.dll' WITH PERMISSION_SET = EXTERNAL_ACCESS;
GO

-- ======== Scalar-functions ================================================================
CREATE FUNCTION dbo.DateTimeToString(@dt DATETIME, @fmt NVARCHAR(MAX)) RETURNS NVARCHAR(MAX)
AS EXTERNAL NAME CLR.UserDefinedFunctions.DateTimeToString;
GO


CREATE FUNCTION dbo.FileExists(@Filename NVARCHAR(MAX)) RETURNS BIT
AS EXTERNAL NAME CLR.UserDefinedFunctions.FileExists;
GO


CREATE FUNCTION dbo.FileGetCreated(@Filename NVARCHAR(MAX)) RETURNS DATETIME
AS EXTERNAL NAME CLR.UserDefinedFunctions.FileGetCreated;
GO


CREATE FUNCTION dbo.FileGetModified(@Filename NVARCHAR(MAX)) RETURNS DATETIME
AS EXTERNAL NAME CLR.UserDefinedFunctions.FileGetModified;
GO


CREATE FUNCTION dbo.FileGetSize(@Filename NVARCHAR(MAX)) RETURNS BIGINT
AS EXTERNAL NAME CLR.UserDefinedFunctions.FileGetSize;
GO


CREATE FUNCTION Reflection(@Data NVARCHAR(MAX)) RETURNS NVARCHAR(MAX) -- test - returns the string passed to it
AS EXTERNAL NAME CLR.UserDefinedFunctions.Reflection;
GO


-- ======== Table-valued functions -- ReadTextFile
CREATE FUNCTION dbo.ReadTextFile(@Filename NVARCHAR(4000))
RETURNS 
TABLE
(
    LineIndex INT,
    Data NVARCHAR(4000)
)
AS
EXTERNAL NAME CLR.UserDefinedFunctions.ReadTextFile
GO


-- ReadXmlDoc
CREATE FUNCTION dbo.ReadXmlDoc(@Filename NVARCHAR(4000))
RETURNS 
TABLE
(
    NodeIndex INT,
    ParentIndex INT,
    DepthIndex INT, 
    ChildCount INT, 
    SiblingIndex INT, 
    SiblingCount INT, 
    IsTerminalNode BIT,
    Tag NVARCHAR(4000),
    IndexPath NVARCHAR(4000),
    SimplePath NVARCHAR(4000), 
    UniquePath NVARCHAR(4000), 
    ExtendedPath NVARCHAR(4000), 
    TextValue NVARCHAR(4000)
)
AS
EXTERNAL NAME CLR.UserDefinedFunctions.ReadXmlDoc
GO

-- SELECT and EXECUTE permissions are set here, but omitted for security reasons

I'm then calling my minimal test function, named Reflection, which simply returns the string passed to it:

SELECT  CLR.dbo.Reflection('hello')

Irrespective of which function I call, or which database I execute in, this error is thrown:

Msg 10314, Level 16, State 11, Line 1
An error occurred in the Microsoft .NET Framework while trying to load assembly id 65544. The server may be running out of resources, or the assembly may not be trusted with PERMISSION_SET = EXTERNAL_ACCESS or UNSAFE. Run the query again, or check documentation to see how to solve the assembly trust issues. For more information about this error:

System.IO.FileLoadException: Could not load file or assembly 'clr, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null' or one of its dependencies. An error relating to security occurred. (Exception from HRESULT: 0x8013150A) System.IO.FileLoadException:

at System.Reflection.Assembly._nLoad(AssemblyName fileName, String codeBase, Evidence assemblySecurity, Assembly locationHint, StackCrawlMark& stackMark, Boolean throwOnFileNotFound, Boolean forIntrospection)
at System.Reflection.Assembly.InternalLoad(AssemblyName assemblyRef, Evidence assemblySecurity, StackCrawlMark& stackMark, Boolean forIntrospection)
at System.Reflection.Assembly.InternalLoad(String assemblyString, Evidence assemblySecurity, StackCrawlMark& stackMark, Boolean forIntrospection)
at System.Reflection.Assembly.Load(String assemblyString)

As I say, it's not a resource issue and it's not a permissions issue and it happens "sometimes". Currently, I haven't been able to get it to run this week. Last week reapplying the drop and create scripts resolved the issue.

When I execute this code as suggested by Solomon below:

SELECT  name,
        is_trustworthy_on, 
        is_db_chaining_on, 
        compatibility_level, 
        owner_sid, 
        collation_name 
FROM    sys.databases 
WHERE   name IN (N'CLR', N'Other_DB')

I get:

name      is_trustworthy_on  is_db_chaining_on  compatibility_level  owner_sid                                                   collation_name
Other_DB  0                  0                  100                  0x0105000000000005150000009530FDDAA5F3AE3A5859ABA1C5FB0000  Latin1_General_CI_AS
CLR       1                  0                  100                  0x0105000000000005150000009530FDDAA5F3AE3A5859ABA17C2E0100  Latin1_General_CI_AS

Added 2 Oct 2019. Additionally, query 1:

SELECT  lgn.[name], 
        lgn.[type_desc], 
        lgn.[sid], 
        CASE lgn.[sid] WHEN SUSER_SID() THEN 1 ELSE 0 END AS [IsCurrent] 
FROM    sys.server_principals lgn 
WHERE   lgn.[sid] IN (0x0105000000000005150000009530FDDAA5F3AE3A5859ABA1C5FB0000, 0x0105000000000005150000009530FDDAA5F3AE3A5859ABA17C2E0100, SUSER_SID());

Results 1:

name       type_desc      sid                                                         IsCurrent
GRP\clone  WINDOWS_LOGIN  0x0105000000000005150000009530FDDAA5F3AE3A5859ABA1C5FB0000  0
GRP\mark   WINDOWS_LOGIN  0x0105000000000005150000009530FDDAA5F3AE3A5859ABA1CD790200  1

Query 2:

USE [CLR];
GO

SELECT  DB_NAME() AS [DB], usr.[name], usr.[type_desc], usr.[sid], CASE usr.[sid] WHEN USER_SID() THEN 1 ELSE 0 END AS [IsCurrent]
FROM    sys.database_principals usr 
WHERE   usr.[sid] IN (0x0105000000000005150000009530FDDAA5F3AE3A5859ABA1C5FB0000, 0x0105000000000005150000009530FDDAA5F3AE3A5859ABA17C2E0100, USER_SID()) 
OR      usr.[name] = N'dbo';

Results 2:

DB   name      type_desc     sid                                                         IsCurrent
CLR  dbo       WINDOWS_USER  0x0105000000000005150000009530FDDAA5F3AE3A5859ABA17C2E0100  0
CLR  GRP\mark  WINDOWS_USER  0x0105000000000005150000009530FDDAA5F3AE3A5859ABA1CD790200  1

Query 3:

USE [Other_DB];
GO

SELECT  DB_NAME() AS [DB], 
        usr.[name], 
        usr.[type_desc], 
        usr.[sid], 
        CASE usr.[sid] WHEN USER_SID() THEN 1 ELSE 0 END AS [IsCurrent] 
FROM    sys.database_principals usr 
WHERE   usr.[sid] IN (0x0105000000000005150000009530FDDAA5F3AE3A5859ABA1C5FB0000, 0x0105000000000005150000009530FDDAA5F3AE3A5859ABA17C2E0100, USER_SID()) 
OR      usr.[name] = N'dbo';

Results 3:

DB        name  type_desc     sid                                                         IsCurrent
Other_DB  dbo   WINDOWS_USER  0x0105000000000005150000009530FDDAA5F3AE3A5859ABA1C5FB0000  0
sql-server
security
sql-server-2008-r2
sqlclr
asked on Stack Overflow Sep 4, 2019 by Mark Roworth • edited Oct 10, 2019 by Solomon Rutzky

2 Answers

0

So I have learnt the following:

  • the following applies to CLRs that are EXTERNAL_ACCESS or UNSAFE in SQL2008R2.
  • when you create the assembly and connect SQL functions to a CLR, it doesn't load the CLR. This makes sense. Why use up memory before you need to.
  • when you first execute a call to the CLR, it will check the SID of the database of the query being executed against the SID of the CLR database. These two SIDs may or may not be the same. A primary reason why they may be different is that the database the query is being run from may have been restored from another server. If they don't agree, this error occurs:
Msg 10314, Level 16, State 11, Line 1
An error occurred in the Microsoft .NET Framework while trying to load assembly id 65544. The server may be running out of resources, or the assembly may not be trusted with PERMISSION_SET = EXTERNAL_ACCESS or UNSAFE. Run the query again, or check documentation to see how to solve the assembly trust issues. For more information about this error: 
System.IO.FileLoadException: Could not load file or assembly 'clr, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null' or one of its dependencies. An error relating to security occurred. (Exception from HRESULT: 0x8013150A)
System.IO.FileLoadException: 
   at System.Reflection.Assembly._nLoad(AssemblyName fileName, String codeBase, Evidence assemblySecurity, Assembly locationHint, StackCrawlMark& stackMark, Boolean throwOnFileNotFound, Boolean forIntrospection)
   at System.Reflection.Assembly.InternalLoad(AssemblyName assemblyRef, Evidence assemblySecurity, StackCrawlMark& stackMark, Boolean forIntrospection)
   at System.Reflection.Assembly.InternalLoad(String assemblyString, Evidence assemblySecurity, StackCrawlMark& stackMark, Boolean forIntrospection)
   at System.Reflection.Assembly.Load(String assemblyString)
answered on Stack Overflow Sep 4, 2019 by Mark Roworth
0

Testing has confirmed that this issue is directly related to the owner of the database containing the SQLCLR Assembly (the CLR database, in this case). Initially the CLR DB was owned by a Windows (Active Directory / AD) account. After switching the database owner over to sa, this behavior stopped happening.

While the problem area has been narrowed down (i.e. database owner where Assembly is located), and a solution found (i.e. change db owner to sa), the exact set of circumstances that cause this behavior has not yet been determined, and I am so far unable to reproduce this behavior based on the information provided.

For the moment, I am also fairly certain that the TRUSTWORTHY ON setting for the db containing the Assembly (the CLR DB) is a primary factor. I believe that when TRUSTWORTHY is enabled, Assemblies set to either EXTERNAL_ACCESS or UNSAFE do an extra, and external, security check. TRUSTWORTHY is only be used here due to the Assembly not being signed. I believe that if TRUSTWORTHY were OFF (i.e. disabled), then the owner of the database would not be a factor, and this behavior wouldn't even be possible in the first place.

OTHER NOTES

  • The term "CLR" should not be used in this context. Terminology should be "Assemblies", "SQLCLR objects", ".NET code", or just "SQLCLR" in general. The CLR is the "Common Language Runtime" that executes the code compiled into the Assemblies. This is similar to the JVM for Java, and we wouldn't refer to Java programs or apps as "JVMs".
  • when you create the assembly and connect SQL functions to a CLR, it doesn't load the CLR.

    No, but only because the CLR is loaded when SQL Server starts and is running the entire time (unless you have "lightweight pooling" / "fiber mode" enabled, and you shouldn't).

    Also, when you either CREATE or ALTER an Assembly, an App Domain is created as the assembly needs to be validated. But, the Assembly itself is not loaded into the App Domain.

  • when you first execute a call to the CLR, it will check the SID of the database of the query being executed against the SID of the CLR database.

    No. The SID of the owner of the "current" DB is not checked (well, not unless there is Cross-DB ownership chaining is being used, and that is not the case here). The SID of the owner of the DB containing the Assembly is checked against the owner_sid record in sys.databases to make sure they match, but only when TRUSTWORTHY is enabled. In such cases, if the owner SID does not match between sys.databases and sys.database_principals (in the DB containing the Assembly), then you will get an error stating that those SIDs must match.

    These two SIDs may or may not be the same. A primary reason why they may be different is that the database the query is being run from may have been restored from another server.

    Not exactly. The owner is initially set by the Login that created (via CREATE DATABASE, attach, or restore) the DB. However, it is fairly easy to change the owner of a database, and so I wouldn't assume that a DB came from another server simply due to having a different owner.

    If they don't agree, this error occurs:

    No. There is no requirement that the owners of these two databases be the same.

  • Regarding the "drop all functions" block of code shown in the top code block of the main question:

    1. Do not use sysobjects / dbo.sysobjects / sys.sysobjects as that is a compatibility view that exists only to allow for backwards compatibility with code written for versions of SQL Server prior to version 2005 (9.0). Starting in SQL Server 2005, you should be using sys.objects, or sys.{anything} instead of the old sys{anything} views.
    2. The datatype for @FunctionName should be sysname (which is a synonym for NVARCHAR(128) ) as that is used for nearly all entity names in SQL Server.
    3. Don't look simply for objects of a certain type as that is not very robust / stable since it can't distinguish between T-SQL wrapper objects associated with different Assemblies. You only have one Assembly now, but it's still an unnecessarily overly simplistic approach when the preferred approach is almost no extra effort. You should start with the sys.assembly_modules system catalog view. For example:

      SELECT obj.[name]
      FROM   sys.assembly_modules asmd
      INNER JOIN sys.assemblies assm
              ON assm.[assembly_id] = asmd.[assembly_id]
      INNER JOIN sys.objects obj
              ON obj.[object_id] = asmd.[object_id]
      WHERE  assm.[name] = N'{assembly_name_here}'
      

      Starting with that query it's easy to get the schema name from obj.[schema_id] and/or use a CASE statement to switch the object type for the DROP statement between FUNCTION, PROCEDURE, TRIGGER, depending on what is needed.

    4. I know this is just a deployment script, but still, instead of using a slow, row-by-row loop, you can drop all functions in a single batch / execution:

      DECLARE @SQL NVARCHAR(MAX) = N'';
      
      SELECT @SQL += N'DROP FUNCTION dbo.' + obj.[name] + N';'
                     + NCHAR(0x0D) + NCHAR(0x0A) -- CR+LF
      FROM   sys.assembly_modules asmd
      INNER JOIN sys.assemblies assm
              ON assm.[assembly_id] = asmd.[assembly_id]
      INNER JOIN sys.objects obj
              ON obj.[object_id] = asmd.[object_id]
      WHERE  assm.[name] = N'{assembly_name_here}';
      
      PRINT @SQL; -- DEBUG (else comment out and UNcomment line below)
      --EXEC (@SQL);
      
  • SET TRUSTWORTHY ON should be avoided, if at all possible. Please see "PLEASE, Please, please Stop Using Impersonation, TRUSTWORTHY, and Cross-DB Ownership Chaining" for details. Instead, sign the assembly with a certificate (i.e. Module Signing). Please see:

  • While this is mentioned in the two posts noted directly above, I find it much better to deploy an assembly from a VARBINARY literal (a.k.a. hex bytes) rather than from a DLL on the file system because that external DLL is now a dependency of the script and can even get out of sync with the creation of the T-SQL wrapper objects in the script. It's also harder to version the deployment script and/or transport it between systems. To easily convert the compiled DLL into a deployable VARBINARY literal, please see the open source tool I created for this purpose: BinaryFormatter.
  • For more info on working with SQLCLR in general, please visit: SQLCLR Info
answered on Stack Overflow Oct 10, 2019 by Solomon Rutzky

User contributions licensed under CC BY-SA 3.0