Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Environment Variables

cicc v13.0 checks 22 distinct environment variables across 36 files containing getenv() calls. Six are NVIDIA-specific (two obfuscated), six come from the LLVM infrastructure, six from the EDG frontend, and the remainder from the build system, memory allocator, and shared ptxas/nvptxcompiler infrastructure. Two of the NVIDIA variables have their names encrypted in the .rodata section using an XOR+ROT13 cipher to prevent discovery through string scanning.

String Deobfuscation Engine

The deobfuscation function sub_8F98A0 at 0x8F98A0 decrypts variable names and option strings from .rodata ciphertext. The same engine is also used for hidden CLI option names (see CLI Flags).

Algorithm: sub_8F98A0

// Reconstructed pseudocode — sub_8F98A0 (0x8F98A0)
// Inputs:
//   ciphertext  — pointer to encrypted bytes in .rodata
//   base        — base address used as key seed (a2)
//   length      — number of bytes to decrypt
//
// Output:
//   plaintext string on stack, null-terminated

char* deobfuscate(const uint8_t* ciphertext, uintptr_t base, size_t length) {
    char buf[64];
    for (size_t i = 0; i < length; i++) {
        uint8_t raw = ciphertext[i];

        // Phase 1: XOR with key derived from position
        uint32_t key = -109 * ((i - base + 97) ^ 0xC5);
        char ch = raw ^ (key & 0xFF);

        // Phase 2: ROT13 on alphabetic characters
        if (ch >= 'A' && ch <= 'Z')
            ch = ((ch - 'A' + 13) % 26) + 'A';
        else if (ch >= 'a' && ch <= 'z')
            ch = ((ch - 'a' + 13) % 26) + 'a';

        buf[i] = ch;
    }
    buf[length] = '\0';
    return buf;
}

Key constant: The multiplier -109 (signed, i.e. 0xFFFFFF93) and the XOR mask 0xC5 together form a position-dependent key stream. The ROT13 phase is applied after the XOR, meaning the plaintext must survive two transformations. This is a weak cipher by design -- it only needs to defeat strings(1) scanning, not serious cryptanalysis.

Obfuscated String Table in .rodata

All obfuscated strings live in a contiguous region near 0x3C23A7B--0x3C23AD6. Each entry is referenced by its end byte address (the deobfuscator walks backward):

End addressLengthDecrypted plaintextPurpose
byte_3C23AD614(option prefix)CLI option prefix for -nvvm-version matching
byte_3C23AC311"nvvm-latest"Option suffix; sets v253 = 1 (Path A)
byte_3C23AB46"nvvm70"Option suffix; sets v253 = 0 (Path B)
byte_3C23AAD13(option name)Option name for error message display
byte_3C23A9F15"NV_NVVM_VERSION"Environment variable name for getenv()
byte_3C23A826"nvvm70"Env var value comparison string
byte_3C23A7B11"nvvm-latest"Env var value comparison string

Additional encrypted copies exist at 0x42812C0 and 0x42812F0 for the two env var names (NV_NVVM_VERSION and LIBNVVM_NVVM_VERSION) used by sub_12B9F70.

Obfuscated CLI Flag (ctor_043)

A separate obfuscation instance at ~0x48EE80 in ctor_043 (0x48D7F0) decrypts a 4-byte hidden cl::opt name from data at unk_3F6F7C7. The algorithm variant uses FNV-1a-like constants:

v40 = v37 ^ (-109 * ((offset + 97) ^ 0x811C9DC5));

The 0x811C9DC5 constant is the FNV-1a 32-bit offset basis. The resulting 4-character option is registered with flag bits 0x87 | 0x38 = hidden + really-hidden, making it invisible even to --help-hidden. This is stored at qword_4F857C0.

NVIDIA-Specific Variables

NVVMCCWIZ

PropertyValue
Checked insub_8F9C90 (real main) at 0x8F9C90, specifically 0x8F9D36
Expected value"553282" (magic number = 0x87142)
EffectSets byte_4F6D280 = 1 -- unlocks developer/wizard mode

Mechanism: The value is parsed via strtol(v, 0, 10) and compared against the integer 553282. Any other value is silently ignored.

What wizard mode does: When byte_4F6D280 = 1:

  • -v flag actually enables verbose output (v259 = byte_4F6D280 instead of 0)
  • -keep flag actually preserves intermediate files (v262 = byte_4F6D280)
  • -dryrun flag enables verbose as a side effect (v259 = byte_4F6D280)
  • -lnk and -opt modes set v262 = byte_4F6D280 (keep temps)

Without wizard mode, -v and -keep are no-ops -- the flags are parsed but have no effect because they set their variables to byte_4F6D280 which is 0.

NVVM_IR_VER_CHK

PropertyValue
Checked insub_12BFF60 at 0x12BFF60 (NVVM IR version verifier, instance 1)
sub_2259720 at 0x2259720 (NVVM IR version verifier, instance 2)
Expected value"0" to disable version checking
EffectControls NVVM IR bitcode version metadata validation

Detailed mechanism (from sub_12BFF60, 9KB):

  1. Reads getenv("NVVM_IR_VER_CHK").
  2. If NULL or strtol(env, 0, 10) != 0: version checking is enabled (default).
  3. If set to "0": version checking is disabled (bypass).

When enabled, the function:

  1. Looks up "nvvmir.version" named metadata via sub_1632310(module, &name).
  2. Also checks "llvm.dbg.cu" metadata (debug compile unit presence).
  3. Iterates metadata operands, deduplicating via an open-addressing hash table:
    • Hash function: (value >> 9) ^ (value >> 4) & mask
    • Tombstone: 0xFFFFFFFFFFFFFFF0 (-16)
    • Empty: 0xFFFFFFFFFFFFFFF8 (-8)
  4. For each unique 2-element version tuple (major, minor):
    • Calls sub_12BDA30(modules, major, minor) for IR compatibility check.
    • Special case: major==2, minor==0 always passes (sentinel for libdevice).
  5. For 4-element tuples (major, minor, debug_major, debug_minor):
    • Calls sub_12BD890(modules, debug_major, debug_minor) for debug version check.
    • Special case: debug_major==3, debug_minor<=2 always passes.

The env var is checked multiple times per invocation: before IR version validation, before debug IR version validation, and at each version tuple comparison. Return code 3 indicates incompatible version.

Current expected versions: nvvmir.version = {2, minor<=0x62}, debug version = {3, minor<=2}.

LIBNVVM_DISABLE_CONCURRENT_API

PropertyValue
Checked inctor_104 at 0x4A5810 (global constructor)
Expected valueAny non-NULL value
EffectSets byte_4F92D70 = 1 -- disables thread-safe libnvvm API usage

Safety valve for environments where concurrent libnvvm compilation causes issues. Any non-NULL value triggers single-threaded API behavior. See Concurrent Compilation.

NV_NVVM_VERSION (Obfuscated)

PropertyValue
Checked insub_12B9F70 at 0x12B9F70, sub_12BB580 at 0x12BB580, sub_8F9C90 at 0x8F9C90
Encrypted at0x3C23A90 and 0x42812C0 (two copies, same ciphertext)
DecryptionXOR with (-109 * ((byte_offset - base + 97) ^ 0xC5)) then ROT13
Expected values"nvvm70" (suppresses check), "nvvm-latest" (forces latest mode)

How it controls compilation path selection in sub_8F9C90:

The dispatch variable v253 starts at 2 (default). When v253 is still 2 at post-parse time (lines 1590--1692):

  1. sub_8F98A0 decrypts the env var name from byte_3C23A9F[-15..0].
  2. Calls getenv(decrypted_name).
  3. Compares the result against two decrypted reference strings:
    • "nvvm70" (from byte_3C23A82): sets v253 = 0 (Path B -- NVVM/bitcode pipeline via sub_1262860 or sub_1265970)
    • "nvvm-latest" (from byte_3C23A7B): sets v253 = 1 (Path A -- PTX pipeline via sub_902D10 or sub_905EE0)
  4. If neither matches: uses (arch > 99) as the tiebreaker, with further modulation by -nvc and -optixir flags.

For multi-stage modes (v263 >= 3), the resolved path also determines which pipeline flag string is appended:

  • Path A (v253 == 1): xmmword_3C23BC0 + "vm-latest" (25 bytes total, decodes to "-nvvm-version=nvvm-latest")
  • Path B (v253 == 0): xmmword_3C23BC0 + "vm70" (20 bytes total, decodes to "-nvvm-version=nvvm70")

The variable name is encrypted in the binary's .rodata section because NVIDIA intended to keep this escape hatch undiscoverable through casual strings(1) scanning. It controls a fundamental compilation mode choice.

LIBNVVM_NVVM_VERSION (Obfuscated)

PropertyValue
Checked insub_12B9F70 at 0x12B9F70
Encrypted at0x42812F0
Expected valuesSame as NV_NVVM_VERSION

Functionally identical to NV_NVVM_VERSION. Both names are checked by the same function sub_12B9F70; this provides an alternative name for the same feature. Likely exists so that libnvvm API users can set LIBNVVM_NVVM_VERSION while standalone cicc users set NV_NVVM_VERSION.

LLVM_OVERRIDE_PRODUCER

PropertyValue
Checked inctor_036 at 0x48CC90, ctor_154 at 0x4CE640
Expected valueAny string
EffectOverrides the producer identification string in output bitcode metadata

Dual constructor behavior:

  • ctor_036 (0x48CC90): Reads LLVM_OVERRIDE_PRODUCER, falls back to "20.0.0" (the true LLVM version). Stored in qword_4F837E0.
  • ctor_154 (0x4CE640): Reads LLVM_OVERRIDE_PRODUCER, falls back to "7.0.1" (the NVVM IR compatibility marker). Stored separately.

The bitcode writer (sub_1538EC0, 58KB) uses the ctor_154 value, producing "LLVM7.0.1" in the IDENTIFICATION_BLOCK. This means the output bitcode claims to be LLVM 7.0.1 format, even though cicc is built on LLVM 20.0.0 internally. Setting LLVM_OVERRIDE_PRODUCER overrides both constructors' values.

CAN_FINALIZE_DEBUG

PropertyValue
Checked insub_60F290 at 0x60F290, sub_4709E0 at 0x4709E0, sub_470DA0 at 0x470DA0
Expected valueControls debug finalization behavior
EffectGates debug information finalization passes

Shared with ptxas and nvptxcompiler (same codebase origin). Controls whether debug information finalization passes execute. When unset, the default behavior applies. Three call sites confirmed.

LLVM Infrastructure Variables

AS_SECURE_LOG_FILE

Checked in ctor_720 at 0x5C0D60. Sets the secure log file path for the integrated assembler, registered as LLVM cl::opt "as-secure-log-file-name". Expected: a file path.

TMPDIR / TMP / TEMP / TEMPDIR

Checked in sub_16C5C30, sub_C843A0, and sub_721330. These are probed in priority order: TMPDIR first, then TMP, TEMP, TEMPDIR. The EDG frontend (sub_721330) only checks TMPDIR and falls back to "/tmp".

PATH

Checked in sub_16C5290, sub_16C7620, sub_C86E60. Standard PATH for findProgramByName lookups.

HOME

Checked in sub_C83840. Used by sys::path::home_directory with getpwuid_r() as fallback.

PWD

Checked in sub_16C56A0, sub_C82800. Used for fast current-directory resolution (faster than getcwd).

TERM

Checked in sub_7216D0 (EDG) and sub_16C6A40/sub_C86300 (LLVM). If TERM=="dumb", terminal colors are disabled. Otherwise, specific terminal type strings (ansi, xterm, screen, linux, cygwin, etc.) are matched by integer comparison to determine color capability.

EDG Frontend Variables

NOCOLOR

Checked in sub_67C750. Respects the no-color.org convention: if set to any value, all diagnostic coloring is disabled.

EDG_COLORS

Checked in sub_67C750. Custom color specification string for EDG diagnostics. Example: "error=01;31:warning=01;35:note=01;36:locus=01:quote=01".

GCC_COLORS

Checked in sub_67C750. Fallback if EDG_COLORS is not set. Default: "error=01;31:warning=01;35:note=01;36:locus=01:quote=01:range1=32". Provides GCC-compatible diagnostic coloring.

USR_INCLUDE

Checked in sub_720A60. Overrides the system include path (default: "/usr/include") for the EDG frontend.

EDG_BASE

Checked in sub_7239A0. Sets the EDG base directory for predefined configuration files. Stored in qword_4F07578.

EDG_MODULES_PATH

Checked in sub_723900. Adds an additional search path for C++ modules in the EDG frontend.

Build System / Parallelism

MAKEFLAGS

PropertyValue
Checked insub_1682BF0 at 0x1682BF0
EffectGNU Make jobserver integration for parallel compilation limiting

Parses for --jobserver-auth= with either:

  • fifo: prefix: FIFO-based jobserver (modern GNU Make)
  • N,M format: pipe file descriptor pair (classic GNU Make)

When detected, cicc integrates with the jobserver to limit concurrent compilation passes.

Memory Allocator

MALLOC_CONF

Checked in sub_12FCDB0 (jemalloc initialization, 131,600 bytes -- the largest function in its range). One of five configuration sources for the bundled jemalloc allocator. Expected: jemalloc config string such as "narenas:2,dirty_decay_ms:0".

Dynamic / Generic Access

Two mechanisms allow runtime access to arbitrary environment variables:

  1. --trace-env=VARNAME CLI flag (in sub_125FB30 and sub_900130): reads the named variable and injects its value into the compilation trace. This is a pass-through mechanism for build system integration.
  2. sub_C86120 (LLVM sys::Process::GetEnv wrapper): generic getenv helper called with dynamic name parameters by LLVM's option processing infrastructure.

Complete Inventory

#Env Var NameOriginObfuscatedCategoryKey Function
1NVVMCCWIZNVIDIAnoDeveloper modesub_8F9C90
2NVVM_IR_VER_CHKNVIDIAnoVersion check gatesub_12BFF60, sub_2259720
3LIBNVVM_DISABLE_CONCURRENT_APINVIDIAnoThread safetyctor_104
4NV_NVVM_VERSIONNVIDIAyesVersion compat / path selectsub_12B9F70, sub_12BB580
5LIBNVVM_NVVM_VERSIONNVIDIAyesVersion compat (alias)sub_12B9F70
6LLVM_OVERRIDE_PRODUCERLLVM/NVIDIAnoBitcode metadatactor_036, ctor_154
7CAN_FINALIZE_DEBUGNVIDIAnoDebug finalizationsub_60F290, sub_4709E0, sub_470DA0
8AS_SECURE_LOG_FILELLVMnoAssembler loggingctor_720
9TMPDIRLLVM/EDGnoTemp directorysub_16C5C30, sub_C843A0, sub_721330
10TMPLLVMnoTemp directory (fallback)sub_16C5C30, sub_C843A0
11TEMPLLVMnoTemp directory (fallback)sub_16C5C30, sub_C843A0
12TEMPDIRLLVMnoTemp directory (fallback)sub_16C5C30, sub_C843A0
13PATHLLVMnoExecutable lookupsub_16C5290, sub_16C7620, sub_C86E60
14HOMELLVMnoHome directorysub_C83840
15PWDLLVMnoWorking directorysub_16C56A0, sub_C82800
16TERMLLVM/EDGnoTerminal typesub_7216D0, sub_16C6A40, sub_C86300
17NOCOLOREDGnoColor disablesub_67C750
18EDG_COLORSEDGnoColor schemesub_67C750
19GCC_COLORSEDGnoColor scheme (fallback)sub_67C750
20USR_INCLUDEEDGnoInclude pathsub_720A60
21EDG_BASEEDGnoEDG base dirsub_7239A0
22EDG_MODULES_PATHEDGnoModule search pathsub_723900
23MAKEFLAGSBuildnoJobserversub_1682BF0
24MALLOC_CONFjemallocnoAllocator configsub_12FCDB0

Decompiler Artifacts

Several getenv("bar") calls appear in ctor_106, ctor_107, ctor_376, ctor_614. These are not real environment variable checks. The pattern getenv("bar") == (char*)-1 is jemalloc's initialization probe testing whether getenv is intercepted by a sanitizer. The string "bar" is a dummy.

"getenv" as a string in ctor_133 (qword_4F9B700[502]) is a function name in a libc symbol table used by the EDG frontend for tracking known standard library functions.

"fegetenv" in sub_E42970 is a math library function name in a builtin table.

Function Map

FunctionAddressSizeRole
StringDeobfuscatesub_8F98A0~400BXOR + ROT13 decryption engine
RealMainsub_8F9C9010,066BMain entry; checks NVVMCCWIZ, dispatches via NV_NVVM_VERSION
NvvmVersionHelpersub_12B9F70~3KBReads NV_NVVM_VERSION / LIBNVVM_NVVM_VERSION; compares values
NvvmVersionHelper2sub_12BB580~3KBSecond call site for NV_NVVM_VERSION
CheckIRVersionsub_12BDA30~1KBIR major/minor compatibility check
CheckDebugVersionsub_12BD890~1KBDebug IR major/minor compatibility check
NVVMIRVersionChecksub_12BFF609KBFull NVVM IR version validator; reads NVVM_IR_VER_CHK
NVVMIRVersionCheck2sub_225972014KBSecond instance of version checker
JemallocInitsub_12FCDB0131,600Bjemalloc config parser; reads MALLOC_CONF
JobserverParsersub_1682BF0~2KBMAKEFLAGS --jobserver-auth parser
GenericGetEnvsub_C86120~100BLLVM sys::Process::GetEnv wrapper
EDGColorInitsub_67C750~2KBNOCOLOR / EDG_COLORS / GCC_COLORS handler

Cross-References