max / alloy
| 1 | //! The setuid helper's wire protocol. |
| 2 | //! |
| 3 | //! Plain text on stdin and stdout, one message per line. Taking `ask` as a |
| 4 | //! closure is what keeps the whole conversation testable against a scripted |
| 5 | //! helper, with no D-Bus and no terminal anywhere near it. |
| 6 | |
| 7 | use ; |
| 8 | use Path; |
| 9 | use ; |
| 10 | use Arc; |
| 11 | use ; |
| 12 | |
| 13 | use ; |
| 14 | |
| 15 | use Prompt; |
| 16 | use cratechild_command; |
| 17 | |
| 18 | /// Run the helper's conversation to its end. |
| 19 | /// |
| 20 | /// `ask` is how a question reaches the screen; it returns once the prompt has |
| 21 | /// been handed over, and the answer comes back through the prompt's own reply |
| 22 | /// channel. Taking it as a closure is what keeps this function testable against |
| 23 | /// a scripted helper with no D-Bus and no terminal anywhere near it. |
| 24 | /// |
| 25 | /// `withdrawn` is polkit's cancellation, shared with every prompt this hands |
| 26 | /// out. Checked between messages so a conversation whose command has gone away |
| 27 | /// stops rather than asking the next question in a sequence nobody is waiting |
| 28 | /// on the end of. |
| 29 | pub |
| 30 | helper: &Path, |
| 31 | user: &str, |
| 32 | cookie: &str, |
| 33 | action_id: &str, |
| 34 | message: &str, |
| 35 | withdrawn: &, |
| 36 | ask: impl Fn -> , |
| 37 | |
| 38 | // Wrapped in the guard on the same expression that spawns it, and that is |
| 39 | // load-bearing rather than style. Everything below this line can leave by |
| 40 | // `?`, and `std::process::Child` has no `Drop`: a helper let go of that way |
| 41 | // is neither killed nor reaped. Measured, not reasoned about — see |
| 42 | // `a_question_nobody_can_receive_still_closes_the_helper`, which failed |
| 43 | // with one process left behind before this existed. |
| 44 | let mut child = Helper |
| 45 | child_command |
| 46 | .arg |
| 47 | .stdin |
| 48 | .stdout |
| 49 | .stderr |
| 50 | .spawn |
| 51 | .with_context?, |
| 52 | ; |
| 53 | |
| 54 | let mut stdin = child.0.stdin.take.context?; |
| 55 | let stdout = child |
| 56 | .0 |
| 57 | .stdout |
| 58 | .take |
| 59 | .context?; |
| 60 | let mut lines = new.lines; |
| 61 | |
| 62 | // The cookie first, on its own line. It is what polkit gave us and what the |
| 63 | // helper hands back to prove this conversation is the one polkit asked for. |
| 64 | // |
| 65 | // Checked for a newline for exactly the reason the answer is, further down, |
| 66 | // and it was the half that was not. The helper's protocol is one line per |
| 67 | // message, so a cookie carrying a newline ends the line early and everything |
| 68 | // after it is read as the *next* message — which at that point in the |
| 69 | // exchange is the response to the first PAM prompt. That is a password |
| 70 | // supplied by the caller without anyone being asked. |
| 71 | // |
| 72 | // Unreachable today and stated anyway, which is this file's habit |
| 73 | // everywhere else: polkitd generates the cookie, the bus lets nobody else |
| 74 | // call this interface (see [`Listener`]), and both of those are somebody |
| 75 | // else's file rather than an invariant this code holds. The same argument |
| 76 | // the `printable` calls are made under. |
| 77 | if cookie.contains |
| 78 | bail!; |
| 79 | |
| 80 | writeln!.context?; |
| 81 | |
| 82 | while let Some = lines.next.transpose.context? |
| 83 | if withdrawn.load |
| 84 | bail!; |
| 85 | |
| 86 | match parse |
| 87 | Some => |
| 88 | let = |
| 89 | new; |
| 90 | ask?; |
| 91 | |
| 92 | // A closed channel is the screen going away mid-prompt, which |
| 93 | // is a dismissal rather than an answer. |
| 94 | let answer = answers.recv.unwrap_or; |
| 95 | let Some = answer else |
| 96 | bail!; |
| 97 | ; |
| 98 | // Checked again here, and not only at the top of the loop. |
| 99 | // The loop's check is taken before the question goes up, so on |
| 100 | // its own it covers a withdrawal that arrives before anyone is |
| 101 | // asked and nothing after: a withdrawal landing while the |
| 102 | // person is typing would be seen by the screen, which takes the |
| 103 | // modal down, and by nothing here until the *next* message — |
| 104 | // and there is no next message, because the answer is written |
| 105 | // first. So an Enter that beats the screen's poll would put the |
| 106 | // password on the wire of a conversation polkit has abandoned, |
| 107 | // which is precisely the failure the flag exists to stop. |
| 108 | // |
| 109 | // What is left after this is the window between the load and |
| 110 | // the write, and it is not closed here on purpose. Closing it |
| 111 | // would mean the withdrawal and the write taking one lock, and |
| 112 | // the withdrawal arrives on |
| 113 | // [`cancel_authentication`](Listener::cancel_authentication), |
| 114 | // which zbus dispatches on the single executor thread this |
| 115 | // handler was made `async` to stop blocking. A write to a pipe |
| 116 | // whose reader is a setuid helper mid-PAM can block, so that |
| 117 | // lock would hand the console's freeze back for a race whose |
| 118 | // outcome is already indistinguishable: a withdrawal that lands |
| 119 | // after the bytes leave is one that lands after the helper has |
| 120 | // them, and no amount of locking on this side changes that. |
| 121 | if withdrawn.load |
| 122 | bail!; |
| 123 | |
| 124 | // The caller's obligation from [`Prompt::answer`], enforced |
| 125 | // where it is relied on. A newline inside the answer would end |
| 126 | // the line early and turn the rest into the next message of a |
| 127 | // protocol that is deciding whether to authorize something, so |
| 128 | // an answer carrying one is refused rather than written. The |
| 129 | // value is not named in the error: it is the password. |
| 130 | if answer.expose.contains |
| 131 | bail!; |
| 132 | |
| 133 | // Written as bytes rather than through `writeln!`, because the |
| 134 | // answer is a `Secret` and a `Secret` is bytes: formatting it |
| 135 | // would mean a `String` copy of the password that nothing |
| 136 | // scrubs, which is the copy the type exists to avoid. |
| 137 | stdin |
| 138 | .write_all |
| 139 | .and_then |
| 140 | .context?; |
| 141 | |
| 142 | Some => return Ok, |
| 143 | Some => bail!, |
| 144 | // PAM_ERROR_MSG and PAM_TEXT_INFO carry text for the user, and |
| 145 | // anything unrecognized is a helper newer than this code. Neither |
| 146 | // is a reason to abandon a conversation that is still going: the |
| 147 | // helper says SUCCESS or FAILURE either way, and that is what this |
| 148 | // waits for. |
| 149 | None => |
| 150 | |
| 151 | |
| 152 | |
| 153 | bail! |
| 154 | |
| 155 | |
| 156 | /// The helper, closed out however the conversation ends. |
| 157 | /// |
| 158 | /// A guard rather than a call at each exit, and the difference was a real leak |
| 159 | /// rather than a tidiness argument. [`converse`] has six places it can leave by |
| 160 | /// `?` — stdin, stdout, the cookie write, the read of each line, the write of |
| 161 | /// the answer, and the `ask` that hands the question to the screen — and |
| 162 | /// `std::process::Child` implements no `Drop`, so a helper let go of on any of |
| 163 | /// them is neither killed nor reaped. |
| 164 | /// |
| 165 | /// The `ask` one is not hypothetical. It is the path [`Agent::drop`] |
| 166 | /// deliberately creates: closing the prompt receiver turns a parked `send` into |
| 167 | /// an error so the conversation ends rather than holding the console open. That |
| 168 | /// case is "the console is quitting", and without this it leaves a setuid helper |
| 169 | /// inside `pam_authenticate` for a console that no longer exists. |
| 170 | /// |
| 171 | /// Killing rather than waiting politely, because every path here has already |
| 172 | /// decided the conversation is over and a helper mid-`pam_authenticate` can sit |
| 173 | /// for as long as its PAM stack wants. The wait is what stops it becoming a |
| 174 | /// zombie for the life of the console. |
| 175 | ; |
| 176 | |
| 177 | |
| 178 | |
| 179 | let _ = self.0.kill; |
| 180 | let _ = self.0.wait; |
| 181 | |
| 182 | |
| 183 | |
| 184 | /// One line of the helper's protocol. |
| 185 | |
| 186 | pub |
| 187 | Prompt , |
| 188 | Success, |
| 189 | Failure, |
| 190 | |
| 191 | |
| 192 | |
| 193 | |
| 194 | // Trailing whitespace is significant in the other direction: the prompt |
| 195 | // is usually `Password: ` and the trailing space is part of what a GUI |
| 196 | // agent would draw. It is trimmed here because the console draws its |
| 197 | // own label and the space would land in the middle of a rendered line. |
| 198 | let = line.split_once.unwrap_or; |
| 199 | match verb |
| 200 | "PAM_PROMPT_ECHO_OFF" => Some |
| 201 | question: rest.trim_end.to_string, |
| 202 | echo: false, |
| 203 | , |
| 204 | "PAM_PROMPT_ECHO_ON" => Some |
| 205 | question: rest.trim_end.to_string, |
| 206 | echo: true, |
| 207 | , |
| 208 | "SUCCESS" => Some, |
| 209 | "FAILURE" => Some, |
| 210 | _ => None, |
| 211 | |
| 212 | |
| 213 | |
| 214 | |
| 215 | |
| 216 | |
| 217 |