/src/libiec61850/fuzz/fuzz_sv_subscriber.c
Line | Count | Source |
1 | | /* |
2 | | * libFuzzer + ASan harness for the IEC 61850-9-2 Sampled Values (SV) subscriber |
3 | | * decode path — the gap noted in GHSA-jh37-3w86-6rw8 / #598: GOOSE is harnessed |
4 | | * in fuzz/, SV was not, and SV is not on OSS-Fuzz, which is why parseASDU() |
5 | | * numeric-field OOB reads sat unfound. |
6 | | * |
7 | | * Entry point: SVReceiver_handleL2Message() — the public API (added 2026-07) |
8 | | * that feeds a raw Ethernet SV frame straight into the library, driving the |
9 | | * full receive chain: parseSVMessage -> parseSVPayload -> parseSequenceOfASDU |
10 | | * -> parseASDU -> listener -> SVSubscriber_ASDU_get*() accessors. |
11 | | * |
12 | | * The listener exercises EVERY numeric getter, exactly as a real SV application |
13 | | * would. Those accessors perform fixed-width reads (SmpCnt 2, ConfRev 4, |
14 | | * RefrTm 8, SmpSynch/SmpMod 1, SmpRate 2). Before a96bd67 parseASDU stored a |
15 | | * bare pointer for a too-short field and the accessor read past it; a96bd67 |
16 | | * rejects the short field before the pointer store, so the accessor sees NULL. |
17 | | * |
18 | | * EXACT-SIZE BUFFER: the frame is copied into an exact-size malloc'd buffer so a |
19 | | * 1-byte over-read at frame end hits ASan's redzone instead of stack padding. |
20 | | */ |
21 | | #include <stdio.h> |
22 | | #include <stdlib.h> |
23 | | #include <string.h> |
24 | | |
25 | | #include "sv_subscriber.h" |
26 | | #include "hal_thread.h" |
27 | | |
28 | | /* A real SV application reads the ASDU fields inside its listener callback. |
29 | | * Calling every accessor here is what turns a stored-but-too-short pointer into |
30 | | * an observable out-of-bounds read. */ |
31 | | static void |
32 | | svListener(SVSubscriber subscriber, void* parameter, SVSubscriber_ASDU asdu) |
33 | 8.98k | { |
34 | 8.98k | (void)subscriber; |
35 | 8.98k | (void)parameter; |
36 | | |
37 | 8.98k | (void)SVSubscriber_ASDU_getSmpCnt(asdu); /* reads 2 bytes */ |
38 | 8.98k | (void)SVSubscriber_ASDU_getConfRev(asdu); /* reads 4 bytes */ |
39 | 8.98k | (void)SVSubscriber_ASDU_getSmpSynch(asdu); /* reads 1 byte */ |
40 | 8.98k | (void)SVSubscriber_ASDU_getSmpMod(asdu); /* reads 1 byte */ |
41 | 8.98k | (void)SVSubscriber_ASDU_getSmpRate(asdu); /* reads 2 bytes */ |
42 | | |
43 | 8.98k | if (SVSubscriber_ASDU_hasRefrTm(asdu)) { |
44 | 546 | (void)SVSubscriber_ASDU_getRefrTmAsMs(asdu); /* reads 8 bytes */ |
45 | 546 | (void)SVSubscriber_ASDU_getRefrTmAsNs(asdu); /* reads 8 bytes */ |
46 | 546 | } |
47 | | |
48 | 8.98k | (void)SVSubscriber_ASDU_getSvId(asdu); |
49 | 8.98k | (void)SVSubscriber_ASDU_getDataSize(asdu); |
50 | 8.98k | } |
51 | | |
52 | | int |
53 | | LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) |
54 | 908 | { |
55 | | /* exact-size copy: any read at buf[size] traps on the ASan redzone */ |
56 | 908 | uint8_t* buf = (uint8_t*)malloc(size ? size : 1); |
57 | 908 | if (!buf) { |
58 | 0 | return 0; |
59 | 0 | } |
60 | 908 | if (size) { |
61 | 908 | memcpy(buf, data, size); |
62 | 908 | } |
63 | | |
64 | 908 | SVReceiver receiver = SVReceiver_create(); |
65 | | /* accept any destination MAC so the fuzzer reaches the decoder */ |
66 | 908 | SVReceiver_disableDestAddrCheck(receiver); |
67 | | |
68 | 908 | uint8_t ethAddr[6] = { 0x01, 0x0c, 0xcd, 0x04, 0x00, 0x00 }; |
69 | 908 | SVSubscriber subscriber = SVSubscriber_create(ethAddr, 0x4000 /* APPID */); |
70 | 908 | SVSubscriber_setListener(subscriber, svListener, NULL); |
71 | 908 | SVReceiver_addSubscriber(receiver, subscriber); |
72 | | |
73 | 908 | SVReceiver_handleL2Message(receiver, buf, (int)size); |
74 | | |
75 | 908 | SVReceiver_destroy(receiver); |
76 | 908 | free(buf); |
77 | 908 | return 0; |
78 | 908 | } |