Trust boundaries: portable vs native
Problem
Language-native serializers—Python pickle, Java serialization, many .NET binary formatters, and similar “dump the object graph” tools—are ergonomic inside one runtime. They often win microbenchmarks on complex graphs and preserve types that portable formats handle awkwardly. Teams then place those bytes on a queue, a cache shared across services, a file that other teams can open, or a network API. The security and portability failure modes are not subtle: gadget chains, version skew that only appears at runtime, and permanent lock-in to one language.
The design question is not “is native fast?” It is which trust and interoperability boundary the bytes cross.
Short answer
Treat language-native encodings as unsafe by default for anything outside a single process trust domain (or a tightly controlled same-stack path with authenticated peers and no untrusted input). Prefer portable formats—text or binary, self-describing or schema-driven—whenever data is stored long-term, shared across languages, accepted from clients, or exposed to multi-tenant input. Native formats remain useful for trusted, same-runtime checkpoints and caches when the threat model explicitly allows them and operators accept the lock-in.
Assumes 201: self-describing vs schema for where field identity lives; this page owns trust and portability policy.
Constraints that matter
| Constraint | Portable (JSON, MessagePack, Protobuf, …) | Language-native (pickle, Java ser, …) |
|---|---|---|
| Who may produce bytes? | Often untrusted clients or other teams | Must be fully trusted writers only |
| Who may consume? | Any language with a library | Typically one runtime family |
| Lifetime | Years on disk / in a log | Often broken by class/layout changes |
| Security | Still need limits and validation | Deserialization can become code execution |
| Ops | Debuggable with open tools (especially text) | Opaque without the exact type graph |
| Performance | Implementation-dependent (see suite) | Sometimes excellent on graphs—irrelevant if unsafe |
Decision frame
Do bytes leave this process / trust domain?
│
no │ yes
│ │
â–Ľ â–Ľ
Native Portable format required
may be (choose family with 101/201 + categories)
OK if
trusted
peers only
| Situation | Prefer | Avoid |
|---|---|---|
| Public or partner HTTP/RPC body | Portable (often JSON + validation, or IDL binary) | Native |
| Multi-language microservices | Portable schema-driven or schemaless binary | Native |
| Object store / lake / long-lived files | Portable (often columnar or Avro/Parquet-class for data) | Native blobs as system of record |
| Redis/memcache value, same service binary only, private network, no user input | Native possible if threat model documented | Native if any other service or language may read |
| ML checkpoint used only by one training stack | Native common; migrate to portable for sharing | Native as the only interchange with production services |
| “Faster than JSON in one language Results” | Use as same-language evidence only | Mandating native on the network |
Failure modes
| Failure | What happens |
|---|---|
| Gadget / RCE | Hostile or merely unexpected bytes trigger class loading or code paths during deserialize |
| Silent version skew | Writer and reader disagree on class shape; errors are runtime and intermittent |
| Polyglot surprise | A second language team cannot consume the cache without rewriting |
| Backup / forensics gap | Incident response cannot inspect payloads without the original deployment |
| False safety | “Internal network” treated as trusted while multi-tenant jobs share the bus |
Real-world sketch
A Python service caches session graphs with pickle in Redis for speed. Latency improves on the language Results-style comparison of native vs JSON. Six months later a second service in Go needs the same session. Options become: keep dual writers, reverse-engineer a fragile parser, or migrate to MessagePack/JSON with an explicit schema. Separately, an SSRF-style bug lets an attacker influence a cache key path; if anything ever deserializes attacker-influenced pickle, the incident class changes from “data issue” to “remote code execution.” The original speed win did not price that risk.
In this suite
| Resource | Role |
|---|---|
| Serialization categories | Language-native family vs portable families |
| Language Overview | Which native codecs are registered (if any) and caveats |
| Language Results | Speed/size within one language—never a reason to put native on the wire alone |
| Using this suite | How not to misread those numbers |
| Engineering perspective | Product framing of native vs portable |
When native and portable entries both appear for a language, compare them only to answer “what do we pay for portability in this runtime?”—not “is native a good public contract?”
Experiments
Question: May these bytes use a language-native codec, or must they be portable?
Setup
- Draw the data path: producer process → store/queue/API → consumers.
- Mark each hop: same process, same trust domain, multi-tenant, public, multi-language.
- List candidate native vs portable codecs for the language (suite categories help label them).
Procedure
- For each hop, answer: Can an untrusted or other-language principal supply or read bytes?
- If yes on any long-lived or cross-service hop → portable required; native disqualified for that hop.
- If no (trusted same-runtime only) → native allowed if threat model and ops accept lock-in.
- Document the policy in the service boundary checklist.
- Optional suite check: compare native vs portable only for performance after policy allows native—never to override a failed trust test.
Decision rule
- Trust/portability failure ⇒ portable, regardless of suite speed.
- Suite timings may choose which portable family/impl, not whether native is safe.
Metrics
| Metric / signal | Role |
|---|---|
| Trust-domain crossing (yes/no per hop) | Primary decision variable |
| Consumer language set | Forces portable if >1 runtime family |
| Attack surface (untrusted producer?) | Forces portable + untrusted input controls |
| Retention / durability | Long-lived native = version-skew risk |
Suite total_median_ns / size |
Secondary, only among policy-allowed codecs |
mean_fidelity |
Among allowed candidates |
Conclusion style: “Queue hop is multi-tenant → portable only; native pickle rejected despite speed.”
Not decision metrics: native microbenchmark wins on unsafe boundaries.
What this suite cannot tell you
- Whether a specific native stack has a known gadget chain in your dependency set.
- Your network trust model, IAM boundaries, or multi-tenant isolation.
- Compliance rules that forbid certain encodings at rest.
- Cross-language feasibility of a native blob (by definition: poor).
Common mistakes
- Equating “internal” with “trusted for deserialize.”
- Using native speed on Results to justify a public or multi-language API.
- Storing native blobs as the system of record “until we rewrite.”
- Assuming schema-driven portable formats are “secure” without resource limits—portable reduces portability risk, not all parser risk.
Key takeaways
- Trust boundary decides portable vs native more than peak ops/s.
- Native = same-runtime convenience; default no for interchange.
- Portable formats are the default once bytes leave the process or meet other languages.
- Suite Results may show native as fast; that is a cost of portability data point, not a security clearance.
- Document the threat model if you still choose native for a private cache.