Skip to content

CWE-114: Process Control - C

Overview

In C, CWE-114 findings come from letting input decide which shared library gets loaded or which process gets executed. On Unix/Linux the calls involved are dlopen(), dlsym(), and the exec*() family; on Windows, LoadLibrary() and CreateProcess(). The attacks that follow are DLL hijacking, LD_PRELOAD, and path manipulation.

Primary Defence: Load libraries by absolute path with dlopen() and LoadLibrary(), check the name against an allowlist before it reaches the path, pin and verify library hashes where the library directory cannot be locked down, and replace shell commands with execve() given an explicit argument array and a minimal environment.

Common Vulnerable Patterns

Using dlopen() with relative path (Unix/Linux)

#include <dlfcn.h>
#include <stdio.h>

// VULNERABLE - plugin_name reaches the path; a slash in it becomes a path, not a name
void load_plugin(const char *plugin_name) {
    char library_path[256];

    // User controls plugin_name - no allowlist, no base directory
    snprintf(library_path, sizeof(library_path), "lib%s.so", plugin_name);

    // A name with a slash is loaded as a path. A name without one is searched
    // for along LD_LIBRARY_PATH and the default directories.
    void *handle = dlopen(library_path, RTLD_LAZY);

    if (!handle) {
        fprintf(stderr, "dlopen failed: %s\n", dlerror());
        return;
    }

    // Execute code from potentially malicious library
}

// Attack: plugin_name = "/../../../tmp/evil" -> dlopen("lib/../../../tmp/evil.so")
// From a working directory of /opt/app that contains the usual lib/, that is
// /opt/app/lib/../../../tmp/evil.so, which resolves to /tmp/evil.so

Why this is vulnerable: Two separate problems sit in these four lines, and they have different fixes and different reachability.

The first is plugin_name reaching the path at all. dlopen() treats any name containing a slash as a pathname and skips the search entirely - but the path is still resolved the ordinary way, relative to the working directory, with every component having to exist. The lib prefix therefore anchors the traversal rather than preventing it, and the payload has to account for it. plugin_name = "../../../tmp/evil" gives lib../../../tmp/evil.so, whose first component is a directory literally named lib..; even where that exists the result is ../tmp/evil.so relative to the working directory, not /tmp/evil.so. The payload that works is plugin_name = "/../../../tmp/evil", giving lib/../../../tmp/evil.so: the first component is now the ordinary lib/ directory, and from a working directory of /opt/app it resolves to /tmp/evil.so. No environment manipulation is needed, and an allowlist plus a fixed base directory is what fixes it.

The general point survives the arithmetic: work out what the template actually produces before writing the payload down. A concatenation with a prefix, a suffix or a base directory constrains the reachable set, and a proof-of-concept the template cannot actually produce sends whoever picks the finding up hunting for something that will not fire.

The second is the search order for a name with no slash: DT_RPATH (consulted only when the object has no DT_RUNPATH), then LD_LIBRARY_PATH, then DT_RUNPATH, then ld.so.cache, then the default directories. The two tags are not interchangeable - RPATH outranks LD_LIBRARY_PATH and RUNPATH is beaten by it, which decides whether an environment variable can redirect a load the binary thought it had pinned. Reaching that requires already controlling the process environment or a directory it searches, which usually means local access or a startup script assembled from editable configuration. The dynamic loader ignores LD_LIBRARY_PATH and LD_PRELOAD for setuid binaries precisely because of this. Worth closing with an absolute path, but do not report it as remotely reachable when only the first problem is.

LoadLibrary() without absolute path (Windows)

#include <windows.h>

// VULNERABLE - Searches a list of directories, several of them often writable
HMODULE load_library_unsafe(const char *dll_name) {
    // Standard search order for an unpackaged app with SafeDllSearchMode on,
    // which is the default and has been since Windows XP SP2:
    // 1. Application directory  ← first, and writable in a per-user install
    // 2. System32
    // 3. 16-bit system directory
    // 4. Windows directory
    // 5. Current directory      ← still searched, but not second
    // 6. PATH directories       ← loose ACLs on an entry are common

    // Any of 1, 5 or 6 that the attacker can write to gives code execution
    HMODULE hModule = LoadLibrary(dll_name);

    if (hModule == NULL) {
        fprintf(stderr, "LoadLibrary failed: %lu\n", GetLastError());
    }

    return hModule;
}

// Attack: place evil.dll in whichever searched directory is writable - most
// often the application directory itself, or a PATH entry with loose ACLs

Why this is vulnerable: LoadLibrary with a bare name searches a list of directories, and code execution follows from any entry on that list the attacker can write to. The often-repeated "the current directory is searched second" describes SafeDllSearchMode being disabled; it has been enabled by default since Windows XP SP2, which puts the current directory below the system and Windows directories. Getting that order right changes which finding is real. The current directory is now a weak position, while the application directory is searched first and is writable by an ordinary user in any per-user install, and a PATH entry with loose ACLs is the other common one. The fix does not change - stop searching - but the triage question is which searched directory the attacker can write to, not where the current directory sits in the list.

system() with user-controlled input

#include <stdlib.h>
#include <stdio.h>

// VULNERABLE - Command injection
void convert_image(const char *input_file) {
    char command[512];

    // User controls input_file - can inject shell commands
    snprintf(command, sizeof(command), "convert %s output.png", input_file);

    // Executes via /bin/sh - subject to shell interpretation
    system(command);
}

// Attack: input_file = "input.jpg; rm -rf /"
// Executes: convert input.jpg; rm -rf / output.png

Why this is vulnerable: system() hands its string to /bin/sh -c, so a shell metacharacter in input_file - ;, |, & - ends the intended convert call and starts a command of the attacker's choosing.

execvp() with unsanitized PATH

#include <unistd.h>
#include <stdio.h>

// VULNERABLE - Uses PATH search, can execute wrong binary
void run_converter(const char *input_file) {
    char *args[] = {"convert", (char *)input_file, "output.png", NULL};

    // execvp() searches PATH for "convert"
    // Attacker can manipulate PATH to execute malicious binary
    execvp("convert", args);

    perror("execvp failed");
}

// Attack: Set PATH=/tmp/evil:$PATH with malicious "convert" binary

Why this is vulnerable: execvp() searches the PATH environment variable for the executable, so whoever controls PATH for this process decides which convert binary runs.

Secure Patterns

dlopen() with absolute path and validation (Unix/Linux)

#include <dlfcn.h>
#include <stdio.h>
#include <string.h>
#include <limits.h>
#include <stdlib.h>
#include <sys/stat.h>

#define LIBRARY_DIR "/opt/app/lib"

static const char *allowed_libraries[] = {
    "libcrypto.so",
    "libssl.so",
    "libcustom.so"
};

// Derive the count from the array. A separate #define is one edit away from
// walking off the end of the allowlist and reading whatever follows it.
#define ALLOWED_LIBRARY_COUNT \
    (sizeof(allowed_libraries) / sizeof(allowed_libraries[0]))

void *load_library_secure(const char *library_name) {
    // Validate library is in allowlist
    int found = 0;
    for (size_t i = 0; i < ALLOWED_LIBRARY_COUNT; i++) {
        if (strcmp(library_name, allowed_libraries[i]) == 0) {
            found = 1;
            break;
        }
    }

    if (!found) {
        fprintf(stderr, "Library not in allowlist: %s\n", library_name);
        return NULL;
    }

    // Construct absolute path
    char absolute_path[PATH_MAX];
    int written = snprintf(absolute_path, sizeof(absolute_path),
                           "%s/%s", LIBRARY_DIR, library_name);
    if (written < 0 || (size_t)written >= sizeof(absolute_path)) {
        fprintf(stderr, "Path too long\n");
        return NULL;
    }

    // Resolve to canonical path (eliminates .., symlinks)
    char canonical_path[PATH_MAX];
    if (realpath(absolute_path, canonical_path) == NULL) {
        perror("realpath failed");
        return NULL;
    }

    // Verify path hasn't escaped library directory.
    // The trailing "/" is load-bearing: comparing against "/opt/app/lib" alone also
    // accepts "/opt/app/lib-backup/evil.so", which is not inside the directory at all.
    if (strncmp(canonical_path, LIBRARY_DIR "/", sizeof(LIBRARY_DIR)) != 0) {
        fprintf(stderr, "Path traversal attempt detected\n");
        return NULL;
    }

    // Verify file exists and is regular file
    struct stat st;
    if (stat(canonical_path, &st) != 0) {
        perror("stat failed");
        return NULL;
    }

    if (!S_ISREG(st.st_mode)) {
        fprintf(stderr, "Not a regular file\n");
        return NULL;
    }

    // Load with absolute path - bypasses LD_LIBRARY_PATH
    void *handle = dlopen(canonical_path, RTLD_NOW | RTLD_LOCAL);

    if (!handle) {
        fprintf(stderr, "dlopen failed: %s\n", dlerror());
        return NULL;
    }

    return handle;
}

Why this works:

  • Absolute paths bypass LD_LIBRARY_PATH and current-directory search.
  • Allowlists ensure only approved libraries are loadable, and the count comes from the array so the two cannot drift apart.
  • realpath() collapses .. and symlinks to prevent traversal.
  • The containment check compares against LIBRARY_DIR "/", so a sibling directory whose name merely starts with the same characters is rejected.
  • File type validation blocks non-regular files.
  • The order matters: allowlist first, then build the path, then canonicalise, then check containment, then load. realpath() before the allowlist would resolve an attacker-supplied name; the containment check before realpath() would compare a path that still contains ...

LoadLibraryEx() with LOAD_LIBRARY_SEARCH_SYSTEM32 (Windows)

#include <windows.h>
#include <strsafe.h>   // StringCchPrintf
#include <tchar.h>     // _tcsrchr, TEXT
#include <stdio.h>

// Load a known system DLL by bare name. Safe because the flag restricts the
// search to System32 - the name never reaches a directory a user can write.
HMODULE load_system_library_secure(const TCHAR *dll_name) {
    HMODULE hModule = LoadLibraryEx(
        dll_name,
        NULL,
        LOAD_LIBRARY_SEARCH_SYSTEM32
    );

    if (hModule == NULL) {
        fprintf(stderr, "LoadLibraryEx failed: %lu\n", GetLastError());
        return NULL;
    }

    return hModule;
}

// Load one of our own DLLs from the application directory.
static const TCHAR *allowed_libraries[] = {
    TEXT("cryptolib.dll"),
    TEXT("imagelib.dll"),
    TEXT("datalib.dll")
};

#define ALLOWED_LIBRARY_COUNT \
    (sizeof(allowed_libraries) / sizeof(allowed_libraries[0]))

HMODULE load_app_library_secure(const TCHAR *dll_name) {
    // The allowlist is not optional. LOAD_LIBRARY_SEARCH_APPLICATION_DIR constrains
    // where a *bare name* is searched for; it does nothing about a name that has been
    // concatenated into a path, and "..\..\Users\Public\evil.dll" is such a name.
    int allowed = 0;
    for (size_t i = 0; i < ALLOWED_LIBRARY_COUNT; i++) {
        if (_tcscmp(dll_name, allowed_libraries[i]) == 0) {
            allowed = 1;
            break;
        }
    }

    if (!allowed) {
        fprintf(stderr, "Library not in allowlist\n");
        return NULL;
    }

    TCHAR app_path[MAX_PATH];
    TCHAR full_path[MAX_PATH];

    // Get the application's own image path
    DWORD len = GetModuleFileName(NULL, app_path, MAX_PATH);
    if (len == 0 || len == MAX_PATH) {   // MAX_PATH means truncated
        fprintf(stderr, "GetModuleFileName failed\n");
        return NULL;
    }

    // Remove executable name, keep directory
    TCHAR *last_slash = _tcsrchr(app_path, TEXT('\\'));
    if (last_slash == NULL) {
        return NULL;
    }
    *last_slash = TEXT('\0');

    if (FAILED(StringCchPrintf(full_path, MAX_PATH,
                               TEXT("%s\\%s"), app_path, dll_name))) {
        return NULL;
    }

    // Absolute path; the flag governs how this DLL's own dependencies are resolved
    return LoadLibraryEx(
        full_path,
        NULL,
        LOAD_LIBRARY_SEARCH_APPLICATION_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32
    );
}

Why this works:

  • LOAD_LIBRARY_SEARCH_SYSTEM32 removes the current directory, the application directory and PATH from the search, which is what closes the classic DLL-hijack.
  • The application-directory variant anchors the path to GetModuleFileName(NULL, ...), so it does not depend on the current directory or on PATH.
  • The allowlist is what stops dll_name being a path. The search flags constrain searching, not concatenation - set the flags, skip the allowlist, and ..\..\Users\Public\evil.dll still loads.
  • Dependency resolution for the loaded DLL uses the same restricted flags, so a hijack one level down is closed too.
  • GetModuleFileName returning MAX_PATH means the path was truncated, not that it succeeded; treating that as success would build a path from a cut-off directory.

execve() with absolute path and cleared environment

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <sys/wait.h>

#define INPUT_DIR  "/opt/app/uploads"
#define OUTPUT_DIR "/opt/app/outputs"

// Containment test. Comparing only strlen(dir) bytes would also accept
// "/opt/app/uploads-public/evil.jpg"; the separator is what makes it a
// directory test rather than a string-prefix test.
static int is_inside(const char *dir, size_t dir_size, const char *path) {
    return strncmp(path, dir, dir_size - 1) == 0 && path[dir_size - 1] == '/';
}

int execute_command_secure(const char *input_name, const char *output_name) {
    // Both names are filenames, not paths. Reject anything that could escape
    // before it is ever joined to a directory.
    if (strchr(input_name, '/') || strchr(output_name, '/') ||
        input_name[0] == '\0' || output_name[0] == '\0') {
        fprintf(stderr, "Names must be plain filenames\n");
        return -1;
    }

    char input_path[PATH_MAX];
    char output_path[PATH_MAX];
    int n = snprintf(input_path, sizeof(input_path), INPUT_DIR "/%s", input_name);
    if (n < 0 || (size_t)n >= sizeof(input_path)) return -1;
    n = snprintf(output_path, sizeof(output_path), OUTPUT_DIR "/%s", output_name);
    if (n < 0 || (size_t)n >= sizeof(output_path)) return -1;

    // The input must exist, so it can be canonicalised and checked directly.
    char input_canonical[PATH_MAX];
    if (realpath(input_path, input_canonical) == NULL) {
        perror("realpath failed for input");
        return -1;
    }

    if (!is_inside(INPUT_DIR, sizeof(INPUT_DIR), input_canonical)) {
        fprintf(stderr, "Input file outside allowed directory\n");
        return -1;
    }

    // The output does NOT exist yet - realpath() returns NULL with ENOENT for it,
    // so canonicalising the destination is not an option. Canonicalise the directory
    // that will hold it instead, and rely on the rejected '/' above for the rest.
    char output_dir_canonical[PATH_MAX];
    if (realpath(OUTPUT_DIR, output_dir_canonical) == NULL) {
        perror("realpath failed for output directory");
        return -1;
    }

    if (strcmp(output_dir_canonical, OUTPUT_DIR) != 0) {
        fprintf(stderr, "Output directory is not where it should be\n");
        return -1;
    }

    // Use absolute path to binary - bypasses PATH
    const char *binary_path = "/usr/bin/convert";

    // Arguments as array (no shell interpretation). "--" stops convert reading a
    // filename as an option: without it, an input named "-write" is a directive.
    char *args[] = {
        "convert",
        "--",
        input_canonical,
        output_path,
        NULL
    };

    // Create minimal safe environment (no LD_PRELOAD, no LD_LIBRARY_PATH)
    char *env[] = {
        "PATH=/usr/bin:/bin",
        "HOME=/tmp",
        "LANG=C",
        NULL
    };

    pid_t pid = fork();

    if (pid == -1) {
        perror("fork failed");
        return -1;
    }

    if (pid == 0) {
        // Child process
        execve(binary_path, args, env);

        // execve only returns on error
        perror("execve failed");
        _exit(1);
    }

    // Parent process - wait for child
    int status;
    if (waitpid(pid, &status, 0) == -1) {
        perror("waitpid failed");
        return -1;
    }

    if (WIFEXITED(status)) {
        return WEXITSTATUS(status);
    }

    return -1;
}

Why this works:

  • Absolute binary paths bypass PATH search hijacking.
  • A minimal explicit environment removes LD_PRELOAD and LD_LIBRARY_PATH while still giving the child the PATH and HOME most programs need. An entirely empty envp is not safer, only more likely to fail.
  • Both names are rejected if they contain / before they are joined to a directory, so neither can be a path at all.
  • The input and the output are validated differently, and that is the point. realpath() returns NULL with ENOENT for a file that does not exist yet, so applying it to an output destination either fails every legitimate call or, if the result is not checked, skips the containment test entirely. The input is canonicalised and checked; the output's directory is.
  • is_inside compares up to and including the separator, so /opt/app/uploads-public does not pass as /opt/app/uploads.
  • execve() avoids shell parsing and metacharacter injection, and -- stops the remaining argument injection where the program supports it.

Secure library loading with hash verification (Unix)

#include <dlfcn.h>
#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
#include <openssl/sha.h>
#include <openssl/crypto.h>
#include <limits.h>

#define LIBRARY_DIR "/opt/app/lib"

typedef struct {
    const char *name;
    const unsigned char *sha256_hash;
} trusted_library_t;

// The fixed size bounds the read: CRYPTO_memcmp below reads all 32 bytes, and
// without it a shorter array would be read past its end. It does NOT check that
// 32 bytes were supplied - C zero-fills a fixed-size array from a short
// initialiser, silently - so the placeholder below is really ab cd ef 00 00 ...
// and matches only a library whose hash ends in 29 zero bytes. Too MANY
// initialisers is a diagnostic; too few is not.
// Generate the real value with `sha256sum libcrypto.so` and paste all 32 bytes.
static const unsigned char libcrypto_hash[SHA256_DIGEST_LENGTH] = {
    0xab, 0xcd, 0xef, /* ... 29 more, not the zeros you get by leaving them out ... */
};

static const trusted_library_t trusted_libraries[] = {
    {"libcrypto.so", libcrypto_hash},
    {NULL, NULL}
};

int verify_library_hash(const char *path, const unsigned char *expected_hash) {
    FILE *f = fopen(path, "rb");
    if (!f) {
        perror("fopen");
        return 0;
    }

    EVP_MD_CTX *ctx = EVP_MD_CTX_new();
    const EVP_MD *md = EVP_sha256();
    unsigned char hash[EVP_MAX_MD_SIZE];
    unsigned int hash_len;

    EVP_DigestInit_ex(ctx, md, NULL);

    unsigned char buffer[8192];
    size_t bytes;
    while ((bytes = fread(buffer, 1, sizeof(buffer), f)) > 0) {
        EVP_DigestUpdate(ctx, buffer, bytes);
    }

    EVP_DigestFinal_ex(ctx, hash, &hash_len);
    EVP_MD_CTX_free(ctx);
    fclose(f);

    // Compare hashes (constant-time to prevent timing attacks)
    return CRYPTO_memcmp(hash, expected_hash, SHA256_DIGEST_LENGTH) == 0;
}

void *load_trusted_library(const char *library_name) {
    // Find in trusted list
    const trusted_library_t *lib = NULL;
    for (int i = 0; trusted_libraries[i].name != NULL; i++) {
        if (strcmp(library_name, trusted_libraries[i].name) == 0) {
            lib = &trusted_libraries[i];
            break;
        }
    }

    if (!lib) {
        fprintf(stderr, "Library not in trusted list\n");
        return NULL;
    }

    // Construct absolute path
    char path[PATH_MAX];
    snprintf(path, sizeof(path), "%s/%s", LIBRARY_DIR, library_name);

    // Verify hash before loading
    if (!verify_library_hash(path, lib->sha256_hash)) {
        fprintf(stderr, "Library hash verification failed - possible tampering\n");
        return NULL;
    }

    // Load verified library
    return dlopen(path, RTLD_NOW | RTLD_LOCAL);
}

Why this works:

  • SHA-256 verification detects tampering or replacement, so an attacker who can write to the library directory still cannot get code loaded.
  • The pinned hash is a fixed-size array, so CRYPTO_memcmp reading 32 bytes cannot read past the end of it. Note what that does not buy: a short initialiser is zero-filled rather than rejected, so an incomplete hash compiles cleanly and then fails closed for the wrong reason. Only an over-long initialiser is a diagnostic, so count the bytes when the value is pasted - the compiler will not.
  • CRYPTO_memcmp compares in constant time, so the check does not leak how much of the expected hash a guess matched.
  • Absolute paths and allowlists reduce the load surface: the name must be in the trusted list, the path is built from a fixed directory, and the bytes must hash correctly.

The gap between the hash and the load. verify_library_hash() opens the file and reads it; dlopen() opens it again. An attacker who can write to LIBRARY_DIR between the two calls gets the unverified file loaded - and being able to write to LIBRARY_DIR is exactly the situation the hash was added for. Where that is the real threat, keep the descriptor: hash from an fd obtained with open(), then load through /proc/self/fd/<n> on Linux, or fdlopen() on FreeBSD. Where the directory is root-owned and the concern is a tampered release rather than a live attacker, the race is not reachable and the simpler form above is enough. Decide which one you have before adding the complexity.

Considerations

Whether the search path is reachable by the attacker. LD_LIBRARY_PATH, LD_PRELOAD and the Windows current-directory search all require the attacker to control something about how the process starts, or a directory it searches. For a service started by systemd with a fixed unit file, running as a dedicated user, out of a root-owned install directory, none of those hold and a dlopen("libfoo.so") finding is not exploitable. For a setuid binary the loader has already stripped the dangerous variables. For a CLI tool users invoke from wherever they happen to be, or an application installed under a user-writable path, they very much do. ls -ld on the library directory and a look at how the process is launched settle it faster than any amount of reading the call site, and "the attacker cannot write anywhere that is searched" is a legitimate reason to record a false positive.

RTLD_LAZY versus RTLD_NOW, and RTLD_LOCAL. RTLD_NOW resolves every symbol at load time, so a library with a missing or unexpected dependency fails immediately rather than at the first call into it - which matters when the point of loading is to check the library is the one you expect. RTLD_LOCAL (the default, but worth stating) keeps the library's symbols out of the global namespace, so a loaded plugin cannot interpose on symbols another library resolves later. Both cost a little startup time and neither changes what can be loaded.

Hash pinning versus filesystem permissions. These solve the same problem and you usually only need one. Permissions are cheaper and do not couple your release process to the library vendor's; a pin is what you want when the directory cannot be locked down, or when the concern is a tampered artefact rather than a live attacker. Pinning also means every library update is a code change, which is a real cost and the reason pinned hashes go stale.

system() has no safe form. Unlike the loader cases there is no configuration that makes it acceptable with attacker-influenced input - it always spawns /bin/sh -c. posix_spawn() or fork() + execve() is the replacement, and if the call genuinely needs a pipeline or a redirect, build it with pipe() and dup2() rather than handing the shell a string.

Testing

There is no compiler warning for a containment check that compares the wrong number of bytes, and no scanner finding for a realpath() whose NULL return goes unchecked - the code builds, runs, and silently skips the test it was written to perform.

  • execute_command_secure("photo.jpg", "photo.png") returns 0 and photo.png appears in OUTPUT_DIR. A destination validated with realpath() fails here with ENOENT on every call, because the file does not exist yet.
  • is_inside("/opt/app/uploads", sizeof("/opt/app/uploads"), "/opt/app/uploads-public/x") returns 0, and the same call for /opt/app/uploads/x returns 1. Compare strlen(dir) bytes instead of including the separator and the first assertion flips; the second passes either way, so testing only the accepted case proves nothing.
  • load_library_secure("libssl.so") returns a non-NULL handle and load_library_secure("../../../tmp/evil.so") returns NULL. Assert both: a function that returns NULL unconditionally satisfies the second on its own.
  • From a working directory containing lib/, call dlopen("lib/../../../tmp/x.so", RTLD_NOW) once and confirm from strace that it opened a path rather than searching. Then try dlopen("lib../../../tmp/x.so", RTLD_NOW) and confirm it does not reach /tmp: its first component is a directory named lib... Which payload works is a property of the concatenation rather than of the loader, and it is worth deriving rather than assuming - a traversal that reads plausibly can resolve somewhere else entirely.
  • Build with -fsanitize=address and run the allowlist loop after adding a fourth entry to the array. With the count derived via sizeof, nothing changes; with a stale #define, ASan reports the read past the end - which in a released binary is a silent fail-open.
  • Assert verify_library_hash returns 1 for the genuine library, not only 0 for a tampered one. A hash left at a placeholder is zero-filled by the compiler with no warning, so the check rejects every library including the correct one - and a test that flips a byte and expects rejection passes against exactly that.

Additional Resources