Skip to main content

Security

This page describes how the library defends against malformed or malicious input, and which of those defenses are proven rather than just enforced. For how to report a vulnerability, and for how we manage dependencies, see the security policy.

At a glance​

DefenseHowChecked by
Memory safetyunsafe code is forbidden in the Rust libraryThe compiler
Restricted sources of panicsPanic-prone constructs are denied, with documented exceptionsclippy, in CI
Proven panic freedomParsing what is received, and formatting and framing what is sent, can't panic, whatever the inputA link-time check, in CI
Bounded resourcesFixed buffers, and limits on how much of a request is processedTests
Protocol validationIEEE 1815-2012 as amended by the technical bulletinsTests, and conformance testing
TLSrustls, mutual authentication, and only the trust you configurerustls

Memory safety​

The Rust library is written in safe Rust: unsafe code is forbidden, which the compiler enforces. The bindings for C, C++, .NET and Java are generated from an API schema, with a thin layer of hand-written adapters. They use unsafe at the boundary with the other language, where they trust the pointers and handles your application passes in. DNP3 protocol parsing is implemented in the safe Rust library, and the bindings adapt its values and iterators for your callbacks.

Restricting sources of panics​

A panic in Rust ends the task or thread it happens in, so a panic that input can trigger is a denial-of-service vulnerability. CI runs clippy on the library with these lints denied:

  • unwrap and expect on Option and Result
  • indexing and slicing that can go out of bounds
  • arithmetic that can overflow, underflow or divide by zero
  • panic!, unreachable!, todo! and unimplemented!
  • slicing a string at a position that may not be a character boundary
  • as conversions, which can truncate or change sign without warning

Tests may unwrap and index. In the library, the few exceptions are places input can't reach, and each one states why.

Proven panic freedom​

The lints restrict panics in our own code, but a call into the standard library or a dependency can still panic. So CI also proves, at link time, that the steps that parse what the library receives, and format and frame what it sends, can't panic for any input, including in the standard library and dependency code they call. Each diagram follows a request or response down from the bytes received, through the processing in your role, and back up to the bytes sent:

The green steps are proven panic-free. The gray ones are covered by the lints but not the proof, and your own handlers are code we can't check.

How the proof works

The proof uses the no-panic technique. Each function on the green path is compiled so that a reachable panic leaves a reference to a symbol that doesn't exist, and the optimizer removes the reference only if it can prove that no path to a panic exists. A test harness calls the functions with input read at run time, so the optimizer can't specialize them to known values, and with output buffers of every size from empty to far larger than a fragment. If the test binary links, these functions can't panic.

Where a step calls code the library doesn't own, such as logging, the harness passes a stand-in, so the proof covers the step around the call but not the call itself. CI links the test binary twice, the second time with overflow checks on, so arithmetic overflow can't panic in these steps either.

The check fails closed: if any of these functions could panic, the test binary doesn't link, and the error names the function. We verified this in each layer by temporarily adding a panic that only specific input could reach; each one broke the link.

The no-panic job in CI runs this check on every pull request and release tag.

What the proof doesn't cover

The asynchronous session code that schedules the master's tasks and processes the outstation's requests, the outstation's walks and searches of its stored static points and attributes, I/O and logging, and code you supply, such as a ReadHandler, an OutstationApplication or a ControlHandler.

Bounded resources​

The buffers that receive DNP3 data are allocated once, at fixed sizes, when a session starts. The outstation caps how many READ headers and controls it processes per request (max_read_request_headers, max_controls_per_request), and budgets the work it spends on a READ or ASSIGN_CLASS, scaled to its database, so a pathological request is answered with an error instead of consuming the CPU.

Protocol validation​

The library follows IEEE 1815-2012 as amended by the DNP Users Group technical bulletins. Requests and responses that don't follow the standard are rejected, or answered with the error it prescribes, and select-before-operate is enforced strictly. Conformance testing checks this behavior.

TLS​

TLS is implemented with rustls, not OpenSSL, and is mutually authenticated.

  • In CertificateMode::AuthorityBased, the library trusts only the certificate authority you configure: it never loads the operating system's trust store or any built-in set of roots.
  • In CertificateMode::SelfSigned, the peer's certificate is compared byte for byte with the one you configure, and certificate chains are rejected.

See TLS for configuration.