Embed Merisar directly in your code.
The SDK is two layers you drop into your project. Layer 1 is a one-line macro that marks functions for cloud-side virtualization. Layer 2 is a linkable C library of in-source security primitives: keyed integrity, self-check, debugger & VM detection, branchless tamper responses, constant-time helpers. Mix them.
Quick start#
Three files, three steps. In your project:
- Grab the headers (
svp_mark.h,svp_guard.h) and the static lib (libsvpguard.a/svpguard.lib). See Install below for exact paths. - Mark the function(s) you want virtualized. That's it for Layer 1.
- Optional: call Layer 2 helpers from your code for an extra belt-and-braces check.
// hello.c
#include "svp/sdk/svp_mark.h"
#include "svp/sdk/svp_guard.h"
uint64_t check_license(const char* key) {
// Layer 2: branchless debugger response, folded into the result.
// No visible "if (debugged)" branch for a reverser to NOP out.
uint64_t h = svp_mac(key, strlen(key), K0, K1);
h ^= svp_poison(SECRET_MASK, svp_debugger_present());
return h;
}
SVP_VIRTUALIZE(check_license); // Layer 1: cloud-side VM lowering
Compile normally (cc hello.c -lsvpguard -o hello.exe). Then POST the binary to the Merisar build service — the marker in .svpmark tells us which functions to virtualize. You get a protected binary back where check_license runs inside a per-build VM and the native-code layer-2 call stays as an extra fallback.
Layer 1 — Source annotations#
Mark the function in source. The protector reads the marker at build time and replaces the function body with a VM call to your virtualized bytecode. No manual.svpmeta files, no fragile offset tables.
Mark fn for full VM lowering.
uint64_t check_license(const char* key) { /* ... */ }
SVP_VIRTUALIZE(check_license);- Place at file scope, immediately after the function definition.
- Function must have external linkage (not static).
- The protector sizes the function from .pdata (framed) or a bounded decode-to-RET (leaf) — if neither works, the function is reported as a native fallback (not silently skipped).
Mark fn for lighter obfuscation (reserved for future levels).
SVP_OBFUSCATE(small_helper);- Currently treated as a lower-strength virtualization request.
- Use for hot-path functions where you want some protection without the full VM dispatch cost.
Install
Where files go in your project:
include/svp/sdk/svp_mark.h— the macros. C and C++ compatible, GCC/Clang/MSVC.include/svp/sdk/svp_guard.h— the runtime API (Layer 2).lib/libsvpguard.a(POSIX) orlib/svpguard.lib(MSVC) — the implementation.
No external dependencies. The macros emit descriptors into a dedicated .svpmark PE section — the protector scans it when you upload the binary.
Layer 2 — Runtime guard library#
A C-ABI linkable library. Call these directly from your own code — they're backed by the same primitives the virtualized functions use, so native and VM-side checks share one implementation. Zero dependencies.
Environment detection
Returns 1 if the process is being debugged, else 0.
if (svp_debugger_present()) { /* ... */ }- Checks four documented signals: IsDebuggerPresent, CheckRemoteDebuggerPresent, PEB.BeingDebugged (direct read, survives API hooks), NtGlobalFlag heap-debug bits.
- Self-directed only. Not debugger-blinding / anti-attach — just a signal you can respond to.
- Treat as advisory: a stealth plugin can hide from all four. Weight it; prefer folding results via svp_poison() over hard gates.
Returns 1 if a hypervisor is present (CPUID), else 0.
int suspect_env = svp_in_vm();- Reads CPUID.1 ECX[31] (hypervisor-present) and the vendor leaf 0x40000000.
- Advisory. Will false-positive on legitimate users running in VMs or on cloud workstations — never hard-gate on this alone.
Keyed integrity (SipHash-2-4)
Keyed MAC over a buffer.
uint64_t tag = svp_mac(blob, len, K0, K1);- SipHash-2-4. Fast on short messages, keyed (not plain checksum).
- Derive K0/K1 at runtime where you can — a key embedded as a literal in the binary is recoverable like any other embedded constant.
Returns 1 iff svp_mac(data,len,k0,k1) == expected. Constant-time tag compare.
if (!svp_verify(blob, len, K0, K1, SEALED_TAG)) corrupt_state();- Use this to detect tampering of data you sealed at build time.
- The tag comparison is constant-time (no timing side channel on the 8 bytes).
MAC a region of the process's own code — detect inline hooks / breakpoints / patches.
uint64_t h = svp_code_mac(&hot_fn, hot_len, K0, K1);
if (h != SEALED_FN_TAG) corrupt_state();- Same primitive as svp_mac. Measure the byte range of the function you sealed at build time.
- For a reliable fn-size, bake it in via linker symbols or compute at build time with a tool — do not guess at runtime.
Branchless tamper response
Branchless select: returns mask when tripped is nonzero, else 0.
// Fold a check into a value — no visible "if (bad)" branch to NOP out.
key ^= svp_poison(SECRET_MASK, svp_debugger_present());- The whole point: tampered code silently produces wrong results — no compare / branch for an attacker to find and skip.
- Compose with svp_debugger_present / svp_in_vm / svp_verify to turn any boolean signal into a poisoned value.
Constant-time helpers
Constant-time byte compare. 1 if equal, 0 otherwise.
if (svp_ct_equal(user_mac, expected_mac, 32)) grant_access();- No early-out: runtime does not depend on where the first differing byte is.
- Use whenever you compare secrets (MACs, tokens, HMAC digests). Never use memcmp for that.
Zero memory such that the compiler cannot elide the write.
svp_secure_zero(derived_key, sizeof derived_key);- Call on sensitive buffers after use — stack-local keys, decrypted material, scrub lists.
- Does not help against swapping / core dumps / cold-boot — those need OS-level mitigations too.
Combining both layers#
The intended power move: virtualize a function (Layer 1) that itself calls Layer 2 primitives. The checks are then obfuscated inside the VM and backed by native self-defense, so an attacker can't neutralize the protection by diffing for a single fixed injection pattern.
#include "svp/sdk/svp_mark.h"
#include "svp/sdk/svp_guard.h"
uint64_t check_license(const char* key) {
uint64_t h = svp_mac(key, strlen(key), K0, K1);
h ^= svp_poison(TAMPER_MASK, svp_debugger_present()); // Layer 2 inside...
return h;
}
SVP_VIRTUALIZE(check_license); // ...a Layer-1 virtualized fn
At build time Merisar lifts check_license into a per-build VM whose opcodes are permuted and whose bytecode is encrypted at rest. The svp_poison() call becomes a VM CALL_HOST that executes the same native primitive — so if an attacker patches around the native path, they still have to defeat the virtualized copy, and vice versa.
Keys & honest limits#
- Keys passed as parameters.
svp_mac/svp_verifytakek0/k1as parameters so you control where they live. For the strongest posture derive them at runtime (e.g. from a server response, a user-provided key, or something hardware-attested) rather than embedding a compile-time constant. Any constant baked into the binary is recoverable — this is true of every protector including commercial ones. - Detection honesty.
svp_debugger_presentandsvp_in_vmare speed bumps. A stealth plugin can defeat all four debug signals; a hypervisor can lie about CPUID. These are useful signals you fold into results, not reliable gates to block execution on. - Branchless responses beat booleans. Prefer
result ^= svp_poison(mask, cond)overif (cond) corrupt_state(). The former leaves nothing for an attacker to pattern-match; the latter is one well-known NOP away from disabled. - The SDK is C-ABI by design. Call it from C, C++, Rust (via extern "C"), Go (via cgo), or any language with a C FFI. The macros in
svp_mark.hare C-only but the marking step can also be replaced by an explicit.svpmetasection if your toolchain doesn't preserve the.svpmarkone.
HTTP API#
Programmatic access for CI, SDK integrations, and anything that isn't a human clicking the dashboard. Every request authenticates with an API key you mint at /dashboard/api-keys.
Auth header: Authorization: Bearer svp_live_.... Keys carry scopes; build creation needs builds:create, listing and status need builds:read.
Create a build
Register a build and get a short-lived GCS signed PUT URL. The server never touches your binary; you upload directly to object storage.
curl -X POST https://merisar.com/api/v1/builds \
-H "Authorization: Bearer svp_live_XXXXXXXXXXXX" \
-H "Content-Type: application/json" \
-d '{
"inputFilename": "app.exe",
"inputSize": 1457920,
"profile": { "base": "medium" }
}'
# → 201
# {
# "buildId": "f1e2d3c4-...",
# "status": "pending_upload",
# "uploadUrl": "https://storage.googleapis.com/...",
# "uploadMethod": "PUT",
# "uploadContentType": "application/octet-stream",
# "uploadMaxBytes": 1457920,
# "uploadExpiresAt": "2026-10-09T18:40:00.000Z",
# "commitUrl": ".../api/v1/builds/f1e2.../commit",
# "statusUrl": ".../api/v1/builds/f1e2..."
# }- Size cap: 128 MiB. Larger inputs are rejected with 400.
- Profile keys:
base = light | medium | virtualized, optionalseed = <hex>. See Layer 1 for which transforms each base runs. - The signed URL is single-use and bound to
Content-Type: application/octet-stream+ the exactContent-Lengthyou declared.
Push the bytes directly to GCS. Nothing proxies through our Next.js server, so this scales independent of your binary size.
curl -X PUT "<uploadUrl>" \
-H "Content-Type: application/octet-stream" \
--data-binary @app.exe
# → 200Tell Merisar the upload is complete. We verify the object landed, match the declared size, flip the row to queued, and enqueue the protector. Idempotent.
curl -X POST https://merisar.com/api/v1/builds/f1e2.../commit \
-H "Authorization: Bearer svp_live_XXXXXXXXXXXX"
# → 200
# { "ok": true, "buildId": "f1e2...", "status": "queued" }Poll a build
Status + metadata. Poll at 1–2 Hz while the build is in flight; stop once status is terminal.
curl https://merisar.com/api/v1/builds/f1e2... \
-H "Authorization: Bearer svp_live_XXXXXXXXXXXX"
# → 200
# {
# "id": "f1e2...",
# "status": "succeeded", // pending | queued | running | succeeded | failed
# "inputFilename": "app.exe",
# "inputSize": 1457920,
# "outputSize": 1521664,
# "durationMs": 8420,
# "fingerprintHex": "a2c9...",
# "reportText": "...", // protector stdout, up to 32 KB
# "errorText": null,
# "createdAt": "...",
# "startedAt": "...",
# "completedAt": "...",
# "downloadUrl": ".../api/builds/f1e2.../download"
# }- Terminal statuses:
succeeded,failed,cancelled. fingerprintHexis the R12 watermark — hand it to/api/forensics/identifyon a recovered binary to trace a leak back to the issuing build.
List up to 50 recent builds for this key's team, newest first.
curl https://merisar.com/api/v1/builds \
-H "Authorization: Bearer svp_live_XXXXXXXXXXXX"
# → 200
# { "builds": [ { "id": "...", "status": "succeeded", ... }, ... ] }Download the protected binary
302 redirect to a short-lived GCS signed GET URL. The download comes directly from GCS; no proxying.
curl -L https://merisar.com/api/builds/f1e2.../download \
-H "Authorization: Bearer svp_live_XXXXXXXXXXXX" \
-o protected.exe- Only works once
status === "succeeded"; otherwise returns 409. - Session cookies also work here — this is the same URL the dashboard "Download" button uses.
- The signed URL expires in 5 minutes; refetch to download again.
Error envelope
Errors return { "error": "<message>" } with the appropriate HTTP status. Common codes:
- 400 — invalid body (schema failure, size out of range, bad buildId format)
- 401 — missing or malformed
Authorizationheader, or key doesn't exist / was revoked - 403 — key is valid but lacks the required scope
- 404 — build id doesn't exist, or belongs to a different team
- 409 — build is not yet in a state that supports this action (e.g. commit before upload lands, or download before success)
- 502 — upstream (Cloud Tasks / worker) rejected the enqueue
Ready to ship it hardened?
Create an account, grab an API key, and your first build is a POST away.