Skip to content

Commit 79a9199

Browse files
PankajBhojwaniyeelam-gordonCopilot
authored
Add fuzzing for wtcli and the com server (#16)
* fuzzer for wtcli * fuzzer for protocol * more issues from merge * address comments * add wtcli to spellcheck * Fix MatchesEventFilter regression and update fuzzing docs - Change structural guard in MatchesEventFilter to return false (reject) instead of true (pass through) when event JSON is missing params object. This matches the old inline behavior where missing fields resolved to empty strings and failed filter comparison. - Update doc/fuzzing.md to remove references to deleted fuzz targets: TranslateKeys, ValidateQuickPickChoices, ValidateSettingsJson. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address review comments: defensive init, error message, naming - BuildSendEventJson: defer outEvt mutation until after paramsJson validation succeeds, preventing partial state on failure. - Improve error message for invalid --json to help users diagnose parse failures vs. non-object JSON. - Rename sendEventPaneTarget to sendEventSessionTarget to reflect that the --pane flag now carries a session ID. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Gordon Lam (SH) <yeelam@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 118bb73 commit 79a9199

9 files changed

Lines changed: 94 additions & 287 deletions

File tree

.github/actions/spelling/allow/apis.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ winsta
197197
winstamin
198198
wmemcmp
199199
wpc
200+
wtcli
200201
WSF
201202
WWH
202203
wwinmain

OpenConsole.slnx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,6 @@
3636
<Build Solution="Debug|x64" Project="false" />
3737
<Build Solution="Release|x64" Project="false" />
3838
</Project>
39-
<Project Path="src/tools/wtcli/ft_fuzzer/WtcliFuzzer.vcxproj" Id="c5fae84e-310e-4cbc-8242-5ad6a37fdaa5">
40-
<Platform Solution="*|Any CPU" Project="Win32" />
41-
<Build Solution="*|Any CPU" Project="false" />
42-
<Build Solution="*|ARM64" Project="false" />
43-
<Build Solution="*|x86" Project="false" />
44-
<Build Solution="AuditMode|x64" Project="false" />
45-
<Build Solution="Debug|x64" Project="false" />
46-
<Build Solution="Release|x64" Project="false" />
47-
</Project>
4839
<Project Path="src/host/ft_host/Host.FeatureTests.vcxproj" Id="8cdb8850-7484-4ec7-b45b-181f85b2ee54">
4940
<BuildDependency Project="src/host/exe/Host.EXE.vcxproj" />
5041
<BuildDependency Project="src/tools/nihilist/Nihilist.vcxproj" />
@@ -1068,6 +1059,15 @@
10681059
</Project>
10691060
</Folder>
10701061
<Project Path="src/tools/wtcli/wtcli.vcxproj" Id="8a3b5e2f-1c4d-4f6a-9b8c-0d1e2f3a4b5c" />
1062+
<Project Path="src/tools/wtcli/ft_fuzzer/WtcliFuzzer.vcxproj" Id="c5fae84e-310e-4cbc-8242-5ad6a37fdaa5">
1063+
<Platform Solution="*|Any CPU" Project="Win32" />
1064+
<Build Solution="*|Any CPU" Project="false" />
1065+
<Build Solution="*|ARM64" Project="false" />
1066+
<Build Solution="*|x86" Project="false" />
1067+
<Build Solution="AuditMode|x64" Project="false" />
1068+
<Build Solution="Debug|x64" Project="false" />
1069+
<Build Solution="Release|x64" Project="false" />
1070+
</Project>
10711071
<Project Path="src/cascadia/TerminalProtocol/TerminalProtocol.vcxproj" Id="f1a2b3c4-d5e6-4f7a-8b9c-0d1e2f3a4b5d" />
10721072
<Project Path="src/cascadia/TerminalProtocol/ft_fuzzer/ProtocolFuzzer.vcxproj" Id="26711d2f-ca10-45be-9e89-43771030bdb0">
10731073
<Platform Solution="*|Any CPU" Project="Win32" />

doc/fuzzing.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ OneFuzz for continuous coverage.
99

1010
| Project | Path | Targets |
1111
|---------|------|---------|
12-
| **WtcliFuzzer** | `src/tools/wtcli/ft_fuzzer/` | `TranslateKeys`, `BuildSendEventJson`, `MatchesEventFilter` |
13-
| **ProtocolFuzzer** | `src/cascadia/TerminalProtocol/ft_fuzzer/` | `ClassifySendEvent`, `ParseSplitDirection`, `ClassifyPaneOutputSource`, `ValidateQuickPickChoices`, `ValidateSettingsJson` |
12+
| **WtcliFuzzer** | `src/tools/wtcli/ft_fuzzer/` | `BuildSendEventJson`, `MatchesEventFilter` |
13+
| **ProtocolFuzzer** | `src/cascadia/TerminalProtocol/ft_fuzzer/` | `ClassifySendEvent`, `ParseSplitDirection`, `ClassifyPaneOutputSource` |
1414
| **OpenConsoleFuzzer** | `src/host/ft_fuzzer/` | `WriteCharsLegacy` (original host fuzzer) |
1515

1616
Each project contains:

src/cascadia/TerminalProtocol/ProtocolParsing.h

Lines changed: 24 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -61,45 +61,30 @@ namespace Microsoft::Terminal::Protocol::Parsing
6161
return SendEventRoute::Invalid;
6262
}
6363

64-
// autofix_state — direct dispatch, no broadcast
65-
if (outEvt.isMember("method") && outEvt["method"].isString() &&
66-
outEvt["method"].asString() == "autofix_state")
67-
{
68-
return SendEventRoute::AutofixState;
69-
}
70-
71-
// agent_status — direct dispatch, no broadcast
72-
if (outEvt.isMember("method") && outEvt["method"].isString() &&
73-
outEvt["method"].asString() == "agent_status")
74-
{
75-
return SendEventRoute::AgentStatus;
76-
}
77-
78-
// close_agent_pane — direct dispatch, no broadcast.
79-
// Emitted by the wta TUI when the user presses Ctrl+C twice.
80-
if (outEvt.isMember("method") && outEvt["method"].isString() &&
81-
outEvt["method"].asString() == "close_agent_pane")
82-
{
83-
return SendEventRoute::CloseAgentPane;
84-
}
85-
86-
// view_changed — direct dispatch, no broadcast.
87-
// Emitted by the wta TUI when its internal view flips
88-
// (Esc out of session view, `/sessions` slash command).
89-
if (outEvt.isMember("method") && outEvt["method"].isString() &&
90-
outEvt["method"].asString() == "view_changed")
91-
{
92-
return SendEventRoute::ViewChanged;
93-
}
94-
95-
// resume_in_new_agent_tab — direct dispatch, no broadcast.
96-
// Emitted by the wta TUI's session management view on Shift+Enter
97-
// over a historical session row. Carries {session_id, cwd}; WT
98-
// creates a new tab + agent pane and calls back with `load_session`.
99-
if (outEvt.isMember("method") && outEvt["method"].isString() &&
100-
outEvt["method"].asString() == "resume_in_new_agent_tab")
101-
{
102-
return SendEventRoute::ResumeInNewAgentTab;
64+
// Check method-based direct dispatch routes
65+
if (outEvt.isMember("method") && outEvt["method"].isString())
66+
{
67+
const auto method = outEvt["method"].asString();
68+
if (method == "autofix_state")
69+
{
70+
return SendEventRoute::AutofixState;
71+
}
72+
if (method == "agent_status")
73+
{
74+
return SendEventRoute::AgentStatus;
75+
}
76+
if (method == "close_agent_pane")
77+
{
78+
return SendEventRoute::CloseAgentPane;
79+
}
80+
if (method == "view_changed")
81+
{
82+
return SendEventRoute::ViewChanged;
83+
}
84+
if (method == "resume_in_new_agent_tab")
85+
{
86+
return SendEventRoute::ResumeInNewAgentTab;
87+
}
10388
}
10489

10590
// Broadcast path: params.event is required
@@ -195,30 +180,4 @@ namespace Microsoft::Terminal::Protocol::Parsing
195180
}
196181
return PaneOutputSource::Scrollback;
197182
}
198-
199-
// ── QuickPick choices validation ──
200-
201-
// Validate that a JSON string is a valid array (for QuickPick choices).
202-
// On success, |outChoices| contains the parsed array.
203-
inline bool ValidateQuickPickChoices(const std::string& choicesJson, Json::Value& outChoices)
204-
{
205-
if (!ParseJson(choicesJson, outChoices))
206-
{
207-
return false;
208-
}
209-
return outChoices.isArray();
210-
}
211-
212-
// ── SetSettings validation ──
213-
214-
// Validate that a string is non-empty valid JSON (for SetSettings).
215-
inline bool ValidateSettingsJson(const std::string& settingsJson)
216-
{
217-
if (settingsJson.empty())
218-
{
219-
return false;
220-
}
221-
Json::Value parsed;
222-
return ParseJson(settingsJson, parsed);
223-
}
224183
}

src/cascadia/TerminalProtocol/ft_fuzzer/fuzzmain.cpp

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22
// Licensed under the MIT license.
33
//
44
// Fuzzing harness for Terminal Protocol server parsing logic.
5-
// Targets: ClassifySendEvent, ParseSplitDirection, ClassifyPaneOutputSource,
6-
// ValidateQuickPickChoices, ValidateSettingsJson.
5+
// Targets: ClassifySendEvent, ParseSplitDirection, ClassifyPaneOutputSource.
76
//
87
// Built under the Fuzzing MSBuild configuration with LibFuzzer
98
// instrumentation; submittable to OneFuzz via the CI pipeline.
@@ -45,19 +44,6 @@ static int FuzzOneInput(const uint8_t* data, size_t size)
4544
ProtocolParsing::ClassifyPaneOutputSource(input);
4645
}
4746

48-
// ── Target 4: ValidateQuickPickChoices ──
49-
// Feed fuzzed data as a JSON choices array.
50-
{
51-
Json::Value choices;
52-
ProtocolParsing::ValidateQuickPickChoices(input, choices);
53-
}
54-
55-
// ── Target 5: ValidateSettingsJson ──
56-
// Feed fuzzed data as settings JSON content.
57-
{
58-
ProtocolParsing::ValidateSettingsJson(input);
59-
}
60-
6147
return 0;
6248
}
6349

src/tools/wtcli/ft_fuzzer/fuzzmain.cpp

Lines changed: 27 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Licensed under the MIT license.
33
//
44
// Fuzzing harness for wtcli CLI utility functions.
5-
// Targets: TranslateKeys, BuildSendEventJson, MatchesEventFilter.
5+
// Targets: BuildSendEventJson, MatchesEventFilter.
66
//
77
// Built under the Fuzzing MSBuild configuration with LibFuzzer
88
// instrumentation; submittable to OneFuzz via the CI pipeline.
@@ -43,66 +43,44 @@ static int FuzzOneInput(const uint8_t* data, size_t size)
4343
}
4444

4545
// Split the entire input into segments for use across all targets.
46-
// We need at least 5 segments: [keys...] [eventType] [paramsJson]
47-
// [paneId] [eventTypeFilter]
48-
auto parts = SplitInput(data, size, 5);
46+
// We need at least 4 segments: [eventType] [paramsJson]
47+
// [sessionId] [eventTypeFilter]
48+
auto parts = SplitInput(data, size, 4);
4949

50-
// ── Target 1: TranslateKeys ──
51-
// Split the first segment on tab bytes to create multiple key entries,
52-
// exercising the key-name matching and multi-key path.
53-
{
54-
std::vector<std::string> keys;
55-
const auto& keyData = parts[0];
56-
size_t start = 0;
57-
for (size_t i = 0; i < keyData.size(); ++i)
58-
{
59-
if (keyData[i] == '\t')
60-
{
61-
keys.emplace_back(keyData.data() + start, i - start);
62-
start = i + 1;
63-
}
64-
}
65-
if (start < keyData.size())
66-
{
67-
keys.emplace_back(keyData.data() + start, keyData.size() - start);
68-
}
69-
if (keys.empty())
70-
{
71-
keys.push_back(keyData);
72-
}
73-
74-
wtcli::TranslateKeys(keys);
75-
}
76-
77-
// ── Target 2: BuildSendEventJson ──
78-
// Fuzz all three input parameters: eventType, paramsJson, and paneId.
50+
// ── Target 1: BuildSendEventJson ──
51+
// Fuzz all three input parameters: eventType, paramsJson, and sessionId.
7952
{
8053
Json::Value evt;
81-
wtcli::BuildSendEventJson(parts[1], parts[2], parts[3], evt);
54+
wtcli::BuildSendEventJson(parts[0], parts[1], parts[2], evt);
8255
}
8356

84-
// ── Target 3: MatchesEventFilter ──
85-
// Construct semi-valid JSON from fuzzed fields so the parser succeeds
86-
// and the deep matching logic (pane_id, wildcard) is actually reached.
57+
// ── Target 2: MatchesEventFilter ──
58+
// Construct valid JSON from fuzzed fields using Json::Value so the parser
59+
// succeeds and the deep matching logic (session_id, wildcard) is reached
60+
// even when fuzzed strings contain quotes/backslashes/control chars.
8761
{
88-
// 3a: Fuzzed event structure with fuzzed filter.
89-
const auto& fuzzedPaneId = parts[3];
90-
const auto& fuzzedEvent = parts[1];
91-
const auto& fuzzedFilter = parts[4];
62+
// 2a: Fuzzed event structure with fuzzed filter.
63+
const auto& fuzzedSessionId = parts[2];
64+
const auto& fuzzedEvent = parts[0];
65+
const auto& fuzzedFilter = parts[3];
9266

93-
std::string syntheticJson =
94-
R"({"params":{"pane_id":")" + fuzzedPaneId +
95-
R"(","event":")" + fuzzedEvent +
96-
R"("}})";
67+
Json::Value params;
68+
params["session_id"] = fuzzedSessionId;
69+
params["event"] = fuzzedEvent;
70+
Json::Value ev;
71+
ev["params"] = params;
72+
Json::StreamWriterBuilder wb;
73+
wb["indentation"] = "";
74+
auto syntheticJson = Json::writeString(wb, ev);
9775

98-
wtcli::MatchesEventFilter(syntheticJson, fuzzedPaneId, fuzzedFilter);
76+
wtcli::MatchesEventFilter(syntheticJson, fuzzedSessionId, fuzzedFilter);
9977

100-
// 3b: Mismatched pane_id — exercises the rejection path.
78+
// 2b: Mismatched session_id — exercises the rejection path.
10179
wtcli::MatchesEventFilter(syntheticJson, "999", fuzzedFilter);
10280

103-
// 3c: Raw fuzzed bytes as event JSON — exercises parse-failure path.
81+
// 2c: Raw fuzzed bytes as event JSON — exercises parse-failure path.
10482
const std::string raw(reinterpret_cast<const char*>(data), size);
105-
wtcli::MatchesEventFilter(raw, fuzzedPaneId, fuzzedFilter);
83+
wtcli::MatchesEventFilter(raw, fuzzedSessionId, fuzzedFilter);
10684
}
10785

10886
return 0;

src/tools/wtcli/main.cpp

Lines changed: 11 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#include <winrt/Microsoft.Terminal.Protocol.h>
77

88
#include "Formatting.h"
9+
#include "wtcli_functions.h"
910

1011
#include <CLI/CLI.hpp>
1112

@@ -688,31 +689,16 @@ int main()
688689
try
689690
{
690691
Json::Value evt;
691-
evt["type"] = "event";
692-
evt["method"] = "agent_event";
693-
694-
Json::Value params;
695-
if (!sendEventJson.empty())
692+
auto resolvedSessionId = !sendEventPaneTarget.empty()
693+
? sendEventPaneTarget
694+
: GuidToString(ResolveSessionId(server, ""));
695+
if (!wtcli::BuildSendEventJson(sendEventType, sendEventJson, resolvedSessionId, evt))
696696
{
697-
Json::CharReaderBuilder rb;
698-
std::string errs;
699-
std::istringstream ss(sendEventJson);
700-
if (!Json::parseFromStream(rb, ss, &params, &errs) || !params.isObject())
701-
{
702-
fprintf(stderr, "Invalid JSON: expected an object\n");
703-
exitCode = 1;
704-
return;
705-
}
697+
fprintf(stderr, "Invalid JSON for --json: value must be a JSON object (e.g. '{\"key\":\"val\"}')\n");
698+
exitCode = 1;
699+
return;
706700
}
707701

708-
params["event"] = sendEventType;
709-
if (!sendEventPaneTarget.empty())
710-
params["session_id"] = sendEventPaneTarget;
711-
else
712-
params["session_id"] = GuidToString(ResolveSessionId(server, ""));
713-
714-
evt["params"] = params;
715-
716702
Json::StreamWriterBuilder wb;
717703
wb["indentation"] = "";
718704
server.SendEvent(winrt::to_hstring(Json::writeString(wb, evt)));
@@ -747,38 +733,10 @@ int main()
747733
auto callback = winrt::make<EventCallback>([&](winrt::hstring const& eventJson) {
748734
auto eventUtf8 = winrt::to_string(eventJson);
749735

750-
// Optionally filter by pane_id and/or event type
751-
if (!listenTarget.empty() || !listenEventFilter.empty())
736+
// Optionally filter by session_id and/or event type
737+
if (!wtcli::MatchesEventFilter(eventUtf8, listenTarget, listenEventFilter))
752738
{
753-
Json::Value ev;
754-
Json::CharReaderBuilder rb;
755-
std::string errs;
756-
std::istringstream ss(eventUtf8);
757-
if (Json::parseFromStream(rb, ss, &ev, &errs))
758-
{
759-
if (!listenTarget.empty())
760-
{
761-
auto sessionId = ev["params"].get("session_id", "").asString();
762-
if (sessionId != listenTarget)
763-
return;
764-
}
765-
766-
if (!listenEventFilter.empty())
767-
{
768-
auto eventType = ev["params"].get("event", "").asString();
769-
// Support trailing wildcard: "agent.*" matches "agent.task.started"
770-
if (listenEventFilter.back() == '*')
771-
{
772-
auto prefix = listenEventFilter.substr(0, listenEventFilter.size() - 1);
773-
if (eventType.substr(0, prefix.size()) != prefix)
774-
return;
775-
}
776-
else if (eventType != listenEventFilter)
777-
{
778-
return;
779-
}
780-
}
781-
}
739+
return;
782740
}
783741

784742
printf("%s\n", eventUtf8.c_str());

src/tools/wtcli/wtcli.vcxproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
</ItemGroup>
2222
<ItemGroup>
2323
<ClInclude Include="Formatting.h" />
24+
<ClInclude Include="wtcli_functions.h" />
2425
</ItemGroup>
2526
<ItemDefinitionGroup>
2627
<ClCompile>

0 commit comments

Comments
 (0)