跳到主要内容

Darra_Profinet_Slave 全栈审计 — 服务层审计报告

  • 审计范围: Darra_Profinet_Slave/Darra_Pnet_Service/(只读审计,不修代码)
  • 审计日期: 2026-08-14
  • 审计项: ① 状态机 ② 生命周期 ③ 配置链路 ④ 日志 ⑤ API
  • 分级: 高 (High) / 中 (Medium) / 低 (Low) / 信息 (Info)
  • 统计: 高 2 / 中 6 / 低 7 / 信息 6

一、状态机审计

1.1 结论

服务侧状态机单一权威 (PnetStateService) 结构健康:全读写锁保护、事件锁外触发、快照接口一次加锁。 枚举已收敛为 5+1 态 (Unknown/Stopped/Initialized/ConfigError/DataExchange/Aborted)。StopCore 早退不更新状态的同族问题已全部修复(其余早退分支逐一核对无同类漏洞)。唯一实质问题 = ConfigError 新状态未同步消费端(GUI/SDK),见 H2。

1.2 状态转换矩阵核对(全部覆盖,无未更新状态的分支)

转换入口状态更新结论
启动成功StartCoreFresh / StartCoreReuseInitialized (connected=false, err=0)
栈启动失败StartControlPlane (pnet_stack_start≠0)Stopped (err=rc)
复用栈启动失败StartCoreReuse step 5Stopped (err=rc)
复用循环启动失败StartCoreReuse step 6Stopped (err=0)
网卡缺失/数据面打开失败StartCoreFresh 阶段 1 catchConfigError (err=Win32, message=缺失网卡名)✓ 不抛不退出
IOCR/START 阶段失败阶段 2/3 catch抛异常(状态保持前值 Stopped/Unknown)✓ 如实
停止 (早退分支)StopCore 首分支Stopped (err=0, message 清空)✓ 2026-08-14 已修(原早退不更新 = GUI 误判"停止后自动启动")
停止 (完整分支)StopCoreStopped
真销毁ShutdownCore / TeardownStackStopped
周期连接SyncDataExchangeState (Connected=1)DataExchange
周期掉线SyncDataExchangeState (Connected=0)Aborted
重复启动Start() 已运行分支忽略(已运行态无需改)
运行期授权失败LicenseRecheckCallback → Stop()Stopped

其余"早退不更新"分支: ShutdownCore 早退(无实例可销毁,进程退出路径无消费者)、Start() 重复调用忽略(状态已是运行态)— 均无漏洞。

1.3 状态与 GUI 观测/启停互锁契约

服务侧契约已满足(StopCore 后必 Stopped、StartStopResponse、statusMessage 透出);但 GUI 消费端契约断裂(见 H2):GUI 运行态判定 = state != "Unknown" && state != "Stopped"ConfigError 被判定"运行中" → 配置页锁定。恢复路径存在但绕(必须先手动点停止解锁,再改网卡)。SDK MapServiceState 无 ConfigError 分支 → 落 default PnetState.Unknown(状态语义静默吞掉)。

1.4 结论细节

  • 枚举序号空洞 (0/1/2/3/6/7) 有意保留,序列化走 ToString 字符串,SDK/GUI 按字符串消费,不破坏(Info)。
  • Initialized 态 doc 注释与实现一致(connected 恒 false)。

二、生命周期审计(stop≠destroy)

2.1 结论

停止≠销毁拆分 (2026-08-14) 整体正确:Stop=软停止(实例/句柄/绑卡/配置/映射全保留)、Shutdown=真销毁(唯一 destroy 路径)、Dispose→Shutdown、OnStop→Shutdown。StartCoreFresh/StartCoreReuse 双路径资源处理基本一致、指纹守卫闭环成立。唯一高问题 = StartCoreFresh 阶段 2/3 失败路径的 Dispose 未包 try/catch(H1)

2.2 证据与发现

[高 H1] StartCoreFresh 阶段 2/3 失败路径 _driverBridge.Dispose() 未包异常

  • 位置: Core/PnetRuntime.cs L538(IOCR 配置失败)、L556(数据面启动失败)— 两处 _driverBridge.Dispose(); 裸调用。
  • 同文件对比: 阶段 1 降级路径 (L496-505) 与 ShutdownCore (L1249-1260) 均 try { Dispose } catch { _driverResidue = true; 记日志 };StartControlPlane 失败 (L573-581) 与数据面循环失败 (L631-639) 也包了 catch。同族四条路径中两条漏包
  • 后果 ①: Dispose 抛 PnetDriverException(驱动解绑彻底失败、残留半死)时 _driverResidue 不置位 → 下次启动不触发 TryRecoverDriverResidue → Probe 必 err=5 死循环(恰是 2026-08-12 修复想根治的场景)。
  • 后果 ②: 原始 InvalidOperationException("驱动 IOCR 配置失败…") 被 Dispose 异常替换 → BuildStart 拿到的 message 语义错乱,且经脱敏后用户完全看不到真实原因。

其余核对(✓):

  • StopCore/ShutdownCore 幂等、数据面状态机前值复位 (_linkDown/_everConnected/_driverStatePrev/_dataPlaneFailures) 双路径同口径 ✓
  • 指纹守卫: BuildConfigFingerprint 覆盖 NetworkInterfaceName/StationName/IP/Mask/Gateway/CycleMs/VendorId/DeviceId/DriverWatchdogMs/DriverSafeInputValue/ProcessImage 尺寸/ConfigXmlPath/NativeDllPath — 漂移 → RebuildFromScratch (安全 destroy + fresh) ✓
  • RebuildFromScratch 在 _lifecycleGate 内调用 ShutdownCore+StartCoreFresh,C# Monitor 可重入 ✓
  • StartCoreReuse 失败路径逆序回收 (栈停→驱动停) 完整 ✓;复用不重复 Probe/Open(B1 单客户端排他正确规避)✓
  • LicenseRecheckTimer 三处启停 (fresh 成功/reuse 成功/Stop/Shutdown) 对称 ✓

[低 L1] StopDataPlaneLoop 3s 超时后继续流程: 线程未退时继续 Dispose(解除映射)→ 数据面线程可能在解映射后访问视图。已有 LogWarning,但无强制兜底(现存设计,低)。

[低 L2] PnetService.OnStop 清理非对称: _apiServer.Stop() 若抛异常,_runtime.Shutdown() 被跳过(同 try 块,无独立 catch)。PnetApiServer.Stop 内部几乎不抛(各段已包 catch),实际风险低,但结构上与 StartCore 的 rollback 模式不对称。

[信息 I2] 指纹不含 ProductName/DriverDevicePath: 二者当前不进 BuildStackCfg / 不参与运行语义,漂移无影响(若未来接入栈配置需补进指纹)。


三、配置链路审计

3.1 结论

三层链路 schema 字段全对齐:GUI ConfigWriter 输出 (带往返校验 VerifyRoundTrip) → runtime-config.xml → 服务消费字段 (StationName/DeviceName/VendorID/DeviceID/InterfaceName/IpAddress/SubnetMask/Gateway/SendClock/SlotLayout Module@Slot·Submodule@Subslot·InputLength·OutputLength) 逐字段一致;样例 XML (bin/…/config/runtime-config.xml) 与 ConfigWriter 输出同构。GUI 应用配置 = SaveToFile + POST /api/config/reload 联动存在(MainForm.cs L1457-1484)。缺网卡降级 (q2) 与重选恢复路径成立(见下)。两个中等问题: reload 的 appsettings 加载口径与启动链不一致 (M3)、reload→start 前过程映像尺寸观察窗口 (M4)。

3.2 证据与发现

[中 M3] BuildConfigReload 重建 IConfiguration 用 optional: false

  • 位置: Api/PnetApiServer.cs L752 AddJsonFile("appsettings.json", optional: false, …)
  • 对比: Program.cs 启动链 (L277) 用 optional: true(2026-08-12 SCM 7031 根治:缺失走默认值不崩溃)。reload 链若 appsettings.json 缺失 → FileNotFoundException → catch 转通用失败响应,且响应文案只说"运行配置 XML 解析未通过"(实际可能是 appsettings 缺失),现场被误导。
  • 不崩溃(已 catch),但两条读取链口径不一致。

[中 M4] reload 后、start 前,/api/io 布局与配置错位窗口

  • reload 把新 ProcessImageInputSize/OutputSize 写入 _config 单例,但 PnetProcessImage 的 Resize 只在 StartCoreFresh 执行 (PnetRuntime.cs L417-421)。
  • 窗口: 用户 reload 后(未 start)读 GET /api/io → inputLength/outputLength 仍是构造时(DI 期)旧尺寸。GUI 应用配置后若立即核对布局,看到旧值。
  • 启动时 Resize 会修正(2026-08-14 IOCR 数据源对齐已覆盖启动路径),窗口仅存在于"reload 后未启动"的观测期。

[信息 I4] 其余对齐确认

  • DeriveProcessImageSizesFromLayout 与 BuildIocrConfigs 推导规则一致(同源 XML 槽位累加,1..1440 钳制)✓
  • ReductionRatio / IoItem 细粒度 (Offset/Direction) 服务不消费 — 已注释声明,属设计选择(服务按子模块总长建连续区)✓
  • PnetConfigService.Load 与 BuildConfigReload 都把最终 XML 绝对路径落 Info 日志("保存了但服务没读到"时现场可对照)✓
  • DriverAdapterName 最终化收敛到 NetworkInterfaceName(单一网口权威)✓

3.3 q2 缺网卡降级与重选恢复边界

  • 降级: 仅覆盖 Open/BindAdapter/ConfigureDevice 阶段(阶段 2/3 IOCR/START 失败仍按"如实拒绝启动"抛异常 — 符合"驱动不在=拒绝启动"铁律,边界如实)。降级态: 进程存活、控制面 (18840/API/日志) 持续服务、状态 ConfigError + statusMessage 带缺失网卡名 ✓
  • 恢复链: GUI 改选网卡 → 保存 XML → POST /api/config/reload(_config.NetworkInterfaceName 更新)→ POST /api/start → _stack==0 → StartCoreFresh → ValidateConfiguredNic → BindAdapter 新网卡 → 必通(新网卡也缺失则再次如实降级,非死锁)。
  • 前提缺口: 见 H2 — GUI 在 ConfigError 态锁定配置页,用户必须先点"停止"(→Stopped 解锁)才能改网卡。服务侧语义正确,GUI 侧绕。
  • 停止后残留: StopCore 早退分支会把 ConfigError 翻回 Stopped 并清空 message ✓(已修)。

四、日志审计(ux11 双写覆盖)

4.1 结论

ux11 文件双写 (FileWarn/FileError) 覆盖了 PnetRuntime 内绝大多数关键失败路径(探测拒启/降级/授权拒启/DLL 加载/栈启动失败/残留恢复/指纹漂移)。剩余缺口集中在 PnetRuntime 之外的宿主与定时器路径(M5/M6),以及文件 sink 自身两个低问题(M1/M2)。空 catch 全仓仅 PnetLogger 两处(控制台写失败/关流,有意静默),无其它 Console-only 静默点。

4.2 证据与发现

[中 M5] PnetService.StartCore 启动失败本身无文件双写

  • 位置: PnetService.cs L88-97 catch 只调 _logger.LogError
  • SCM 模式下 Console provider 丢弃,该错误只进 EventLog ("DarraPnet" 源)。而 StartCore 失败(如 API 18840 端口占用回滚、运行时空网卡名抛错)恰是 ux11 修复的原始目标场景("服务启动失败日志静默")。PnetRuntime 内部路径有双写,宿主层没有。

[中 M6] 授权周期复查自停 + IOCR 构建失败具体原因无文件双写

  • LicenseRecheckCallback 自停 (PnetRuntime.cs L982-985) 仅 _logger.LogError(EventLog 有,文件无)— 自停是"现场突然停站"的第一排查入口,应双写。
  • BuildIocrConfigs 的具体失败原因(XML 缺 SlotLayout/长度非法/超 1440 字节/MAC 解析失败)只经阶段 2 的通用 FileError("驱动 IOCR 配置失败") 兜底,具体原因仅 EventLog。文件侧定位不到是哪条规则拒绝。

[中 M1] PnetLogger.Get(module) 忽略 module 参数

  • 位置: Diagnostics/PnetLogger.cs L83-96 — 无论传入何模块名,一律返回 _default(构造时写死 "PnetService")。
  • 后果: 全部文件日志行的模块列恒为 [PnetService](与控制台 ILogger 的模块名不一致),按模块过滤诊断失效。注释声称"模块名相同时返回同一实例",实现是"全部模块同一实例"。

[中 M2] 日志落盘路径口径分裂

  • PnetApiServer.cs L51 注释: "现场排障查 %ProgramData%\Darra\Pnet 日志"。
  • 实际: PnetLogger 落 AppContext.BaseDirectory/logs(= 安装根 C:\DARRA\Profinet_Slave\logs,PnetLogger.cs L74);另有 EventLog "DarraPnet"。注释路径不存在 → 现场按注释找日志必扑空。

其余核对(✓): PnetLogger 线程安全(单文件流+整行锁)、按天滚动、AutoFlush、FileShare.ReadWrite、写失败降级不抛 ✓;Program.cs 控制台模式授权拒绝仅 Console.WriteLine(控制台模式本身可见,可接受)。


五、API 审计

5.1 结论

端点清单与 /api/info 列表、switch 路由三方一致(13 端点)。18840 绑定/监听异常处理链完整。错误响应形状双轨并存是 SDK 契约的有意设计。发现 3 个低问题 (L3/L4/L6) 与 1 个信息项 (I1)。

5.2 证据与发现

18840 绑定/监听异常处理链(✓ 完整): PnetApiServer.Start_listener.Start()(端口占用 → HttpListenerException)不自行 catch → PnetService.StartCore catch → rollback _runtime.Shutdown()(幂等)→ rethrow → PnetServiceBootstrap catch → StopApplication 干净停止(SCM 不触发 7031 恢复循环)。监听循环自身的 GetContextAsync 异常分"正常退出路径"(Stop 关闭)与"异常记日志 continue"两轨 ✓。

[低 L3] 400 响应回显请求体: WriteIoAsync (L956/964/972) 与 WriteDiagAsync 的参数错误消息拼接 + requestBody — 把客户端 body(可能含输出区 hex 数据)原样回显进响应,与 D 节"响应不透传内部信息"口径风格不一致(body 是客户端自己数据,风险低,但形状不统一)。

[低 L4] AlarmAckResponse.AcknowledgedCount 注释与实现不符: 注释 (L1544) "ack 单条恒 1",实际单条 ack 路径不设置该字段(恒 0)。注释与代码不一致。

[低 L6] 方法不匹配返回 404 而非 405: GET /api/start 等落到统一 "未知端点" 404。语义应为 405(端点存在、方法不允许)。SDK 只判 message,影响低。

[信息 I1] 错误形状双轨 = SDK 契约有意设计: 失败分两类 — ① WriteErrorAsync: HTTP 4xx/5xx + {success:false, message, code, errorCode, timestamp};② StartStop/ConfigReload: HTTP 200 + {ok:false, message, errorCode, timestamp}。SDK PostServiceStartStop 按 "ok" 消费(DarraPnet.cs),不叠加第二套布尔键。已文档化,不是缺陷;但未来新端点须明确归入哪一轨。errorCode 透出已全端点一致(rc/运行态码/客户端错误 0 三口径)✓。

其余核对(✓):

  • 请求体 64KB 上限 + 10s 限时 + 停止取消链接(7031 根治)✓
  • 并发 16 信号量 + Stop 排空 3s 上限(Release 只放已 Wait 到的槽位,防 SemaphoreFullException)✓
  • 鉴权: 无密钥=回环放行(监听本身仅 127.0.0.1),有密钥=恒定时间比较 ✓
  • POST /api/start 降级态返回 Ok=false + statusMessage(引导改网卡)✓
  • GET /api/diag 未映射如实返回 Ok=false(不虚构计数器)✓
  • 死模型 StatusResponse (PnetApiModels.cs) 无引用保留(注释声明"其他 agent 域,保留不动")— 提示(低,非本域清除)。

六、问题分级汇总

级别数量编号与一句话
2H1 阶段2/3失败路径 Dispose 裸调用(残留标志丢失+异常替换);H2 ConfigError 状态未同步 GUI/SDK(GUI 误锁配置、SDK 静默映射 Unknown)
6M1 PnetLogger 忽略模块名;M2 日志路径注释分裂 (%ProgramData% vs BaseDirectory\logs);M3 reload 的 appsettings optional:false 与启动链不一致;M4 reload→start 前 /api/io 布局旧值窗口;M5 StartCore 失败无文件双写;M6 授权自停/IOCR 具体原因无文件双写
7L1 StopDataPlaneLoop 3s 超时后继续;L2 OnStop 清理非对称;L3 400 回显请求体;L4 AcknowledgedCount 注释不符;L5 StatusResponse 死模型;L6 404 非 405;L7(含于 M4 备注)reload 后 ConfigError 文案残留
信息6I1 双错误形状=SDK 契约有意设计;I2 指纹不含 ProductName/DriverDevicePath;I3 枚举序号空洞有意;I4 三层 schema 对齐✓+IoItem/ReductionRatio 设计选择;I5 q2 降级恢复链✓;I6 18840 异常链✓

七、修复优先级建议(供主对话编排,非本 agent 修复)

  1. H1(先修): PnetRuntime.cs L538/L556 两处 Dispose 包 try/catch(对齐 L496-505 模式:置 _driverResidue + 记日志 + 保留原异常语义)。文件域 = Core/PnetRuntime.cs。
  2. H2(契约同步): 服务侧已正确,需 GUI (MainForm 运行态判定加 "ConfigError" 分支:非运行态+显示 statusMessage) 与 SDK (MapServiceState 加映射,或至少注释声明) 同步 — 属 GUI/SDK 文件域,与其他 agent 协调。
  3. M1/M2/M5/M6(日志): 全部在服务域,可一个 agent 批量修(PnetLogger 模块缓存字典 + 路径注释 + StartCore/LicenseRecheck/BuildIocrConfigs FileError 双写)。
  4. M3/M4(配置): M3 一行改 optional:true;M4 在 BuildConfigReload 成功后同步 Resize(需与生命周期锁协调,建议仅在非运行态时 Resize)。