-
Notifications
You must be signed in to change notification settings - Fork 3
fix(955): Race condition & TOCTOU remediation -- PR #24 audit fixes #25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1a56926
7fc6b4b
2c337a7
e35028f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -785,7 +785,19 @@ private void LoadConfig() | |
| selectedTargetCount = activeCount; | ||
|
|
||
| Button modeBtn = GetModeButton(activeMode); | ||
| if (modeBtn != null) HighlightModeButton(modeBtn); | ||
| if (modeBtn != null) | ||
| { | ||
| HighlightModeButton(modeBtn); | ||
| } | ||
| else | ||
| { | ||
| // [Build 954]: Saved mode is deprecated/unrecognized -- normalize both vars to RMA baseline. | ||
| Print("[WARN][954] Unrecognized saved mode '" + activeMode + "' -- falling back to RMA."); | ||
| activeMode = "RMA"; | ||
| selectedConfigMode = "RMA"; | ||
| modeBtn = GetModeButton("RMA"); | ||
| if (modeBtn != null) HighlightModeButton(modeBtn); | ||
| } | ||
|
|
||
| // Apply active mode+count settings to UI | ||
| ApplySettings(fullConfig.GetSettings(activeMode, activeCount)); | ||
|
|
@@ -2986,67 +2998,8 @@ private void SyncAll_Click(object sender, RoutedEventArgs e) | |
|
|
||
| private void ConnectToStrategy() | ||
| { | ||
| try | ||
| { | ||
| lock (tcpLock) | ||
| { | ||
| if (isConnected) return; | ||
|
|
||
| tcpClient = new TcpClient(); | ||
| tcpClient.Connect("127.0.0.1", IpcPort); | ||
| tcpStream = tcpClient.GetStream(); | ||
| isConnected = true; | ||
| // [Build 934]: Reset retry counters on successful connect | ||
| _ipcRetryCount = 0; | ||
| _lastRetryLogTime = DateTime.MinValue; | ||
|
|
||
| Print($"V12 Panel: Strategy connected on port {IpcPort} ?"); | ||
|
|
||
| if (ChartControl != null) | ||
| { | ||
| ChartControl.Dispatcher.BeginInvoke(new Action(() => | ||
| { | ||
| if (hubStatusLed != null) | ||
| { | ||
| hubStatusLed.Background = GreenFg; | ||
| hubStatusLed.ToolTip = "IPC Connected"; | ||
| } | ||
| })); | ||
| } | ||
|
|
||
| receiveThread = new Thread(ReceiveLoop) { IsBackground = true, Name = "V12_Std_Receive" }; | ||
| receiveThread.Start(); | ||
|
|
||
| SendCommand("GET_LAYOUT"); | ||
| } | ||
| } | ||
| catch (Exception) | ||
| { | ||
| isConnected = false; | ||
| _ipcRetryCount++; | ||
|
|
||
| // [Build 934]: Log only on first failure and then at most once per 60 seconds | ||
| if (_ipcRetryCount == 1 || (DateTime.Now - _lastRetryLogTime).TotalSeconds >= 60) | ||
| { | ||
| Print($"V12 Panel: Strategy offline -- retrying in background (attempt #{_ipcRetryCount})"); | ||
| _lastRetryLogTime = DateTime.Now; | ||
| } | ||
|
|
||
| if (ChartControl != null) | ||
| { | ||
| ChartControl.Dispatcher.BeginInvoke(new Action(() => | ||
| { | ||
| if (hubStatusLed != null) | ||
| { | ||
| hubStatusLed.Background = TextMuted; | ||
| hubStatusLed.ToolTip = "Waiting for Strategy (retrying...)"; | ||
| } | ||
| })); | ||
| } | ||
|
|
||
| // [Build 933]: Start retry loop on initial failure (market closed / Strategy not yet live). | ||
| ScheduleReconnect(); | ||
| } | ||
| // [Build 954]: IPC deprecated. Strategy no longer hosts IPC server (Phase 6 pruning). | ||
| // [Build 955]: Dead code removed. SendCommand() is safely no-oped via tcpStream null guard. | ||
| } | ||
|
Comment on lines
2999
to
3003
|
||
|
|
||
| private void DisconnectFromStrategy() | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -369,8 +369,20 @@ private bool HandleOrderCancelled(Order order) | |||||||||||||||||||
| { | ||||||||||||||||||||
| if (kvp.Value.OldOrder == order && activePositions.TryGetValue(kvp.Key, out var pos)) | ||||||||||||||||||||
| { | ||||||||||||||||||||
| if (pos.RemainingContracts > 0) | ||||||||||||||||||||
| CreateNewStopOrder(kvp.Key, pos.RemainingContracts, kvp.Value.StopPrice, kvp.Value.Direction); | ||||||||||||||||||||
| // Build 955: Snapshot qty under stateLock -- single atomic read for both check and use. | ||||||||||||||||||||
| int _stopQty; | ||||||||||||||||||||
| lock (stateLock) { _stopQty = pos.RemainingContracts; } | ||||||||||||||||||||
| if (_stopQty > 0) | ||||||||||||||||||||
| { | ||||||||||||||||||||
| CreateNewStopOrder(kvp.Key, _stopQty, kvp.Value.StopPrice, kvp.Value.Direction); | ||||||||||||||||||||
| // Build 950: Restore OCO-cascade-cancelled targets after stop replacement. | ||||||||||||||||||||
| if (kvp.Value.BracketRestorationNeeded && kvp.Value.CapturedTargets != null) | ||||||||||||||||||||
| { | ||||||||||||||||||||
| TargetSnapshot[] _mSnap = kvp.Value.CapturedTargets; | ||||||||||||||||||||
| string _mKey = kvp.Key; | ||||||||||||||||||||
| TriggerCustomEvent(o => RestoreCascadedTargets(_mKey, _mSnap), null); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
| if (pendingStopReplacements.TryRemove(kvp.Key, out _)) Interlocked.Decrement(ref pendingReplacementCount); | ||||||||||||||||||||
| handled = true; | ||||||||||||||||||||
| break; | ||||||||||||||||||||
|
|
@@ -578,6 +590,39 @@ private void HandleMatchedFollowerOrder(string matchedEntry, PositionInfo matche | |||||||||||||||||||
| } | ||||||||||||||||||||
| else | ||||||||||||||||||||
| { | ||||||||||||||||||||
| // Build 950: Follower stop replacement -- mirrors HandleOrderCancelled master path. | ||||||||||||||||||||
| // Follower stop cancels arrive via OnAccountOrderUpdate (not OnOrderUpdate), so | ||||||||||||||||||||
| // HandleOrderCancelled never fires for them. Match pendingStopReplacements here. | ||||||||||||||||||||
| // This block is in the else branch because stop orders are not in entryOrders. | ||||||||||||||||||||
| if (order.Name.StartsWith("Stop_") || order.Name.StartsWith("S_")) | ||||||||||||||||||||
| { | ||||||||||||||||||||
| foreach (var _psr in pendingStopReplacements.ToArray()) | ||||||||||||||||||||
| { | ||||||||||||||||||||
| if (_psr.Value.OldOrder == order) | ||||||||||||||||||||
|
||||||||||||||||||||
| if (_psr.Value.OldOrder == order) | |
| var _oldOrder = _psr.Value.OldOrder; | |
| // Match by reference when possible, but fall back to OrderId equality to handle | |
| // cases where the callback supplies a different Order instance for the same broker order. | |
| if (_oldOrder == order | |
| || (_oldOrder != null | |
| && order != null | |
| && !string.IsNullOrEmpty(_oldOrder.OrderId) | |
| && string.Equals(_oldOrder.OrderId, order.OrderId, StringComparison.Ordinal))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TryRemove is unconditional on activePositions.TryGetValue — asymmetric with master path
In the master path (HandleOrderCancelled, line 370), the pending record is only removed when activePositions.TryGetValue succeeds (the TryRemove is inside the combined if condition). Here in the follower path the TryRemove at line 620 is outside the activePositions.TryGetValue block, so the pending record is always cleaned up even when the position key is missing from activePositions.
If a position is cleaned up from activePositions (e.g., by the emergency flatten) just before this callback fires, the pending stop replacement is silently dropped without a new stop being issued and without any log entry. The V8.30 timeout that would normally catch a stale pending in the master path is bypassed because the pending no longer exists.
Adding a diagnostic log when the position is missing improves observability:
if (activePositions.TryGetValue(_psr.Key, out _rPos))
{
// ... qty check and CreateNewStopOrder ...
}
else
{
Print(string.Format("[B955] WARN: Follower stop cancel matched pending '{0}' but position not in activePositions -- pending cleaned up without replacement.", _psr.Key));
}
if (pendingStopReplacements.TryRemove(_psr.Key, out _))
Interlocked.Decrement(ref pendingReplacementCount);
return;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This section claims
BUILD_TAG = "950"in the listed file changes, but the strategyBUILD_TAGwas updated to 955 in this PR. Please update the walkthrough to reflect the correct build tag (or make the note build-agnostic) to avoid future confusion during verification/audits.