/*
  Copyright 2024 Northern.tech AS

  This file is part of CFEngine 3 - written and maintained by Northern.tech AS.

  This program is free software; you can redistribute it and/or modify it
  under the terms of the GNU General Public License as published by the
  Free Software Foundation; version 3.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software
  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA

  To the extent this program is licensed as part of the Enterprise
  versions of CFEngine, the applicable Commercial Open Source License
  (COSL) may apply to this file if you as a licensee so wish it. See
  included file COSL.txt.
*/

#include <cf3.defs.h>

#include <actuator.h>
#include <files_names.h>
#include <files_interfaces.h>
#include <files_operators.h>
#include <files_lib.h>
#include <item_lib.h>
#include <conversion.h>
#include <match_scope.h>
#include <string_lib.h>
#include <string_sequence.h>
#include <systype.h>
#include <pipes.h>
#include <nfs.h>
#include <misc_lib.h>
#include <rlist.h>
#include <eval_context.h>
#include <timeout.h>

/* seconds */
#define RPCTIMEOUT 60

static int FSTAB_EDITS = 0; /* GLOBAL_X */
static Item *FSTABLIST = NULL; /* GLOBAL_X */

static void GetHostAndSource(const char *buf, char *host, char *source);

static char **ParseOptionsList(const char *opts);
static void FreeOptionsList(char **arr);

static void AugmentMountInfo(Seq *list, char *host, char *source, char *mounton, char *fstype, char *options);
static bool MatchFSInFstab(char *match);
static void DeleteThisItem(Item **liststart, Item *entry);
static char *GetFstabEntryOptions(char *mountpt);
static void ReplaceFstabEntry(Item **liststart, char *mountpt, char *new_entry);

static const char *const VMOUNTCOMM[] =
{
    [PLATFORM_CONTEXT_UNKNOWN] = "",
    [PLATFORM_CONTEXT_OPENVZ] = "/bin/mount -va",          /* virt_host_vz_vzps */
    [PLATFORM_CONTEXT_HP] = "/sbin/mount -ea",             /* hpux */
    [PLATFORM_CONTEXT_AIX] = "/usr/sbin/mount -t nfs",     /* aix */
    [PLATFORM_CONTEXT_LINUX] = "/bin/mount -va",           /* linux */
    [PLATFORM_CONTEXT_BUSYBOX] = "/bin/mount -va",         /* linux */
    [PLATFORM_CONTEXT_SOLARIS] = "/usr/sbin/mount -a",     /* solaris */
    [PLATFORM_CONTEXT_SUN_SOLARIS] = "/usr/sbin/mount -a", /* solaris */
    [PLATFORM_CONTEXT_FREEBSD] = "/sbin/mount -va",        /* freebsd */
    [PLATFORM_CONTEXT_NETBSD] = "/sbin/mount -a",          /* netbsd */
    [PLATFORM_CONTEXT_CRAYOS] = "/etc/mount -va",          /* cray */
    [PLATFORM_CONTEXT_WINDOWS_NT] = "/bin/sh /etc/fstab",  /* NT - possible security issue */
    [PLATFORM_CONTEXT_SYSTEMV] = "/sbin/mountall",         /* Unixware */
    [PLATFORM_CONTEXT_OPENBSD] = "/sbin/mount",            /* openbsd */
    [PLATFORM_CONTEXT_CFSCO] = "/etc/mountall",            /* sco */
    [PLATFORM_CONTEXT_DARWIN] = "/sbin/mount -va",         /* darwin */
    [PLATFORM_CONTEXT_QNX] = "/bin/mount -v",              /* qnx */
    [PLATFORM_CONTEXT_DRAGONFLY] = "/sbin/mount -va",      /* dragonfly */
    [PLATFORM_CONTEXT_MINGW] = "mingw-invalid",            /* mingw */
    [PLATFORM_CONTEXT_VMWARE] = "/bin/mount -a",           /* vmware */
    [PLATFORM_CONTEXT_ANDROID] = "",                       /* android */
};

static const char *const VUNMOUNTCOMM[] =
{
    [PLATFORM_CONTEXT_UNKNOWN] = "",
    [PLATFORM_CONTEXT_OPENVZ] = "/bin/umount",          /* virt_host_vz_vzps */
    [PLATFORM_CONTEXT_HP] = "/sbin/umount",             /* hpux */
    [PLATFORM_CONTEXT_AIX] = "/usr/sbin/umount",        /* aix */
    [PLATFORM_CONTEXT_LINUX] = "/bin/umount",           /* linux */
    [PLATFORM_CONTEXT_BUSYBOX] = "/bin/umount",         /* linux */
    [PLATFORM_CONTEXT_SOLARIS] = "/etc/umount",         /* solaris */
    [PLATFORM_CONTEXT_SUN_SOLARIS] = "/etc/umount",     /* solaris */
    [PLATFORM_CONTEXT_FREEBSD] = "/sbin/umount",        /* freebsd */
    [PLATFORM_CONTEXT_NETBSD] = "/sbin/umount",         /* netbsd */
    [PLATFORM_CONTEXT_CRAYOS] = "/etc/umount",          /* cray */
    [PLATFORM_CONTEXT_WINDOWS_NT] = "/bin/umount",      /* NT */
    [PLATFORM_CONTEXT_SYSTEMV] = "/sbin/umount",        /* Unixware */
    [PLATFORM_CONTEXT_OPENBSD] = "/sbin/umount",        /* openbsd */
    [PLATFORM_CONTEXT_CFSCO] = "/etc/umount",           /* sco */
    [PLATFORM_CONTEXT_DARWIN] = "/sbin/umount",         /* darwin */
    [PLATFORM_CONTEXT_QNX] = "/bin/umount",             /* qnx */
    [PLATFORM_CONTEXT_DRAGONFLY] = "/sbin/umount",      /* dragonfly */
    [PLATFORM_CONTEXT_MINGW] = "mingw-invalid",         /* mingw */
    [PLATFORM_CONTEXT_VMWARE] = "/bin/umount",          /* vmware */
    [PLATFORM_CONTEXT_ANDROID] = "/system/xbin/umount", /* android */
};

static const char *const VMOUNTOPTS[] =
{
    [PLATFORM_CONTEXT_UNKNOWN] = "",
    [PLATFORM_CONTEXT_OPENVZ] = "defaults",          /* virt_host_vz_vzps */
    [PLATFORM_CONTEXT_HP] = "bg,hard,intr",          /* hpux */
    [PLATFORM_CONTEXT_AIX] = "bg,hard,intr",         /* aix */
    [PLATFORM_CONTEXT_LINUX] = "defaults",           /* linux */
    [PLATFORM_CONTEXT_BUSYBOX] = "defaults",         /* linux */
    [PLATFORM_CONTEXT_SOLARIS] = "bg,hard,intr",     /* solaris */
    [PLATFORM_CONTEXT_SUN_SOLARIS] = "bg,hard,intr", /* solaris */
    [PLATFORM_CONTEXT_FREEBSD] = "bg,intr",          /* freebsd */
    [PLATFORM_CONTEXT_NETBSD] = "-i,-b",             /* netbsd */
    [PLATFORM_CONTEXT_CRAYOS] = "bg,hard,intr",      /* cray */
    [PLATFORM_CONTEXT_WINDOWS_NT] = "",              /* NT */
    [PLATFORM_CONTEXT_SYSTEMV] = "bg,hard,intr",     /* Unixware */
    [PLATFORM_CONTEXT_OPENBSD] = "-i,-b",            /* openbsd */
    [PLATFORM_CONTEXT_CFSCO] = "bg,hard,intr",       /* sco */
    [PLATFORM_CONTEXT_DARWIN] = "-i,-b",             /* darwin */
    [PLATFORM_CONTEXT_QNX] = "bg,hard,intr",         /* qnx */
    [PLATFORM_CONTEXT_DRAGONFLY] = "bg,intr",        /* dragonfly */
    [PLATFORM_CONTEXT_MINGW] = "mingw-invalid",      /* mingw */
    [PLATFORM_CONTEXT_VMWARE] = "defaults",          /* vmstate */
    [PLATFORM_CONTEXT_ANDROID] = "defaults",         /* android */
};

static void GetHostAndSource(const char *buf, char *host, char *source)
/* Extracts from buffer host & source of nfs, cifs and panfs network systems */
{
    assert(StringStartsWith(buf, "panfs://")
           || StringStartsWith(buf, "//")
           || strchr(buf, ':') != NULL);
    int index = 0; // Index for buf and host
    int source_index = 0;
    size_t slashes = 0;

    // panfs & cifs
    if (StringStartsWith(buf, "panfs://") || StringStartsWith(buf, "//"))
    {
        // copy host
        while (slashes != 3)
        {
            // TODO: Add example of what this string looks like
            if (buf[index] == '/' || buf[index] == '\\')
            {
                slashes++;
            }
            host[index] = buf[index];
            index++;
        }
        host[--index] = '\0';
    }
    // nfs
    else
    {
        while (buf[index] != ':')
        {
            host[index] = buf[index];
            index++;
        }
        host[index++] = '\0';
    }

    // copy source
    while ((buf[index] != '\0') && (buf[index] != ' '))
    {
        source[source_index++] = buf[index++];
    }
    source[source_index] = '\0';
}

/* Parse comma-separated options string into an array of individual option strings.
 * Returns a newly allocated array of char* pointers, terminated by NULL.
 * Caller must free the array itself (not the individual strings). */
static char **ParseOptionsList(const char *opts)
{
    if (opts == NULL || opts[0] == '\0')
    {
        char **arr = xcalloc(1, sizeof(char *));
        arr[0] = NULL;
        return arr;
    }

    /* First pass: count options */
    int count = 0;
    for (const char *p = opts; *p; p++)
    {
        if (*p == ',')
            count++;
    }
    count++; /* number of tokens = number of commas + 1 */

    char **arr = xcalloc(count + 1, sizeof(char *));
    char *copy = xstrdup(opts);
    int idx = 0;
    char *token = strtok(copy, ",");
    while (token != NULL && idx < count)
    {
        arr[idx++] = xstrdup(token);
        token = strtok(NULL, ",");
    }
    free(copy);
    return arr;
}

static void FreeOptionsList(char **arr)
{
    if (arr == NULL)
        return;
    for (int i = 0; arr[i] != NULL; i++)
        free(arr[i]);
    free(arr);
}

bool OptionsSubsetMatches(const char *promised_opts, const char *actual_opts)
/* Check whether all options specified in 'promised_opts' are present in
 * 'actual_opts'.  Kernel-added NFS auto-negotiated options (vers=, rsize=,
 * wsize=, timeo=, retrans=, sec=, addr=, mountport=, mountproto=,
 * mountvers=, rsize, wsize, namlen, local_lock, mountaddr) are ignored in
 * the actual set.  Contradictory options (e.g. noatime vs relatime, hard
 * vs soft) cause a mismatch.  Ordering differences are irrelevant. */
{
    if (promised_opts == NULL || promised_opts[0] == '\0')
        return true;

    char **promised = ParseOptionsList(promised_opts);
    char **actual = ParseOptionsList(actual_opts);

    bool mismatch = false;

    for (int p = 0; promised[p] != NULL; p++)
    {
        char *popt = promised[p];

        /* The kernel never echoes the pseudo-option "defaults" in its
         * resolved set (it expands to rw,suid,dev,exec,auto,nouser,async).
         * Its material, checkable assertion is "rw" (i.e. not read-only), so
         * match it against that rather than looking for a literal "defaults". */
        if (strcmp(popt, "defaults") == 0)
        {
            popt = "rw";
        }

        bool found = false;
        bool contradicted = false;

        for (int a = 0; actual[a] != NULL; a++)
        {
            char *aopt = actual[a];

            /* Direct match */
            if (strcmp(popt, aopt) == 0)
            {
                found = true;
                break;
            }

            /* Inverse pair check via "no" prefix */
            if (strncmp(popt, "no", 2) == 0)
            {
                if (strcmp(popt + 2, aopt) == 0)
                    contradicted = true;
            }
            else if (strncmp(aopt, "no", 2) == 0)
            {
                if (strcmp(popt, aopt + 2) == 0)
                    contradicted = true;
            }

            /* Known inverse pairs without "no" prefix */
            if ((strcmp(popt, "noatime") == 0 && strcmp(aopt, "relatime") == 0) ||
                (strcmp(popt, "relatime") == 0 && strcmp(aopt, "noatime") == 0))
            {
                contradicted = true;
            }
            if ((strcmp(popt, "hard") == 0 && strcmp(aopt, "soft") == 0) ||
                (strcmp(popt, "soft") == 0 && strcmp(aopt, "hard") == 0))
            {
                contradicted = true;
            }
            if ((strcmp(popt, "sync") == 0 && strcmp(aopt, "async") == 0) ||
                (strcmp(popt, "async") == 0 && strcmp(aopt, "sync") == 0))
            {
                contradicted = true;
            }
            if ((strcmp(popt, "ro") == 0 && strcmp(aopt, "rw") == 0) ||
                (strcmp(popt, "rw") == 0 && strcmp(aopt, "ro") == 0))
            {
                contradicted = true;
            }

            /* Alias: user says "tcp", actual has "proto=tcp" */
            if ((strcmp(popt, "tcp") == 0 && strcmp(aopt, "proto=tcp") == 0) ||
                (strcmp(popt, "udp") == 0 && strcmp(aopt, "proto=udp") == 0) ||
                (strcmp(popt, "proto=tcp") == 0 && strcmp(aopt, "tcp") == 0) ||
                (strcmp(popt, "proto=udp") == 0 && strcmp(aopt, "udp") == 0))
            {
                found = true;
                break;
            }
        }

        if (contradicted)
        {
            mismatch = true;
            break;
        }
        if (!found)
        {
            mismatch = true;
            break;
        }
    }

    FreeOptionsList(promised);
    FreeOptionsList(actual);

    return !mismatch;
}

bool LoadMountInfo(Seq *list)
/* This is, in fact, the most portable way to read the mount info! */
/* Depressing, isn't it? */
{
    FILE *pp;
    char buf1[CF_BUFSIZE], buf2[CF_BUFSIZE], buf3[CF_BUFSIZE];
    int i;
    bool nfs = false, panfs = false, cifs = false;

    // get mount command without parameters
    for (i = 0; VMOUNTCOMM[VSYSTEMHARDCLASS][i] != ' '; i++)
    {
        buf1[i] = VMOUNTCOMM[VSYSTEMHARDCLASS][i];
    }

    buf1[i] = '\0';

    SetTimeOut(RPCTIMEOUT);

    if ((pp = cf_popen(buf1, "r", true)) == NULL)
    {
        Log(LOG_LEVEL_ERR, "Can't open '%s'. (cf_popen: %s)", buf1, GetErrorStr());
        return false;
    }

    size_t vbuff_size = CF_BUFSIZE;
    char *vbuff = xmalloc(vbuff_size);

    for (;;)
    {
        buf1[0] = buf2[0] = buf3[0] = '\0';
        nfs = false;

        ssize_t res = CfReadLine(&vbuff, &vbuff_size, pp);
        if (res == -1)
        {
            if (!feof(pp))
            {
                Log(LOG_LEVEL_ERR, "Unable to read list of mounted filesystems. (fread: %s)", GetErrorStr());
                cf_pclose(pp);
                free(vbuff);
                return false;
            }
            else
            {
                break;
            }
        }

        Seq *words = SeqStringFromString(vbuff, ' ');

        if (SeqStringContains(words, "panfs"))
        {
            panfs = true;
        }
        else if (SeqStringContains(words, "nfs"))
        {
            nfs = true;
        }
        else if (SeqStringContains(words, "cifs"))
        {
            cifs = true;
        }

        SeqDestroy(words);

        // security note: buff is CF_BUFSIZE, so that is the max that can be written to buf1, buf2 or buf3
        sscanf(vbuff, "%s%s%s", buf1, buf2, buf3);

        if ((vbuff[0] == '\0') || (vbuff[0] == '\n'))
        {
            break;
        }

        if (strstr(vbuff, "not responding"))
        {
            Log(LOG_LEVEL_ERR, "%s", vbuff);
        }

        if (strstr(vbuff, "be root"))
        {
            Log(LOG_LEVEL_ERR, "Mount access is denied. You must be root. Use the -n option to run safely");
        }

        if ((strstr(vbuff, "retrying")) || (strstr(vbuff, "denied")) || (strstr(vbuff, "backgrounding")))
        {
            continue;
        }

        if ((strstr(vbuff, "exceeded")) || (strstr(vbuff, "busy")))
        {
            continue;
        }

        if (strstr(vbuff, "RPC"))
        {
            Log(LOG_LEVEL_INFO, "There was an RPC timeout. Aborting mount operations.");
            Log(LOG_LEVEL_INFO, "Session failed while trying to talk to remote host");
            Log(LOG_LEVEL_INFO, "%s", vbuff);
            cf_pclose(pp);
            free(vbuff);
            return false;
        }

        // host: max FQDN is 255 chars (max IPv6 with IPv4 tunneling is 45 chars)
        // source, mounton: hardcoding max path length to 1023; longer is very unlikely
        char host[256], source[1024], mounton[1024];
        host[0] = source[0] = mounton[0] = '\0';


#if defined(__sun) || defined(__hpux)
        if (IsAbsoluteFileName(buf3) && !cifs)
        {
            strlcpy(host, "localhost", sizeof(host));
            strlcpy(mounton, buf1, sizeof(mounton));
        }
        else
        {
            if (cifs || panfs)
            {
                GetHostAndSource(buf1, host, source);
            }
            else
            {
                sscanf(buf1, "%255[^:]:%1023s", host, source);
            }
            strlcpy(mounton, buf1, sizeof(mounton));
        }
#elif defined(_AIX)
        /* skip header */

        if (IsAbsoluteFileName(buf1) && !cifs)
        {
            strlcpy(host, "localhost", sizeof(host));
            strlcpy(mounton, buf2, sizeof(mounton));
        }
        else
        {
            strlcpy(host, buf1, sizeof(host));
            strlcpy(source, buf1, sizeof(source));
            strlcpy(mounton, buf3, sizeof(mounton));
        }
#elif defined(__CYGWIN__)
        strlcpy(mounton, buf2, sizeof(mounton));
        strlcpy(host, buf1, sizeof(host));
#elif defined(sco) || defined(__SCO_DS)
        Log(LOG_LEVEL_ERR, "Don't understand SCO mount format, no data");
#else
        if (IsAbsoluteFileName(buf1) && !cifs)
        {
            Log(LOG_LEVEL_VERBOSE,
                "'%s' is a local absolute file name, setting host to localhost",
                buf1);
            strlcpy(host, "localhost", sizeof(host));
            strlcpy(mounton, buf3, sizeof(mounton));
        }
        else
        {
            if (cifs || panfs)
            {
                GetHostAndSource(buf1, host, source);
            }
            else
            {
                sscanf(buf1, "%255[^:]:%1023s", host, source);
            }
            strlcpy(mounton, buf3, sizeof(mounton));
        }
#endif

        Log(LOG_LEVEL_DEBUG, "LoadMountInfo: host '%s', source '%s', mounton '%s'", host, source, mounton);

        /* Extract the actual mount options from the parenthesized portion of mount output.
         * mount -va output format: host:source on /mountpoint type fstype (opts) */
        char mountopts[CF_BUFSIZE];
        mountopts[0] = '\0';
        char *paren = strstr(vbuff, "(");
        if (paren != NULL)
        {
            char *end = strchr(paren, ')');
            if (end != NULL)
            {
                strlcpy(mountopts, paren + 1, sizeof(mountopts));
                /* Strip trailing whitespace */
                size_t len = strlen(mountopts);
                while (len > 0 && (mountopts[len - 1] == ' ' || mountopts[len - 1] == '\t'))
                {
                    mountopts[--len] = '\0';
                }
            }
        }

        if (panfs)
        {
            AugmentMountInfo(list, host, source, mounton, "panfs", mountopts);
        }
        else if (nfs)
        {
            AugmentMountInfo(list, host, source, mounton, "nfs", mountopts);
        }
        else if (cifs)
        {
            AugmentMountInfo(list, host, source, mounton, "cifs", mountopts);
        }
        else
        {
            AugmentMountInfo(list, host, source, mounton, NULL, mountopts);
        }
    }

    free(vbuff);
    alarm(0);
    signal(SIGALRM, SIG_DFL);
    cf_pclose(pp);
    return true;
}

/*******************************************************************/

static void AugmentMountInfo(Seq *list, char *host, char *source, char *mounton, char *fstype, char *options)
{
    Mount *entry = xcalloc(1, sizeof(Mount));

    if (host)
    {
        entry->host = xstrdup(host);
    }

    if (source)
    {
        entry->source = xstrdup(source);
    }

    if (mounton)
    {
        entry->mounton = xstrdup(mounton);
    }

    /* Store the fstype in options so IsForeignFileSystem can detect
     * foreign filesystems via strstr(entry->options, "nfs"/"panfs"/"cifs"). */
    if (fstype)
    {
        entry->options = xstrdup(fstype);
    }

    /* Store the full kernel-resolved options in raw_opts.
     * For unmounted filesystems (options == NULL or empty), raw_opts stays NULL
     * and will be checked in FileSystemMountedCorrectly as "not mounted". */
    if (options != NULL && options[0] != '\0')
    {
        entry->raw_opts = xstrdup(options);
    }

    SeqAppend(list, entry);
}

/*******************************************************************/

void DeleteMountInfo(Seq *list)
{
    for (size_t i = 0; i < SeqLength(list); i++)
    {
        Mount *entry = SeqAt(list, i);

        free(entry->host);
        free(entry->source);
        free(entry->mounton);
        free(entry->options);
        free(entry->raw_opts);
    }

    SeqClear(list);
}

/*******************************************************************/

int VerifyInFstab(EvalContext *ctx, char *name, const Attributes *a, const Promise *pp, PromiseResult *result)
/* Ensure filesystem IS in fstab, and return no of changes */
{
    assert(a != NULL);
    char fstab[CF_BUFSIZE];
    char *host, *rmountpt, *mountpt, *fstype, *opts;

    if (!FSTABLIST)
    {
        if (!LoadFileAsItemList(&FSTABLIST, VFSTAB[VSYSTEMHARDCLASS], a->edits, false))
        {
            Log(LOG_LEVEL_ERR, "Couldn't open '%s'", VFSTAB[VSYSTEMHARDCLASS]);
            return 0;
        }
        else
        {
            FSTAB_EDITS = 0;
        }
    }

    if (a->mount.mount_options)
    {
        opts = Rlist2String(a->mount.mount_options, ",");
    }
    else
    {
        opts = xstrdup(VMOUNTOPTS[VSYSTEMHARDCLASS]);
    }

    host = a->mount.mount_server;
    rmountpt = a->mount.mount_source;
    mountpt = name;
    fstype = a->mount.mount_type;

    char device[CF_BUFSIZE];
    if (StringEqual(fstype, "cifs") || StringEqual(fstype, "panfs"))
    {
        NDEBUG_UNUSED int ret = snprintf(device, sizeof(device), "%s%s", host, rmountpt);
        assert(ret >= 0 && ret < CF_BUFSIZE);
    }
    else
    {
        NDEBUG_UNUSED int ret = snprintf(device, sizeof(device), "%s:%s", host, rmountpt);
        assert(ret >= 0 && ret < CF_BUFSIZE);
    }

#if defined(__QNX__) || defined(__QNXNTO__)
    // QNX documents 4 fstab fields : https://www.qnx.com/developers/docs/7.1/index.html#com.qnx.doc.neutrino.utilities/topic/f/fstab.html
    // specialdevice mountpoint type mountoptions
    // TODO Remove DUMP and PASS options used here (unsupported)?
    NDEBUG_UNUSED int ret = snprintf(fstab, CF_BUFSIZE, "%s \t %s %s\t%s 0 0", device, mountpt, fstype, opts);
    assert(ret >= 0 && ret < CF_BUFSIZE);
#elif defined(_CRAY)
    char fstype_upper[CF_BUFSIZE];
    strlcpy(fstype_upper, fstype, CF_BUFSIZE);
    ToUpperStrInplace(fstype_upper);

    NDEBUG_UNUSED int ret = snprintf(fstab, CF_BUFSIZE, "%s \t %s %s\t%s", device, mountpt, fstype_upper, opts);
    assert(ret >= 0 && ret < CF_BUFSIZE);
    break;
#elif defined(__hpux)
    // HP-UX documents 7 fstab fields: https://nixdoc.net/man-pages/HP-UX/man4/fstab.4.html
    // deviceSpecialFile directory type options backupFrequency passNumber comment
    // TODO Bring promise comment in as the 7th comment field # promise comment (stripped of newlines)
    NDEBUG_UNUSED int ret = snprintf(fstab, CF_BUFSIZE, "%s %s \t %s \t %s 0 0", device, mountpt, fstype, opts);
    assert(ret >= 0 && ret < CF_BUFSIZE);
#elif defined(_AIX)
    // AIX uses /etc/filesystems: https://www.ibm.com/docs/en/aix/7.2.0?topic=files-filesystems-file
    NDEBUG_UNUSED int ret = snprintf(fstab, CF_BUFSIZE,
                                     "%s:\n\tdev\t= %s\n\ttype\t= %s\n\tvfs\t= %s\n\tnodename\t= %s\n\tmount\t= true\n\toptions\t= %s\n\taccount\t= false\n",
                                     mountpt, rmountpt, fstype, fstype, host, opts);
    assert(ret >= 0 && ret < CF_BUFSIZE);
#elif defined(__linux__)
    NDEBUG_UNUSED int ret = snprintf(fstab, CF_BUFSIZE, "%s \t %s \t %s \t %s", device, mountpt, fstype, opts);
    assert(ret >= 0 && ret < CF_BUFSIZE);
#elif defined(__NetBSD__) || defined(__OpenBSD__) || defined(__DragonFly__) || defined(__FreeBSD__) || defined(__APPLE__)
    // BSDs document 6 fstab fields https://man.freebsd.org/cgi/man.cgi?fstab(5)
    // Device Mountpoint FStype Options Dump Pass
    NDEBUG_UNUSED int ret = snprintf(fstab, CF_BUFSIZE, "%s \t %s \t %s \t %s 0 0", device, mountpt, fstype, opts);
    assert(ret >= 0 && ret < CF_BUFSIZE);
#elif defined(__sun) || defined(sco) || defined(__SCO_DS)
    // SunOS uses /etc/fstab and documents 7 fields: https://docs.oracle.com/cd/E19455-01/805-6331/fsadm-59727/index.html
    // deviceToMount deviceToFsck mountPoint FStype fsckPass automount? mountOptions
    // - is used for deviceToFsck for read-only and network based file systems
    NDEBUG_UNUSED int ret = snprintf(fstab, CF_BUFSIZE, "%s - %s %s - yes %s", device, mountpt, fstype, opts);
    assert(ret >= 0 && ret < CF_BUFSIZE);
#elif defined(__CYGWIN__)
    // https://cygwin.com/cygwin-ug-net/using.html#mount-table
    NDEBUG_UNUSED int ret = snprintf(fstab, CF_BUFSIZE, "/bin/mount %s %s", device, mountpt);
    assert(ret >= 0 && ret < CF_BUFSIZE);
#else
  #error "Could not determine format of fstab entry on this platform."
#endif

    Log(LOG_LEVEL_VERBOSE, "Verifying '%s' in '%s'", mountpt, VFSTAB[VSYSTEMHARDCLASS]);

    int changes = 0;

    if (!MatchFSInFstab(mountpt))
    {
        /* CFE-90: Entry not in fstab - add it */
        AppendItem(&FSTABLIST, fstab, NULL);
        FSTAB_EDITS++;
        cfPS(ctx, LOG_LEVEL_INFO, PROMISE_RESULT_CHANGE, pp, a, "Adding file system entry '%s' to '%s'", fstab,
             VFSTAB[VSYSTEMHARDCLASS]);
        *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
        changes += 1;
    }
    else
    {
        /* CFE-90: Entry exists - check if options differ and update if needed */
        char *existing_opts = GetFstabEntryOptions(mountpt);
        if (existing_opts != NULL && strcmp(existing_opts, opts) != 0)
        {
            /* Replace the entire fstab entry with the corrected options */
            ReplaceFstabEntry(&FSTABLIST, mountpt, fstab);
            FSTAB_EDITS++;
            cfPS(ctx, LOG_LEVEL_INFO, PROMISE_RESULT_CHANGE, pp, a, "Updating file system entry for '%s' in '%s' (options: '%s' -> '%s')",
                 mountpt, VFSTAB[VSYSTEMHARDCLASS], existing_opts, opts);
            *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
            changes += 1;
        }
        free(existing_opts);
    }

    free(opts);
    return changes;
}

/*******************************************************************/

int VerifyNotInFstab(EvalContext *ctx, char *name, const Attributes *a, const Promise *pp, PromiseResult *result)
/* Ensure filesystem is NOT in fstab, and return no of changes */
{
    char regex[CF_BUFSIZE];
    char *host, *mountpt;
    Item *ip;

    if (!FSTABLIST)
    {
        if (!LoadFileAsItemList(&FSTABLIST, VFSTAB[VSYSTEMHARDCLASS], a->edits, false))
        {
            Log(LOG_LEVEL_ERR, "Couldn't open '%s'", VFSTAB[VSYSTEMHARDCLASS]);
            return 0;
        }
        else
        {
            FSTAB_EDITS = 0;
        }
    }

    host = a->mount.mount_server;
    mountpt = name;
    int changes = 0;
    if (MatchFSInFstab(mountpt))
    {
        if (a->mount.editfstab)
        {
#if defined(_AIX)
            FILE *pfp;
            char aixcomm[CF_BUFSIZE];

            snprintf(aixcomm, CF_BUFSIZE, "/usr/sbin/rmnfsmnt -f %s", mountpt);

            if ((pfp = cf_popen(aixcomm, "r", true)) == NULL)
            {
                cfPS(ctx, LOG_LEVEL_ERR, PROMISE_RESULT_FAIL, pp, a, "Failed to invoke /usr/sbin/rmnfsmnt to edit fstab");
                *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
                return 0;
            }

            size_t line_size = CF_BUFSIZE;
            char *line = xmalloc(line_size);

            for (;;)
            {
                ssize_t res = getline(&line, &line_size, pfp);

                if (res == -1)
                {
                    if (!feof(pfp))
                    {
                        cfPS(ctx, LOG_LEVEL_ERR, PROMISE_RESULT_FAIL, pp, a, "Unable to read output of /bin/rmnfsmnt");
                        *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
                        cf_pclose(pfp);
                        free(line);
                        return 0;
                    }
                    else
                    {
                        break;
                    }
                }

                if (line[0] == '#')
                {
                    continue;
                }

                if (strstr(line, "busy"))
                {
                    cfPS(ctx, LOG_LEVEL_ERR, PROMISE_RESULT_INTERRUPTED, pp, a, "The device under '%s' cannot be removed from '%s'",
                         mountpt, VFSTAB[VSYSTEMHARDCLASS]);
                    *result = PromiseResultUpdate(*result, PROMISE_RESULT_INTERRUPTED);
                    free(line);
                    return 0;
                }
            }

            free(line);
            cf_pclose(pfp);

            return 0;       /* ignore internal editing for aix , always returns 0 changes */
#else
            Item* next;
            snprintf(regex, CF_BUFSIZE, ".*[\\s]+%s[\\s]+.*", mountpt);

            for (ip = FSTABLIST; ip != NULL; ip = next)
            {
                next = ip->next;
                if (FullTextMatch(ctx, regex, ip->name))
                {
                    cfPS(ctx, LOG_LEVEL_INFO, PROMISE_RESULT_CHANGE, pp, a, "Deleting file system mounted on '%s'", host);
                    *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
                    // Check host name matches too?
                    DeleteThisItem(&FSTABLIST, ip);
                    FSTAB_EDITS++;
                    changes += 1;
                }
            }
#endif
        }
    }

    return changes;
}

/*******************************************************************/

PromiseResult VerifyMount(EvalContext *ctx, char *name, const Attributes *a, const Promise *pp)
{
    assert(a != NULL);

    char comm[CF_BUFSIZE];
    FILE *pfp;
    char *host, *rmountpt, *mountpt, *opts=NULL;

    host = a->mount.mount_server;
    rmountpt = a->mount.mount_source;
    mountpt = name;

    /* Check for options required for this mount - i.e., -o ro,rsize, etc. */
    if (a->mount.mount_options)
    {
        opts = Rlist2String(a->mount.mount_options, ",");
    }
    else
    {
        opts = xstrdup(VMOUNTOPTS[VSYSTEMHARDCLASS]);
    }

    PromiseResult result = PROMISE_RESULT_NOOP;

    /* CFE-3366: gate the mount on the promise action, not just DONTDO, so a
     * dry-run (or action_policy => "warn") reports a warning and does not
     * define promise_repaired for a run that changes nothing. */
    if (!MakingInternalChanges(ctx, pp, a, &result, "mount '%s' to keep promise", mountpt))
    {
        free(opts);
        return result;
    }

    if (StringEqual(a->mount.mount_type, "panfs"))
    {
        snprintf(comm, CF_BUFSIZE, "%s -t panfs -o %s %s%s %s", CommandArg0(VMOUNTCOMM[VSYSTEMHARDCLASS]), opts, host, rmountpt, mountpt);
    }
    else if (StringEqual(a->mount.mount_type, "cifs"))
    {
        snprintf(comm, CF_BUFSIZE, "%s -t cifs -o %s %s%s %s", CommandArg0(VMOUNTCOMM[VSYSTEMHARDCLASS]), opts, host, rmountpt, mountpt);
    }
    else
    {
        snprintf(comm, CF_BUFSIZE, "%s -o %s %s:%s %s", CommandArg0(VMOUNTCOMM[VSYSTEMHARDCLASS]), opts, host, rmountpt, mountpt);
    }

    if ((pfp = cf_popen(comm, "r", true)) == NULL)
    {
        Log(LOG_LEVEL_ERR, "Failed to open pipe from '%s'", CommandArg0(VMOUNTCOMM[VSYSTEMHARDCLASS]));
        free(opts);
        return PROMISE_RESULT_FAIL;
    }

    size_t line_size = CF_BUFSIZE;
    char *line = xmalloc(line_size);

    ssize_t res = CfReadLine(&line, &line_size, pfp);

    if (res == -1)
    {
        if (!feof(pfp))
        {
            Log(LOG_LEVEL_ERR, "Unable to read output of mount command. (fread: %s)", GetErrorStr());
            cf_pclose(pfp);
            free(line);
            free(opts);
            return PROMISE_RESULT_FAIL;
        }
    }
    else if ((strstr(line, "busy")) || (strstr(line, "Busy")))
    {
        cfPS(ctx, LOG_LEVEL_ERR, PROMISE_RESULT_INTERRUPTED, pp, a, "The device under '%s' cannot be mounted", mountpt);
        result = PromiseResultUpdate(result, PROMISE_RESULT_INTERRUPTED);
        cf_pclose(pfp);
        free(line);
        free(opts);
        return result;
    }

    free(line);
    cf_pclose(pfp);

    /* Since opts is either Rlist2String or xstrdup'd, we need to always free it */
    free(opts);

    cfPS(ctx, LOG_LEVEL_INFO, PROMISE_RESULT_CHANGE, pp, a, "Mounting '%s' to keep promise", mountpt);
    result = PromiseResultUpdate(result, PROMISE_RESULT_CHANGE);

    return result;
}

/*******************************************************************/

PromiseResult VerifyUnmount(EvalContext *ctx, char *name, const Attributes *a, const Promise *pp)
{
    char comm[CF_BUFSIZE];
    FILE *pfp;
    char *mountpt;

    mountpt = name;

    PromiseResult result = PROMISE_RESULT_NOOP;

    /* CFE-3366: gate the unmount on the promise action, not just DONTDO, so a
     * dry-run (or action_policy => "warn") reports a warning and does not
     * define promise_repaired for a run that changes nothing. */
    if (!MakingInternalChanges(ctx, pp, a, &result, "unmount '%s' to keep promise", mountpt))
    {
        return result;
    }

    snprintf(comm, CF_BUFSIZE, "%s %s", VUNMOUNTCOMM[VSYSTEMHARDCLASS], mountpt);

    if ((pfp = cf_popen(comm, "r", true)) == NULL)
    {
        Log(LOG_LEVEL_ERR, "Failed to open pipe from %s", VUNMOUNTCOMM[VSYSTEMHARDCLASS]);
        return result;
    }

    size_t line_size = CF_BUFSIZE;
    char *line = xmalloc(line_size);

    ssize_t res = CfReadLine(&line, &line_size, pfp);
    if (res == -1)
    {
        cf_pclose(pfp);
        free(line);

        if (!feof(pfp))
        {
            Log(LOG_LEVEL_ERR, "Unable to read output of unmount command. (fread: %s)", GetErrorStr());
            return result;
        }
    }
    else if (res > 0 && ((strstr(line, "busy")) || (strstr(line, "Busy"))))
    {
        cfPS(ctx, LOG_LEVEL_ERR, PROMISE_RESULT_INTERRUPTED, pp, a, "The device under '%s' cannot be unmounted", mountpt);
        result = PromiseResultUpdate(result, PROMISE_RESULT_INTERRUPTED);
        cf_pclose(pfp);
        free(line);
        return result;
    }

    cfPS(ctx, LOG_LEVEL_INFO, PROMISE_RESULT_CHANGE, pp, a, "Unmounting '%s' to keep promise", mountpt);
    result = PromiseResultUpdate(result, PROMISE_RESULT_CHANGE);
    return result;
}

/*******************************************************************/

static bool MatchFSInFstab(char *match)
{
    Item *ip;
    const char delimit[]=" \t\r\n\v\f";
    char *fstab_line, *token;

    for (ip = FSTABLIST; ip != NULL; ip = ip->next)
    {
        fstab_line = xstrdup(ip->name);
        if(strncmp(fstab_line, "#", 1) != 0)
        {
            token = strtok(fstab_line, delimit);
            while (token != NULL)
            {
                if(strcmp(token, match) == 0)
                {
                    free(fstab_line);
                    return true;
                }
                token = strtok(NULL, delimit);
            }
        }
        free(fstab_line);
    }

    return false;
}

/*******************************************************************/

void MountAll()
{
    FILE *pp;

    if (DONTDO)
    {
        Log(LOG_LEVEL_VERBOSE, "Promised to mount filesystem, but not on this trial run");
        return;
    }
    else
    {
        Log(LOG_LEVEL_VERBOSE, "Attempting to mount all filesystems.");
    }

#if defined(__CYGWIN__)
    /* This is a shell script. Make sure it hasn't been compromised. */

    struct stat sb;

    if (stat("/etc/fstab", &sb) == -1)
    {
        int fd;
        if ((fd = creat("/etc/fstab", 0755)) > 0)
        {
            if (write(fd, "#!/bin/sh\n\n", 10) != 10)
            {
                UnexpectedError("Failed to write to file '/etc/fstab'");
            }
            close(fd);
        }
        else
        {
            if (sb.st_mode & (S_IWOTH | S_IWGRP))
            {
                Log(LOG_LEVEL_ERR, "File /etc/fstab was insecure. Cannot mount filesystems.");
                return;
            }
        }
    }
#endif

    SetTimeOut(RPCTIMEOUT);

    const char *cmd = VMOUNTCOMM[VSYSTEMHARDCLASS];

    if ((pp = cf_popen(cmd, "r", true)) == NULL)
    {
        Log(LOG_LEVEL_ERR, "Failed to open pipe from '%s'. (cf_popen: %s)",
            cmd, GetErrorStr());
        return;
    }

    size_t line_size = CF_BUFSIZE;
    char *line = xmalloc(line_size);

    for (;;)
    {
        ssize_t res = CfReadLine(&line, &line_size, pp);
        if (res == -1)
        {
            if (!feof(pp))
            {
                Log(LOG_LEVEL_ERR,
                    "Error reading output of command '%s' (ferror: %s)",
                    cmd, GetErrorStr());
            }
            break;
        }

        if ((strstr(line, "already mounted")) || (strstr(line, "exceeded")) || (strstr(line, "determined")))
        {
            continue;
        }

        if (strstr(line, "not supported"))
        {
            continue;
        }

        if ((strstr(line, "denied")) || (strstr(line, "RPC")))
        {
            Log(LOG_LEVEL_ERR,
                "There was a mount error while trying to mount the filesystems"
                " (command '%s')", cmd);
            break;
        }

        if ((strstr(line, "trying")) && (!strstr(line, "NFS version 2")) && (!strstr(line, "vers 3")))
        {
            Log(LOG_LEVEL_ERR,
                "Attempting filesystems mount aborted because command"
                " '%s' went into a retry loop", cmd);
            break;
        }
    }

    free(line);
    alarm(0);
    signal(SIGALRM, SIG_DFL);
    cf_pclose(pp);
}

/*******************************************************************/
/* Addendum                                                        */
/*******************************************************************/

static void DeleteThisItem(Item **liststart, Item *entry)
{
    Item *ip, *sp;

    if (entry != NULL)
    {
        if (entry->name != NULL)
        {
            free(entry->name);
        }

        sp = entry->next;

        if (entry == *liststart)
        {
            *liststart = sp;
        }
        else
        {
            for (ip = *liststart; ip->next != entry; ip = ip->next)
            {
            }

            ip->next = sp;
        }

        free((char *) entry);
    }
}

/*******************************************************************/
/* CFE-90: Helper functions for fstab options comparison          */
/*******************************************************************/

static char *GetFstabEntryOptions(char *mountpt)
/* Extract the options field from the fstab entry matching mountpt.
 * Returns a dynamically allocated string or NULL. */
{
    for (Item *ip = FSTABLIST; ip != NULL; ip = ip->next)
    {
        if (ip->name == NULL || ip->name[0] == '#')
        {
            continue;
        }

        /* Parse the fstab line to find the options field */
        char *orig = xstrdup(ip->name);
        char *saveptr = NULL;
        char *token = strtok_r(orig, " \t", &saveptr);
        int field = 0;
        bool found = false;

        while (token != NULL && !found)
        {
            if (field == 1)
            {
                if (strcmp(token, mountpt) == 0)
                {
                    /* Found matching mountpoint - skip field 2 (fstype), return field 3 (options) */
                    char *skip_tok = strtok_r(NULL, " \t", &saveptr); /* field 2: type */
                    if (skip_tok != NULL)
                    {
                        char *tok = strtok_r(NULL, " \t", &saveptr); /* field 3: options */
                        if (tok != NULL)
                        {
                            free(orig);
                            return xstrdup(tok);
                        }
                    }
                }
            }
            field++;
            token = strtok_r(NULL, " \t", &saveptr);
        }
        free(orig);
    }

    return NULL;
}

static void ReplaceFstabEntry(Item **liststart, char *mountpt, char *new_entry)
/* Replace the fstab entry for mountpt with new_entry */
{
    for (Item *ip = *liststart; ip != NULL; ip = ip->next)
    {
        if (ip->name != NULL && ip->name[0] != '#')
        {
            char *orig = xstrdup(ip->name);
            char *saveptr = NULL;
            char *token = strtok_r(orig, " \t", &saveptr);
            int field = 0;
            bool found = false;

            while (token != NULL && !found)
            {
                if (field == 1)
                {
                    if (strcmp(token, mountpt) == 0)
                    {
                        /* Found matching mountpoint - replace the entire line */
                        free(ip->name);
                        ip->name = xstrdup(new_entry);
                        found = true;
                    }
                }
                field++;
                token = strtok_r(NULL, " \t", &saveptr);
            }
            free(orig);
            if (found)
            {
                break;
            }
        }
    }
}

/*******************************************************************/
/* CFE-90: Remount reconciliation                                  */
/*******************************************************************/

static bool LiveMountConverged(const char *name, const Attributes *a)
/* Re-read the mount table from scratch (the cached global list is stale after
 * a mount operation) and report whether the filesystem now mounted at 'name'
 * satisfies the promise: correct source (when specified) and, when options are
 * promised, a superset of the promised options.  The kernel returns success
 * from a remount even when it silently ignores unsupported options, so we must
 * verify the resulting state rather than trust the command's exit status. */
{
    assert(a != NULL);
    Seq *tmp = SeqNew(100, free);
    bool converged = false;

    if (LoadMountInfo(tmp))
    {
        for (size_t i = 0; i < SeqLength(tmp); i++)
        {
            Mount *mp = SeqAt(tmp, i);
            if (mp == NULL || mp->mounton == NULL || strcmp(mp->mounton, name) != 0)
            {
                continue;
            }

            /* Something is mounted here - check source, then options. */
            if ((a->mount.mount_source != NULL)
                && ((mp->source == NULL) || (strcmp(mp->source, a->mount.mount_source) != 0)))
            {
                converged = false;
            }
            else if (a->mount.mount_options != NULL)
            {
                char *opts = Rlist2String(a->mount.mount_options, ",");
                converged = (mp->raw_opts != NULL) && OptionsSubsetMatches(opts, mp->raw_opts);
                free(opts);
            }
            else
            {
                converged = true;
            }
            break;
        }
    }

    DeleteMountInfo(tmp);
    SeqDestroy(tmp);
    return converged;
}

PromiseResult ReconcileMountOptions(EvalContext *ctx, char *name, const Attributes *a, const Promise *pp)
/* CFE-90: Reconcile an already-mounted filesystem whose source or options
 * diverge from the promise.  Tries each mechanism in a->mount.remount_methods
 * in order (default: "remount" then "unmount_mount"), verifying after each that
 * the live mount now satisfies the promise and stopping as soon as it does.
 * Honors DONTDO.  A failed reconciliation is reported via cfPS as its own
 * outcome, independent of any fstab edit the caller performs afterwards. */
{
    assert(a != NULL);
    PromiseResult result = PROMISE_RESULT_NOOP;
    char *opts = Rlist2String(a->mount.mount_options, ",");
    int timeout = (a->mount.remount_timeout != CF_NOINT) ? a->mount.remount_timeout : RPCTIMEOUT;

    /* Ordered method list: promise-specified, else the default escalation. */
    Seq *methods = SeqNew(4, NULL); /* borrows const char*, does not own them */
    if (a->mount.remount_methods != NULL)
    {
        for (const Rlist *rp = a->mount.remount_methods; rp != NULL; rp = rp->next)
        {
            SeqAppend(methods, RlistScalarValue(rp));
        }
    }
    else
    {
        SeqAppend(methods, "remount");
        SeqAppend(methods, "unmount_mount");
    }

    /* Dry-run / simulate / warn-only: report the pending change as a warning
     * and make no change, rather than reporting PROMISE_RESULT_CHANGE. The
     * latter would define the classes body's promise_repaired set on a run
     * that touches nothing (CFE-3366: check the promise action, not just
     * DONTDO - a fault the sibling VerifyMount/VerifyUnmount still have). A
     * remount is a live side effect not captured by the simulate sandbox, so
     * MakingInternalChanges (normal-mode only) is the correct gate; it also
     * honours action_policy => "warn". */
    if (!MakingInternalChanges(ctx, pp, a, &result,
             "reconcile mount '%s' to promised options '%s'", name,
             (opts != NULL) ? opts : ""))
    {
        SeqDestroy(methods);
        free(opts);
        return result;
    }

    bool converged = false;
    for (size_t i = 0; (i < SeqLength(methods)) && !converged; i++)
    {
        const char *method = SeqAt(methods, i);

        if (strcmp(method, "remount") == 0)
        {
            char comm[CF_BUFSIZE];
            if ((opts != NULL) && (opts[0] != '\0'))
            {
                snprintf(comm, CF_BUFSIZE, "%s -o remount,%s %s",
                         CommandArg0(VMOUNTCOMM[VSYSTEMHARDCLASS]), opts, name);
            }
            else
            {
                snprintf(comm, CF_BUFSIZE, "%s -o remount %s",
                         CommandArg0(VMOUNTCOMM[VSYSTEMHARDCLASS]), name);
            }

            Log(LOG_LEVEL_VERBOSE, "Reconciling '%s' via remount: %s", name, comm);
            SetTimeOut(timeout);

            FILE *pfp = cf_popen(comm, "r", true);
            if (pfp == NULL)
            {
                Log(LOG_LEVEL_ERR, "Failed to open pipe from '%s'", CommandArg0(VMOUNTCOMM[VSYSTEMHARDCLASS]));
            }
            else
            {
                size_t line_size = CF_BUFSIZE;
                char *line = xmalloc(line_size);
                while (CfReadLine(&line, &line_size, pfp) != -1)
                {
                    /* drain command output */
                }
                free(line);
                cf_pclose(pfp);
            }
        }
        else if (strcmp(method, "unmount_mount") == 0)
        {
            /* Reuse the tested helpers - both honor DONTDO and build the correct
             * per-fstype command.  This also handles a wrong-source mount, which
             * an in-place remount cannot fix. */
            Log(LOG_LEVEL_VERBOSE, "Reconciling '%s' via unmount + mount", name);
            SetTimeOut(timeout);
            result = PromiseResultUpdate(result, VerifyUnmount(ctx, name, a, pp));
            result = PromiseResultUpdate(result, VerifyMount(ctx, name, a, pp));
        }
        else
        {
            Log(LOG_LEVEL_WARNING, "Unknown remount_method '%s' for '%s' - skipping", method, name);
            continue;
        }

        /* Verify-after-act: confirm the live mount actually satisfies the promise. */
        if (LiveMountConverged(name, a))
        {
            cfPS(ctx, LOG_LEVEL_INFO, PROMISE_RESULT_CHANGE, pp, a,
                 "Reconciled mount '%s' to promised options '%s' via '%s'", name,
                 (opts != NULL) ? opts : "", method);
            result = PromiseResultUpdate(result, PROMISE_RESULT_CHANGE);
            converged = true;
        }
    }

    if (!converged)
    {
        cfPS(ctx, LOG_LEVEL_ERR, PROMISE_RESULT_FAIL, pp, a,
             "Could not reconcile mount '%s' to promised options '%s'", name,
             (opts != NULL) ? opts : "");
        result = PromiseResultUpdate(result, PROMISE_RESULT_FAIL);
    }

    SeqDestroy(methods);
    free(opts);
    return result;
}

void CleanupNFS(void)
{
    Log(LOG_LEVEL_VERBOSE, "Number of changes observed in '%s' is %d", VFSTAB[VSYSTEMHARDCLASS], FSTAB_EDITS);

    if (FSTAB_EDITS && FSTABLIST && !DONTDO)
    {
        RawSaveItemList(FSTABLIST, VFSTAB[VSYSTEMHARDCLASS], NewLineMode_Unix);
        DeleteItemList(FSTABLIST);
        FSTABLIST = NULL;
        FSTAB_EDITS = 0;
    }
}
