DF-3728 / harness.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 | /* * DF-3728 harness โ ciss_notify_complete() fatal-error path * (sys/dev/raid/ciss/ciss.c:3619-3627) releases the notify request back to * the free queue WITHOUT clearing sc->ciss_periodic_notify (unlike the two * other exit paths at ciss.c:3648 and 3653). The dangling pointer is then * reused by the next ciss_periodic() tick via ciss_notify_event() * (ciss.c:3526): the SAME ciss_request is handed out to a new submitter * (CAM I/O or a BMIC command) AND re-used/re-posted by the notify chain, * giving double-ownership, command clobbering (ciss_preen_command + * notify CDB overwrite) and a double completion โ the CAM ccb gets * xpt_done()'d twice (use-after-free in CAM). * * This userspace replica models the request/free-list state machine exactly * and drives the fatal-error sequence. Trigger precondition in the kernel: * the adapter must return a fatal command status (!= SUCCESS, * TARGET_STATUS, TIMEOUT โ e.g. CISS_CMD_STATUS_PROTOCOL_ERROR or * HARDWARE_ERROR) for the Notify-on-Event command (flaky/distressed * firmware), then 10s later ciss_periodic restarts the chain. * * Build: cc -O2 -Wall -o harness harness.c * Fixed: cc -DFIXED ... adds `sc->ciss_periodic_notify = NULL;` to the * fatal path (fix.diff). */ #include <stdio.h> #include <stdlib.h> #include <string.h> /* ---- statuses (cissreg.h:122-134) ---- */ #define CISS_CMD_STATUS_SUCCESS 0 #define CISS_CMD_STATUS_TARGET_STATUS 1 #define CISS_CMD_STATUS_PROTOCOL_ERROR 5 #define CISS_CMD_STATUS_HARDWARE_ERROR 6 #define CISS_CMD_STATUS_TIMEOUT 11 #define CISS_FLAG_NOTIFY_OK (1<<0) #define CISS_MAX_REQUESTS 16 struct ciss_request_rep { int cr_tag; int cr_on_freeq; int cr_busy; /* CISS_REQ_BUSY */ int cr_posts; /* times posted to adapter */ int cr_completions; /* times adapter completed it */ void *cr_data; /* owner's data buffer */ const char *cr_owner; /* who owns it now */ }; static struct { struct ciss_request_rep req[CISS_MAX_REQUESTS]; struct ciss_request_rep *freeq[CISS_MAX_REQUESTS]; int nfree; struct ciss_request_rep *periodic_notify; int flags; int cam_callbacks; /* xpt_done() invocations */ void *cam_ccb; } sc; static void notify_complete_rep(struct ciss_request_rep *cr); static struct ciss_request_rep * get_request(void) /* ciss_get_request, LIFO freeq */ { if (sc.nfree == 0) return NULL; return sc.freeq[--sc.nfree]; } static void release_request(struct ciss_request_rep *cr) /* ciss_release_request */ { sc.freeq[sc.nfree++] = cr; cr->cr_on_freeq = 1; cr->cr_owner = "freeq"; } static void post(struct ciss_request_rep *cr) /* ciss_start -> adapter IPQ */ { cr->cr_posts++; cr->cr_busy = 1; } static void adapter_complete(struct ciss_request_rep *cr) /* ciss_complete dispatch */ { cr->cr_completions++; cr->cr_busy = 0; /* in the kernel the callback of the CURRENT owner runs here */ if (strcmp(cr->cr_owner, "CAM") == 0) sc.cam_callbacks++; /* ciss_cam_complete -> xpt_done(ccb) */ else if (strcmp(cr->cr_owner, "notify") == 0) notify_complete_rep(cr); } static void notify_event(void) /* ciss_notify_event (ciss.c:3517) */ { struct ciss_request_rep *cr = sc.periodic_notify; /* ciss.c:3526 */ if (cr == NULL) { cr = get_request(); /* ciss.c:3530 */ if (cr == NULL) return; sc.periodic_notify = cr; /* 3534 */ cr->cr_owner = "notify"; cr->cr_data = (void *)0x51100000UL; /* kmalloc(512) โ placeholder */ } /* ciss.c:3544: if (cr->cr_data == NULL) alloc โ NOTE: for the stale * pointer this reads the NEW owner's data pointer, skips alloc, and * the notify CDB below CLOBBERS the new owner's command. */ cr->cr_data = cr->cr_data ? cr->cr_data : (void *)0x51100000UL; post(cr); /* ciss_start, 3579 */ sc.flags |= CISS_FLAG_NOTIFY_OK; /* 3593 */ } static void notify_complete_rep(struct ciss_request_rep *cr) /* ciss_notify_complete */ { int command_status = CISS_CMD_STATUS_PROTOCOL_ERROR; /* fatal */ /* ciss.c:3619-3627 โ the fatal-error path */ if ((command_status != CISS_CMD_STATUS_SUCCESS) && (command_status != CISS_CMD_STATUS_TARGET_STATUS) && (command_status != CISS_CMD_STATUS_TIMEOUT)) { printf(" notify_complete: fatal status %d -> release_request(cr)\n" " (ciss.c:3624 releases; ciss.c:3625 clears NOTIFY_OK;\n" " ciss_periodic_notify is NOT cleared โ dangling)\n", command_status); release_request(cr); /* ciss.c:3624 */ sc.flags &= ~CISS_FLAG_NOTIFY_OK; /* ciss.c:3625 */ #ifdef FIXED sc.periodic_notify = NULL; /* the fix */ #endif return; } } int main(void) { struct ciss_request_rep *cam_cr; int i, oob = 0; printf("== DF-3728 ciss_notify_complete dangling periodic_notify ==\n\n"); /* populate free queue (tag order like ciss_init_requests) */ for (i = CISS_MAX_REQUESTS - 1; i >= 1; i--) { sc.req[i].cr_tag = i; release_request(&sc.req[i]); } /* step 1: periodic starts the notify chain */ notify_event(); printf("step1: notify chain started, periodic_notify=tag%d, posts=%d\n\n", sc.periodic_notify->cr_tag, sc.periodic_notify->cr_posts); /* step 2: adapter completes the notify command with a FATAL status */ adapter_complete(sc.periodic_notify); printf("step2: fatal completion done; periodic_notify = %s\n", sc.periodic_notify == NULL ? "NULL (cleared)" : (sc.periodic_notify->cr_on_freeq ? "dangling (request back on freeq)" : "still owned")); if (sc.periodic_notify == NULL) { printf(" (with the fix the pointer is cleared โ no" " dangling reuse)\n\n"); return 0; } printf("\n"); /* step 3: a CAM I/O grabs the SAME request off the free queue */ cam_cr = get_request(); if (cam_cr == NULL || cam_cr != sc.periodic_notify) { printf("step3: free queue returned a different request โ " "scenario not exercised\n"); return 1; } cam_cr->cr_owner = "CAM"; cam_cr->cr_data = (void *)0x52200000UL; /* csio->data_ptr */ sc.cam_ccb = (void *)0x53300000UL; printf("step3: CAM I/O reuses tag%d (data=%p), posts=%d\n\n", cam_cr->cr_tag, cam_cr->cr_data, cam_cr->cr_posts); post(cam_cr); /* step 4: 10s later ciss_periodic sees !NOTIFY_OK -> notify_event() * follows the STALE periodic_notify (ciss.c:3526) and double-uses it */ printf("step4: ciss_periodic -> ciss_notify_event uses stale pointer:\n"); notify_event(); if (cam_cr->cr_posts != 2 || sc.periodic_notify->cr_data == (void *)0x52200000UL) oob = 1; printf(" tag%d now posts=%d (DOUBLE POST), data=%p (CAM's buffer" " kept: 0x%x), completions so far=%d\n\n", cam_cr->cr_tag, cam_cr->cr_posts, sc.periodic_notify->cr_data, sc.periodic_notify->cr_data == (void *)0x52200000UL, cam_cr->cr_completions); /* step 5: the adapter completes EACH post of the command once */ for (i = 0; i < cam_cr->cr_posts; i++) adapter_complete(cam_cr); printf("step5: two completions delivered -> CAM callback (xpt_done) ran" " %d time(s) for ONE ccb\n\n", sc.cam_callbacks); if (oob || sc.cam_callbacks >= 2) { printf("[BUG REPRODUCED] double-ownership of ciss_request tag%d:\n" " - re-posted while in flight by CAM (posts=%d)\n" " - CAM ccb completed twice (xpt_done x%d => CAM UAF)\n" " - notify CDB/preen clobbered the in-flight CAM command\n", cam_cr->cr_tag, cam_cr->cr_posts, sc.cam_callbacks); return 0; } #ifdef FIXED printf("[FIXED] fatal path clears periodic_notify: notify chain gets a\n" "fresh request; CAM request untouched (posts=%d, callbacks=%d).\n", cam_cr->cr_posts, sc.cam_callbacks); return 0; #else printf("[NOT REPRODUCED]\n"); return 1; #endif } |