TLDR
Obfuscate dialect, operation, attribute, type, pass names to prevent reverse-engineering of release binaries. Guarded by a CMake flag.
static constexpr StringLiteral private_dialect::MulOp::getOperationName() {
// With MLIR_PRIVATE_NAME_OBFUSCATOR=""
return StringLiteral("privd.mul");
// With MLIR_PRIVATE_NAME_OBFUSCATOR="md5sum"
return StringLiteral("_1e8d1fee65b16e634839dea9f85d3429._353942263d1bedfbe06b7bfa78226253");
}
Motivation
We are working on an MLIR-based compiler (CUDA Tile IR) that features a public-facing dialect alongside several private, internal dialects. For IP protection and security reasons, we want to avoid exposing our internal compiler design in public binaries. MLIR retains op names, attribute names, type names and pass names as strings. These may give insight about compiler internals that we are not ready to share publicly yet.
Proposal
Distinguish between public and private dialects / passes with an ODS flag a CMake option.
Public dialects are part of the public API surface. They may be parsed / printed, have CAPI support / Python bindings, have bytecode support, have user-readable verifier error messages, etc.
Private dialects have no public API surface. Should users find a way to, e.g., dump a private op (e.g., by triggering an internal error), all they see is encrypted op names.
- Add a new
fieldCMake optionbit isPrivate = 0;to the TableGen classes for dialects and passesMLIR_PRIVATE_DIALECTSthat specifies all private dialects as a comma-separated list. - Add a new CMake option
MLIR_PRIVATE_NAME_OBFUSCATOR(default: empty string) that specifies an external command for obfuscating names. Test cases can usemd5sum. MLIR_PRIVATE_NAME_OBFUSCATORenables obfuscation for all private dialectsand passes.- If obfuscation is enabled, the TableGen code generator obfuscates dialect names, op/attribute/type mnemonics
and pass names/argumentswithMLIR_PRIVATE_NAME_OBFUSCATOR. - If obfuscation is enabled, the TableGen code generator does not generate parse/print methods for ops in private dialects. They will print with the generic op format and with obfuscated names. (However, it is expected that such ops would never be printed in the first place.)
- Add a new CMake option
MLIR_PRIVATE_PASSESthat obfuscates names and arguments of all passes. Descriptions are stripped entirely (empty string).
The build process can now generate two binaries: an external release binary (to be shipped to customers) and an internal binary with non-obfuscated names. The internal binary can parse both external/internal dialects and provides better error messages.
Details and Consequences
- The proposed mechanism is entirely ODS based. No changes needed to core MLIR data structures.
- The obfuscator (
MLIR_PRIVATE_NAME_OBFUSCATOR) must be deterministic and produce a unique name. The same obfuscator must be used across all translation units. - Name obfuscation is at compile time and one-way. I don’t have a use case where an obfuscated op name would have to be “decrypted” within release binary code.
- Code that refers to dialects / passes with their name or ops / types / attributes with their mnemonics in the form of a string (as opposed to the C++ class name) will no longer work. E.g., you can no longer write
if (op->getDialect()->getDialectNamespace() == "arith"). - Obfuscated ops cannot be matched with
transform.match.operation_name. - Attribute / property names of operations (not attribute mnemonics!) are excluded from this RFC because
op->getAttr("name", value)is a somewhat common pattern. - Obfuscated dialects are not supported by the MLIR bytecode infrastructure. (Bytecode round-trips successfully within builds that share a obfuscator, but bytecode produced with one obfuscator cannot be read by a build with a different obfuscator or no obfuscator.)
- Verifier error messages use obfuscated op / dialect names.
- Lit tests for private dialects must be run with binaries that were built without
MLIR_PRIVATE_NAME_OBFUSCATOR.
Alternatives
Operation names could potentially be stripped entirely, i.e., replaced with empty strings. That would require changes to OperationName and/or MLIRContext. For example in MLIRContext:
/// This is a mapping from operation name to the operation info describing it.
llvm::StringMap<std::unique_ptr<OperationName::Impl>> operations;
An obfuscated op name, as opposed to an empty op name, has an advantage: customers can send meaningful “encrypted” error messages back to the developer. Obfuscated names still appear in diagnostics, so a customer can paste their error message back to the developer’s internal tools, which can de-obfuscate it with a reverse lookup table built at release time.
Prototype
Proof of concept only.