A Free Model Called a C++ Patch Safe. A Symbol Manifest Made It Prove ABI Before Merge.

# cpp# ai# testing# devops
A Free Model Called a C++ Patch Safe. A Symbol Manifest Made It Prove ABI Before Merge.Finley Zhou

The team shipped a C++ plugin SDK with a promise. Plugins compiled against version 1.0 would load...

The team shipped a C++ plugin SDK with a promise. Plugins compiled against version 1.0 would load without recompilation for two years. The promise broke on a Thursday. A model-assisted review added one field to a public struct. The reviewer labeled the change safe. The library compiled. The new feature worked. An older plugin then crashed in plugin_load. The model had read the patch in isolation. It never saw the ABI boundary.

The field was int timeout_ms. It looked harmless. The public header changed from:

struct PluginConfig {
  int max_retries;
};
Enter fullscreen mode Exit fullscreen mode

to:

struct PluginConfig {
  int max_retries;
  int timeout_ms; // added by model review
};
Enter fullscreen mode Exit fullscreen mode

The old plugin passed a PluginConfig that was four bytes. The new library expected eight. The generated code read past the caller's object. Sanitizers caught the corruption later. The team removed the patch and reverted the release. Then they asked a harder question. Could they keep using free model review without turning every patch into a manual linker audit?

They did not want to train a local model or rent another GPU for a simple check. They used MonkeyCode's free model access to propose edits. Disclosure: This article was prepared as part of MonkeyCode's product outreach. The free server option hosted a small ABI worker during the evaluation. The worker did not ask the model whether a patch was ABI safe. It compared the exported surface and the public type layouts before and after the patch.

The gate had two layers. First, a release manifest recorded every exported symbol. nm -D --defined-only produced the list. Second, abidiff from libabigail compared the debug-info-backed type graphs. The manifest was cheap and fast. The abidiff run was the real veto.

Build the baseline library and capture the manifest:

cat > plugin_v1.h <<'EOF'
#pragma once
struct PluginConfig {
  int max_retries;
};

#ifdef PLUGIN_EXPORTS
#define PLUGIN_API __attribute__((visibility("default")))
#else
#define PLUGIN_API
#endif

extern "C" PLUGIN_API int plugin_load(const PluginConfig& cfg);
EOF

cat > plugin.cpp <<'EOF'
#include "plugin_v1.h"
int plugin_load(const PluginConfig& cfg) {
  return cfg.max_retries > 0 ? 0 : 1;
}
EOF

g++ -std=c++17 -shared -fPIC -fvisibility=hidden -DPLUGIN_EXPORTS plugin.cpp -o libplugin_v1.so
nm -D --defined-only libplugin_v1.so | awk '{print $3}' | sort > baseline.symbols
Enter fullscreen mode Exit fullscreen mode

The gate runner compared the patched library against the baseline:

#!/usr/bin/env python3
import json, subprocess, sys

def main() -> int:
    baseline, patched, old_headers, new_headers = sys.argv[1:5]
    result = subprocess.run(
        [
            "abidiff",
            "--headers-dir1", old_headers,
            "--headers-dir2", new_headers,
            baseline,
            patched,
        ],
        text=True,
        capture_output=True,
    )
    report = (result.stdout + result.stderr)[-6000:]
    verdict = {
        "merge": (result.returncode & 4) == 0,
        "exit": result.returncode,
        "report": report,
    }
    print(json.dumps(verdict, indent=2))
    return 0 if verdict["merge"] else 1

if __name__ == "__main__":
    raise SystemExit(main())
Enter fullscreen mode Exit fullscreen mode

The CI job called the runner with four paths. A nonzero exit blocked merge. When the field addition landed, abidiff reported a type size change. The model's "safe" label no longer mattered. The patch went back with the first 6,000 characters of the report. The author fixed the ABI by moving the new field into an opaque extension map instead of the public struct.

The free model review still ran. It suggested better naming, spotted a missing null check, and proposed test cases. But the model never got the final word on public headers. That was the point.

To host the worker on the free server, the team wrapped the same logic in a tiny HTTP endpoint:

from flask import Flask, request, jsonify
import subprocess

app = Flask(__name__)

@app.post("/check-abi")
def check_abi():
    body = request.get_json(force=True)
    result = subprocess.run(
        [
            "abidiff",
            "--headers-dir1", body["old_headers"],
            "--headers-dir2", body["new_headers"],
            body["baseline"],
            body["patched"],
        ],
        text=True,
        capture_output=True,
    )
    return jsonify(
        {
            "merge": (result.returncode & 4) == 0,
            "exit": result.returncode,
            "report": (result.stdout + result.stderr)[-6000:],
        }
    )
Enter fullscreen mode Exit fullscreen mode

The endpoint turned the check into a reusable CI service. The baseline and patched shared objects were uploaded as build artifacts. The headers were unpacked from the source tree. The worker returned a merge boolean and a report snippet. The model access stayed on the patch-proposal side. The free server stayed on the enforcement side. Neither side trusted the other.

Limitations:

  • abidiff does not catch semantic changes that keep the ABI identical. A function can still return the wrong value.
  • It needs debug information. Stripped release binaries give weaker results.
  • Intentional ABI breaks need an allow-list. A major version bump should not fail on expected removals.
  • C++ exceptions, templates, and macros are not fully covered by symbol diffing. The gate is not a substitute for plugin tests.

Who should not use this:

  • Teams without a public ABI promise. The gate adds process without much value.
  • Projects that already publish formal ABI baselines and use heavy contract tooling.
  • Teams without debug symbols or reproducible build artifacts.

The final workflow was simple. A release manifest recorded the exported surface. A free model proposed edits. A small server compared the ABI before and after. The merge decision came from the comparison, not from the model. That is how the SDK kept its two-year promise without banning model review.