Double xor optimisation

I'm new to programming with zig and I've tried to write a small xor cipher with a static key on linux. Here's my code :

const std = @import("std");
const write = std.os.linux.write;
const key = "abc";
const secret = xor("hello\n"); // removed if optimised


fn xor(comptime msg: []const u8) [msg.len]u8 {
	var xored_msg: [msg.len]u8 = undefined;
	for (&xored_msg, 0..) |*c, i| {
		c.* = msg[i] ^ key[i%key.len];
	}
	return xored_msg;
}


pub fn main() void {
	const dec_msg = xor(&secret);
	_ = write(1, &dec_msg, dec_msg.len);
}

I compile with optimisations (zig build-exe xor_decryption.zig -O ReleaseFast). To chech the code, I disassembled the code using gdb but I saw this :

0x0000000001001a84 <+4>:     movl   $0x6c6c6568,-0x8(%rbp)<br>
0x0000000001001a8b <+11>:    movw   $0xa6f,-0x4(%rbp)

This means that zig has detected an useless xor but I want to keep this xor.
I know that in C, you can use __attribute__((optimize("O0"))) to specify a non-optimised function or the #pragma GCC optimize macro. I also know that it's possible to use volatile to prevent a variable from being optimised.
I think this is possible with 2 xor functions but I don't want any repetition. I also think that it might be possible to write the xor function directly in inline assembly.
I hope that was clear and I'd be interested to hear a solution.

First a mandatory mention that " Security through obscurity" is not a good security practice. I would strongly suggest against storing any secret keys in user-facing code.

As for your particular problem, per-function optimization levels don’t exist yet (replace @setRuntimeSafety with @optimizeFor · Issue #978 · ziglang/zig · GitHub).
For now it appears that a mixture of noinline and std.mem.doNotOptimizeAway works, but I’m not sure how stable it is:

noinline fn xor(comptime msg: []const u8) [msg.len]u8 {
    std.mem.doNotOptimizeAway(msg);
1 Like

I think your issue is just that your message is comptime-known, so the xored message is also comptime-known.

If you read the message from a file, the compiler cannot optimize it.

It won’t optimize it away if your secret text is longer. That could change with an updated LLVM, so I wouldn’t rely on it. But then again, a user running a debugger isn’t a huge step up from them grepping a binary, either…

Binary obfuscation can be effective as part of a defense-in-depth strategy. It doesn’t stop them, but it does slow them down. And occasionally it’s unavoidable to be forced to have secrets in a binary (but yes, best to avoid it). Tigress lists a lot of interesting techniques one can use to obfuscate code.

Creating your own stack-based VM interpreter to run obfuscated bytecode is a fun one (although lots of effort).

Release build will optimize away the code that has no effect. Part of the optimization also aggressively precomputes expressions during compilation. Do a print on the result or use doNotOptimizeAway() on the result to force side effect. You might need to use input from the command line arguments or a file to avoid constant data being optimized away.

Edit: also I think if you mark your function as extern, the compiler thinks it’s library export API function and won’t optimize it away.