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
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
/*!
The motd crate exposes a mechanism for dumping the current MOTD
on linux. In order to work around some issues with how pam_motd.so
handles permissions, it must re-exec the current binary to make
use of the LD_PRELOAD trick. You must make sure that your binary
can handle this re-execing by registering `motd::handle_reexec()`
in your main function. It is a no-op unless a few magic environment
variables have been set, so you don't need to worry about it impacting
the way your binary behaves otherwise.

Your main should look like this:

```
fn main() {
    motd::handle_reexec();

    // ...
}
```

then elsewhere in your code you can call value to get
the motd message like

```
# fn main() -> Result<(), motd::Error> {
# motd::handle_reexec();
let motd_resolver = motd::Resolver::new(motd::PamMotdResolutionStrategy::Auto)?;
let motd_msg = motd_resolver.value(motd::ArgResolutionStrategy::Auto)?;
# Ok(())
# }
```
*/

#![allow(clippy::needless_doctest_main)]

// We use pam-sys directly rather than higher level wrapper crates
// like pam or pam-client because we are only going to use libpam
// to get a PamHandle so that we can directly dlopen and call into
// pam_motd.so. I tried making a pam service that only contains pam_motd.so
// in its service config file, but it prompted me for a username and password
// anyway. Displaying the motd should not require credentials. By manually
// loading pam_motd.so we can avoid this.

use std::{
    env, ffi,
    fmt::Debug,
    fs, io,
    io::{BufRead, Write},
    mem,
    path::{Path, PathBuf},
    process::Command,
    ptr, slice,
};

use dlopen2::wrapper::WrapperApi;
use log::warn;
use pam_sys::types::{PamMessageStyle, PamReturnCode};
use serde_derive::{Deserialize, Serialize};

macro_rules! merr {
    ($($arg:tt)*) => {{
        Error::Err { msg: format!($($arg)*) }
    }}
}

const PAM_MOTD_NAME: &str = "pam_motd.so";
const LIB_DIR: &str = "/usr/lib";
const PAM_DIR: [&str; 2] = ["/etc", "pam.d"];

const PAM_MOTD_SO_ARG_ENV_VAR: &str = "RUST_MOTD_CRATE__INTERNAL__PAM_MOTD_SO_ARG";
const ARG_RESOLVER_ARG_ENV_VAR: &str = "RUST_MOTD_CRATE__INTERNAL__ARG_RESOLVER_ARG";

/// Resolver knows how to fetch the current motd by re-execing
/// the current binary to get handle_reexec to call pam_motd.so.
#[derive(Debug, Clone)]
pub struct Resolver {
    pam_motd_so_path: PathBuf,
}

impl Resolver {
    /// Create a new Resolver based on the given strategy for
    /// finding pam_motd.so. The path to the file will be cached
    /// since some strategies for finding the shared library can
    /// be fairly expensive.
    pub fn new(so_finder: PamMotdResolutionStrategy) -> Result<Self, Error> {
        Ok(Resolver {
            pam_motd_so_path: so_finder.resolve()?,
        })
    }

    /// Get the current value of the motd. Works by re-execing
    /// the current binary in order to use the LD_PRELOAD trick,
    /// so make sure you have called `motd::handle_reexec()`
    /// in your main function.
    pub fn value(&self, arg_resolver: ArgResolutionStrategy) -> Result<String, Error> {
        let overlay_so = OverlaySo::new()?;

        let arg_resolver_json = serde_json::to_string(&arg_resolver)
            .map_err(|e| merr!("serializing arg_resolver: {}", e))?;
        let out = Command::new("/proc/self/exe")
            .env(
                PAM_MOTD_SO_ARG_ENV_VAR,
                self.pam_motd_so_path
                    .to_str()
                    .ok_or(merr!("could not convert so path to str"))?,
            )
            .env(ARG_RESOLVER_ARG_ENV_VAR, arg_resolver_json)
            .env("LD_PRELOAD", overlay_so.path())
            .output()
            .map_err(|e| merr!("error re-execing self: {}", e))?;

        if !out.status.success() {
            return Err(merr!("failed to re-exec, bad status = {}", out.status));
        }

        if !out.stderr.is_empty() {
            println!("{}", String::from_utf8_lossy(out.stdout.as_slice()));

            let stderr = String::from_utf8_lossy(out.stderr.as_slice());
            return Err(merr!("in re-execed process: {}", stderr));
        }

        if !out.stdout.is_empty() {
            return Ok(String::from_utf8_lossy(out.stdout.as_slice()).into());
        }

        Err(merr!("no motd output"))
    }
}

/// You MUST call this routine in the main function of the binary that uses
/// the motd crate. In order to work around an issue where `pam_motd.so` thinks
/// it is running as root, we use LD_PRELOAD to stub out some privilege
/// juggling methods. To do this, the value() routine re-execs the current binary,
/// and it is in this re-execd process that we actually load and call into
/// `pam_motd.so`.
pub fn handle_reexec() {
    if let (Ok(pam_motd_so), Ok(arg_resolver_json)) = (
        env::var(PAM_MOTD_SO_ARG_ENV_VAR),
        env::var(ARG_RESOLVER_ARG_ENV_VAR),
    ) {
        match reexec_call_so(pam_motd_so.as_str(), arg_resolver_json.as_str()) {
            Ok(motd_msg) => print!("{}", motd_msg),
            Err(e) => eprintln!("{}", e),
        }
        std::process::exit(0);
    }
}

/// Actually load and call the `pam_motd.so`
fn reexec_call_so(pam_motd_so: &str, arg_resolver_json: &str) -> Result<String, Error> {
    let pam_motd_so = PathBuf::from(pam_motd_so);
    let arg_resolver: ArgResolutionStrategy = serde_json::from_str(arg_resolver_json)
        .map_err(|e| merr!("parsing arg_resolver arg: {}", e))?;

    let mut conv_data = ConvData::new();
    let pam_conv = pam_sys::types::PamConversation {
        conv: Some(conv_handler),
        data_ptr: &mut conv_data as *mut ConvData as *mut libc::c_void,
    };

    let mut passwd_str_buf: [libc::c_char; 1024 * 4] = [0; 1024 * 4];
    let mut passwd = libc::passwd {
        pw_name: ptr::null_mut(),
        pw_passwd: ptr::null_mut(),
        pw_uid: 0,
        pw_gid: 0,
        pw_gecos: ptr::null_mut(),
        pw_dir: ptr::null_mut(),
        pw_shell: ptr::null_mut(),
    };
    let mut passwd_res_ptr: *mut libc::passwd = ptr::null_mut();
    // Safety: pretty much pure ffi, the errono access follows the instructions documented
    //         in man getpwuid.
    unsafe {
        let errno = libc::getpwuid_r(
            libc::getuid(),
            &mut passwd,
            passwd_str_buf.as_mut_ptr(),
            passwd_str_buf.len(),
            &mut passwd_res_ptr as *mut *mut libc::passwd,
        );
        if passwd_res_ptr.is_null() {
            if errno == 0 {
                return Err(merr!("could not find current user, should be impossible"));
            } else {
                return Err(merr!(
                    "error resolving user passwd: {}",
                    io::Error::from_raw_os_error(errno)
                ));
            }
        }
    };

    // Safety: user is documented to be nullable, pretty much just doing
    //         standard ffi otherwise. Cleanup is handled by our RAII
    //         wrapper.
    let pam_h = unsafe {
        PamHandle::start(
            "rust-motd-bogus--pam-service--".as_ptr() as *const libc::c_char,
            passwd.pw_name,
            &pam_conv,
        )?
    };

    // Now the unsafe party really gets started! Time to directly dl_open
    // pam_motd.so.

    // Safety: pretty much just pure ffi around dlopen
    let pam_motd_so: dlopen2::wrapper::Container<PamMotdSo> =
        unsafe { dlopen2::wrapper::Container::load(pam_motd_so) }
            .map_err(|e| merr!("loading pam_motd.so: {}", e))?;

    let mut args = arg_resolver
        .resolve()?
        .into_iter()
        .map(|a| ffi::CString::new(a).map_err(|e| merr!("creating arg: {:?}", e)))
        .collect::<Result<Vec<_>, Error>>()?;
    let mut arg_ptrs = args
        .iter_mut()
        .map(|s| s.as_ptr() as *mut libc::c_char)
        .collect::<Vec<_>>();
    // Safety: this routine must be present according to the contract of pam_motd.so,
    //         which is stable since it is part of the interface that the main pam
    //         module uses to talk to it.
    let code = unsafe {
        pam_motd_so.pam_sm_open_session(
            pam_h.pam_h as *mut pam_sys::PamHandle,
            pam_sys::PamFlag::NONE as libc::c_int,
            args.len() as libc::c_int,
            arg_ptrs.as_mut_ptr(),
        )
    };
    let code = PamReturnCode::from(code);
    if !(code == PamReturnCode::SUCCESS || code == PamReturnCode::IGNORE) {
        return Err(merr!("unexpected return code from pam_motd.so: {:?}", code));
    }

    // we skip calling pam_sm_close_session since it is stubbed out for pam_motd.so

    if !conv_data.errs.is_empty() {
        let mut err_msgs = vec![];
        for err in conv_data.errs.into_iter() {
            err_msgs.push(err.to_string());
        }
        Err(merr!(
            "collecting pam_motd.so results: {}",
            err_msgs.join(" AND ")
        ))
    } else {
        Ok(conv_data.motd_msg)
    }
}

/// Errors encountered while resolving the message of the day.
#[non_exhaustive]
#[derive(Debug)]
pub enum Error {
    /// An opaque error with a useful debugging message but
    /// which callers should not dispatch on.
    Err {
        msg: String,
    },
    __NonExhaustive,
}

impl std::fmt::Display for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
        match self {
            Error::Err { msg } => write!(f, "{}", msg)?,
            _ => write!(f, "{:?}", self)?,
        }

        Ok(())
    }
}

impl std::error::Error for Error {}

//
// PamHandle impl
//

struct PamHandle {
    pam_h: *const pam_sys::PamHandle,
    error_status: libc::c_int,
}

impl PamHandle {
    /// Create a new RAII wrapper around a pam_sys::PamHandle. A very thin
    /// wrapper around pam_sys::raw::pam_start.
    ///
    /// Safety: see man pam_start for semantics. The Drop impl calls pam_end,
    ///         so the caller must keep that in mind.
    unsafe fn start(
        service_name: *const libc::c_char,
        user: *const libc::c_char,
        pam_conv: &pam_sys::PamConversation,
    ) -> Result<Self, Error> {
        let mut pam_h: *const pam_sys::PamHandle = ptr::null();

        let code = pam_sys::raw::pam_start(service_name, user, pam_conv, &mut pam_h);

        let code = PamReturnCode::from(code);
        if code != PamReturnCode::SUCCESS {
            return Err(merr!("starting pam session: error code = {}", code));
        }
        Ok(PamHandle {
            pam_h,
            error_status: 0,
        })
    }
}

impl std::ops::Drop for PamHandle {
    fn drop(&mut self) {
        let code = unsafe {
            // Error status is threaded down to cleanup functions for data that
            // has been set by pam_set_data. We don't really use it.
            //
            // Safety: this is fine as long as the caller has not mutated pam_h.
            //
            //         The cast to mut is pretty unfortunate, but required because
            //         pam_end needs a mut pointer. The underlying c functions all
            //         take mut pointers, but the sys crate has chosen to convert
            //         them to const pointers for most functions, presumably to
            //         signal that a pam_sys::PanHandle is an opaque type the user should
            //         never directly manipulate.
            pam_sys::raw::pam_end(self.pam_h as *mut pam_sys::PamHandle, self.error_status)
        };
        let code = PamReturnCode::from(code);

        if code != PamReturnCode::SUCCESS {
            warn!("error 'pam_end'ing a pam handle: {:?}", code);
        }
    }
}

//
// .so file wrapper
//

#[derive(WrapperApi)]
struct PamMotdSo {
    pam_sm_open_session: unsafe extern "C" fn(
        pam_h: *mut pam_sys::PamHandle,
        flags: libc::c_int,
        argc: libc::c_int,
        argv: *mut *mut libc::c_char,
    ) -> libc::c_int,
}

//
// Conversation callbacks
//

// The blob of user-data for the conversation handler. repr(C)
// since I'm not sure it is safe to pass things across ffi boundaries
// without a defined repr (I'm pretty sure it would be fine, but
// I just want to be on the safe side).
#[repr(C)]
struct ConvData {
    motd_msg: String,
    errs: Vec<Error>,
}

impl ConvData {
    fn new() -> Self {
        ConvData {
            motd_msg: String::from(""),
            errs: vec![],
        }
    }
}

// The handler routine that gets invoked every time pam_motd.so prints
// a message or an error.
//
// See `man 3 pam_conv` for details about the semantics of this callback.
extern "C" fn conv_handler(
    num_msg: libc::c_int,
    msgs: *mut *mut pam_sys::PamMessage,
    resp: *mut *mut pam_sys::PamResponse,
    appdata_ptr: *mut libc::c_void,
) -> libc::c_int {
    // Safety: num_msgs is documented to be the length of the msgs array
    //         in the pam_conv man pange.
    assert!(num_msg >= 0);
    let msgs = unsafe { slice::from_raw_parts(msgs, num_msg as usize) };

    // Safety: `man pam_conv` says that the caller will free() the resp array
    //         after every call, so we must calloc a new one. It also expects
    //         there to be exactly one response slot per message.
    //
    //         It is ok to assigning to `*resp` because the man page documents the
    //         resp array to be a pointer to an array of PamResponses, rather than
    //         an array of PamResponse pointers. This means the double indirection
    //         is in order for us to write to the output variable, so the caller
    //         is responsible for making sure there is a word there for us to write
    //         to.
    unsafe {
        *resp = {
            let alloc_size = mem::size_of::<pam_sys::PamResponse>() * num_msg as usize;
            let resp_buf = libc::calloc(alloc_size, 1);
            mem::transmute(resp_buf)
        };
    }

    // Safety: we cast from a `&ConvData` to a `*mut c_void` when creating the PamConversation,
    //         so we are casting to the right type. The assertion against null means that
    //         it is safe to convert to a reference.
    assert!(!appdata_ptr.is_null());
    let conv_data = unsafe { &mut *mem::transmute::<_, *mut ConvData>(appdata_ptr) };

    #[allow(clippy::needless_range_loop)]
    for i in 0..(num_msg as usize) {
        // Safety: the caller is responsible for giving us a complete message list.
        //         Any issue with this operation would be due to an issue with how the
        //         caller has set things up since the loop means we are in-bounds.
        let msg = unsafe { *msgs[i] };

        let msg_style = pam_sys::types::PamMessageStyle::from(msg.msg_style);
        match msg_style {
            PamMessageStyle::PROMPT_ECHO_OFF => {
                conv_data
                    .errs
                    .push(merr!("pam_motd.so asked for a password"));
            }
            PamMessageStyle::PROMPT_ECHO_ON => {
                conv_data
                    .errs
                    .push(merr!("pam_motd.so asked for a username"));
            }
            PamMessageStyle::TEXT_INFO => {
                // Safety: pam_motd.so will give us a valid cstring here.
                let msg = unsafe { ffi::CStr::from_ptr(msg.msg) };
                match msg.to_str() {
                    Ok(s) => conv_data.motd_msg.push_str(s),
                    Err(e) => conv_data
                        .errs
                        .push(merr!("err converting motd chunk: {}", e)),
                }
            }
            PamMessageStyle::ERROR_MSG => {
                // Safety: pam_motd.so will give us a valid cstring here.
                let msg = unsafe { ffi::CStr::from_ptr(msg.msg) };
                match msg.to_str() {
                    Ok(s) => conv_data.errs.push(merr!("pam_mod.so says '{}'", s)),
                    Err(e) => conv_data
                        .errs
                        .push(merr!("err converting motd err msg: {}", e)),
                }
            }
        }
    }

    PamReturnCode::SUCCESS as libc::c_int
}

//
// .so discovery logic
//

/// Specifies the strategy to use in order to find the pam_motd.so file
/// to interrogate for the motd message.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub enum PamMotdResolutionStrategy {
    /// Use the exact path to the given file. Do not attempt to do any
    /// searching.
    Exact(PathBuf),
    /// Search the given list of directories. If recursive is true, also
    /// search any subdirectories they have.
    Search { dirs: Vec<PathBuf>, recursive: bool },
    /// A good default. Equivalent to `Search { dirs: vec!["/usr/lib"], recursive: true }`.
    Auto,
}

impl PamMotdResolutionStrategy {
    fn resolve(&self) -> Result<PathBuf, Error> {
        match self {
            PamMotdResolutionStrategy::Exact(path) => Ok(path.to_path_buf()),
            PamMotdResolutionStrategy::Search { dirs, recursive } => {
                let mut err = None;

                for dir in dirs.iter() {
                    match Self::find_file(*recursive, dir, PAM_MOTD_NAME) {
                        Ok(path) => return Ok(path),
                        Err(e) => {
                            err = Some(e);
                        }
                    }
                }

                Err(err.unwrap_or(merr!("no directories to search provided")))
            }
            PamMotdResolutionStrategy::Auto => PamMotdResolutionStrategy::Search {
                dirs: vec![LIB_DIR.into()],
                recursive: true,
            }
            .resolve(),
        }
    }

    /// Search a directory to find a regular file with the given name.
    ///
    /// Used to automatically resolve pam_motd.so by recursively searching /usr/lib.
    fn find_file<P>(recursive: bool, dir: P, fname: &str) -> Result<PathBuf, Error>
    where
        P: AsRef<Path> + Debug,
    {
        if !dir.as_ref().is_dir() {
            return Err(merr!("{:?} is not a directory", dir));
        }

        let mut traversal = walkdir::WalkDir::new(&dir);
        if !recursive {
            traversal = traversal.max_depth(1);
        }
        for entry in traversal.into_iter().flatten() {
            if entry
                .path()
                .file_name()
                .map(|n| n == fname)
                .unwrap_or(false)
            {
                return Ok(PathBuf::from(entry.path()));
            }
        }

        Err(merr!("file {:?} not found in {:?}", fname, dir))
    }
}

//
// arg slurping logic
//

/// The strategy to use to determine which args should be passed to `pam_motd.so`.
/// pam configuration often includes arguments to various pam modules, and `pam_motd.so`
/// is one such module. You likely want to match the args that the config passes into
/// the module.
///
/// In all cases, the "noupdate" arg will be included since without it debian flavored
/// `pam_motd.so`s will fail for want of write permissions on the motd file. Non-debian
/// `pam_motd.so`s just write an error to syslog and trundle along for unknown args, so
/// this should not cause an issue in general.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub enum ArgResolutionStrategy {
    /// Pass the exact arg vector given with not parsing or resolution.
    Exact(Vec<String>),
    /// Parse the given service files (found in `/etc/pam.d/{service}`) looking for
    /// `pam_motd.so` entries and slurping any `motd=` or `motd_dir=` arguments. Multiple entries
    /// combine args into a single arg list. Afterwards, the args are deduped. If the service
    /// does not have a file, it is ignored.
    MatchServices(Vec<String>),
    /// A good default. Equivalent to `MatchServices(vec!["ssh", "login"])`
    Auto,
}

impl ArgResolutionStrategy {
    fn resolve(self) -> Result<Vec<String>, Error> {
        match self {
            ArgResolutionStrategy::Exact(args) => Ok(args),
            ArgResolutionStrategy::Auto => ArgResolutionStrategy::MatchServices(vec![
                String::from("ssh"),
                String::from("login"),
            ])
            .resolve(),
            ArgResolutionStrategy::MatchServices(services) => {
                let mut args = vec![];
                for service in services.into_iter() {
                    let mut service_path = PathBuf::new();
                    for part in PAM_DIR.iter() {
                        service_path.push(part);
                    }
                    service_path.push(service);

                    args.extend(Self::slurp_args(service_path)?);
                }

                // remove duplicates since parsing multiple service files means we probably
                // have some.
                args.sort_unstable();
                args.dedup();

                // make sure the debian variant still works
                args.push(String::from("noupdate"));

                Ok(args)
            }
        }
    }

    fn slurp_args<P: AsRef<Path> + Debug>(service_file: P) -> Result<Vec<String>, Error> {
        if !service_file.as_ref().is_file() {
            // ignore any missing services
            return Ok(vec![]);
        }

        let file = fs::File::open(&service_file)
            .map_err(|e| merr!("opening {:?} to parse args: {:?}", &service_file, e))?;
        let reader = io::BufReader::new(file);

        let mut args = vec![];
        for line in reader.lines() {
            let line = line.map_err(|e| merr!("reading line from {:?}: {:?}", &service_file, e))?;
            let line = line.trim();
            if line.starts_with('#') || line.is_empty() {
                continue;
            }

            if line.starts_with("@include") {
                // we need to recursively parse the included service
                let parts: Vec<&str> = line.split_whitespace().collect();
                if parts.len() != 2 {
                    warn!(
                        "expect exactly 1 argument to @include, got {}",
                        parts.len() - 1
                    );
                }

                let mut included_service_path = PathBuf::new();
                for part in PAM_DIR.iter() {
                    included_service_path.push(part);
                }
                included_service_path.push(parts[1]);

                args.extend(Self::slurp_args(included_service_path)?);

                continue;
            }

            let parts: Vec<&str> = line.split_whitespace().collect();
            if parts.len() < 3 {
                warn!("expect at least 3 parts for a pam module config");
                // likely a blank line
                continue;
            }

            let module = parts[2];
            if module != "pam_motd.so" {
                continue;
            }
            for arg in &parts[3..] {
                if *arg != "noupdate" {
                    args.push(String::from(*arg));
                }
            }
        }

        Ok(args)
    }
}

/// A handle to an overlay .so file. It is normally stored as embedded data in the motd
/// rlib, but for the life of one of these handles it gets written out to a tmp file.
/// The overlay file gets cleaned up when this handle falls out of scope.
#[derive(Debug)]
struct OverlaySo {
    _overlay_dir: tempfile::TempDir,
    path: PathBuf,
}

impl OverlaySo {
    fn new() -> Result<Self, Error> {
        let overlay_blob = include_bytes!(concat!(env!("OUT_DIR"), "/pam_motd_overlay.so"));

        let overlay_dir = tempfile::TempDir::with_prefix("pam_motd_overlay")
            .map_err(|e| merr!("making tmp pam_motd_overlay.so dir: {}", e))?;
        let mut path = PathBuf::from(overlay_dir.path());
        path.push("pam_motd_overlay.so");

        let mut overlay_file =
            fs::File::create(&path).map_err(|e| merr!("making pam_motd_overlay.so: {}", e))?;
        overlay_file
            .write_all(overlay_blob)
            .map_err(|e| merr!("writing pam_motd_overlay.so: {}", e))?;

        Ok(OverlaySo {
            _overlay_dir: overlay_dir,
            path,
        })
    }

    fn path(&self) -> &Path {
        self.path.as_path()
    }
}