SQL Anywhere · · 5 min read

SQL Anywhere 0.6.0: a shared object is not a stranger you should trust

Extensions run inside your database process with all of its privileges. 0.6.0 adds a signed extension repository, a versioned ABI — and an honest note about where the lock doesn't reach yet.

SQL Anywhere 0.6.0: a shared object is not a stranger you should trust

Most of what a database does, it does quietly and correctly, and you never think about it. And then there's the one moment that ought to make you nervous: the moment you hand it a loadable extension — a .so, a .dylib, a file someone built — and say run this, with everything you've got.

Because that's what an extension is. It isn't a plugin sandboxed behind an API. It's native code that runs inside your database process, with the full privileges of your database process. And for most of this project's life, installing one meant downloading a shared object from a release page and — this is the honest word for it — hoping. Hoping the download wasn't tampered with. Hoping it was built by who you thought. Hoping it matched the engine you were loading it into.

0.6.0 is the release that stops asking you to hope. Here's why, and how, with the actual mechanics.

Why: two different ways an extension can betray you

There are two failure modes, and they're worth keeping separate, because 0.6.0 answers them separately.

The first is trust. A checksum next to a download proves the file arrived intact. It proves nothing about who made it — a compromised release, a stolen CI token, a bad actor with write access all produce a file with a perfectly matching checksum. For code that runs with your data's full privileges, "it downloaded without corruption" is a comfortingly precise answer to a question nobody was asking.

The second is compatibility. An extension is compiled against one copy of a header, and then loaded by whatever host library the user happens to have. If the host's internal structure has grown a field since the extension was built, the extension reads past the end of it — and in the worst case reads an integer where it expects a function pointer, and calls it. That's not a clean error. That's undefined behaviour with your process's name on it.

How: a repository that signs what it ships

Every SQL Anywhere release now carries three things beside the extension archives:

crsqlite-v0.6.0-aarch64-apple-darwin.tar.gz
crsqlite-v0.6.0-x86_64-unknown-linux-gnu.tar.gz
SHA256SUMS          ← integrity
MANIFEST.json       ← what each artifact is, and the ABI version it was built against
MANIFEST.json.sig   ← a detached Ed25519 signature over that manifest's exact bytes

And there's a tool that checks them, in the order that matters:

cargo xtask verify-extensions
  → verify the signature over the manifest
  → then each artifact's digest
  → then that each artifact's interface version is one this host implements

An unsigned release cannot pass as a signed one: verify-extensions fails unless you explicitly tell it --allow-unsigned. The signing keys carry short ids so rotating one doesn't need a flag day. And the key generation — cargo xtask extension-keygen — is a one-time, deliberately manual step, because CI must never mint its own trust root. A robot that can sign its own releases is a robot you're trusting exactly as much as you were trusting hope.

How: an ABI that knows its own version

The compatibility problem gets fixed at the root. The thunk SQL Anywhere hands to an extension now carries an iVersion as its first member, and the header gained a guard:

#if SQLANYWHERE_API_ATLEAST(1)
    sqlanywhere_close_hook(db);   // only if the host actually offers it
#endif

So a newer extension loaded into an older host asks "do you implement at least version 1?" and, if the answer is no, declines the feature instead of reading past the end of a structure and calling whatever it finds. The mismatch becomes a thing an extension detects, not a thing it crashes on. And because the manifest records the interface version each artifact was built against, a bad pairing is caught before the file is ever opened.

The one honest catch (and why it's the last one)

Adding a version field to a structure means moving its layout, exactly once. So there's a real cost, and 0.6.0 states it plainly rather than burying it:

Breaking change. A prebuilt extension built against 0.5.2 or earlier reads that new interface-version integer where it expects a function pointer and calls it. Loading one into 0.6.0 kills the process — verified, SIGBUS. Re-download the v0.6.0 builds.

That's the whole break. It's narrow — the SQLite C API is unchanged and your database files stay byte-compatible with stock SQLite, so nothing but loadable extensions is affected — and it's the last time the layout can move, because the version field is precisely what lets every future mismatch be caught instead of crashed on. You pay the compatibility tax once, to never pay the crash again.

The part I like best: what it deliberately does not do

Here's the sentence from the new EXTENSION_REPOSITORY.md docs that made me want to write this article:

The loader still opens any file you point it at, and making it enforce signatures is a policy decision with open questions listed there.

That is not the confident close a marketing page wants. It's better than one. 0.6.0 gives you the tools to verify — the sums, the manifest, the signature, the version check — and then tells you, in writing, that the loader itself will still load an unsigned file if you ask it to, and that turning verify into enforce is a real decision with real trade-offs nobody has finished thinking through.

Because the alternative — quietly pretending the door is locked when it's only lockable — is exactly the kind of comforting lie a database has no business telling you. A tool that hands you a good lock and is honest that you have to turn it is worth more than one that claims a safety it hasn't built.

Where it fits

SQL Anywhere is an embeddable, replication-ready SQL engine built on SQLite — a drop-in for the file format you already trust, with the replication, CRDT merge, vector search and offline writes bolted on above it. Extensions like cr-sqlite are how a lot of that arrives. 0.6.0 doesn't add a feature to the shop window so much as make the thing under it something you can check instead of trust — which, for code that runs with your data's full privileges, is the feature that should have come first.

Verify the lock. We wrote down where it doesn't reach yet.