chiark / gitweb /
Apply OpenSSL exception to Hippotat files
[hippotat.git] / server / daemon.rs
1 // Copyright 2021-2022 Ian Jackson and contributors to Hippotat
2 // SPDX-License-Identifier: GPL-3.0-or-later WITH LicenseRef-Hippotat-OpenSSL-Exception
3 // There is NO WARRANTY.
4
5 use std::convert::TryInto;
6 use std::ffi::CStr;
7 use std::io::IoSlice;
8 use std::os::raw::c_int;
9 use std::os::unix::io::RawFd;
10 use std::slice;
11 use std::str;
12 use std::thread::panicking;
13
14 use extend::ext;
15
16 use nix::errno::*;
17 use nix::fcntl::*;
18 use nix::unistd::*;
19 use nix::sys::stat::*;
20 use nix::sys::signal::*;
21 use nix::sys::uio::*;
22 use nix::sys::wait::*;
23
24 use hippotat::prelude as prelude;
25 use prelude::default;
26
27 pub struct Daemoniser {
28   drop_bomb: Option<()>,
29   intermediate_pid: Pid,
30   null_fd: RawFd,
31   st_wfd: RawFd,
32 }
33
34 fn crashv(ms: &[IoSlice<'_>]) -> ! {
35   unsafe {
36     let _ = writev(2, ms);
37     libc::_exit(18);
38   }
39 }
40   
41 macro_rules! crashv { { $( $m:expr ),* $(,)? } => {
42   match [
43     "hippotatd: ",
44     $( $m, )*
45     "\n",
46   ] {
47     ms => {
48       let ms = ms.map(|m| IoSlice::new(m.as_bytes()));
49       crashv(&ms)
50     }
51   }
52 } }
53
54 macro_rules! cstr { { $b:tt } => {
55   CStr::from_bytes_with_nul($b)
56     .unwrap_or_else(|_| crashm("cstr not nul terminated?! bug!"))
57 } }
58
59 fn crashm(m: &str) -> ! {
60   crashv!(m)
61 }
62 fn crashe(m: &str, e: Errno) -> ! {
63   crashv!(m, ": ", e.desc())
64 }
65
66 #[ext]
67 impl<T> nix::Result<T> {
68   fn context(self, m: &str) -> T {
69     match self {
70       Ok(y) => y,
71       Err(e) => crashe(m, e),
72     }
73   }
74 }
75
76 const ITOA_BUFL: usize = 12;
77 fn c_itoa(value: c_int, buf: &mut [u8; ITOA_BUFL]) -> &str {
78   unsafe {
79     *buf = [b'.'; ITOA_BUFL];
80     libc::snprintf({ let buf: *mut u8 = buf.as_mut_ptr(); buf as *mut i8 },
81                    ITOA_BUFL-2,
82                    cstr!(b"%x\0").as_ptr(),
83                    value);
84   }
85   let s = buf.splitn(2, |&c| c == b'\0').next()
86     .unwrap_or_else(|| crashm("splitn no next"));
87   str::from_utf8(s).unwrap_or_else(|_| crashm("non-utf-8 from snprintf!"))
88 }
89
90 unsafe fn mdup2(oldfd: RawFd, newfd: RawFd, what: &str) {
91   match dup2(oldfd, newfd) {
92     Ok(got) if got == newfd => { },
93     Ok(_) => crashm("dup2 gave wrong return value"),
94     Err(e) => crashv!("dup2 ", what, ": ", e.desc()),
95   }
96 }
97
98 unsafe fn write_status(st_wfd: RawFd, estatus: u8) {
99   match write(st_wfd, slice::from_ref(&estatus)) {
100     Ok(1) => {}
101     Ok(_) => crashm("write child startup exit status: short write"),
102     Err(e) => crashe("write child startup exit status", e),
103   }
104 }
105
106 unsafe fn parent(st_rfd: RawFd) -> ! {
107   let mut exitstatus = 0u8;
108   loop {
109     match read(st_rfd, slice::from_mut(&mut exitstatus)) {
110       Ok(0) => crashm("startup/daemonisation failed"),
111       Ok(1) => libc::_exit(exitstatus.into()),
112       Ok(_) => crashm("read startup: excess read!"),
113       Err(e) if e == Errno::EINTR => continue,
114       Err(e) => crashe("read startup signal pipe", e),
115     }
116   }
117 }
118
119 unsafe fn intermediate(child: Pid, st_wfd: RawFd) -> ! {
120   let mut wstatus: c_int = 0;
121
122   let r = libc::waitpid(child.as_raw(), &mut wstatus, 0);
123   if r == -1 { crashe("await child startup status",
124                       nix::errno::from_i32(errno())) }
125   if r != child.as_raw() { crashm("await child startup status: wrong pid") }
126
127   let cooked = WaitStatus::from_raw(child, wstatus)
128     .context("await child startup status: convert wait status");
129   match cooked {
130     WaitStatus::Exited(_, estatus) => {
131       let estatus: u8 = estatus.try_into()
132         .unwrap_or_else(|_| crashm(
133           "await child startup status: exit status out of range!"));
134       write_status(st_wfd, estatus);
135       libc::_exit(0);
136     }
137
138     WaitStatus::Signaled(_, signal, coredump) => {
139       crashv!("startup failed: died due to signal: ", signal.as_str(),
140               if coredump { " (core dumped)" } else { "" });
141     },
142
143     _ => {
144       crashv!("child startup exit status was strange!  0x",
145               c_itoa(wstatus, &mut default()))
146     }
147   }
148 }
149
150 impl Daemoniser {
151   /// Start daemonising - call before any threads created!
152   pub fn phase1() -> Self {
153     unsafe {
154       let null_fd = open(cstr!(b"/dev/null\0"), OFlag::O_RDWR, Mode::empty())
155         .context("open /dev/null");
156       mdup2(null_fd, 0, "null onto stdin");
157
158       let (st_rfd, st_wfd) = pipe().context("pipe");
159
160       match fork().context("fork (1)") {
161         ForkResult::Child => { }
162         ForkResult::Parent { child: _ } => {
163           close(st_wfd).context("close st_wfd pipe");
164           parent(st_rfd)
165         },
166       }
167
168       close(st_rfd).context("close st_rfd pipe");
169       setsid().context("setsid");
170       let intermediate_pid = Pid::this();
171
172       match fork().context("fork (2)") {
173         ForkResult::Child => { }
174         ForkResult::Parent { child } => {
175           intermediate(child, st_wfd)
176         },
177       }
178
179       Daemoniser {
180         drop_bomb: Some(()),
181         intermediate_pid,
182         null_fd,
183         st_wfd,
184       }
185     }
186   }
187
188   pub fn complete(mut self) {
189     unsafe {
190       mdup2(self.null_fd, 1, "null over stdin");
191
192       if Pid::parent() != self.intermediate_pid {
193         crashm(
194           "startup complete, but our parent is no longer the intermediate?");
195       }
196       kill(self.intermediate_pid, Some(Signal::SIGKILL))
197         .context("kill intermediate (after startup complete)");
198
199       write_status(self.st_wfd, 0);
200       mdup2(self.null_fd, 2, "null over stderrr");
201
202       self.drop_bomb.take();
203     }
204   }
205 }
206
207 impl Drop for Daemoniser {
208   fn drop(&mut self) {
209     if let Some(()) = self.drop_bomb.take() {
210       if panicking() {
211         // We will crash in due course, having printed some messages
212         // to stderr, presumably.
213         return
214       } else {
215         panic!("Daemonizer object dropped unexpectedly, startup failed");
216       }
217     }
218   }
219 }