Signing DXIL Post-Compile

Microsoft’s DirectX shader compiler now compiles on Linux, and can generate both SPIR-V and DXIL from HLSL. However, in order to create shaders from DXIL in a running application without using development or experimental mode, the DXIL must be validated with the official dxil.dll binary releases from Microsoft.

The actual validation logic isn’t a secret, as it is public in the GitHub repository, but the official binaries are built at a specific revision so that hardware drivers have a well-known and deterministic set of rules to rely on.

When compilation is performed, there is a sneaky LoadLibrary call for dxil.dll, and if found, the shader byte code result will be officially signed. If the library is not found, then the shader byte code will be unsigned. In both situations, all validation rules are still evaluated.

A warning message will be displayed in this case:

warning: DXIL.dll not found. Resulting DXIL will not be signed for use in release environments.

If Windows is not in developer / test mode, attempting to create a shader object using unsigned shader byte code will produce an error like the following:

D3D12 ERROR: ID3D12Device::CreateComputeShader: Compute Shader is corrupt or in an unrecognized format. [ STATE_CREATION ERROR #322: CREATECOMPUTESHADER_INVALIDSHADERBYTECODE]

The dxil.dll binary is published as part of the official Windows SDK. You can get the latest one that supports DXR from the latest insider preview SDK.

Make sure you don’t use the release published on GitHub here, as this is an ancient version that will fail on even trivial validation:

$ PS H:\Repositories\dxil-signing> .\dxil-signing.exe -i simple.dxil -o simple_signed.dxil
Loading input file: simple.dxil
The dxil container failed validation
Error:
TODO - Metadata must be well-formed in operand count and types

The Windows SDK binaries install in versioned paths like the following:
C:\Program Files (x86)\Windows Kits\10\bin\10.0.17754.0\x64

The interfaces described in this post all exist in dxcapi.h.

Verify Binary Signing

When doing post-compile signing, it is important to have a method to query whether or not a shader has been previously signed, in order to prevent redundant signing work. This is also valuable for re-signing shader binaries without actually recompiling them when a new validator is released.

A DXIL binary blob starts with a header which is defined in include/dxc/HLSL/DxilContainer.h:

/// Use this type to describe a DXIL container of parts.
struct DxilContainerHeader {
  uint32_t              HeaderFourCC;
  DxilContainerHash     Hash;
  DxilContainerVersion  Version;
  uint32_t              ContainerSizeInBytes; // From start of this header
  uint32_t              PartCount;
  // Structure is followed by uint32_t PartOffset[PartCount];
  // The offset is to a DxilPartHeader.
};

This header can be simplified to the following:

struct DxilMinimalHeader
{
  UINT32 four_cc;
  UINT32 hash_digest[4];
};

An unsigned dxil binary will have a hash_digest that is zero-filled. This means that we can perform a very fast check for signed vs unsigned data:

inline bool is_dxil_signed(void* buffer)
{
  DxilMinimalHeader* header = reinterpret_cast<DxilMinimalHeader*>(buffer);
  bool has_digest = false;
  has_digest |= header->hash_digest[0] != 0x0;
  has_digest |= header->hash_digest[1] != 0x0;
  has_digest |= header->hash_digest[2] != 0x0;
  has_digest |= header->hash_digest[3] != 0x0;
  return has_digest;
}

There is no proper API for querying whether or not a binary is signed, but this will always be a reliable way to check the hash, since the header can never change for DxilContainers without breaking backwards compatibility.

Populating a DXIL Container

The validation flow requires a IDxcBlob instance, which is assumed to be the same DxilContainer produced when using the /Fo option with dxc.exe or dxl.exe (the result of dxcutil::AssembleToContainer).

Since we are trying to validate shader byte code using just raw data and length, we have to jump through a few hoops to make this work. The approach is to use IDxcLibrary::CreateBlobWithEncodingFromPinned, which returns an interface pointer derived from IDxcBlob, so it can be cast from IDxcBlobEncoding.

struct __declspec(uuid("8BA5FB08-5195-40e2-AC58-0D989C3A0102"))
IDxcBlob : public IUnknown {
public:
  virtual LPVOID STDMETHODCALLTYPE GetBufferPointer(void) = 0;
  virtual SIZE_T STDMETHODCALLTYPE GetBufferSize(void) = 0;
};

struct __declspec(uuid("7241d424-2646-4191-97c0-98e96e42fc68"))
IDxcBlobEncoding : public IDxcBlob {
public:
  virtual HRESULT STDMETHODCALLTYPE GetEncoding(_Out_ BOOL *pKnown,
                                                _Out_ UINT32 *pCodePage) = 0;
};

// On IDxcLibrary
virtual HRESULT STDMETHODCALLTYPE CreateBlobWithEncodingFromPinned(
  _In_bytecount_(size) LPCVOID pText, UINT32 size, UINT32 codePage,
  _COM_Outptr_ IDxcBlobEncoding **pBlobEncoding) = 0;

NOTE: CreateBlobWithEncodingFromPinned has a codePage argument, but since we’re loading raw binary data (not UTF-8, etc.), we can just specify 0.

The tricky thing is getting an instance of IDxcLibrary, as currently only dxcompiler.dll implements this interface, not dxil.dll.

Microsoft is now aware of this issue, but for now we have to load dxcompiler.dll and GetProcAddress on that for its DxcCreateInstance function, in order to wrap raw memory in an IDxcBlob interface.

DXC_API_IMPORT HRESULT __stdcall DxcCreateInstance(
  _In_ REFCLSID   rclsid,
  _In_ REFIID     riid,
  _Out_ LPVOID*   ppv
  );

Example code without error handling:

HMODULE dxc_module = ::LoadLibrary("dxcompiler.dll");
DxcCreateInstanceProc dxc_create_func = (DxcCreateInstanceProc)::GetProcAddress(dxc_module, "DxcCreateInstance");

ComPtr<IDxcLibrary> library;
dxc_create_func(CLSID_DxcLibrary, __uuidof(IDxcLibrary), (void**)&library);

std::vector<uint8_t> dxil_data;
// ... populate dxil_data with shader byte code

ComPtr<IDxcBlobEncoding> container_blob;

library->CreateBlobWithEncodingFromPinned(
  (BYTE*)dxil_data.data(),
  (UINT32)dxil_data.size(),
  0 /* binary, no code page */,
  container_blob.GetAddressOf());

Performing Validation and Signing

There is currently no command line utility that can perform validation. There is dxv.exe, but this utility only assembles .il files into a container and runs the validation rules, without saving out signed shader byte code. In the future, perhaps this tool can be extended to support saving out validated and signed containers.

The dxil.dll library exposes the IDxcValidator interface for validating and signing DXIL.

struct __declspec(uuid("A6E82BD2-1FD7-4826-9811-2857E797F49A"))
IDxcValidator : public IUnknown {
  // Validate a shader.
  virtual HRESULT STDMETHODCALLTYPE Validate(
    _In_ IDxcBlob *pShader,                       // Shader to validate.
    _In_ UINT32 Flags,                            // Validation flags.
    _COM_Outptr_ IDxcOperationResult **ppResult   // Validation output status, buffer, and errors
  ) = 0;
};

The validator can be loaded from dxil.dll like this (no error handling):

HMODULE dxil_module = ::LoadLibrary("dxil.dll");
DxcCreateInstanceProc dxil_create_func = (DxcCreateInstanceProc)GetProcAddress(dxil_module, "DxcCreateInstance");

ComPtr<IDxcValidator> validator;
dxil_create_func(CLSID_DxcValidator, __uuidof(IDxcValidator), (void**)&validator);

The Validate() method takes our DXIL container, validation flags (can just use 0), and returns an IDxcOperationResult, which contains the result or any errors that occurred during validation.

struct __declspec(uuid("CEDB484A-D4E9-445A-B991-CA21CA157DC2"))
IDxcOperationResult : public IUnknown {
  virtual HRESULT STDMETHODCALLTYPE GetStatus(_Out_ HRESULT *pStatus) = 0;
  virtual HRESULT STDMETHODCALLTYPE GetResult(_COM_Outptr_result_maybenull_ IDxcBlob **pResult) = 0;
  virtual HRESULT STDMETHODCALLTYPE GetErrorBuffer(_COM_Outptr_result_maybenull_ IDxcBlobEncoding **pErrors) = 0;
};

Validation can be invoked like this (no error handlng):

ComPtr<IDxcOperationResult> result;
validator->Validate(container_blob.Get(), DxcValidatorFlags_InPlaceEdit, &result);

A very useful optimization is the DxcValidatorFlags_InPlaceEdit flag, which will avoid an extra container copy owned by dxil.dll. In the case of this example, the dxil_data byte vector wrapped by container_blob will be modified in place, saving a redundant allocation and copy.

To check if validation failed, the operation result has a GetStatus() method which can be queried. If the result is a failure, GetErrorBuffer() can be used to get at the UTF-8 string error data.

HRESULT validateStatus;
result->GetStatus(&validateStatus);
if (FAILED(validateStatus))
{
  std::cout << "The dxil container failed validation" << std::endl;

  ComPtr<IDxcBlobEncoding> printBlob, printBlobUtf8;
  result->GetErrorBuffer(&printBlob);
  library->GetBlobAsUtf8(printBlob.Get(), printBlobUtf8.GetAddressOf());

  std::string errorString;
  if (printBlobUtf8)
  {
    errorString = reinterpret_cast<const char*>(printBlobUtf8->GetBufferPointer());
  }

  std::cout << "Error: " << std::endl << errorString << std::endl;
}

If no error has occured, the contents of the dxil_data byte vector can be written out!

FILE* output_fh = fopen("signed.dxil", "wb");
fwrite(dxil_data.data(), 1, dxil_data.size(), output_fh);
fclose(output_fh);

Before signing:

After signing:

Query Validation Version

Proper pipelines should use full content addressed identities for determining if build results are out of date, such as hashing the dxcompiler.dll and dxil.dll binaries, but it can still be helpful to print out friendly version info to the user.

In order to get the validator version, first create an instance of IDxcValidator, and then QueryInterface for IDxcVersion.

static const UINT32 DxcVersionInfoFlags_None = 0;
static const UINT32 DxcVersionInfoFlags_Debug = 1; // Matches VS_FF_DEBUG
static const UINT32 DxcVersionInfoFlags_Internal = 2; // Internal Validator (non-signing)

struct __declspec(uuid("b04f5b50-2059-4f12-a8ff-a1e0cde1cc7e"))
IDxcVersionInfo : public IUnknown {
  virtual HRESULT STDMETHODCALLTYPE GetVersion(_Out_ UINT32 *pMajor, _Out_ UINT32 *pMinor) = 0;
  virtual HRESULT STDMETHODCALLTYPE GetFlags(_Out_ UINT32 *pFlags) = 0;
};

The GetVersion interface method will return the major and minor components of the validator version.

ComPtr<IDxcVersionInfo> version_info;
validator->QueryInterface(__uuidof(IDxcVersionInfo), (void**)&version_info);
UINT32 major = 0;
UINT32 minor = 0;
version_info->GetVersion(&major, &minor);
std::cout << "Validator version: " << major << "." << minor << std::endl;

DXIL Signing Utility

I have written a command line tool that performs the operations described in this post, located here.

$ PS H:\Repositories\dxil-signing> .\dxil-signing.exe --help
DXIL Signing Utility
Usage: H:\Repositories\dxil-signing\dxil-signing.exe [OPTIONS]

Options:
  -h,--help                   Print this help message and exit
  -i,--input TEXT REQUIRED    Input unsigned dxil file
  -o,--output TEXT REQUIRED   Output signed dxil file

This tool does the following:

  • Loads an unsigned dxil binary from disk
  • Verifies the data isn’t already signed
  • Performs validation and signing
  • On error, prints any validation errors that occur
  • On success, saves the signed dxil binary to disk
$ PS H:\Repositories\dxil-signing> .\dxil-signing.exe -i simple.dxil -o simple_signed.dxil
Loading input file: simple.dxil
Validator version: 1.3
Saving output file: simple_signed.dxil

Validation and Signing on Linux

The ultimate goal is to run the dxil-signing.exe utility on Linux using Wine, completely removing Windows (end to end) as a build machine requirement.

The first attempt at running my utility using Wine failed with:

$ root@bb4b86201fcd:/app# wine dxil-signing.exe --help
wine: Call from 0x7b44c2b7 to unimplemented function ucrtbase.dll.__processing_throw, aborting
wine: Unimplemented function ucrtbase.dll.__processing_throw called at address 0x7b44c2b7 (thread 0010), starting debugger...
Can't attach process 000f: error 5

Running the debug binary was a bit more insightful:

$ root@bb4b86201fcd:/app# wine dxil-signing.exe --help
0010:err:module:import_dll Library MSVCP140D.dll (which is needed by L"Z:\\app\\dxil-signing.exe") not found
0010:err:module:import_dll Library VCRUNTIME140D.dll (which is needed by L"Z:\\app\\dxil-signing.exe") not found
0010:err:module:import_dll Library ucrtbased.dll (which is needed by L"Z:\\app\\dxil-signing.exe") not found
0010:err:module:attach_dlls Importing dlls for L"Z:\\app\\dxil-signing.exe" failed, status c0000135

I changed the solution to build with static CRT, and then I was able to load the executable correctly:

$ root@bb4b86201fcd:/app# wine dxil-signing.exe --help
DXIL Signing Utility
Usage: Z:\app\dxil-signing.exe [OPTIONS]

Options:
  -h,--help                   Print this help message and exit
  -i,--input TEXT REQUIRED    Input unsigned dxil file
  -o,--output TEXT REQUIRED   Output signed dxil file

However, compilation itself still fails (unsurprisingly, as all the DXC stuff is performed on-demand):

$ root@bb4b86201fcd:/app# wine dxil-signing.exe -i simple.dxil -o test.dxil
Loading input file: simple.dxil
wine: Call from 0x7b44d447 to unimplemented function api-ms-win-crt-private-l1-1-0.dll._o__initialize_onexit_table, aborting
wine: Call from 0x7bc5bf2c to unimplemented function api-ms-win-crt-private-l1-1-0.dll._o__seh_filter_dll, aborting
wine: Call from 0x7bc5bf2c to unimplemented function api-ms-win-crt-private-l1-1-0.dll._o__seh_filter_dll, aborting
...
wine: Call from 0x7bc5bf2c to unimplemented function api-ms-win-crt-private-l1-1-0.dll._o__seh_filter_dll, aborting
wine: Call from 0x7bc5bf2c to unimplemented function api-ms-win-crt-private-l1-1-0.dll._o__seh_filter_dll, aborting
wine: Call from 0x7bc5bf2c to unimplemented function api-ms-win-crt-private-l1-1-0.dll._o__seh_filter_dll, aborting
wine: Call from 0x7bc5bf2c to unimplemented function api-ms-win-crt-private-l1-1-0.dll._o__seh_filter_dll, aborting
0010:err:seh:setup_exception stack overflow 1952 bytes in thread 0010 eip 000000007bc96ae9 esp 0000000000140e70 stack 0x140000-0x141000-0x240000

Using time honoured printf debugging, I narrowed the error to the LoadLibrary call for dxil.dll, though it succeeds with dxcompiler.dll. I am following up with Microsoft and WineHQ on this problem.

In a more heavyweight manner, I also tried to run dxc.exe in its entirety using Wine, and I get a similar unimplemented SEH filter stack overflow.

A later post should hopefully provide a resolution to this - either in the form of an official Linux binary, patches to Wine, or a more Wine-friendly dxil.dll binary.

UPDATE: The Wine issue has been solved, and discussed in a follow-up post!.

Special Thanks

  • Tex Riddell (Microsoft)
  • Amar Patel (Microsoft)

© 2024. All rights reserved.