unsafe in Rust isn’t a sin; it’s a boundary. It marks the place where the compiler stops checking and a human takes responsibility. The useful question isn’t “did you use it” but “can someone tell, at a glance, where it is and whether they signed up for it.” gm-crypto-rs answers that with crate boundaries.

The core crate, gmcrypto-core, carries #![forbid(unsafe_code)] — not deny, which a stray allow can loosen, but forbid, which holds for the whole crate and can’t be locally overridden. Every line of the SM2/SM3/SM4 core is checked by the compiler; forbid leaves no room for an exception. That’s the property I want true by default and checkable without reading the code: install the crate with its defaults, and none of this project’s own code that you run carries unsafe.

But the fast paths want it. The bitsliced SM4 S-box reaches for SIMD intrinsics; so does the carryless-multiply GHASH behind SM4’s AEAD modes. Those intrinsics are unsafe — there’s no safe way to spell them today. The performance is worth having; the question is where it’s allowed to live.

The obvious move is to gate them behind feature flags in the same crate and call it done. I didn’t, because a feature that introduces unsafe into a forbid(unsafe_code) crate is a contradiction you can only resolve by weakening the forbid. So all of that SIMD code lives in a separate crate, gmcrypto-simd, reached only through opt-in features — the bitsliced S-box behind sm4-bitsliced-simd, the GHASH path behind sm4-aead. The core keeps its forbid intact; the unsafe has a name, a boundary, and an opt-in.

The cost is real, and it’s the right one to pay: those paths aren’t the default. A stock build runs the slower linear-scan S-box. You reach the SIMD versions by pulling in a crate and turning on a feature — a deliberate, visible choice — and in exchange you can audit the project’s safety story by crate name instead of grepping for unsafe. Someone who never opts in never compiles a line of unsafe from this project.

The pattern outlives the crypto. When a performance exception needs unsafe, don’t dilute the safety of the thing everyone uses in order to get it. Put the exception behind a boundary — a separate crate, a named feature, a stated cost — so the safe path stays the default and the unsafe path is a decision someone made on purpose. The crate split, the features, and the forbid are all in the public source if you’d rather read the boundary than take my word for it.