Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .bazelrc
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,21 @@ test:ubsan --test_timeout=300,300,900,3600
# Hardcoded rpath: .../lib/clang/21/... must match MODULE.bazel llvm major and Xcode.
build:ubsan_macos --linkopt=-Wl,-rpath,/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/21/lib/darwin

# libFuzzer configuration (//library/tests/fuzz:*_fuzz)
# Usage: bazel run --config=fuzz //library/tests/fuzz:pbn_fuzz -- -runs=100000
# Uses the registered hermetic LLVM toolchain, which ships libclang_rt.fuzzer;
# Apple's toolchain does not, so do not combine with --config=asan on macOS
# (build:asan_macos switches to the Xcode toolchain). On Linux, adding
# --config=asan gives fuzzing with memory-error detection. The corpus-replay
# tests (//library/tests/fuzz:*_fuzz_corpus_test) need none of this and run
# under the ordinary configs.
build:fuzz --copt=-fsanitize=fuzzer-no-link
build:fuzz --copt=-fno-omit-frame-pointer
build:fuzz --linkopt=-fsanitize=fuzzer
build:fuzz --strip=never
build:fuzz --features=dbg
build:fuzz --compilation_mode=dbg

# MemorySanitizer configuration (Linux x86_64 only)
# Usage: bazel test --config=msan //path:target
# Selects @llvm_toolchain_msan (instrumented libc++ overlay) and enables the
Expand Down
154 changes: 154 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
# Security Policy

## What DDS is, for threat-modelling purposes

DDS is an **in-process library**, not a service. It opens no sockets and
crosses no privilege boundary. In the normal deployment the input is a bridge
deal supplied by the calling application — usually that application's own data,
or a hand a user typed in themselves.

It does, however, carry **process-local solver resources** that outlive an
individual call: a transposition table and per-thread working memory managed
through the legacy `SetResources()` and `FreeMemory()`, and a worker pool held
in a function-local static that persists for the lifetime of the process.
Calls are therefore not isolated from one another, so corruption caused by one
call can be observed by a later one.

Thread counts work differently from memory, and the distinction matters:

- The legacy **memory** settings are process-wide. One component's
`SetResources()` choice applies to every other user of the library in the
same process.
- The **worker pool** is shared process-wide, but how many of its workers a
given call uses is decided **per call**, by the `maxThreads` argument of the
`*N` and `*X` batch entry points. A value of 0 selects hardware concurrency.
- `SetMaxThreads()` sets nothing. It is a deprecated alias of
`InitializeStaticMemory()` and its argument is ignored; internal batch
threading was removed. Do not treat it as a resource limit. In the modern
C++ API the embedding application controls concurrency, typically with one
`SolverContext` per worker thread.

This matters when judging the severity of a memory-safety bug here. A defect
reachable only from data the caller already controls, in a library running in
the caller's own address space, is a robustness problem rather than a
privilege-escalation vector: there is no boundary being crossed.

Two deployments raise the stakes, and one lowers them:

- **PBN input from files** is the one classic untrusted-input path.
`convert_from_pbn()` and the `*PBN` entry points parse externally authored
text.
- **Server-side use** — any service that accepts user-submitted deals or PBN
over a network — makes everything below remotely reachable. If you are
building one, read "If you expose DDS to untrusted input" below.
- **The WebAssembly build** contains the blast radius. A memory error stays
inside the module's linear memory and cannot corrupt the host page, so the
browser deployment is substantially better protected than the native ones.

## Input trust model

**DDS assumes trusted, well-formed input.** It validates enough to catch honest
caller mistakes; it is not hardened against adversarial input, and the coverage
is uneven across entry points:

- `SolveBoard()` validates thoroughly — parameter ranges, card counts,
duplicate cards, already-played cards — and returns a specific `RETURN_*`
code. See `board_range_checks()` and `board_value_checks()` in
`library/src/solver_if.cpp`.
- The par entry points validate the double dummy table
(`par_table_checks()`) and their `dealer` and `vulnerable` parameters. Both
checks were added after fuzzing found an out-of-range table overflowing a
fixed character buffer and a negative `dealer` indexing a string table.
- `CalcDDtable()`, `CalcDDtablePBN()`, `CalcAllTables*()` and the C++
`calc_dd_table()` overloads validate the deal (`table_deal_checks()`) with
the same three rules `SolveBoard()` enforces: rank bits in range, no
duplicate cards, equal card counts per hand.
- `CalcAllTablesN()` and `CalcAllTablesPBNN()` additionally range-check
`no_of_tables` against the fixed capacity of their arrays. **`CalcAllTablesX()`
and `CalcAllTablesPBNX()` do not**: they exist precisely to accept an
arbitrary deal count, allocate for it, and guard only against integer
overflow. Their count is bounded by the caller, not by the library, and the
array must actually hold that many deals — so a caller exposing these two to
untrusted input must bound the count itself, both against a hostile value
and against memory exhaustion.
- `convert_from_pbn()` silently ignores characters it does not recognise
rather than rejecting the string, so a PBN deal with an invalid rank parses
one card short. The resulting deal is now rejected downstream, but the error
code says `RETURN_CARD_COUNT` rather than `RETURN_PBN_FAULT`.

Callers that cannot guarantee well-formed input should validate at their own
boundary rather than rely on the library to do it.

## Known unfixed issues

There are currently **no known unfixed memory-safety defects**.

Findings are tracked, with reproducers and analysis, in
[`library/tests/fuzz/findings/README.md`](library/tests/fuzz/findings/README.md),
which also records those already fixed and the reproducers kept as regression
seeds. Open findings are documented there openly, because DDS is a library
whose consumers need the information to judge their own exposure.

## If you expose DDS to untrusted input

The library was not designed for this. If you must:

1. **Validate at your boundary.** Reject deals that are not 13 cards per hand
and tables whose entries fall outside 0-13, before calling DDS. If you use
`CalcAllTablesX()` or `CalcAllTablesPBNX()`, bound the deal count yourself:
those two accept an arbitrary count by design and the library will not cap
it for you.
2. **Check return codes.** Every entry point that validates returns a specific
`RETURN_*` value rather than throwing; a caller that ignores it will treat
an unset result structure as a real answer.
3. **Sandbox it.** Run the solver in a separate process with memory and CPU
limits. DDS allocates a large transposition table and its search is
recursive, so resource exhaustion is a denial-of-service consideration
independent of any memory-safety bug. Worker counts are chosen per call by
the `maxThreads` argument of the `*N` and `*X` entry points, where 0 means
hardware concurrency; pass an explicit cap rather than relying on
`SetMaxThreads()`, which is deprecated and ignores its argument.
4. **Consider the WebAssembly build**, whose sandbox contains memory errors by
construction.
5. Note that `DumpInput()` writes a `dump.txt` file into the process working
directory whenever input is rejected. Define `DDS_NO_DUMP_ON_ERROR` to
compile it out.

## Testing and tooling

The project runs AddressSanitizer, ThreadSanitizer and UndefinedBehaviorSanitizer
in CI on Linux and macOS, and carries libFuzzer harnesses for five
input-handling surfaces — four single-deal entry points plus the batch table
API:

```
bazel test --config=asan //library/...
bazel test //library/tests/fuzz/...
bazel run --config=fuzz //library/tests/fuzz:pbn_fuzz -- \
library/tests/fuzz/corpus/pbn -runs=1000000
```

See [`library/tests/fuzz/README.md`](library/tests/fuzz/README.md) for how to
run a campaign and triage a finding. New reproducers are welcome as pull
requests against `library/tests/fuzz/corpus/` once the underlying defect is
fixed.

## Reporting a vulnerability

Please report suspected security issues through
**[GitHub's private vulnerability reporting](https://github.com/dds-bridge/dds/security/advisories/new)**
on this repository, which keeps the report private until a fix is available.

If you would rather not use GitHub, open a regular issue asking for a private
contact, without including details of the problem.

Please include the DDS version or commit, the platform and compiler, a
reproducer (a corpus file for one of the fuzz harnesses is ideal), and any
sanitizer output.

**What to expect.** DDS is maintained by a small group of volunteers, so please
allow time for a response. Issues in the categories described under "Input trust
model" above are likely to be treated as ordinary bugs and fixed in the open,
since the trust model is documented rather than implied. Issues that break the
documented model — anything reachable with well-formed, legal input, or any
out-of-bounds *write* — will be handled privately until fixed.
15 changes: 9 additions & 6 deletions doc/dll-description.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ <h3 id="the-multi-thread-double-dummy-solver-functions">The Multi-Thread Double
<p>The double dummy trick values for all 5 * 4 = 20 possible combinations of a hand's trump strain and declarer hand alternatives are solved by a single call to one of the functions <code>CalcDDtable</code> and <code>CalcDDtablePBN</code>. Threads are allocated per strain. in order to save computations.</p>
<p>To obtain better utilization of available threads, the double dummy (DD) tables can be grouped using one of the functions <code>CalcAllTables</code> and <code>CalcAllTablesPBN</code>.</p>
<p>Solving hands can be done much more quickly using one of the multi-thread alternatives for calling SolveBoard. Then a number of hands are grouped for a single call to one of the functions <code>SolveAllBoards</code>, <code>SolveAllChunksBin</code> and <code>SolveAllChunksPBN</code>. The hands are then solved in parallel using the available threads.</p>
<p>The number of threads is automatically configured by DDS on Windows, taking into account the number of processor cores and available memory. The number of threads can be influenced using by calling <code>SetMaxThreads</code>. This function should probably always be called on Linux/Mac, with a zero argument for auto-configuration.</p>
<p>The number of threads is automatically configured by DDS, taking into account the number of processor cores and available memory. <strong><code>SetMaxThreads</code> no longer influences this: its argument is ignored and it is a deprecated alias of <code>InitializeStaticMemory()</code>.</strong> Worker counts are chosen per call by the <code>maxThreads</code> argument of the <code>*N</code> and <code>*X</code> entry points, where 0 selects auto-configuration.</p>
<p>Calling <code>FreeMemory</code> causes DDS to give up its dynamically allocated memory.</p>
<h3 id="the-par-calculation-functions">The PAR Calculation Functions</h3>
<p>The PAR calculation functions find the optimal contract(s) assuming open cards and optimal bidding from both sides. In very rare cases it matters which side or hand that starts the bidding, i.e. which side or hand that is first to bid its optimal contract.</p>
Expand Down Expand Up @@ -315,7 +315,7 @@ <h3 id="double-dummy-value-analyser-functions">Double Dummy Value Analyser Funct
</tr>
<tr><td colspan="4">&nbsp;</td></tr>
<tr>
<td><code><a href="#SetMaxThreads">SetMaxThreads</a></code></td><td><code>int&nbsp;userThreads</code></td><td>PBN</td><td>Used at initial start and can also be called with a request for allocating memory for a specified number of threads. Is apparently¸mandatory on Linux and Mac (optional on Windows)</td>
<td><code><a href="#SetMaxThreads">SetMaxThreads</a></code></td><td><code>int&nbsp;userThreads</code></td><td>PBN</td><td>Deprecated alias of InitializeStaticMemory(); userThreads is ignored and it does not limit the thread count.</td>
</tr>
<tr><td colspan="4">&nbsp;</td></tr>
<tr>
Expand Down Expand Up @@ -1120,14 +1120,14 @@ <h2 id="functions">Functions</h2>
</tbody>
</table>

<p>SetMaxThreads returns the actual number of threads.</p>
<p>SetMaxThreads returns nothing and ignores its argument; it is a deprecated alias of InitializeStaticMemory().</p>
<p>DDS has a preferred memory size per thread, currently about 95 MB, and a maximum memory size per thread, currently about 160 MB. It will also not use more than 70% of the available memory. It will not create more threads than there are processor cores, as this will only require more memory and will not improve performance. Within these constraints, DDS auto-configures the
number of threads.</p>
<p>DDS first detects the number of cores and the available memory. If this doesn't work for some reason, it defaults to 1 thread which is allowed to use the maximum memory size per thread.</p>
<p>DDS then checks whether a number of threads equal to the number of cores will fit within the available memory when each thread may use the maximum memory per thread. If there is not enough memory for this, DDS scales back its ambition. If there is enough memory for the preferred memory size, then DDS still creates a number of threads equal to the number of cores. If there is not even enough memory for this, DDS scales back the number of threads to fit within the memory.</p>
<p>The user can suggest to DDS a number of threads by calling SetMaxThreads. DDS will never create more threads than requested, but it may create fewer if there is not enough memory, calculated as above. Calling SetMaxThreads is optional, not mandatory. DDS will always select a suitable number of threads on its own.</p>
<p>It may be possible, especially on non-Windows systems, to call SetMaxThreads() actively, even though the user does not want to influence the default values. In this case, use a 0 argument.</p>
<p>SetMaxThreads can be called multiple times even within the same session. So it is theoretically possible to change the number of threads dynamically. </p>
<p>SetMaxThreads no longer influences the thread count: its argument is ignored and internal, global batch threading was removed. To cap workers, pass an explicit maxThreads to the *N or *X entry points, or manage concurrency in the calling application (typically one SolverContext per thread). DDS will otherwise select a suitable number of threads on its own.</p>
<p>Calling SetMaxThreads() is harmless but has no effect beyond initialisation; InitializeStaticMemory() is the non-deprecated spelling.</p>
<p>SetMaxThreads can be called multiple times, but it cannot change the number of threads: use the per-call maxThreads argument instead.</p>
<p>It is possible to ask DDS to give up its dynamically allocated memory by calling FreeMemory. This could be useful for instance if there is a long pause where DDS is not used within a session. DDS will free its memory when the DLL detaches from the user program, so there is no need for the user to call this function before detaching.
<a name="ReturnCodes"></a></p>
<h2 id="return-codes">Return codes</h2>
Expand Down Expand Up @@ -1217,6 +1217,9 @@ <h2 id="return-codes">Return codes</h2>
<tr>
<td>-301</td><td>RETURN_CHUNK_SIZE</td><td>SolveAllChunks\*(), returned when the chunk size is < 1.</td>
</tr>
<tr>
<td>-401</td><td>RETURN_PAR_TABLE_FAULT</td><td>Par(), SidesPar(), SidesParBin(), DealerPar(), DealerParBin(), returned when the double dummy table pointer is null, or a table entry is outside the range 0 to 13.</td>
</tr>
</tbody>
</table>

Expand Down
Loading