ADR 0017: SCTP Transport Strategy and Unsafe-FFI Sys-Crate Boundary
Status
Accepted (amended 2026-09-05 by ADR 0020 for the narrowly scoped read-only verified snapshot VFS; implementation qualification is pending)
Date
2026-06-13
Context
ADR 0014 §8 states unsafe_code = "forbid" is workspace-wide and
"non-negotiable, which also rules out FFI-based protocol libraries (see ADR
0013)." ADR 0013 rejected Option C — FFI to the srsRAN/OAI C NGAP codec —
because foreign C code parsing attacker-controlled bytes turns memory-safety bugs
into SDK security issues.
opc-sctp is required for CNFs that terminate
N2/NGAP or other SCTP interfaces. Unlike NGAP, SCTP is not a codec — it is an
OS transport. Linux implements SCTP in the kernel (lksctp); a userspace
program reaches it through SCTP sockets:
socket(AF_INET, SOCK_STREAM|SOCK_SEQPACKET, IPPROTO_SCTP), SCTP setsockopt
options, sendmsg/recvmsg with SCTP control messages, and, where necessary,
thin libsctp helper calls such as bind/send/receive variants over the same
kernel SCTP UAPI. Rust's std and tokio expose no SCTP socket API, so
reaching kernel SCTP requires libc/UAPI FFI, which is unsafe. ADR 0014 §8 was
written for protocol codec libraries and did not anticipate an OS-transport
syscall surface.
The distinction is decisive:
- ADR 0013's rejected FFI links a large foreign C parser (thousands of lines) that consumes attacker-controlled wire bytes. The attack surface is the C code itself.
- SCTP FFI is a thin wrapper over kernel socket UAPI and optional
libsctphelper functions that themselves configure or call the kernel SCTP stack. The SCTP protocol implementation is the kernel — already trusted, exactly as for TCP/UDP. This is the same category ofunsafethattokio/mioalready use internally for socket I/O in the workspace. The "foreign C parsing attacker bytes" risk ADR 0013 guarded against simply is not present.
Options
- A. Kernel SCTP behind a narrow
opc-libsctp-syssys crate. Thinlibc/SCTP-UAPI FFI in one crate, includinglibsctphelpers only where the Linux SCTP API requires them; a safeopc-sctpwrapper above it. Linux-only. - B. Userspace SCTP stack (pure Rust). Reimplement the SCTP transport protocol with no FFI. Rejected: a from-scratch transport-protocol implementation is large and security-sensitive (association state machine, retransmission, multihoming, chunk bundling) and is more likely to harbor exploitable bugs than thin syscall FFI over the hardened kernel stack; no maintained pure-Rust SCTP stack exists to adopt.
- C. Omit SCTP from the SDK. Ship no SCTP transport. Acceptable only if the first production CNF does not terminate N2/NGAP or any SCTP interface; it blocks N2-terminating CNFs.
Decision
Amend ADR 0014 §8 to permit a narrow, explicitly allowlisted unsafe exception pattern for Linux kernel UAPI sys crates, and adopt Option A when an SCTP-terminating CNF is in scope:
opc-libsctp-sysprovides thin FFI over Linux SCTP socket UAPI and minimallibsctphelpers where required. It is the only SCTP workspace crate permitted to containunsafe; follow-on Linux kernel UAPI exceptions such asopc-linux-xfrm-sys,opc-linux-gtpu-sys, andopc-fs-verity-sys, and narrow reviewed FFI boundaries such asopc-sqlite-file-control-sys, must be separately and explicitly allowlisted by the same mechanical gate. The fs-verity boundary is limited to enabling and measuring the fixed version-1, SHA-256, 4096-byte-block profile through already-open file descriptors; it accepts no paths or file contents. The SQLite production boundary is limited toSQLITE_FCNTL_HAS_MOVED,SQLITE_FCNTL_VFSNAME,SQLITE_FCNTL_FILE_POINTER, andSQLITE_FCNTL_JOURNAL_POINTER: it returns only movement state or owned duplicate descriptors after authenticating the bundled Linux Unix VFS. Its opt-in test feature may register one non-default VFS that rejects unnamed temporary opens and delegates named opens to the bundled default VFS. This does not authorize borrowed-handle exposure, pathname authority, another opcode, or general SQLite FFI. ADR 0020 adds one explicit exception: a non-default, read-only snapshot VFS whose owned buffers are verified in safe Rust, with no write, journal, WAL, temporary, mmap, or arbitrary-path access. Each allowlisted sys crate does not inherit[workspace.lints](so the workspace-wideunsafe_code = "forbid"stays in force for every other crate); it sets its own local crate policy (unsafe_code = "allow"plusunsafe_op_in_unsafe_fn = "deny", or equivalent crate attributes) that allowsunsafeonly there, with a// SAFETY:comment required on every allowedunsafetoken (unsafeblock,unsafe fn,unsafe impl,unsafe trait, or unsafe extern block).opc-sctp(the public crate) is#![forbid(unsafe_code)]and exposes only safe async abstractions (associations, messages, events) over the sys crate, integrated withtokio::io::unix::AsyncFd(the spec's async model). Its manifest must declare the tokio features it relies on, includingnet, instead of relying on feature unification from unrelated workspace crates.- Boundary is enforced mechanically.
scripts/check-management-plane-policy.py --checktoken-scans OpenPacketCore workspace crate sources and assertsunsafeappears only in explicitly allowlisted sys crates (opc-libsctp-sysand later, reviewed kernel-UAPI boundaries such asopc-linux-xfrm-sys,opc-linux-gtpu-sys, and the fixed-profile descriptor-onlyopc-fs-verity-sys, plus the pinned file-control, test-only VFS, and ADR 0020 read-only snapshot VFS boundary inopc-sqlite-file-control-sys); the same gate also rejects each allowed sys crate if it inherits[workspace.lints], rejects it if it lacks the required local unsafe lint policy, and requires each allowedunsafetoken in that sys crate to be documented by an adjacentSAFETY:comment. The CI job runs this gate, so the exception cannot silently spread or become undocumented. - ABI safety. Every C struct crossing the boundary has a struct-layout
(size/alignment/offset) test. Kernel-UAPI sys crates build and run their
admitted Linux tests in CI and fail explicitly on other platforms.
Non-Linux coverage is compile-only and limited to the crate/target
combinations in CI.
In particular,
opc-fs-verity-sysexposes its descriptor API only on Unix: Linux provides the reviewed ioctl implementation, non-Linux Unix targets compile an explicit unsupported result, compile-checked for FreeBSD and Apple but not executed in CI, and non-Unix targets are outside this crate's admitted platform surface. - This exception pattern does not reopen ADR 0013. It authorizes FFI only to explicitly reviewed trusted Linux kernel UAPI boundaries such as SCTP socket/XFRM netlink calls and minimal helper calls that wrap those UAPIs. FFI that links a foreign C protocol codec (parsing attacker-controlled bytes — NGAP/NAS/etc.) remains rejected; those stay pure-Rust per ADR 0013/0015.
- SCTP is implemented per Option A behind this boundary, never as scattered
unsafeand never as a userspace reimplementation without revisiting this ADR.
Consequences
- The workspace gains small, auditable OpenPacketCore Linux UAPI sys crates
containing
unsafe; downstream carrier auditors review those explicitly allowlisted sys crates rather than a diffuse unsafe surface, andunsafe_code = "forbid"remains true everywhere else. - The CI gate from point 3 exists, mirroring the "policy must be mechanically enforced" lesson of ADR 0014.
opc-sctpuses the non-inheritance mechanism andAsyncFdmodel described by this ADR. Its README and tests record the current capability profile.- Static multihoming stays inside the same boundary: the sys crate owns the packed Linux bindx/connectx and bounded address-list UAPI, while the safe crate validates complete address sets, preserves the single-address syscall path, and exposes typed capability and kernel-active-address evidence.
- NGAP-over-SCTP wiring (PPID 60) is separate integration work and is not authorized to use FFI for the NGAP codec itself.
Receive ownership amendment — 2026-09-17
The safe wrapper owns partial DATA at the socket boundary so cancellation or an interleaved notification cannot return a tail as a fresh record. The async scratch gate still serializes kernel reads. A separate synchronous mutex owns the accumulator and terminal flag; it is held only around bounded assembly, never across I/O or an await. Close clears partial data under that same mutex, including while a receive is suspended. No cancellation cleanup guard or background receive task is required.
Known non-lifecycle notifications preserve the partial record. Matching association change/shutdown invalidates it; an ambiguous event while partial DATA exists fails closed. Complete ancillary record identity must remain consistent, with ordered SSN checked and unordered SSN/TSN progress treated per RFC 4960. This changes the documented cancellation contract without adding FFI, dependencies or NGAP policy. The conformance scope separates this reliability slice from the remaining typed N2 generation, stream-reset, bounded restart and protection boundaries tracked by #788.