IMetaDataEmit::DefineUserString returns HRESULT: -2147024882 (0x8007000E E_OUTOFMEMORY)

2

I'm playing around with the unmanaged Profiling interfaces for the CLR.

When running with a netcoreapp3.1 or net5.0 console application, in ICorProfilerCallback::JITCompilationStarted or in ICorProfilerCallback::ModuleLoadFinished, any call to IMetaDataEmit::DefineUserString to store a string literal in the console app module and return an mdString token, returns a HRESULT of -2147024882 (0x8007000E E_OUTOFMEMORY). The call returns this same HRESULT no matter what values are passed to IMetaDataEmit::DefineUserString.

The .NET application is super simple

using System;

namespace dotnetapp
{
    class Program
    {
        static void Main(string[] args)
        {
            WriteEnvironmentVariable("CORECLR_ENABLE_PROFILING");
            WriteEnvironmentVariable("CORECLR_PROFILER");
            WriteEnvironmentVariable("CORECLR_PROFILER_PATH");
            Console.WriteLine("Hello World!");
        }

        static void WriteEnvironmentVariable(string name)
        {
            var value = Environment.GetEnvironmentVariable(name);
            Console.WriteLine($"{name} = {value}");
        }
    }
}

and is built and run with

dotnet build -c Debug dotnetapp.csproj
dotnet bin/Debug/net5.0/dotnetapp.dll

With the relevant Core CLR profiling environment variables set when running the application

CORECLR_ENABLE_PROFILING=1
CORECLR_PROFILER={PROFILER CLSID}
CORECLR_PROFILER_PATH=clr_profiler.dll

The profiler is written in Rust using com-rs, and the call to IMetaDataEmit::DefineUserString is defined as

impl IMetaDataEmit {
    pub fn define_user_string(&self, str: &str) -> Result<mdString, HRESULT> {
        let mut md_string = mdStringNil;
        let mut wide_string = U16CString::from_str(str).unwrap();
        let len = wide_string.len() as ULONG;
        let ptr = wide_string.as_ptr();
        let hr = unsafe { self.DefineUserString(ptr, len, &mut md_string) };
        if FAILED(hr) {
            log::error!("define user string '{}' failed. HRESULT: {} {:X}", str, hr, hr);
            return Err(hr);
        }
        log::trace!("md_string token {}", md_string);
        Ok(md_string)
    }
}

where the unsafe call is to comr-rs generated function

com::interfaces! {
    #[uuid("BA3FEE4C-ECB9-4E41-83B7-183FA41CD859")]
    pub unsafe interface IMetaDataEmit: IUnknown {
        // functions ordered by IMetaDataEmit layout
        fn DefineUserString(&self,
            szString: LPCWSTR,
            cchString: ULONG,
            pstk: *mut mdString,
        ) -> HRESULT;
    }
}

I'm using U16CString to create a *const u16 pointer in other places in the profiler to pass LPCWSTR to interface functions, such as IMetaDataImport::EnumMethodsWithName, so I don't think this is an issue, but thought I'd mention it.

The log for the failed call is

TRACE [imetadata_emit] wide_string UCString { inner: [71, 111, 111, 100, 98, 121, 101, 32, 87, 111, 114, 108, 100, 33, 0] }, len 14
ERROR [imetadata_emit] define user string 'Goodbye World!' failed. HRESULT: -2147024882 8007000E

where UCString.inner is the Vec<u16> to which a pointer is passed to IMetaDataEmit::DefineUserString.

IMetaDataEmit is retrieved from the stored ICorProfilerInfo passed to the profiler on initialize using ICorProfilerInfo::GetModuleMetaData, using the CorOpenFlags ofRead and ofWrite

impl ICorProfilerInfo {
    pub fn get_module_metadata<I: Interface>(
        &self,
        module_id: ModuleID,
        open_flags: CorOpenFlags,
    ) -> Result<I, HRESULT> {
        let mut unknown = None;
        let hr = unsafe {
            self.GetModuleMetaData(module_id, open_flags.bits(), &I::IID as REFIID, &mut unknown as *mut _ as *mut *mut IUnknown)
        };

        if FAILED(hr) {
            log::error!("error fetching metadata for module_id {}, HRESULT: {:X}", module_id, hr);
            return Err(hr);
        }
        Ok(unknown.unwrap())
    }
}

where GetModuleMetaData is defined on the ICorProfilerInfo generated with com::interfaces! macro

com::interfaces! {
    #[uuid("28B5557D-3F3F-48b4-90B2-5F9EEA2F6C48")]
    pub unsafe interface ICorProfilerInfo: IUnknown {
        // functions ordered by ICorProfilerInfo layout
        fn GetModuleMetaData(&self,
            moduleId: ModuleID,
            dwOpenFlags: DWORD,
            riid: REFIID,
            ppOut: *mut *mut IUnknown,
        ) -> HRESULT;
    }
}

It seems like I'm missing something in Rust somewhere. Retrieving data from ICorProfilerInfo, IMetaDataImport, IMetaDataImport2 work, as does getting and modifying IL function bodies (changing existing instructions). A thought I had is whether IMetaDataEmit might need to be mutable, but I don't think that needs to be the case, as changes to metadata occur on the C++ runtime side of the FFI boundary.

EDIT

I put together a simple C++ profiler that calls IMetaDataEmit::DefineUserString in ICorProfilerCallback::ModuleLoadFinished and that works as expected on the sample .NET application, so this indicates that the issue is in the Rust code somewhere.

Looking through the runtime code, I think RegMeta::DefineUserString is the implementation of DefineUserString and tracing the code paths, I think the E_OUTOFMEMORY is coming from StgBlobPool::AddBlob.

c#
.net-core
rust
clr
clr-profiling-api
asked on Stack Overflow Apr 9, 2021 by Russ Cam • edited Apr 13, 2021 by Russ Cam

1 Answer

1

The issue was (apparently) due to an incorrect definition of the IMetaDataEmit interface.

Whatever the target language is, a COM interface definition must match the original binary layout exactly: all methods in same order (don't trust MSDN visual order on this), starting with derived interfaces methods (IUnknown, etc.), and exact binary-compatible signature for each method.

answered on Stack Overflow Apr 13, 2021 by Simon Mourier

User contributions licensed under CC BY-SA 3.0