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:
@@ -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; 31–65-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)
|
||||
@@ -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);
|
||||
|
||||
@@ -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 */
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,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')
|
||||
|
||||
Reference in New Issue
Block a user