Harden SSH parsing and add notice tooling

- Enforce exact service and channel names with bounded failure parsing
- Add hash-pinned offline notice assembly and regression coverage
- Record advisory dispositions, provenance, integration evidence, and
  remaining gates
This commit is contained in:
2026-09-16 15:06:38 +02:00
parent bea33e1c95
commit 51f835c22f
29 changed files with 3332 additions and 46 deletions
+17
View File
@@ -0,0 +1,17 @@
# Release notice tool tests
Run `python3 tests/release_notices/run.py` from the repository root. Uses Python's
standard library and isolated temporary fixtures only; no managed package,
SDK, toolchain, PlatformIO, network, or device is required. Linux/POSIX path and
descriptor semantics match the notice tool.
Covers exact full-text/excerpt preservation and manifests; deterministic bytes
across moved roots/changed mtimes; missing, empty, changed and oversized inputs;
source body drift outside excerpts; bounds/schema; traversal and symlinks in
input/output ancestry; FIFOs/directories; existing user-data preservation;
explicit output requirement; unlisted secret/config/build exclusion; incomplete
write behavior; and success/failure CLI exits.
Real installed-input assembly and recipient delivery are separate checks; see
`docs/release_packaging.md`. Passing these tests is not license clearance or
proof of corresponding-source compliance.
+296
View File
@@ -0,0 +1,296 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-3.0-only
"""Temporary-fixture contract tests; no installed dependencies or device required."""
import copy
import importlib.util
import json
import os
from pathlib import Path
import subprocess
import sys
import tempfile
import unittest
from unittest import mock
sys.dont_write_bytecode = True
PROJECT = Path(__file__).absolute().parents[2]
SPEC = importlib.util.spec_from_file_location("release_notices", PROJECT / "tools/release_notices.py")
notices = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(notices)
class BundleTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory(prefix="release-notices-test-")
self.addCleanup(self.temp.cleanup)
self.base = Path(self.temp.name)
self.roots = {name: self.base / name for name in notices.ROOTS}
for root in self.roots.values():
root.mkdir()
self.catalog = {"schema": 1, "snapshot": {"fixture": "1"}, "inputs": []}
self.add_input("project", "LICENSE", b"Full license\nCopyright holder\n")
self.add_input("sdk", "nested/COPYING", b"First grant\nSecond grant\nDisclaimer\n")
self.add_input("toolchain", "source.c", b"/* full notice */\nint code;\n", [0, 18])
self.output = self.base / "bundle"
def add_input(self, root, path, data, span=None):
target = self.roots[root] / path
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(data)
payload = data if span is None else data[span[0]:span[1]]
self.catalog["inputs"].append({
"root": root, "path": path, "size": len(data),
"sha256": notices.digest(data), "range": span,
"output_sha256": notices.digest(payload), "purpose": "test fixture",
})
def run_bundle(self, output=None, catalog=None):
return notices.assemble(self.roots, output or self.output,
notices.json_bytes(catalog or self.catalog))
def assert_preflight_failure(self):
with self.assertRaises((notices.NoticeError, OSError)):
self.run_bundle()
self.assertFalse(self.output.exists())
@staticmethod
def contents(root):
return {p.relative_to(root).as_posix(): p.read_bytes()
for p in root.rglob("*") if p.is_file()}
def test_exact_bytes_and_manifest(self):
manifest = self.run_bundle()
self.assertEqual(json.loads((self.output / "manifest.json").read_bytes()), manifest)
for entry in manifest["inputs"]:
data = (self.output / entry["output"]).read_bytes()
self.assertEqual(notices.digest(data), entry["output_sha256"])
self.assertEqual(len(data), entry["output_size"])
self.assertEqual((self.output / "inputs/sdk/nested/COPYING").read_bytes(),
b"First grant\nSecond grant\nDisclaimer\n")
self.assertEqual((self.output / "inputs/toolchain/source.c.notice.txt").read_bytes(),
b"/* full notice */\n")
self.assertNotIn(str(self.base), (self.output / "manifest.json").read_text())
self.assertEqual(self.output.stat().st_mode & 0o777, 0o700)
def test_deterministic_order_paths_and_mtime(self):
self.run_bundle()
first = self.contents(self.output)
# Moving the input roots and changing source mtimes must not affect bytes.
for name, root in list(self.roots.items()):
moved = self.base / (name + "-moved")
root.rename(moved)
self.roots[name] = moved
for file in moved.rglob("*"):
os.utime(file, (123456789, 123456789))
self.run_bundle(self.base / "second")
self.assertEqual(first, self.contents(self.base / "second"))
def test_missing_source(self):
(self.roots["sdk"] / "nested/COPYING").unlink()
self.assert_preflight_failure()
def test_changed_source_same_size(self):
path = self.roots["project"] / "LICENSE"
path.write_bytes(b"x" * path.stat().st_size)
self.assert_preflight_failure()
def test_changed_non_notice_source_body(self):
path = self.roots["toolchain"] / "source.c"
path.write_bytes(path.read_bytes().replace(b"code", b"evil"))
self.assert_preflight_failure()
def test_empty_source(self):
(self.roots["project"] / "LICENSE").write_bytes(b"")
self.assert_preflight_failure()
def test_growing_source(self):
path = self.roots["project"] / "LICENSE"
with path.open("ab") as stream:
stream.write(b"unexpected")
self.assert_preflight_failure()
def test_existing_directory_never_modified(self):
self.output.mkdir()
marker = self.output / "user-data"
marker.write_bytes(b"keep me")
with self.assertRaises(FileExistsError):
self.run_bundle()
self.assertEqual(self.contents(self.output), {"user-data": b"keep me"})
def test_existing_empty_directory_rejected(self):
self.output.mkdir()
with self.assertRaises(FileExistsError):
self.run_bundle()
self.assertEqual(list(self.output.iterdir()), [])
def test_existing_file_never_modified(self):
self.output.write_bytes(b"user data")
with self.assertRaises(FileExistsError):
self.run_bundle()
self.assertEqual(self.output.read_bytes(), b"user data")
def test_output_symlink_rejected(self):
self.output.symlink_to(self.base / "absent")
with self.assertRaises(FileExistsError):
self.run_bundle()
self.assertTrue(self.output.is_symlink())
self.assertFalse((self.base / "absent").exists())
def test_output_parent_symlink_rejected(self):
link = self.base / "link"
link.symlink_to(self.base, target_is_directory=True)
with self.assertRaises(OSError):
self.run_bundle(link / "bundle")
self.assertFalse(self.output.exists())
def test_output_inside_inputs_rejected(self):
for root in self.roots.values():
with self.subTest(root=root), self.assertRaises(notices.NoticeError):
self.run_bundle(root / "bundle")
self.assertFalse((root / "bundle").exists())
def test_parent_traversal_output_rejected(self):
with self.assertRaises(notices.NoticeError):
self.run_bundle(self.base / "project/../bundle")
self.assertFalse(self.output.exists())
def test_missing_parent_not_created(self):
with self.assertRaises(FileNotFoundError):
self.run_bundle(self.base / "absent/bundle")
self.assertFalse((self.base / "absent").exists())
def test_source_file_symlink_even_to_identical_bytes_rejected(self):
path = self.roots["project"] / "LICENSE"
other = self.base / "other"
path.rename(other)
path.symlink_to(other)
self.assert_preflight_failure()
def test_source_directory_symlink_rejected(self):
path = self.roots["sdk"] / "nested"
other = self.base / "other"
path.rename(other)
path.symlink_to(other, target_is_directory=True)
self.assert_preflight_failure()
def test_input_root_symlink_rejected(self):
root = self.roots["sdk"]
other = self.base / "other"
root.rename(other)
root.symlink_to(other, target_is_directory=True)
self.assert_preflight_failure()
def test_fifo_does_not_block(self):
path = self.roots["project"] / "LICENSE"
path.unlink()
os.mkfifo(path)
self.assert_preflight_failure()
def test_directory_is_not_a_notice(self):
path = self.roots["project"] / "LICENSE"
path.unlink()
path.mkdir()
self.assert_preflight_failure()
def test_catalog_paths_rejected(self):
for path in ("../secret", "/etc/passwd", "nested/../../secret", "a//b",
"./LICENSE", "a\\b", "", "a/./b", "a\x00b"):
with self.subTest(path=path):
catalog = copy.deepcopy(self.catalog)
catalog["inputs"][0]["path"] = path
with self.assertRaises(notices.NoticeError):
self.run_bundle(catalog=catalog)
self.assertFalse(self.output.exists())
def test_invalid_catalog_entries(self):
for field, value in (("root", "unknown"), ("size", 0),
("size", notices.MAX_FILE + 1), ("size", True),
("sha256", "wrong"), ("range", [-1, 2]),
("range", [0, 99999]), ("range", [2, 1]),
("output_sha256", "0" * 64)):
with self.subTest(field=field, value=value):
catalog = copy.deepcopy(self.catalog)
catalog["inputs"][0][field] = value
with self.assertRaises(notices.NoticeError):
self.run_bundle(catalog=catalog)
self.assertFalse(self.output.exists())
def test_duplicate_input(self):
self.catalog["inputs"].append(self.catalog["inputs"][0])
self.assert_preflight_failure()
def test_entry_count_bound(self):
self.catalog["inputs"] *= notices.MAX_ENTRIES
self.assert_preflight_failure()
def test_total_size_bound(self):
with mock.patch.object(notices, "MAX_TOTAL", 1):
self.assert_preflight_failure()
def test_unlisted_secrets_builds_and_configs_never_read(self):
for name in ("sdkconfig", ".env", "device.pem", ".pio/build/firmware.bin",
"backups/credentials.json"):
path = self.roots["project"] / name
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(b"SECRET_DO_NOT_COPY")
original = notices.read_bounded
calls = []
def tracked(fd, path, limit=notices.MAX_FILE):
calls.append(path)
return original(fd, path, limit)
with mock.patch.object(notices, "read_bounded", side_effect=tracked):
self.run_bundle()
self.assertEqual(set(calls), {e["path"] for e in self.catalog["inputs"]})
self.assertFalse(any(b"SECRET_DO_NOT_COPY" in b for b in self.contents(self.output).values()))
def test_failed_write_has_no_completion_marker_or_cleanup(self):
original = notices.write_new
def fail(fd, path, data):
if path == "README.txt":
raise OSError("injected write failure")
original(fd, path, data)
with mock.patch.object(notices, "write_new", side_effect=fail):
with self.assertRaises(OSError):
self.run_bundle()
self.assertTrue(self.output.exists())
self.assertFalse((self.output / "manifest.json").exists())
# Retrying cannot overwrite or delete even this partial output.
with self.assertRaises(FileExistsError):
self.run_bundle()
def test_binary_and_non_utf8_output_rejected(self):
for data in (b"binary\x00notice", b"invalid\xffnotice"):
with self.subTest(data=data):
self.catalog["inputs"] = []
self.add_input("project", "bad", data)
self.assert_preflight_failure()
def test_cli_requires_explicit_output(self):
result = subprocess.run([sys.executable, str(PROJECT / "tools/release_notices.py"),
"--sdk-root", str(self.roots["sdk"]),
"--toolchain-root", str(self.roots["toolchain"])],
capture_output=True, timeout=10)
self.assertEqual(result.returncode, 2)
self.assertIn(b"--output", result.stderr)
def test_cli_with_isolated_policy_and_sources(self):
policy = self.base / "policy"
(policy / "tools").mkdir(parents=True)
(policy / "third_party/release-notices").mkdir(parents=True)
script = policy / "tools/release_notices.py"
script.write_bytes((PROJECT / "tools/release_notices.py").read_bytes())
(policy / notices.CATALOG).write_bytes(notices.json_bytes(self.catalog))
command = [sys.executable, str(script), "--project-root", str(self.roots["project"]),
"--sdk-root", str(self.roots["sdk"]), "--toolchain-root",
str(self.roots["toolchain"]), "--output", str(self.output)]
result = subprocess.run(command, capture_output=True, timeout=10)
self.assertEqual(result.returncode, 0, result.stderr)
before = self.contents(self.output)
result = subprocess.run(command, capture_output=True, timeout=10)
self.assertEqual(result.returncode, 1)
self.assertEqual(before, self.contents(self.output))
if __name__ == "__main__":
unittest.main(verbosity=2)
+98 -17
View File
@@ -4,6 +4,7 @@ Run from the project root (installed pinned sources and a host C compiler requir
```sh
CCACHE_DISABLE=1 python3 tests/wolfssh_parser_contract/run.py
CCACHE_DISABLE=1 python3 tests/wolfssh_parser_contract/review.py --profile
CCACHE_DISABLE=1 python3 tests/wolfssh_auth_contract/run.py --host-only
CCACHE_DISABLE=1 python3 tests/sdk_security_overrides/run.py
```
@@ -17,7 +18,47 @@ run with guard pages and UBSan trap instrumentation, both with and without
CMake source replacement using fixtures. This is **not** a claim that an existing
production generated file or firmware binary contains these edits.
## Reviewed upstream evidence and exact implementation scope
## 2026-09-16 remaining-parser review
The [finite applicability review](../../docs/ssh_parser_remaining_review.md) closes
PR899's current-profile disposition and corrects the earlier description of PR902:
902 is **service-name validation**, not channel-callback hardening.
- PR899 client RSA/ECC key-skip hunks remain unapplied: RSA is disabled, and the
sole client host-key call chain is blocked by current server KEX ordering.
- The current `DoChannelFailure` now bounds exactly one recipient ID, verifies the
local channel, and consumes it before returning the existing fatal
`WS_CHANOPEN_FAILED`. No state or channel mutation is introduced.
- The server subset of PR902 rejects every name except exact `ssh-userauth` before
publishing the index/state transition. The owner closes on the error; unlike
upstream, no best-effort disconnect packet is queued. Client service acceptance
stays unchanged and unreachable in the current server role.
- PR918/919 forwarding fixes are not applied with `WOLFSSH_FWD` disabled.
- The follow-up closes `DoChannelRequest` prefix/NUL aliases: all nine recognized
names require exact length **before** exact byte comparison. Supported branch
bodies and unknown-request success/ACK handling remain unchanged. Production
enables TERM but not SHELL/AGENT: PTY/exit branches are present, window-change and
agent branches absent. Disabled branches are tested separately without enabling
firmware features.
`pr899.patch`, `pr902.patch`, `pr918.patch`, `pr919.patch` and `provenance.json`
archive bytes fetched on 2026-09-16 and verified equal to the corresponding upstream
commit patches. Tests pin hashes, commits and URLs independently. `review.py`
reverses only the independently specified new notice, two initial handler deltas
and nine exact channel-name predicates and
requires the **whole prior original+ordering+parser generated-source hash**. This
fences unchanged client parsers, packet dispatch, crypto callers and request branch bodies without
normalizing away edits. The existing exact original hash and exact-once anchors
remain mandatory. No ordering delta or crypto configuration is changed.
`--profile` reads the unique saved production compile command, checks whether its
input is exactly the known prior baseline or fresh current source, replays real
Xtensa feature preprocessing, and syntax-checks a temporary fresh source. A prior
input is explicitly reported as **not regenerated**, never current-build evidence.
Existing strict build-registration suites still reject stale generated bytes.
No network or production build-tree writes occur in either test command.
## Earlier upstream evidence and retained implementation scope
Official diffs fetched and inspected on 2026-09-15:
@@ -39,8 +80,8 @@ Covered:
validates the full string with `GetSize` before `GetString`, retaining the old
strict `< WOLFSSH_MAX_NAMESZ` limit rather than upstream's truncation behavior.
Failure leaves the caller index and client state untouched. Successful state
transition remains exactly the old one; service-name semantic validation is
not added. The original `GetSize` already uses bounded subtraction and needs
transition remains exactly the old one for `ssh-userauth`; the 2026-09-16
addition above rejects other names. The original `GetSize` already uses bounded subtraction and needs
no change. `GetString` now uses it and rejects zero output capacity before
subtraction/copy; ordinary bounded truncation semantics remain unchanged.
- **881 subset:** `DoChannelWindowAdjust` rejects addition exceeding the 32-bit
@@ -77,22 +118,56 @@ both enabled in this server's reviewed profile. Advertisement is not treated as
- **892:** client `DoServiceAccept`, agent key preparation, daemon authentication,
Windows terminal changes. Password framing/wiping is the existing local
correction, intentionally not replaced with upstream's later formulation.
- **899:** no hunks applied. `ParseRSAPubKey`/`ParseECCPubKey` skips require separate
client/KEX reachability analysis (not the server's `DoUserAuthRequestEcc`).
The old `DoChannelFailure` does not parse a channel ID at all; changing only its
`len != 0` typo would not establish a bounded channel-ID parser. Its existing
behavior is left unchanged rather than claiming the later parser contract.
Windows port/terminal hunks are out of scope.
- **899:** unused client key skips and Windows port/terminal hunks are deliberately
not applied; their current-profile applicability is resolved in the new review,
not a claim that the dependency is fixed for clients. CHANNEL_FAILURE is covered
by the bounded local adaptation above.
- **880:** certificate RSA, agent, daemon, terminal, TPM and SCP changes are not
applied. No complete PR-880 closure is claimed.
- Message ordering/state machine (including CVE-2025-14942), service semantics,
standalone ECC curve-name/key-blob semantic validation, other parsers, client
- Message ordering/state machine (including CVE-2025-14942) has its separate
restricted-profile suite. Standalone ECC curve-name/key-blob semantic validation,
other parsers, client
behavior and broader crypto advisories are outside this slice. ECC point/import
validation belongs to the separate crypto-policy owner and is not changed here.
Existing account/key authorization, numeric r/s validity and Ed25519 raw
signature-size/crypto validity checks remain owned by their existing layers.
## Test boundaries
## Exact channel-request and application-gate tests
`channel_request.py` extracts complete generated `DoChannelRequest`, `GetBoolean`,
`GetUint32`, `GetSize`, `GetString`, `GetStringAlloc` and `GetStringRef` functions.
It independently hash-pins/extracts the actual application `accept_shell`,
`reject_channel_request` and complete `process_handshake`, and checks callback
registration and per-slot context wiring. `channel_request.c` runs those real
bodies with channel/context layouts, routing, allocation and platform doubles.
This is not a task/broker/console integration test.
Five feature profiles run in both stack modes with guard pages and UBSan traps:
production TERM-only, no-terminal, SHELL-only, TERM+SHELL and TERM+SHELL+AGENT.
Alternative flags affect host fixtures only. The matrix covers all nine names:
valid requests; every proper prefix (including empty); suffixes; embedded NULs
and same-length wrong bytes at every position; 3165-byte names; every header,
name, boolean and payload truncation; nonzero offsets; oversized/wrapping declared
lengths; reply/no-reply; invalid channel and send failure; resize callback absence
and failure. Instrumented comparisons assert the read length equals the initialized
name length, catching removed short-name guards even inside the accessible stack
buffer. Unknown/disabled names must not parse payload fields or invoke callbacks,
but retain existing success/ACK/consumption behavior.
Real callback/admission cases verify shell-only routing for both roles; null
callback context and absent shell callback; authentication/principal/currentness
requirements; rejected exec/subsystem after a prior shell, even with no reply;
and optional PTY callback behavior. Malformed exec/subsystem payloads still invoke
the existing rejecting callbacks and never become shell admission, matching the
unchanged handler bodies.
**2,737 cases per stack mode** for each TERM-containing profile, **2,735** for each
of the other two profiles. **18 prefix/length-guard mutations** and **two actual
application admission-gate mutations** are rejected, in addition to the existing
11 parser mutations. The independent whole-source reversal checks that only the
nine predicates change within `DoChannelRequest`, with no branch refactor.
## Earlier parser test boundaries
The C matrix exercises zero/truncated/exact/oversized/wrapping lengths, invalid
and nonzero offsets, zero-capacity output, copy canaries, window overflow boundary
@@ -115,15 +190,21 @@ covers both labels, all truncations, shortened/oversized/wrapping/trailing
signature strings, exact raw-signature forwarding and unchanged streamed message
bytes. Both paths retain crypto rejection behavior using doubles.
Validation: **3,124 cases per stack mode** (both pass with UBSan trap mode), plus
**six guard-removal mutations rejected**: ECC nested read bound, inner/outer exact
consumption, Ed25519 key/signature OR checks, and Ed25519 exact consumption. The
Validation: **3,258 cases per stack mode** (both pass with UBSan trap mode), plus
**11 guard-removal mutations rejected**: ECC nested read bound, inner/outer exact
consumption, Ed25519 key/signature OR checks, Ed25519 exact consumption, service
length/byte equality and channel-failure bounded read/exact end/known recipient.
`remaining.c` adds 134 counted cases, plus assertions for null arguments and an
unknown channel: all service truncations, prefix/suffix/embedded-NUL names,
nonzero offsets, channel truncation/trailing bytes, and wrapping indices. The
mutation copies exist only in temporary test files; core dumps are disabled for
those intentionally failing runs. These are framing-valid fixtures with crypto
doubles, not independently verified real signatures.
The runner also compares complete password, packet dispatch, public-key dispatch and selected deferred
functions against the pre-slice generated baseline to fence accidental changes.
The runner also compares complete password, packet dispatch, public-key dispatch and unused
key parsers against the pre-parser generated baseline to fence accidental changes.
The independent whole-source hash contract separately preserves the exact ordering
and all prior parser changes while allowing only this review's three corrections.
The separate auth suite executes its 135 password/control-flow cases, including
payload wipe, callback framing and asynchronous pending retention. No whole-library
fuzzing, real SSH exchange, firmware compile, hardware timing or security sign-off
@@ -0,0 +1,375 @@
/* SPDX-License-Identifier: GPL-3.0-only
* Execute extracted generated handler/helpers and real application callbacks /
* complete handshake admission. OS, channel, allocator and routing are doubles. */
#include <assert.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
typedef uint8_t byte;
typedef uint32_t word32;
#define WS_SUCCESS 0
#define WS_BUFFER_E -1
#define WS_INVALID_CHANID -2
#define WS_MEMORY_E -3
#define WS_FATAL_ERROR -4
#define UINT32_SZ 4
#define BOOLEAN_SZ 1
#define WOLFSSH_MAX_NAMESZ 32
#define WS_CHANNEL_ID_SELF 0
#define WOLFSSH_SESSION_SHELL 1
#define WOLFSSH_SESSION_EXEC 2
#define WOLFSSH_SESSION_SUBSYSTEM 3
#define CLIENT_DONE 42
#define WLOG(...) ((void)0)
#define WMEMCPY memcpy
#define WSTRNCMP strncmp
#define WMALLOC(n,h,t) malloc(n)
#define WFREE(p,h,t) free(p)
typedef struct { int sessionType; char *command; } WOLFSSH_CHANNEL;
typedef struct {
void *heap;
int (*channelReqShellCb)(WOLFSSH_CHANNEL*, void*);
int (*channelReqExecCb)(WOLFSSH_CHANNEL*, void*);
int (*channelReqSubsysCb)(WOLFSSH_CHANNEL*, void*);
void *agentCb;
} Context;
typedef struct WOLFSSH {
Context *ctx;
void *channelReqCtx, *termCtx;
int clientState, useAgent;
byte *modes;
word32 modesSz, widthChar, heightRows, widthPixels, heightPixels, exitStatus;
int (*termResizeCb)(struct WOLFSSH*, word32, word32, word32, word32, void*);
} WOLFSSH;
static WOLFSSH_CHANNEL channel;
static unsigned finds, replies, shell_calls, exec_calls, subsys_calls, resizes, reads32, cases;
static int reply_ok, reply_error, resize_error;
static word32 expected_type_length;
static int compare_type(const void *a, const void *b, size_t n)
{
/* This also detects an unguarded read of the uninitialized short-name tail
* inside the handler's accessible local 32-byte buffer. */
assert(n == expected_type_length);
return memcmp(a,b,n);
}
#define WMEMCMP compare_type
static void ato32(const byte *p, word32 *v)
{ reads32++; *v=((word32)p[0]<<24)|((word32)p[1]<<16)|((word32)p[2]<<8)|p[3]; }
static void put(byte *p, word32 v)
{ p[0]=v>>24; p[1]=v>>16; p[2]=v>>8; p[3]=v; }
static WOLFSSH_CHANNEL *ChannelFind(WOLFSSH *ssh, word32 id, int side)
{ assert(side==WS_CHANNEL_ID_SELF); finds++; return id==7?&channel:NULL; }
static int SendChannelSuccess(WOLFSSH *ssh, word32 id, int success)
{ replies++; reply_ok=success; return reply_error; }
#include "channel_actual.c"
/* Minimal external dependencies for the complete real process_handshake body. */
typedef int esp_err_t;
#define ESP_OK 0
#define ESP_ERR_INVALID_STATE -1
#define SSH_TRANSPORT_WOLFSSH_READ_BUDGET 99
#define USER_ROLE_USER 1
#define USER_ROLE_ADMIN 2
#define SSH_TRANSPORT_ROUTE_BROKER 1
#define SSH_TRANSPORT_ROUTE_ADMIN_CONSOLE 2
#define SSH_TRANSPORT_SESSION_ACTIVE 3
typedef struct { int role; } Principal;
typedef struct {
WOLFSSH *ssh;
bool shell_requested, authenticated, principal_valid;
Principal principal;
int64_t handshake_deadline_us, last_reconcile_us;
unsigned io_read_budget;
size_t console_slot_index;
int route, state;
} ssh_slot_t;
typedef struct { size_t slot_index; } admin_ssh_console_token_t;
static struct {
unsigned request_rejections, handshake_timeouts, broker_failures,
admin_console_admission_failures, admin_console_admissions,
handshake_successes, handshake_failures;
} s_counters;
static int s_admin_console_owner;
static bool principal_is_current=true;
static unsigned closes, broker_routes, admin_routes;
static void add_counter(unsigned *p, unsigned n) { *p+=n; }
static int64_t esp_timer_get_time(void) { return 1; }
static void request_slot_close(ssh_slot_t *s, bool revoked) { closes++; }
static int wolfSSH_accept(WOLFSSH *ssh) { return WS_SUCCESS; }
static int wolfSSH_GetSessionType(WOLFSSH *ssh) { return channel.sessionType; }
static esp_err_t user_database_principal_is_current(Principal *p, bool *current)
{ *current=principal_is_current; return ESP_OK; }
static esp_err_t connect_broker(ssh_slot_t *s, size_t i) { broker_routes++; return ESP_OK; }
static admin_ssh_console_token_t admin_console_token(ssh_slot_t *s, size_t i)
{ return (admin_ssh_console_token_t){i}; }
static esp_err_t admin_ssh_console_open_available(admin_ssh_console_token_t *t,
Principal *p, int *owner) { admin_routes++; return ESP_OK; }
static bool slot_principal_is_current(ssh_slot_t *s) { return principal_is_current; }
static void disconnect_failed_admission(ssh_slot_t *s) {}
static bool wolfssh_would_block(WOLFSSH *ssh, int result) { return false; }
#include "channel_application.c"
static int shell_cb(WOLFSSH_CHANNEL *c, void *s)
{ shell_calls++; assert(c==&channel); return accept_shell(c,s); }
static int exec_cb(WOLFSSH_CHANNEL *c, void *s)
{ exec_calls++; assert(c==&channel); return reject_channel_request(c,s); }
static int subsys_cb(WOLFSSH_CHANNEL *c, void *s)
{ subsys_calls++; assert(c==&channel); return reject_channel_request(c,s); }
static int resize_cb(WOLFSSH *s, word32 w, word32 h, word32 x, word32 y, void *ctx)
{ resizes++; assert(w==80 && h==24 && x==640 && y==480); return resize_error; }
enum { ENV, SHELL, EXEC, SUBSYS, PTY, WINDOW, STATUS, SIGNAL, AGENT, UNKNOWN };
static const char *names[]={"env","shell","exec","subsystem","pty-req",
"window-change","exit-status","exit-signal","auth-agent-req@openssh.com"};
static int enabled(int kind)
{
switch(kind) {
case ENV: case SHELL: case EXEC: case SUBSYS: return 1;
#ifdef WOLFSSH_TERM
case PTY: return 1;
#endif
#if defined(WOLFSSH_TERM) && defined(WOLFSSH_SHELL)
case WINDOW: return 1;
#endif
#if defined(WOLFSSH_TERM) || defined(WOLFSSH_SHELL)
case STATUS: case SIGNAL: return 1;
#endif
#ifdef WOLFSSH_AGENT
case AGENT: return 1;
#endif
default: return 0;
}
}
static word32 append_string(byte *p, const void *s, word32 n)
{ put(p,n); memcpy(p+4,s,n); return n+4; }
static word32 payload(byte *p, int kind)
{
word32 n=0;
if(kind==ENV) {
n+=append_string(p+n,"A",1); n+=append_string(p+n,"B",1);
} else if(kind==EXEC || kind==SUBSYS) {
n+=append_string(p+n,"cmd",3);
} else if(kind==PTY || kind==WINDOW) {
if(kind==PTY) n+=append_string(p+n,"xterm",5);
put(p+n,80); put(p+n+4,24); put(p+n+8,640); put(p+n+12,480); n+=16;
if(kind==PTY) n+=append_string(p+n,"\0",1);
} else if(kind==STATUS) {
put(p,123); n=4;
} else if(kind==SIGNAL) {
n+=append_string(p+n,"TERM",4); p[n++]=0;
n+=append_string(p+n,"msg",3); n+=append_string(p+n,"en",2);
}
return n;
}
static word32 packet(byte *p, const byte *name, word32 n, int kind, int want)
{
put(p,7); word32 size=4+append_string(p+4,name,n); p[size++]=(byte)want;
return size+payload(p+size,kind);
}
static void reset(WOLFSSH *ssh, Context *ctx, ssh_slot_t *slot)
{
free(channel.command); free(ssh->modes);
memset(&channel,0,sizeof(channel)); memset(ssh,0,sizeof(*ssh));
memset(ctx,0,sizeof(*ctx)); memset(slot,0,sizeof(*slot));
ctx->channelReqShellCb=shell_cb; ctx->channelReqExecCb=exec_cb;
ctx->channelReqSubsysCb=subsys_cb; ctx->agentCb=ctx;
ssh->ctx=ctx; ssh->channelReqCtx=slot; ssh->termResizeCb=resize_cb;
ssh->clientState=9; ssh->exitStatus=99;
slot->ssh=ssh; slot->authenticated=true; slot->principal_valid=true;
slot->principal.role=USER_ROLE_USER; slot->handshake_deadline_us=100;
finds=replies=shell_calls=exec_calls=subsys_calls=resizes=reads32=0;
closes=broker_routes=admin_routes=0; reply_ok=-1; reply_error=resize_error=0;
principal_is_current=true; memset(&s_counters,0,sizeof(s_counters));
}
static void unchanged(const WOLFSSH *ssh, const ssh_slot_t *slot)
{
assert(!shell_calls && !exec_calls && !subsys_calls && !resizes);
assert(!slot->shell_requested && channel.sessionType==0 && !channel.command);
assert(ssh->clientState==9 && !ssh->modes && !ssh->modesSz);
assert(!ssh->widthChar && !ssh->heightRows && !ssh->widthPixels && !ssh->heightPixels);
assert(ssh->exitStatus==99 && !ssh->useAgent);
}
static void run_name(byte *end, const byte *name, word32 n, int wire_kind,
int selected, int want, word32 offset)
{
byte frame[256]; memset(frame,0xcc,sizeof(frame));
word32 len=offset+packet(frame+offset,name,n,wire_kind,want);
byte *p=end-len; memcpy(p,frame,len);
WOLFSSH ssh={0}; Context ctx; ssh_slot_t slot; reset(&ssh,&ctx,&slot);
expected_type_length=n<32?n:31;
word32 idx=offset;
assert(DoChannelRequest(&ssh,p,len,&idx)==WS_SUCCESS);
assert(idx==len && finds==1 && replies==(unsigned)want);
assert(memcmp(p,frame,len)==0);
if(!enabled(selected)) {
unchanged(&ssh,&slot);
assert(reads32==2); /* No payload parser for unknown/disabled names. */
assert(!want || reply_ok==1); /* Preserve unknown-request policy. */
} else {
const unsigned extra_reads[]={2,0,1,1,6,4,1,3,0};
assert(reads32==2+extra_reads[selected]);
assert(shell_calls==(unsigned)(selected==SHELL));
assert(exec_calls==(unsigned)(selected==EXEC));
assert(subsys_calls==(unsigned)(selected==SUBSYS));
assert(!want || reply_ok==(selected!=EXEC && selected!=SUBSYS));
assert(slot.shell_requested==(selected==SHELL));
if(selected==SHELL || selected==EXEC || selected==SUBSYS) {
assert(ssh.clientState==CLIENT_DONE);
assert(channel.sessionType==(selected==SHELL?WOLFSSH_SESSION_SHELL:
selected==EXEC?WOLFSSH_SESSION_EXEC:WOLFSSH_SESSION_SUBSYSTEM));
if(selected!=SHELL) assert(strcmp(channel.command,"cmd")==0);
} else assert(ssh.clientState==9 && channel.sessionType==0);
if(selected==PTY || selected==WINDOW) {
assert(resizes==1 && ssh.widthChar==80 && ssh.heightRows==24);
if(selected==PTY) assert(ssh.modesSz==1 && ssh.modes[0]==0);
} else assert(!resizes);
assert(ssh.exitStatus==(selected==STATUS?123u:99u));
assert(ssh.useAgent==(selected==AGENT));
}
/* Actual application gate: even success/ACK is not shell admission. */
process_handshake(&slot,0);
assert(broker_routes==(unsigned)(selected==SHELL));
assert(closes==(unsigned)(selected!=SHELL));
free(channel.command); channel.command=NULL; free(ssh.modes);
cases++;
}
static void names_matrix(byte *end)
{
for(int k=ENV;k<UNKNOWN;k++) {
word32 n=(word32)strlen(names[k]); byte name[80]; memcpy(name,names[k],n);
for(int want=0;want<=1;want++) for(word32 off=0;off<=3;off+=3) {
run_name(end,name,n,k,k,want,off);
/* Every proper prefix, including the empty name. */
for(word32 j=0;j<n;j++) run_name(end,name,j,k,UNKNOWN,want,off);
name[n]='x'; run_name(end,name,n+1,k,UNKNOWN,want,off);
name[n]=0; name[n+1]='x'; run_name(end,name,n+2,k,UNKNOWN,want,off);
for(word32 j=0;j<n;j++) {
byte saved=name[j]; name[j]=0;
run_name(end,name,n,k,UNKNOWN,want,off); name[j]=saved;
name[j]='!'; run_name(end,name,n,k,UNKNOWN,want,off); name[j]=saved;
}
for(word32 size=31;size<=65;size++) {
memset(name+n,'x',size-n);
run_name(end,name,size,k,UNKNOWN,want,off);
}
}
}
}
static void truncations(byte *end)
{
for(int k=ENV;k<UNKNOWN;k++) {
word32 n=(word32)strlen(names[k]); byte frame[256];
word32 len=packet(frame,(const byte*)names[k],n,k,1);
for(word32 size=0;size<len;size++) {
byte *p=end-size; memcpy(p,frame,size);
WOLFSSH ssh={0}; Context ctx; ssh_slot_t slot; reset(&ssh,&ctx,&slot);
word32 idx=0; expected_type_length=n;
int ret=DoChannelRequest(&ssh,p,size,&idx);
if(size<9+n) {
assert(ret==WS_BUFFER_E && idx==0 && finds==0 && replies==0);
unchanged(&ssh,&slot);
} else if(enabled(k)) {
assert(ret==WS_BUFFER_E && idx==0 && replies==1 && !reply_ok);
/* Preserve old exec/subsystem callback invocation even if their
* payload parse failed; actual project callbacks only reject. */
assert(!shell_calls && exec_calls==(unsigned)(k==EXEC));
assert(subsys_calls==(unsigned)(k==SUBSYS) && !slot.shell_requested);
assert(!resizes && !ssh.modes);
} else {
assert(ret==WS_SUCCESS && idx==size && replies==1 && reply_ok);
unchanged(&ssh,&slot);
}
process_handshake(&slot,0);
assert(closes==1 && !broker_routes && !admin_routes);
free(channel.command); channel.command=NULL; free(ssh.modes); cases++;
}
}
/* Declared oversized/wrapping name lengths must fail before callbacks. */
const word32 bad[]={UINT32_MAX,UINT32_MAX-3,100};
for(unsigned j=0;j<3;j++) {
byte *p=end-9; put(p,7); put(p+4,bad[j]); p[8]=1;
WOLFSSH ssh={0}; Context ctx; ssh_slot_t slot; reset(&ssh,&ctx,&slot);
word32 idx=0; assert(DoChannelRequest(&ssh,p,9,&idx)==WS_BUFFER_E);
assert(idx==0 && !finds && !replies); unchanged(&ssh,&slot); cases++;
}
}
static void callback_gates(byte *end)
{
for(int k=SHELL;k<=SUBSYS;k++) for(int mode=0;mode<8;mode++) {
WOLFSSH ssh={0}; Context ctx; ssh_slot_t slot; reset(&ssh,&ctx,&slot);
byte frame[64]; word32 n=(word32)strlen(names[k]);
word32 len=packet(frame,(const byte*)names[k],n,k,1);
byte *p=end-len; memcpy(p,frame,len); word32 idx=0; expected_type_length=n;
if(mode==0) ssh.channelReqCtx=NULL;
if(mode==1) ctx.channelReqShellCb=NULL;
if(mode==2) slot.authenticated=false;
if(mode==3) slot.principal_valid=false;
if(mode==4) principal_is_current=false;
if(mode==5) slot.principal.role=USER_ROLE_ADMIN;
if(mode==6) slot.principal.role=0;
assert(DoChannelRequest(&ssh,p,len,&idx)==WS_SUCCESS);
if(k!=SHELL) assert(!reply_ok && !slot.shell_requested);
if(k==SHELL && mode==0) assert(!reply_ok && !slot.shell_requested);
process_handshake(&slot,0);
bool admit=k==SHELL && (mode==5 || mode==7);
assert(closes==(unsigned)!admit);
assert(broker_routes==(unsigned)(admit && mode==7));
assert(admin_routes==(unsigned)(admit && mode==5));
assert(slot.state==(admit?SSH_TRANSPORT_SESSION_ACTIVE:0));
free(channel.command); channel.command=NULL; free(ssh.modes); cases++;
}
/* A prior shell callback must not authorize a subsequent exec/subsystem,
* even without a requested rejection reply. Execute both real dispatches. */
for(int k=EXEC;k<=SUBSYS;k++) for(int want=0;want<=1;want++) {
WOLFSSH ssh={0}; Context ctx; ssh_slot_t slot; reset(&ssh,&ctx,&slot);
byte frame[64]; word32 len=packet(frame,(const byte*)"shell",5,SHELL,0);
byte *p=end-len; memcpy(p,frame,len); word32 idx=0; expected_type_length=5;
assert(DoChannelRequest(&ssh,p,len,&idx)==WS_SUCCESS && slot.shell_requested);
word32 n=(word32)strlen(names[k]);
len=packet(frame,(const byte*)names[k],n,k,want);
p=end-len; memcpy(p,frame,len); idx=0; expected_type_length=n;
assert(DoChannelRequest(&ssh,p,len,&idx)==WS_SUCCESS && slot.shell_requested);
assert(channel.sessionType!=WOLFSSH_SESSION_SHELL);
assert(replies==(unsigned)want && (!want || !reply_ok));
process_handshake(&slot,0);
assert(closes==1 && !broker_routes && !admin_routes);
free(channel.command); channel.command=NULL; free(ssh.modes); cases++;
}
/* Production does not install a terminal resize callback. Also preserve
* the original callback-error propagation when one is installed. */
if(enabled(PTY)) for(int mode=0;mode<2;mode++) {
WOLFSSH ssh={0}; Context ctx; ssh_slot_t slot; reset(&ssh,&ctx,&slot);
byte frame[128]; word32 len=packet(frame,(const byte*)"pty-req",7,PTY,1);
byte *p=end-len; memcpy(p,frame,len); word32 idx=0; expected_type_length=7;
if(mode==0) ssh.termResizeCb=NULL; else resize_error=WS_FATAL_ERROR;
assert(DoChannelRequest(&ssh,p,len,&idx)==(mode?WS_FATAL_ERROR:WS_SUCCESS));
assert(ssh.widthChar==80 && ssh.modesSz==1 && resizes==(unsigned)mode);
assert(!slot.shell_requested && !shell_calls && reply_ok==!mode);
assert(idx==(mode?0:len));
free(ssh.modes); cases++;
}
/* Existing unknown-recipient and send failure behavior is not refactored. */
for(int mode=0;mode<2;mode++) {
WOLFSSH ssh={0}; Context ctx; ssh_slot_t slot; reset(&ssh,&ctx,&slot);
byte frame[64]; word32 len=packet(frame,(const byte*)"shell",5,SHELL,1);
if(mode==0) put(frame,8); else reply_error=WS_FATAL_ERROR;
byte *p=end-len; memcpy(p,frame,len); word32 idx=0; expected_type_length=5;
assert(DoChannelRequest(&ssh,p,len,&idx)==(mode?WS_FATAL_ERROR:WS_INVALID_CHANID));
assert(replies==1 && reply_ok==mode);
if(mode==0) unchanged(&ssh,&slot);
free(ssh.modes); cases++;
}
}
int main(void)
{
long page=sysconf(_SC_PAGESIZE); assert(page>0);
byte *p=mmap(NULL,(size_t)page*2,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0);
assert(p!=MAP_FAILED && mprotect(p+page,page,PROT_NONE)==0);
names_matrix(p+page); truncations(p+page); callback_gates(p+page);
assert(munmap(p,(size_t)page*2)==0);
printf("PASS: %u generated channel-name/payload/callback/admission cases\n",cases);
return 0;
}
@@ -0,0 +1,86 @@
"""Exact generated channel handler plus pinned real application callback gates."""
import hashlib
import os
from pathlib import Path
import subprocess
HERE = Path(__file__).resolve().parent
ROOT = HERE.parents[1]
APP_PINS = {
'accept_shell': '0b625bd3e2fe109f62d8b4d97315c81b05cfd7a8b994e6f1079cff9f8ca99e09',
'reject_channel_request': '871a700936602815f072c240c5a0c232dd113f7ad8744367cda15034d4682a49',
'process_handshake': '2ac274e755d553dac4c19aa45240307ad01b1f68d23a5cf2977e4466cdbf5aba',
}
def run_contracts(work, source, extract):
names = ('GetBoolean', 'GetUint32', 'GetSize', 'GetString', 'GetStringAlloc',
'GetStringRef', 'DoChannelRequest')
functions = '\n'.join(extract(source, name) for name in names)
actual = work / 'channel_actual.c'
actual.write_text(functions)
app = (ROOT / 'src/ssh_transport.c').read_text()
bodies = []
for name, sha in APP_PINS.items():
body = extract(app, name)
assert hashlib.sha256(body.encode()).hexdigest() == sha, name
bodies.append(body)
# Registered functions and per-session context must still be the ones tested.
for setter, callback in (('Shell', 'accept_shell'), ('Exec', 'reject_channel_request'),
('Subsys', 'reject_channel_request')):
assert app.count(f'wolfSSH_CTX_SetChannelReq{setter}Cb(context, {callback})') == 1
assert app.count('wolfSSH_SetChannelReqCtx(slot->ssh, slot);') == 1
(work / 'channel_application.c').write_text('\n'.join(bodies))
flags = ['cc', '-std=gnu11', '-O2', '-Wall', '-Wextra', '-Werror',
'-Wno-unused-parameter', '-Wno-unused-function', '-I', str(work),
str(HERE / 'channel_request.c')]
env = {**os.environ, 'CCACHE_DISABLE': '1'}
profiles = (('production-term', ('WOLFSSH_TERM',)),
('no-terminal', ()), ('shell-only', ('WOLFSSH_SHELL',)),
('term-shell', ('WOLFSSH_TERM', 'WOLFSSH_SHELL')),
('all-branches', ('WOLFSSH_TERM', 'WOLFSSH_SHELL', 'WOLFSSH_AGENT')))
for label, macros in profiles:
for small in (False, True):
binary = work / 'channel-contract'
defines = [f'-D{macro}' for macro in macros]
if small:
defines.append('-DWOLFSSH_SMALL_STACK')
subprocess.run([*flags, *defines, '-fsanitize=undefined',
'-fsanitize-undefined-trap-on-error', '-o', str(binary)],
check=True, timeout=30, env=env)
subprocess.run([str(binary)], check=True, timeout=30)
print(f'PASS: {label} channel requests, both stack modes', flush=True)
# Each guard must be behaviorally relevant, including inactive firmware
# branches tested separately, not enabled in the production configuration.
mutations = []
for name in ('env', 'shell', 'exec', 'subsystem', 'pty-req', 'window-change',
'exit-status', 'exit-signal', 'auth-agent-req@openssh.com'):
guard = f'typeSz == sizeof("{name}") - 1 &&\n '
comparison = f'WMEMCMP(type, "{name}", sizeof("{name}") - 1) == 0'
mutations.append((name + ' original prefix', guard + comparison,
f'WSTRNCMP(type, "{name}", typeSz) == 0'))
mutations.append((name + ' length guard', guard, ''))
for label, old, new in mutations:
assert functions.count(old) == 1, label
actual.write_text(functions.replace(old, new))
binary = work / 'channel-mutation'
subprocess.run([*flags, '-DWOLFSSH_TERM', '-DWOLFSSH_SHELL', '-DWOLFSSH_AGENT',
'-o', str(binary)], check=True, timeout=30, env=env)
result = subprocess.run([str(binary)], capture_output=True, timeout=30)
assert result.returncode != 0, f'Undetected channel mutation: {label}'
actual.write_text(functions)
print(f'PASS: {len(mutations)} channel prefix/length-guard mutations rejected', flush=True)
application = '\n'.join(bodies)
for old, new in (
('!slot->shell_requested ||\n ', ''),
('wolfSSH_GetSessionType(slot->ssh) != WOLFSSH_SESSION_SHELL', 'false'),
):
assert application.count(old) == 1
(work / 'channel_application.c').write_text(application.replace(old, new))
binary = work / 'admission-mutation'
subprocess.run([*flags, '-DWOLFSSH_TERM', '-o', str(binary)],
check=True, timeout=30, env=env)
result = subprocess.run([str(binary)], capture_output=True, timeout=30)
assert result.returncode != 0, f'Undetected admission gate mutation: {old}'
(work / 'channel_application.c').write_text(application)
print('PASS: 2 real application shell-admission guard mutations rejected', flush=True)
+10 -5
View File
@@ -20,6 +20,8 @@ typedef uint32_t word32;
#define WS_CRYPTO_FAILED -7
#define WS_ECC_E -8
#define WS_ED25519_E -9
#define WS_INVALID_STATE_E -10
#define WS_CHANOPEN_FAILED -11
#define MSGID_USERAUTH_REQUEST 50
#define MSG_ID_SZ 1
#define BOOLEAN_SZ 1
@@ -141,10 +143,10 @@ static void parsers(byte *end)
assert(DoIgnore(&ssh,p,n,&idx)==(good?0:WS_BUFFER_E));
if (good) assert(idx==4+lengths[j]);
idx=0; ssh.clientState=9;
int service=good && lengths[j]<WOLFSSH_MAX_NAMESZ;
assert(DoServiceRequest(&ssh,p,n,&idx)==(service?0:WS_BUFFER_E));
assert(ssh.clientState==(service?42:9));
assert(idx==(service?4+lengths[j]:0));
int bounded=good && lengths[j]<WOLFSSH_MAX_NAMESZ;
assert(DoServiceRequest(&ssh,p,n,&idx)==
(bounded?WS_INVALID_STATE_E:WS_BUFFER_E));
assert(ssh.clientState==9 && idx==0);
char out[10]; memset(out, 0x55, sizeof(out));
word32 cap=8; idx=0;
assert(GetString(out+1,&cap,p,n,&idx)==(good?0:WS_BUFFER_E));
@@ -169,7 +171,7 @@ static void parsers(byte *end)
byte p[12]={0}; put(p+3,5); word32 idx=3;
assert(DoIgnore(&ssh,p,12,&idx)==0 && idx==12);
put(p+3,0); idx=3;
assert(DoServiceRequest(&ssh,p,7,&idx)==0 && idx==7);
assert(DoServiceRequest(&ssh,p,7,&idx)==WS_INVALID_STATE_E && idx==3);
}
static void windows(byte *end)
{
@@ -243,12 +245,15 @@ static void ecc(byte *end)
}
#include "auth_framing.c"
#include "remaining.c"
int main(void)
{
long page=sysconf(_SC_PAGESIZE); assert(page>0);
byte *map=mmap(NULL,(size_t)page*2,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0);
assert(map!=MAP_FAILED && mprotect(map+page,page,PROT_NONE)==0);
parsers(map+page); windows(map+page); ecc(map+page);
remaining_parsers(map+page);
ecc_framing(map+page); ed25519_framing(map+page);
assert(munmap(map,(size_t)page*2)==0);
printf("PASS: %u parser/window/ECC/Ed25519 cases, guard pages + UBSan trap\n",cases);
+113
View File
@@ -0,0 +1,113 @@
From d2eeec5e263a4821c90805963eeb0666e99868a6 Mon Sep 17 00:00:00 2001
From: Yosuke Shimizu <yosuke@wolfssl.com>
Date: Tue, 24 Mar 2026 11:16:16 +0900
Subject: [PATCH] Fix minor issues
---
src/internal.c | 17 ++++++-----------
src/port.c | 14 ++++++++------
src/wolfterm.c | 2 +-
3 files changed, 15 insertions(+), 18 deletions(-)
diff --git a/src/internal.c b/src/internal.c
index 77f165dbb..8dc13dab6 100644
--- a/src/internal.c
+++ b/src/internal.c
@@ -4880,16 +4880,14 @@ static int ParseRSAPubKey(WOLFSSH *ssh,
byte* n;
word32 nSz;
word32 pubKeyIdx = 0;
- word32 scratch;
ret = wc_InitRsaKey(&sigKeyBlock_ptr->sk.rsa.key, ssh->ctx->heap);
if (ret != 0)
ret = WS_RSA_E;
- if (ret == 0)
- ret = GetUint32(&scratch, pubKey, pubKeySz, &pubKeyIdx);
- /* This is the algo name. */
+ /* Skip the algo name. */
+ if (ret == WS_SUCCESS)
+ ret = GetSkip(pubKey, pubKeySz, &pubKeyIdx);
if (ret == WS_SUCCESS) {
- pubKeyIdx += scratch;
ret = GetUint32(&eSz, pubKey, pubKeySz, &pubKeyIdx);
if (ret == WS_SUCCESS && eSz > pubKeySz - pubKeyIdx)
ret = WS_BUFFER_E;
@@ -4932,7 +4930,6 @@ static int ParseECCPubKey(WOLFSSH *ssh,
const byte* q;
word32 qSz, pubKeyIdx = 0;
int primeId = 0;
- word32 scratch;
ret = wc_ecc_init_ex(&sigKeyBlock_ptr->sk.ecc.key, ssh->ctx->heap,
INVALID_DEVID);
@@ -4958,12 +4955,10 @@ static int ParseECCPubKey(WOLFSSH *ssh,
/* Skip the curve name since we're getting it from the algo. */
if (ret == WS_SUCCESS)
- ret = GetUint32(&scratch, pubKey, pubKeySz, &pubKeyIdx);
+ ret = GetSkip(pubKey, pubKeySz, &pubKeyIdx);
- if (ret == WS_SUCCESS) {
- pubKeyIdx += scratch;
+ if (ret == WS_SUCCESS)
ret = GetStringRef(&qSz, &q, pubKey, pubKeySz, &pubKeyIdx);
- }
if (ret == WS_SUCCESS) {
ret = wc_ecc_import_x963_ex(q, qSz,
@@ -9407,7 +9402,7 @@ static int DoChannelFailure(WOLFSSH* ssh, byte* buf, word32 len, word32* idx)
WLOG(WS_LOG_DEBUG, "Entering DoChannelFailure()");
- if (ssh == NULL || buf == NULL || len != 0 || idx == NULL)
+ if (ssh == NULL || buf == NULL || len == 0 || idx == NULL)
ret = WS_BAD_ARGUMENT;
if (ret == WS_SUCCESS)
diff --git a/src/port.c b/src/port.c
index 37ee3ffc7..79546e890 100644
--- a/src/port.c
+++ b/src/port.c
@@ -267,7 +267,7 @@ void* WS_CreateFileA(const char* fileName, unsigned long desiredAccess,
void* WS_FindFirstFileA(const char* fileName,
char* realFileName, size_t realFileNameSz, int* isDir, void* heap)
{
- HANDLE findHandle = NULL;
+ HANDLE findHandle = INVALID_HANDLE_VALUE;
WIN32_FIND_DATAW findFileData;
wchar_t* unicodeFileName;
size_t unicodeFileNameSz = 0;
@@ -295,12 +295,14 @@ void* WS_FindFirstFileA(const char* fileName,
WFREE(unicodeFileName, heap, PORT_DYNTYPE_STRING);
- error = wcstombs_s(NULL, realFileName, realFileNameSz,
- findFileData.cFileName, realFileNameSz);
+ if (findHandle != INVALID_HANDLE_VALUE) {
+ error = wcstombs_s(NULL, realFileName, realFileNameSz,
+ findFileData.cFileName, realFileNameSz);
- if (isDir != NULL) {
- *isDir =
- (findFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
+ if (isDir != NULL) {
+ *isDir =
+ (findFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
+ }
}
return (void*)findHandle;
diff --git a/src/wolfterm.c b/src/wolfterm.c
index 63e69d679..e7abe907e 100644
--- a/src/wolfterm.c
+++ b/src/wolfterm.c
@@ -181,7 +181,7 @@ static void doDisplayAttributes(WOLFSSH* ssh, WOLFSSH_HANDLE handle, word32* arg
break;
case 30: /* set black foreground */
- SetConsoleTextAttribute(handle, (atr & ~(WS_MASK_RBGBG)));
+ SetConsoleTextAttribute(handle, (atr & ~(WS_MASK_RBGFG)));
break;
case 31: /* red foreground */
+55
View File
@@ -0,0 +1,55 @@
From ffa646a4b9d47d5d9d6127db140c433c58b1e276 Mon Sep 17 00:00:00 2001
From: Paul Adelsbach <paul.adelsbach@wolfssl.com>
Date: Tue, 7 Apr 2026 08:51:55 -0700
Subject: [PATCH] Add validation for accept request and reply
---
src/internal.c | 28 ++++++++++++++++++++++++++++
1 file changed, 28 insertions(+)
diff --git a/src/internal.c b/src/internal.c
index 77f165dbb..b9b795582 100644
--- a/src/internal.c
+++ b/src/internal.c
@@ -6539,6 +6539,20 @@ static int DoServiceRequest(WOLFSSH* ssh,
ret = GetString(name, &nameSz, buf, len, idx);
+ /* Requested service must be 'ssh-userauth' */
+ if (ret == WS_SUCCESS) {
+ const char* nameUserAuth = IdToName(ID_SERVICE_USERAUTH);
+ if (nameUserAuth == NULL
+ || nameSz != (word32)XSTRLEN(nameUserAuth)
+ || XMEMCMP(name, nameUserAuth, nameSz) != 0) {
+ WLOG(WS_LOG_DEBUG, "Requested unsupported service: %s", name);
+ /* Terminate session, ignore result of disconnect attempt */
+ (void)SendDisconnect(ssh,
+ WOLFSSH_DISCONNECT_SERVICE_NOT_AVAILABLE);
+ ret = WS_INVALID_STATE_E;
+ }
+ }
+
if (ret == WS_SUCCESS) {
WLOG(WS_LOG_DEBUG, "Requesting service: %s", name);
ssh->clientState = CLIENT_USERAUTH_REQUEST_DONE;
@@ -6557,6 +6571,20 @@ static int DoServiceAccept(WOLFSSH* ssh,
ret = GetString(name, &nameSz, buf, len, idx);
+ /* Accepted service must be 'ssh-userauth' */
+ if (ret == WS_SUCCESS) {
+ const char* nameUserAuth = IdToName(ID_SERVICE_USERAUTH);
+ if (nameUserAuth == NULL
+ || nameSz != (word32)XSTRLEN(nameUserAuth)
+ || XMEMCMP(name, nameUserAuth, nameSz) != 0) {
+ WLOG(WS_LOG_DEBUG, "Accepted unexpected service: %s", name);
+ /* Terminate session, ignore result of disconnect attempt */
+ (void)SendDisconnect(ssh,
+ WOLFSSH_DISCONNECT_SERVICE_NOT_AVAILABLE);
+ ret = WS_INVALID_STATE_E;
+ }
+ }
+
if (ret == WS_SUCCESS) {
WLOG(WS_LOG_DEBUG, "Accepted service: %s", name);
ssh->serverState = SERVER_USERAUTH_REQUEST_DONE;
+307
View File
@@ -0,0 +1,307 @@
From fd82a4bcf55935f0801b14bca6be9c71e32ae914 Mon Sep 17 00:00:00 2001
From: Yosuke Shimizu <yosuke@wolfssl.com>
Date: Wed, 15 Apr 2026 11:33:07 +0900
Subject: [PATCH] Fix DoGlobalRequestFwd and Add the regress tests
---
src/internal.c | 27 +++++-
tests/regress.c | 214 ++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 237 insertions(+), 4 deletions(-)
diff --git a/src/internal.c b/src/internal.c
index 11902b7b4..5db18ddb4 100644
--- a/src/internal.c
+++ b/src/internal.c
@@ -8496,16 +8496,35 @@ static int DoGlobalRequestFwd(WOLFSSH* ssh,
isCancel ? " cancel" : "", bindAddr, bindPort);
}
- if (ret == WS_SUCCESS && wantReply) {
- ret = SendGlobalRequestFwdSuccess(ssh, 1, bindPort);
- }
-
if (ret == WS_SUCCESS) {
if (ssh->ctx->fwdCb) {
ret = ssh->ctx->fwdCb(isCancel ? WOLFSSH_FWD_REMOTE_CLEANUP :
WOLFSSH_FWD_REMOTE_SETUP,
ssh->fwdCbCtx, bindAddr, bindPort);
}
+ else {
+ WLOG(WS_LOG_WARN, "No forwarding callback set, rejecting request. "
+ "Set one with wolfSSH_CTX_SetFwdCb().");
+ ret = WS_UNIMPLEMENTED_E;
+ }
+ }
+
+ if (wantReply) {
+ if (ret == WS_SUCCESS) {
+ if (isCancel) {
+ ret = SendRequestSuccess(ssh, 1);
+ }
+ else {
+ ret = SendGlobalRequestFwdSuccess(ssh, 1, bindPort);
+ }
+ }
+ else {
+ ret = SendRequestSuccess(ssh, 0);
+ }
+ }
+ else if (ret == WS_UNIMPLEMENTED_E) {
+ /* No reply expected; silently reject without terminating connection. */
+ ret = WS_SUCCESS;
}
if (bindAddr != NULL)
diff --git a/tests/regress.c b/tests/regress.c
index 5d069d7fc..bf37202d1 100644
--- a/tests/regress.c
+++ b/tests/regress.c
@@ -231,6 +231,21 @@ static word32 BuildDirectTcpipExtra(const char* host, word32 hostPort,
return idx;
}
+
+static word32 BuildGlobalRequestFwdPacket(const char* bindAddr, word32 bindPort,
+ int isCancel, byte wantReply, byte* out, word32 outSz)
+{
+ byte payload[256];
+ word32 idx = 0;
+ const char* reqName = isCancel ? "cancel-tcpip-forward" : "tcpip-forward";
+
+ idx = AppendString(payload, sizeof(payload), idx, reqName);
+ idx = AppendByte (payload, sizeof(payload), idx, wantReply);
+ idx = AppendString(payload, sizeof(payload), idx, bindAddr);
+ idx = AppendUint32(payload, sizeof(payload), idx, bindPort);
+
+ return WrapPacket(MSGID_GLOBAL_REQUEST, payload, idx, out, outSz);
+}
#endif
/* Simple in-memory transport harness */
@@ -957,6 +972,94 @@ static void AssertChannelOpenFailResponse(const ChannelOpenHarness* harness,
AssertTrue(harness->ssh->channelList == NULL);
}
+#ifdef WOLFSSH_FWD
+static word32 ParsePayloadLen(const byte* packet, word32 packetSz)
+{
+ word32 packetLen;
+ byte padLen;
+
+ AssertNotNull(packet);
+ AssertTrue(packetSz >= 6);
+
+ WMEMCPY(&packetLen, packet, sizeof(packetLen));
+ packetLen = ntohl(packetLen);
+ padLen = packet[4];
+
+ AssertTrue(packetLen >= (word32)padLen + 1);
+ AssertTrue(packetSz >= packetLen + 4);
+
+ return packetLen - padLen - 1;
+}
+
+static const byte* ParseGlobalRequestName(const byte* packet, word32 packetSz,
+ word32* nameSz)
+{
+ word32 packetLen;
+ word32 payloadLen;
+ word32 strSz;
+ const byte* payload;
+
+ AssertNotNull(packet);
+ AssertNotNull(nameSz);
+ AssertTrue(packetSz >= 10);
+
+ WMEMCPY(&packetLen, packet, sizeof(packetLen));
+ packetLen = ntohl(packetLen);
+ AssertTrue(packetSz >= packetLen + 4);
+
+ payloadLen = ParsePayloadLen(packet, packetSz);
+ payload = packet + 5;
+
+ AssertTrue(payloadLen >= 1 + sizeof(word32));
+ AssertIntEQ(payload[0], MSGID_GLOBAL_REQUEST);
+
+ WMEMCPY(&strSz, payload + 1, sizeof(strSz));
+ strSz = ntohl(strSz);
+ AssertTrue(payloadLen >= 1 + sizeof(word32) + strSz);
+
+ *nameSz = strSz;
+ return payload + 1 + sizeof(word32);
+}
+
+static void AssertGlobalRequestReply(const ChannelOpenHarness* harness,
+ byte expectedMsgId)
+{
+ byte msgId;
+ word32 payloadLen;
+
+ AssertTrue(harness->io.outSz > 0);
+ msgId = ParseMsgId(harness->io.out, harness->io.outSz);
+ AssertIntEQ(msgId, expectedMsgId);
+
+ payloadLen = ParsePayloadLen(harness->io.out, harness->io.outSz);
+ if (expectedMsgId == MSGID_REQUEST_FAILURE) {
+ AssertIntEQ(payloadLen, 1);
+ }
+ else if (expectedMsgId == MSGID_REQUEST_SUCCESS) {
+ const byte* reqName;
+ word32 reqNameSz;
+
+ reqName = ParseGlobalRequestName(harness->io.in, harness->io.inSz,
+ &reqNameSz);
+
+ if (reqNameSz == sizeof("tcpip-forward") - 1 &&
+ WMEMCMP(reqName, "tcpip-forward",
+ sizeof("tcpip-forward") - 1) == 0) {
+ AssertIntEQ(payloadLen, 5);
+ }
+ else if (reqNameSz == sizeof("cancel-tcpip-forward") - 1 &&
+ WMEMCMP(reqName, "cancel-tcpip-forward",
+ sizeof("cancel-tcpip-forward") - 1) == 0) {
+ AssertIntEQ(payloadLen, 1);
+ }
+ else {
+ Fail(("unexpected global request name"),
+ ("%.*s", (int)reqNameSz, reqName));
+ }
+ }
+}
+#endif
+
static int RejectChannelOpenCb(WOLFSSH_CHANNEL* channel, void* ctx)
{
(void)channel;
@@ -978,6 +1081,17 @@ static int RejectDirectTcpipSetup(WS_FwdCbAction action, void* ctx,
return WS_SUCCESS;
}
+
+static int AcceptFwdCb(WS_FwdCbAction action, void* ctx,
+ const char* host, word32 port)
+{
+ (void)action;
+ (void)ctx;
+ (void)host;
+ (void)port;
+
+ return WS_SUCCESS;
+}
#endif
@@ -1242,6 +1356,101 @@ static void TestDirectTcpipNoFwdCbSendsOpenFail(void)
FreeChannelOpenHarness(&harness);
}
+
+static void TestGlobalRequestFwdNoCbSendsFailure(void)
+{
+ ChannelOpenHarness harness;
+ byte in[256];
+ word32 inSz;
+ int ret;
+
+ inSz = BuildGlobalRequestFwdPacket("0.0.0.0", 2222, 0, 1, in, sizeof(in));
+ InitChannelOpenHarness(&harness, in, inSz);
+ /* no fwdCb registered */
+
+ ret = DoReceive(harness.ssh);
+
+ AssertIntEQ(ret, WS_SUCCESS);
+ AssertGlobalRequestReply(&harness, MSGID_REQUEST_FAILURE);
+
+ FreeChannelOpenHarness(&harness);
+}
+
+static void TestGlobalRequestFwdNoCbNoReplyKeepsConnection(void)
+{
+ ChannelOpenHarness harness;
+ byte in[256];
+ word32 inSz;
+ int ret;
+
+ /* wantReply=0: no reply sent, connection must stay alive */
+ inSz = BuildGlobalRequestFwdPacket("0.0.0.0", 2222, 0, 0, in, sizeof(in));
+ InitChannelOpenHarness(&harness, in, inSz);
+ /* no fwdCb registered */
+
+ ret = DoReceive(harness.ssh);
+
+ AssertIntEQ(ret, WS_SUCCESS);
+ AssertIntEQ(harness.io.outSz, 0); /* no reply sent */
+
+ FreeChannelOpenHarness(&harness);
+}
+
+static void TestGlobalRequestFwdWithCbSendsSuccess(void)
+{
+ ChannelOpenHarness harness;
+ byte in[256];
+ word32 inSz;
+ int ret;
+
+ inSz = BuildGlobalRequestFwdPacket("0.0.0.0", 2222, 0, 1, in, sizeof(in));
+ InitChannelOpenHarness(&harness, in, inSz);
+ AssertIntEQ(wolfSSH_CTX_SetFwdCb(harness.ctx, AcceptFwdCb, NULL), WS_SUCCESS);
+
+ ret = DoReceive(harness.ssh);
+
+ AssertIntEQ(ret, WS_SUCCESS);
+ AssertGlobalRequestReply(&harness, MSGID_REQUEST_SUCCESS);
+
+ FreeChannelOpenHarness(&harness);
+}
+
+static void TestGlobalRequestFwdCancelNoCbSendsFailure(void)
+{
+ ChannelOpenHarness harness;
+ byte in[256];
+ word32 inSz;
+ int ret;
+
+ inSz = BuildGlobalRequestFwdPacket("0.0.0.0", 2222, 1, 1, in, sizeof(in));
+ InitChannelOpenHarness(&harness, in, inSz);
+
+ ret = DoReceive(harness.ssh);
+
+ AssertIntEQ(ret, WS_SUCCESS);
+ AssertGlobalRequestReply(&harness, MSGID_REQUEST_FAILURE);
+
+ FreeChannelOpenHarness(&harness);
+}
+
+static void TestGlobalRequestFwdCancelWithCbSendsSuccess(void)
+{
+ ChannelOpenHarness harness;
+ byte in[256];
+ word32 inSz;
+ int ret;
+
+ inSz = BuildGlobalRequestFwdPacket("0.0.0.0", 2222, 1, 1, in, sizeof(in));
+ InitChannelOpenHarness(&harness, in, inSz);
+ AssertIntEQ(wolfSSH_CTX_SetFwdCb(harness.ctx, AcceptFwdCb, NULL), WS_SUCCESS);
+
+ ret = DoReceive(harness.ssh);
+
+ AssertIntEQ(ret, WS_SUCCESS);
+ AssertGlobalRequestReply(&harness, MSGID_REQUEST_SUCCESS);
+
+ FreeChannelOpenHarness(&harness);
+}
#endif
#ifdef WOLFSSH_AGENT
@@ -1707,6 +1916,11 @@ int main(int argc, char** argv)
#ifdef WOLFSSH_FWD
TestDirectTcpipRejectSendsOpenFail();
TestDirectTcpipNoFwdCbSendsOpenFail();
+ TestGlobalRequestFwdNoCbSendsFailure();
+ TestGlobalRequestFwdNoCbNoReplyKeepsConnection();
+ TestGlobalRequestFwdWithCbSendsSuccess();
+ TestGlobalRequestFwdCancelNoCbSendsFailure();
+ TestGlobalRequestFwdCancelWithCbSendsSuccess();
#endif
#ifdef WOLFSSH_AGENT
TestAgentChannelNullAgentSendsOpenFail();
+69
View File
@@ -0,0 +1,69 @@
From 0317c40fc131fab952d291d43c56c7b7ce5f4303 Mon Sep 17 00:00:00 2001
From: Yosuke Shimizu <yosuke@wolfssl.com>
Date: Wed, 15 Apr 2026 13:37:21 +0900
Subject: [PATCH] Fix DoChannelOpen() and Add regress test
---
src/internal.c | 6 ++++++
tests/regress.c | 24 ++++++++++++++++++++++++
2 files changed, 30 insertions(+)
diff --git a/src/internal.c b/src/internal.c
index 1202d132e..a0df29600 100644
--- a/src/internal.c
+++ b/src/internal.c
@@ -8739,6 +8739,12 @@ static int DoChannelOpen(WOLFSSH* ssh,
ssh->fwdCbCtx, NULL, newChannel->channel);
}
}
+ else {
+ WLOG(WS_LOG_WARN, "No forward callback set for direct-tcpip channel,"
+ " failing channel open");
+ fail_reason = OPEN_ADMINISTRATIVELY_PROHIBITED;
+ ret = WS_ERROR;
+ }
}
#endif /* WOLFSSH_FWD */
if (ret == WS_SUCCESS) {
diff --git a/tests/regress.c b/tests/regress.c
index 321151b8d..bfc719315 100644
--- a/tests/regress.c
+++ b/tests/regress.c
@@ -1184,6 +1184,29 @@ static void TestDirectTcpipRejectSendsOpenFail(void)
FreeChannelOpenHarness(&harness);
}
+
+static void TestDirectTcpipNoFwdCbSendsOpenFail(void)
+{
+ ChannelOpenHarness harness;
+ byte extra[128];
+ byte in[192];
+ word32 extraSz;
+ word32 inSz;
+ int ret;
+
+ extraSz = BuildDirectTcpipExtra("127.0.0.1", 8080, "127.0.0.1", 2222,
+ extra, sizeof(extra));
+ inSz = BuildChannelOpenPacket("direct-tcpip", 9, 0x4000, 0x8000,
+ extra, extraSz, in, sizeof(in));
+
+ InitChannelOpenHarness(&harness, in, inSz);
+ /* Intentionally do NOT register fwdCb */
+
+ ret = DoReceive(harness.ssh);
+ AssertChannelOpenFailResponse(&harness, ret);
+
+ FreeChannelOpenHarness(&harness);
+}
#endif
#ifdef WOLFSSH_AGENT
@@ -1648,6 +1671,7 @@ int main(int argc, char** argv)
TestChannelOpenCallbackRejectSendsOpenFail();
#ifdef WOLFSSH_FWD
TestDirectTcpipRejectSendsOpenFail();
+ TestDirectTcpipNoFwdCbSendsOpenFail();
#endif
#ifdef WOLFSSH_AGENT
TestAgentChannelNullAgentSendsOpenFail();
@@ -0,0 +1,26 @@
{
"899": {
"url": "https://patch-diff.githubusercontent.com/raw/wolfSSL/wolfssh/pull/899.patch",
"commit_url": "https://github.com/wolfSSL/wolfssh/commit/d2eeec5e263a4821c90805963eeb0666e99868a6.patch",
"commit": "d2eeec5e263a4821c90805963eeb0666e99868a6",
"sha256": "e33c8b0aaa3c5a5d6c5201e6141cafd2383a1ed6931fea6655a9b757fbe0b0ce"
},
"902": {
"url": "https://patch-diff.githubusercontent.com/raw/wolfSSL/wolfssh/pull/902.patch",
"commit_url": "https://github.com/wolfSSL/wolfssh/commit/ffa646a4b9d47d5d9d6127db140c433c58b1e276.patch",
"commit": "ffa646a4b9d47d5d9d6127db140c433c58b1e276",
"sha256": "4dc3a69f8cecb34f5091b9c22e0012ea6020b7ede9c1ab3de4d96264168c345a"
},
"918": {
"url": "https://patch-diff.githubusercontent.com/raw/wolfSSL/wolfssh/pull/918.patch",
"commit_url": "https://github.com/wolfSSL/wolfssh/commit/fd82a4bcf55935f0801b14bca6be9c71e32ae914.patch",
"commit": "fd82a4bcf55935f0801b14bca6be9c71e32ae914",
"sha256": "a2a59707086c6273a2c339e63db49846a5a1862c73924b60a845a03d7d41be41"
},
"919": {
"url": "https://patch-diff.githubusercontent.com/raw/wolfSSL/wolfssh/pull/919.patch",
"commit_url": "https://github.com/wolfSSL/wolfssh/commit/0317c40fc131fab952d291d43c56c7b7ce5f4303.patch",
"commit": "0317c40fc131fab952d291d43c56c7b7ce5f4303",
"sha256": "753fffea9925deeed4ddfc7aeba1fce0e950602b52ed1434bd817c2d48857b2f"
}
}
+65
View File
@@ -0,0 +1,65 @@
/* SPDX-License-Identifier: GPL-3.0-only */
static void remaining_parsers(byte *end)
{
WOLFSSH ssh = {.clientState=9};
const byte service[] = "ssh-userauth";
for (word32 offset=0; offset<=3; offset++) {
for (word32 size=0; size<=16; size++) {
byte *p=end-offset-size;
memset(p,0,offset+size);
if (size>=4) {
put(p+offset,12);
memcpy(p+offset+4,service,size-4);
}
word32 idx=offset;
ssh.clientState=9;
assert(DoServiceRequest(&ssh,p,offset+size,&idx)==
(size==16?WS_SUCCESS:WS_BUFFER_E));
assert(idx==(size==16?offset+size:offset));
assert(ssh.clientState==(size==16?42:9));
cases++;
}
}
/* Equal-length mismatch at every byte, embedded NUL, prefix and suffix. */
for (word32 n=0;n<=13;n++) {
byte *p=end-4-n;
put(p,n); memcpy(p+4,service,n);
word32 idx=0; ssh.clientState=9;
assert(DoServiceRequest(&ssh,p,n+4,&idx)==
(n==12?WS_SUCCESS:WS_INVALID_STATE_E));
assert(idx==(n==12?n+4:0) && ssh.clientState==(n==12?42:9));
cases++;
}
for (word32 pos=0;pos<12;pos++) {
byte *p=end-16; put(p,12); memcpy(p+4,service,12); p[4+pos]=0;
word32 idx=0; ssh.clientState=9;
assert(DoServiceRequest(&ssh,p,16,&idx)==WS_INVALID_STATE_E);
assert(idx==0 && ssh.clientState==9); cases++;
}
/* CHANNEL_FAILURE has exactly one recipient; never mutate session/channel. */
for (word32 offset=0;offset<=3;offset++) {
for (word32 size=0;size<=8;size++) {
byte *p=end-offset-size; memset(p,0,offset+size);
if(size>=4) put(p+offset,7);
word32 idx=offset; finds=0; channel.peerWindowSz=123;
ssh.clientState=9;
assert(DoChannelFailure(&ssh,p,offset+size,&idx)==
(size==4?WS_CHANOPEN_FAILED:WS_BUFFER_E));
assert(idx==(size==4?offset+4:offset));
assert(finds==(size==4?1u:0u));
assert(ssh.clientState==9 && channel.peerWindowSz==123); cases++;
}
}
byte *p=end-4; put(p,8); word32 idx=0;
assert(DoChannelFailure(&ssh,p,4,&idx)==WS_INVALID_CHANID && idx==0);
const word32 invalid[]={4,5,UINT32_MAX-3,UINT32_MAX};
for(unsigned j=0;j<4;j++) {
idx=invalid[j]; finds=0;
assert(DoChannelFailure(&ssh,p,4,&idx)==WS_BUFFER_E);
assert(idx==invalid[j] && finds==0); cases++;
}
idx=0;
assert(DoChannelFailure(NULL,p,4,&idx)==WS_BAD_ARGUMENT);
assert(DoChannelFailure(&ssh,NULL,4,&idx)==WS_BAD_ARGUMENT);
assert(DoChannelFailure(&ssh,p,4,NULL)==WS_BAD_ARGUMENT);
}
+181
View File
@@ -0,0 +1,181 @@
#!/usr/bin/env python3
"""Finite remaining-parser source/provenance contract; optional read-only profile replay."""
import hashlib
import json
import os
from pathlib import Path
import re
import shlex
import subprocess
import sys
sys.dont_write_bytecode = True
ROOT = Path(__file__).resolve().parents[2]
HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(ROOT / 'tests/wolfssh_auth_contract'))
from run import extract
from security_overrides import ENTRIES, render_entry
# Independently specified delta against the reviewed original+ordering+parser
# baseline. Reversing precisely these bytes must recover its whole-source hash.
BASELINE_SHA = '4948f8c447670eb54153dd1f3db69e4fa3092f7d7f7ed58a18a8fa05fcd168ca'
NOTICE = '''/* Server parser review modified 2026-09-16: bounded CHANNEL_FAILURE
* and ssh-userauth service validation; PR899/902 subset, not full PRs.
* Local follow-up: exact bounded channel-request names.
* Provenance/limits: docs/ssh_parser_remaining_review.md.
*/
'''
SERVICE = ''' /* PR902 current-server subset: reject before publishing the transition.
* The owner closes on this error; no best-effort disconnect is queued. */
if (nameSz != sizeof("ssh-userauth") - 1 ||
WMEMCMP(serviceName, "ssh-userauth", sizeof("ssh-userauth") - 1) != 0)
return WS_INVALID_STATE_E;
'''
OLD_FAILURE = ''' if (ssh == NULL || buf == NULL || len != 0 || idx == NULL)
ret = WS_BAD_ARGUMENT;
if (ret == WS_SUCCESS)
ret = WS_CHANOPEN_FAILED;'''
NEW_FAILURE = ''' word32 begin, channelId;
if (ssh == NULL || buf == NULL || idx == NULL)
return WS_BAD_ARGUMENT;
begin = *idx;
ret = GetUint32(&channelId, buf, len, &begin);
if (ret != WS_SUCCESS)
return ret;
if (begin != len)
return WS_BUFFER_E;
if (ChannelFind(ssh, channelId, WS_CHANNEL_ID_SELF) == NULL)
return WS_INVALID_CHANID;
*idx = begin;
ret = WS_CHANOPEN_FAILED;'''
PINS = {
'tools/wolfssh_order/delta.json': '6a81376fe3ffc5f449cde105402963f52d2d78cc844153e869a7e1e0f734fb76',
'managed_components/wolfssl__wolfssl/wolfcrypt/src/signature.c': '62ab3db3dfd251b2a2c73b69ef05aab6085d2e0d673fd9159514b3ee261cea4f',
}
PATCHES = {
'899': ('d2eeec5e263a4821c90805963eeb0666e99868a6', 'e33c8b0aaa3c5a5d6c5201e6141cafd2383a1ed6931fea6655a9b757fbe0b0ce'),
'902': ('ffa646a4b9d47d5d9d6127db140c433c58b1e276', '4dc3a69f8cecb34f5091b9c22e0012ea6020b7ede9c1ab3de4d96264168c345a'),
'918': ('fd82a4bcf55935f0801b14bca6be9c71e32ae914', 'a2a59707086c6273a2c339e63db49846a5a1862c73924b60a845a03d7d41be41'),
'919': ('0317c40fc131fab952d291d43c56c7b7ce5f4303', '753fffea9925deeed4ddfc7aeba1fce0e950602b52ed1434bd817c2d48857b2f'),
}
def digest(data):
return hashlib.sha256(data).hexdigest()
def check_sources(original, generated):
assert digest(original.read_bytes()) == \
'81ff1f9166708abd5c2911e9fe57c0aee01c88b5d3f68c909ee8a856d37f36a9'
text = generated.decode()
prior = text
for new, old in ((NOTICE, ''), (SERVICE, ''), (NEW_FAILURE, OLD_FAILURE)):
assert prior.count(new) == 1, 'Independent delta anchor changed'
prior = prior.replace(new, old)
# Independent name list/format, never imported from the generator's edits.
for name in ('env', 'shell', 'exec', 'subsystem', 'pty-req', 'window-change',
'exit-status', 'exit-signal', 'auth-agent-req@openssh.com'):
new = (f'typeSz == sizeof("{name}") - 1 &&\n'
f' WMEMCMP(type, "{name}", sizeof("{name}") - 1) == 0')
old = f'WSTRNCMP(type, "{name}", typeSz) == 0'
assert prior.count(new) == 1, name
prior = prior.replace(new, old)
assert digest(prior.encode()) == BASELINE_SHA, 'Unreviewed generated-source delta'
assert extract(prior, 'DoChannelRequest') == extract(original.read_text(), 'DoChannelRequest')
for path, sha in PINS.items():
assert digest((ROOT / path).read_bytes()) == sha, path
provenance = json.loads((HERE / 'provenance.json').read_text())
assert set(provenance) == set(PATCHES)
for number, (commit, sha) in PATCHES.items():
data = (HERE / f'pr{number}.patch').read_bytes()
assert digest(data) == sha and data.startswith(f'From {commit} '.encode())
assert provenance[number] == {
'url': f'https://patch-diff.githubusercontent.com/raw/wolfSSL/wolfssh/pull/{number}.patch',
'commit_url': f'https://github.com/wolfSSL/wolfssh/commit/{commit}.patch',
'commit': commit, 'sha256': sha,
}
for name in ('ParseRSAPubKey', 'ParseECCPubKey', 'ParsePubKey', 'DoKexDhReply',
'DoChannelOpen', 'DoGlobalRequest',
'DoServiceAccept'):
# DoKexDhReply has an ordering delta, checked by the whole baseline pin.
if name != 'DoKexDhReply':
assert extract(text, name) == extract(original.read_text(), name), name
# SignHEcdsa has preprocessor-selected bodies after its signature; compare
# its complete region rather than using the single-body extractor.
start, end = 'static int SignHEcdsa(', 'static int SignH('
def signer_region(source):
begin = source.index(start)
return source[begin:source.index(end, begin)]
assert signer_region(text) == signer_region(original.read_text())
assert len(re.findall(r'\bParsePubKey\s*\(', text)) == 2
assert 'ParsePubKey(ssh, sigKeyBlock_ptr, pubKey, pubKeySz)' in extract(text, 'DoKexDhReply')
assert len(re.findall(r'\bParseECCPubKey\s*\(', text)) == 2
assert len(re.findall(r'\bParseRSAPubKey\s*\(', text)) == 2
packet = extract(text, 'DoPacket')
assert packet.index('IsMessageAllowed(ssh, msg, WS_MSG_RECV)') < packet.index('switch (msg)')
print('PASS: independent whole-source delta, unchanged ordering/client/request branch bodies, archived PR provenance')
return prior.encode()
def check_profile(generated, prior):
databases = list((ROOT / '.pio/build').glob('*/compile_commands.json'))
assert len(databases) == 1, 'Need one unambiguous production compile database'
entries = json.loads(databases[0].read_text())
matches = []
for entry in entries:
path = (Path(entry['directory']) / entry['file']).resolve()
assert path != ROOT / 'managed_components/wolfssl__wolfssh/src/internal.c'
if path.parts[-3:] == ('security_overrides', 'wolfssh_internal', 'internal.c'):
matches.append((entry, path))
assert len(matches) == 1
entry, path = matches[0]
actual = path.read_bytes()
assert actual in (generated, prior), 'Unreviewed production generated input'
args = entry.get('arguments') or shlex.split(entry['command'])
clean = []; skip = False
for arg in args:
if skip:
skip = False
elif arg in ('-o', '-MF', '-MT', '-MQ'):
skip = True
elif arg not in ('-c', '-MD', '-MMD', '-MP'):
clean.append(arg)
result = subprocess.run(clean + ['-E', '-dM'], cwd=entry['directory'],
capture_output=True, text=True, check=True, timeout=60,
env={**os.environ, 'CCACHE_DISABLE': '1'})
macros = dict(re.findall(r'^#define (\w+)(?: (.*))?$', result.stdout, re.M))
assert macros.get('LIBWOLFSSH_VERSION_HEX') == '0x01004020'
for name in ('WOLFSSH_FWD', 'WOLFSSH_AGENT', 'WOLFSSH_CERTS', 'WOLFSSH_SFTP',
'WOLFSSH_SCP', 'WOLFSSH_NO_ECDSA', 'WOLFSSH_NO_ED25519',
'NO_WOLFSSH_SERVER', 'NO_WOLFSSH_CLIENT', 'WOLFSSH_SHELL'):
assert name not in macros, name
for name in ('WOLFSSH_NO_RSA', 'WOLFSSH_NO_DH', 'WOLFSSL_VALIDATE_ECC_IMPORT',
'WOLFSSL_ECDHX_SHARED_NOT_ZERO', 'CURVE25519_SMALL', 'ED25519_SMALL',
'WOLFSSH_TERM'):
assert name in macros, name
# Compile the fresh source with the saved real target flags, without changing
# the production build tree or creating objects/dependency files.
import tempfile
with tempfile.TemporaryDirectory(prefix='ssh-parser-syntax-') as directory:
fresh = Path(directory) / 'internal.c'; fresh.write_bytes(generated)
syntax = [str(fresh) if (Path(entry['directory']) / arg).resolve() == path
else arg for arg in clean]
subprocess.run(syntax + ['-fsyntax-only'], cwd=entry['directory'], check=True,
timeout=60, env={**os.environ, 'CCACHE_DISABLE': '1'})
print('PASS: actual Xtensa profile + fresh generated source syntax (no objects/build regeneration)')
print('Production generated source:', 'current' if actual == generated else
'REVIEWED PRIOR BASELINE; regeneration/build still required')
if __name__ == '__main__':
assert sys.argv[1:] in ([], ['--profile'])
entry = next(e for e in ENTRIES if e.name == 'wolfssh_internal')
original, generated = render_entry(entry, {'project': ROOT})
prior = check_sources(original, generated)
print('Fresh generated SHA-256:', digest(generated))
if '--profile' in sys.argv:
check_profile(generated, prior)
+18 -2
View File
@@ -18,15 +18,17 @@ entry = next(e for e in ENTRIES if e.name == 'wolfssh_internal')
assert entry.sha256 == '81ff1f9166708abd5c2911e9fe57c0aee01c88b5d3f68c909ee8a856d37f36a9'
original, generated = render_entry(entry, {'project': ROOT})
names = ('GetUint32', 'GetSize', 'GetString', 'GetSkip', 'GetStringRef',
'DoIgnore', 'DoServiceRequest', 'DoChannelWindowAdjust', 'DoUserAuthRequestEcc',
'DoIgnore', 'DoServiceRequest', 'DoChannelFailure', 'DoChannelWindowAdjust', 'DoUserAuthRequestEcc',
'DoUserAuthRequestEd25519')
# Parser edits must not change the independently applied ordering/password logic.
from security_overrides import apply_edits, MODIFICATION_NOTICE, WOLFSSH_PARSER_EDITS
baseline = MODIFICATION_NOTICE + apply_edits(original.read_text(), tuple(
edit for edit in entry.edits if edit not in WOLFSSH_PARSER_EDITS))
for name in ('DoUserAuthRequestPassword', 'DoPacket', 'DoChannelFailure',
for name in ('DoUserAuthRequestPassword', 'DoPacket',
'ParseRSAPubKey', 'ParseECCPubKey', 'DoUserAuthRequestPublicKey'):
assert extract(generated.decode(), name) == extract(baseline, name), name
from review import check_sources
check_sources(original, generated)
with tempfile.TemporaryDirectory(prefix='wolfssh-parser-') as directory:
work = Path(directory)
# Read back the actual generated bytes, not a parallel implementation.
@@ -46,6 +48,18 @@ with tempfile.TemporaryDirectory(prefix='wolfssh-parser-') as directory:
# Prove negative fixtures detect removal of each new boundary/type guard.
# Mutations affect only temporary extracted host copies, never the override.
mutations = (
('Service exact length', 'DoServiceRequest',
(('nameSz != sizeof("ssh-userauth") - 1 ||', '0 ||'),)),
('Service exact bytes', 'DoServiceRequest',
(('WMEMCMP(serviceName, "ssh-userauth", sizeof("ssh-userauth") - 1) != 0', '0'),)),
('Failure bounded recipient', 'DoChannelFailure',
(('ret = GetUint32(&channelId, buf, len, &begin);',
'ato32(buf + begin, &channelId); begin += 4; ret = WS_SUCCESS;'),)),
('Failure exact consumption', 'DoChannelFailure',
(('if (begin != len)', 'if (0)'),)),
('Failure known recipient', 'DoChannelFailure',
(('if (ChannelFind(ssh, channelId, WS_CHANNEL_ID_SELF) == NULL)',
'if (0 && ChannelFind(ssh, channelId, WS_CHANNEL_ID_SELF) == NULL)'),)),
('ECC nested read boundary', 'DoUserAuthRequestEcc',
(('pk->signature, sz, &i)', 'pk->signature, pk->signatureSz, &i)'),)),
('ECC inner exact consumption', 'DoUserAuthRequestEcc',
@@ -76,5 +90,7 @@ with tempfile.TemporaryDirectory(prefix='wolfssh-parser-') as directory:
result = subprocess.run([str(binary)], capture_output=True, timeout=30)
assert result.returncode != 0, f'Undetected mutation: {label}'
print(f'PASS: {len(mutations)} parser guard-removal mutations rejected')
from channel_request import run_contracts
run_contracts(work, source.read_text(), extract)
print('PASS: exact original hash; generated parser; parser-isolated ordering/password/deferred functions')
print('NOTE: production build-tree registration/firmware not regenerated or validated')