Commit message: Issue 4711 - SIGSEV with sync_repl (#4738)

Bug description:
	sync_repl sends back entries identified with a unique
	identifier that is 'nsuniqueid'. If 'nsuniqueid' is
	missing, then it may crash

Fix description:
	Check a nsuniqueid is available else returns OP_ERR

relates: https://github.com/389ds/389-ds-base/issues/4711

Reviewed by: Pierre Rogier, James Chapman, William Brown (Thanks!)

Platforms tested:  F33
CVE: CVE-2021-3514
Vulnerability description: When using a sync_repl client in 389-ds-base, an authenticated attacker can cause a NULL pointer dereference using a specially crafted query, causing a crash.

Project: 389-ds-base
File: sync_util.c

sync_create_sync_done_control(LDAPControl **ctrlp, int refresh, char *cookie)
{
    int rc;
    BerElement *ber;
    struct berval *bvp;

    if (ctrlp == NULL || (ber = der_alloc()) == NULL) {
        return (LDAP_OPERATIONS_ERROR);
    }

    *ctrlp = NULL;

    if (cookie) {
        if ((rc = ber_printf(ber, "{s", cookie)) != -1) {
            if (refresh) {
                rc = ber_printf(ber, "b}", refresh);
            } else {
                rc = ber_printf(ber, "}");
            }
        }
    } else {
        if (refresh) {
            rc = ber_printf(ber, "{b}", refresh);
        } else {
            rc = ber_printf(ber, "{}");
        }
    }
    if (rc != -1) {
        rc = ber_flatten(ber, &bvp);
    }
    ber_free(ber, 1);

    if (rc == -1) {
        return (LDAP_OPERATIONS_ERROR);
    }

    *ctrlp = (LDAPControl *)slapi_ch_malloc(sizeof(LDAPControl));
    (*ctrlp)->ldctl_iscritical = 0;
    (*ctrlp)->ldctl_oid = slapi_ch_strdup(LDAP_CONTROL_SYNC_DONE);
    (*ctrlp)->ldctl_value = *bvp; /* struct copy */

    bvp->bv_val = NULL;
    ber_bvfree(bvp);

    return (LDAP_SUCCESS);
}

Project: 389-ds-base
File: sync_util.c

sync_ulong2olcsn(unsigned long chgnr, char *buf) {
    PR_ASSERT(buf);
    unsigned long x = chgnr + CSN_OFFSET;
    time_t epoch = x;
    struct tm t = {0};
    localtime_r(&epoch, &t);
    strftime(buf, 15, "%Y%m%d%H%M%S", &t);
}

Project: 389-ds-base
File: sync_util.c

create_syncinfo_value(int type, const char *cookie, const char **uuids)
{
    BerElement *ber;
    struct berval *bvp = NULL;

    if ((ber = der_alloc()) == NULL) {
        return (NULL);
    }

    /*
     * ber_tag_t is an unsigned integer of at least 32 bits
     * used to represent a BER tag. It is commonly equivalent
     * to a unsigned long.
     * ...
     * ber_printf(...)
     * t
     *   Tag of the next element. A pointer to a ber_tag_t should be supplied. 
     */

    ber_tag_t btag = (ber_tag_t)type;

    switch (type) {
    case LDAP_TAG_SYNC_NEW_COOKIE:
        ber_printf(ber, "to", btag, cookie);
        break;
    case LDAP_TAG_SYNC_REFRESH_DELETE:
    case LDAP_TAG_SYNC_REFRESH_PRESENT:
        ber_printf(ber, "t{", btag);
        if (cookie) {
            ber_printf(ber, "s", cookie);
        }
        /* ber_printf(ber, "b",1); */
        ber_printf(ber, "}");
        break;
    case LDAP_TAG_SYNC_ID_SET:
        ber_printf(ber, "t{", btag);
        if (cookie) {
            ber_printf(ber, "s", cookie);
        }
        if (uuids) {
            ber_printf(ber, "b[v]", 1, uuids);
        }
        ber_printf(ber, "}");
        break;
    default:
        break;
    }
    ber_flatten(ber, &bvp);
    ber_free(ber, 1);

    return (bvp);
}

Project: 389-ds-base
File: sync_util.c

sync_cookie_create(Slapi_PBlock *pb, Sync_Cookie *client_cookie)
{
    Sync_CallBackData scbd = {0};
    int rc = 0;
    Sync_Cookie *sc = (Sync_Cookie *)slapi_ch_calloc(1, sizeof(Sync_Cookie));

    scbd.cb_err = SYNC_CALLBACK_PREINIT;
    rc = sync_cookie_get_change_info(&scbd);

    if (rc == 0) {
        /* If the client is in openldap compat, we need to generate the same. */
        if (client_cookie && client_cookie->openldap_compat) {
            sc->openldap_compat = client_cookie->openldap_compat;
            sc->cookie_client_signature = slapi_ch_strdup(client_cookie->cookie_client_signature);
            sc->cookie_server_signature = NULL;
        } else {
            sc->openldap_compat = PR_FALSE;
            sc->cookie_server_signature = sync_cookie_get_server_info(pb);
            sc->cookie_client_signature = sync_cookie_get_client_info(pb);
        }

        if (scbd.cb_err == SYNC_CALLBACK_PREINIT) {
            /* changenr is not initialized. */
            sc->cookie_change_info = 0;
        } else {
            sc->cookie_change_info = scbd.changenr;
        }
    } else {
        slapi_ch_free((void **)&sc);
        sc = NULL;
    }

    return (sc);
}

Project: 389-ds-base
File: sync_util.c

sync_olcsn2ulong(char *csn) {
    struct tm pt = {0};
    char *ret = strptime(csn, "%Y%m%d%H%M%S", &pt);
    PR_ASSERT(ret);
    if (ret == NULL) {
        return SYNC_INVALID_CHANGENUM;
    }
    time_t pepoch = mktime(&pt);
    unsigned long px = (unsigned long)pepoch;
    PR_ASSERT(px >= CSN_OFFSET);
    if (px < CSN_OFFSET) {
        return SYNC_INVALID_CHANGENUM;
    }
    return px - CSN_OFFSET;
}

Project: 389-ds-base
File: sync_util.c

sync_result_msg(Slapi_PBlock *pb, Sync_Cookie *cookie)
{
    int rc = 0;
    char *cookiestr = sync_cookie2str(cookie);

    LDAPControl **ctrl = (LDAPControl **)slapi_ch_calloc(2, sizeof(LDAPControl *));

    if (cookie->openldap_compat) {
        sync_create_sync_done_control(&ctrl[0], 1, cookiestr);
    } else {
        sync_create_sync_done_control(&ctrl[0], 0, cookiestr);
    }
    slapi_pblock_set(pb, SLAPI_RESCONTROLS, ctrl);
    slapi_send_ldap_result(pb, 0, NULL, NULL, 0, NULL);

    slapi_ch_free((void **)&cookiestr);
    return (rc);
}

Project: 389-ds-base
File: sync_util.c

sync_cookie_parse(char *cookie, PRBool *cookie_refresh, PRBool *allow_openldap_compat)
{
    char *p = NULL;
    char *q = NULL;
    Sync_Cookie *sc = NULL;

    /* This is an rfc compliant initial refresh request */
    if (cookie == NULL || *cookie == '\0') {
        *cookie_refresh = PR_TRUE;
        return NULL;
    }

    /* get ready to parse. */
    p = q = cookie;

    sc = (Sync_Cookie *)slapi_ch_calloc(1, sizeof(Sync_Cookie));
    if (strncmp(cookie, "rid=", 4) == 0) {
        if (*allow_openldap_compat != PR_TRUE) {
            slapi_log_err(SLAPI_LOG_ERR, SYNC_PLUGIN_SUBSYSTEM, "sync_cookie_parse - An openldap sync request was made, but " SYNC_ALLOW_OPENLDAP_COMPAT " is false\n");
            slapi_log_err(SLAPI_LOG_ERR, SYNC_PLUGIN_SUBSYSTEM, "sync_cookie_parse - To enable this run 'dsconf <instance> plugin contentsync set --allow-openldap on'\n");
            goto error_return;
        }
        /*
         * We are in openldap mode.
         * The cookies are:
         * rid=123,csn=20200525051329.534174Z#000000#000#000000
         */
        sc->openldap_compat = PR_TRUE;
        p = strchr(q, ',');
        if (p == NULL) {
            /* No CSN following the rid, must be an init request. */
            *cookie_refresh = PR_TRUE;
            /* We need to keep the client rid though */
            sc->cookie_client_signature = slapi_ch_strdup(q);
            /* server sig and change info do not need to be set. */
            sc->cookie_server_signature = NULL;
            sc->cookie_change_info = 0;
        } else {
            /* Ensure that this really is a csn= */
            if (strncmp(p, ",csn=", 5) != 0) {
                /* Yeah nahhhhhhh */
                goto error_return;
            }
            /* We dont care about the remainder after the . */
            if (strlen(p) < 20) {
                /* Probably a corrupt CSN. We need at least 20 chars. */
                goto error_return;
            }
            /*
             * Replace the , with a '\0' This makes q -> p a str of the rid.
             * rid=123,csn=19700101001640.000000Z#000000#000#000000
             * ^      ^
             * q      p
             * rid=123\0csn=19700101001640.000000Z#000000#000#000000
             */
            PR_ASSERT(p[0] == ',');
            p[0] = '\0';
            /*
             * Now terminate the ulong which is our change num so we can parse it.
             * rid=123\0csn=19700101001640.000000Z#000000#000#000000
             * ^       ^                  ^
             * q       p[0]               p[19]
             * rid=123\0csn=19700101001640\0...
             */
            PR_ASSERT(p[19] == '.');
            p[19] = '\0';
            /*
             * And move the pointer up to the start of the int we need to parse.
             * rid=123\0csn=19700101001640\0...
             * ^       ^
             * q       p +5 -->
             * rid=123\0csn=19700101001640\0...
             * ^            ^
             * q            p
             */
            p = p + 5;
            PR_ASSERT(strlen(p) == 14);
            /* We are now ready to parse the csn and create a cookie! */
            sc->cookie_client_signature = slapi_ch_strdup(q);
            sc->cookie_server_signature = NULL;
            /* Get the change number from the string */
            sc->cookie_change_info = sync_olcsn2ulong(p);
            if (SYNC_INVALID_CHANGENUM == sc->cookie_change_info) {
                /* Sad trombone */
                goto error_return;
            }
            /* Done! 🎉 */
        }
    } else {
        /*
         * Format of the 389 cookie: server_signature#client_signature#change_info_number
         * If the cookie is malformed, NULL is returned.
         */
        p = strchr(q, '#');
        if (p) {
            *p = '\0';
            sc->cookie_server_signature = slapi_ch_strdup(q);
            q = p + 1;
            p = strchr(q, '#');
            if (p) {
                *p = '\0';
                sc->cookie_client_signature = slapi_ch_strdup(q);
                sc->cookie_change_info = sync_number2ulong(p + 1);
                if (SYNC_INVALID_CHANGENUM == sc->cookie_change_info) {
                    goto error_return;
                }
            } else {
                goto error_return;
            }
        }
    }
    return (sc);
error_return:
    slapi_ch_free_string(&(sc->cookie_client_signature));
    slapi_ch_free_string(&(sc->cookie_server_signature));
    slapi_ch_free((void **)&sc);
    return NULL;
}

Project: 389-ds-base
File: sync_util.c

sync_entryuuid2uuid(const char *entryuuid)
{
    char *uuid;
    char u[17] = {0};

    u[0] = slapi_str_to_u8(entryuuid);
    u[1] = slapi_str_to_u8(entryuuid + 2);
    u[2] = slapi_str_to_u8(entryuuid + 4);
    u[3] = slapi_str_to_u8(entryuuid + 6);

    u[4] = slapi_str_to_u8(entryuuid + 9);
    u[5] = slapi_str_to_u8(entryuuid + 11);

    u[6] = slapi_str_to_u8(entryuuid + 14);
    u[7] = slapi_str_to_u8(entryuuid + 16);

    u[8] = slapi_str_to_u8(entryuuid + 19);
    u[9] = slapi_str_to_u8(entryuuid + 21);

    u[10] = slapi_str_to_u8(entryuuid + 24);
    u[11] = slapi_str_to_u8(entryuuid + 26);
    u[12] = slapi_str_to_u8(entryuuid + 28);
    u[13] = slapi_str_to_u8(entryuuid + 30);
    u[14] = slapi_str_to_u8(entryuuid + 32);
    u[15] = slapi_str_to_u8(entryuuid + 34);

    uuid = slapi_ch_malloc(sizeof(u));
    memcpy(uuid, u, sizeof(u));

    return (uuid);
}

Project: 389-ds-base
File: sync_util.c

sync_create_state_control(Slapi_Entry *e, LDAPControl **ctrlp, int type, Sync_Cookie *cookie, PRBool openldap_compat)
{
    int rc;
    BerElement *ber;
    struct berval *bvp;
    char *uuid;
    Slapi_Attr *attr = NULL;
    Slapi_Value *val = NULL;

    if (type == LDAP_SYNC_NONE || ctrlp == NULL || (ber = der_alloc()) == NULL) {
        return (LDAP_OPERATIONS_ERROR);
    }

    *ctrlp = NULL;

    if (openldap_compat) {
        slapi_entry_attr_find(e, SLAPI_ATTR_ENTRYUUID, &attr);
        if (!attr) {
            /*
             * We can't proceed from here. We are in openldap mode, but some entries don't
             * have their UUID. This means that the tree could be corrupted on the openldap
             * server, so we have to stop now.
             */
            slapi_log_err(SLAPI_LOG_ERR, SYNC_PLUGIN_SUBSYSTEM,
                          "sync_create_state_control - Some entries are missing entryUUID. Unable to proceed. You may need to re-run the entryuuid fixup\n");
            return (LDAP_OPERATIONS_ERROR);
        }
        slapi_attr_first_value(attr, &val);
        uuid = sync_entryuuid2uuid(slapi_value_get_string(val));
    } else {
        slapi_entry_attr_find(e, SLAPI_ATTR_UNIQUEID, &attr);
        slapi_attr_first_value(attr, &val);
        if ((attr == NULL) || (val == NULL)) {
            /* It may happen with entries in special backends
             * such like cn=config, cn=shema, cn=monitor...
             */
            slapi_log_err(SLAPI_LOG_ERR, SYNC_PLUGIN_SUBSYSTEM,
                          "sync_create_state_control - Entries are missing nsuniqueid. Unable to proceed.\n");
            return (LDAP_OPERATIONS_ERROR);
        }
        uuid = sync_nsuniqueid2uuid(slapi_value_get_string(val));
    }

    if ((rc = ber_printf(ber, "{eo", type, uuid, 16)) != -1) {
        if (cookie) {
            char *cookiestr = sync_cookie2str(cookie);
            rc = ber_printf(ber, "s}", cookiestr);
            slapi_ch_free((void **)&cookiestr);
        } else {
            rc = ber_printf(ber, "}");
        }
    }
    if (rc != -1) {
        rc = ber_flatten(ber, &bvp);
    }
    ber_free(ber, 1);
    slapi_ch_free((void **)&uuid);

    if (rc == -1) {
        return (LDAP_OPERATIONS_ERROR);
    }

    *ctrlp = (LDAPControl *)slapi_ch_malloc(sizeof(LDAPControl));
    (*ctrlp)->ldctl_iscritical = 0;
    (*ctrlp)->ldctl_oid = slapi_ch_strdup(LDAP_CONTROL_SYNC_STATE);
    (*ctrlp)->ldctl_value = *bvp; /* struct copy */

    bvp->bv_val = NULL;
    ber_bvfree(bvp);

    return (LDAP_SUCCESS);
}

Project: 389-ds-base
File: sync_util.c

sync_cookie2str(Sync_Cookie *cookie)
{
    char *cookiestr = NULL;

    if (cookie) {
        if (cookie->openldap_compat) {
            char buf[16] = {0};
            sync_ulong2olcsn(cookie->cookie_change_info, buf);
            cookiestr = slapi_ch_smprintf("%s,csn=%s.000000Z#000000#000#000000",
                                          cookie->cookie_client_signature,
                                          buf);
        } else {
            cookiestr = slapi_ch_smprintf("%s#%s#%lu",
                                          cookie->cookie_server_signature,
                                          cookie->cookie_client_signature,
                                          cookie->cookie_change_info);
        }
    }
    return (cookiestr);
}

Project: 389-ds-base
File: sync_util.c

sync_cookie_isvalid(Sync_Cookie *testcookie, Sync_Cookie *refcookie)
{
    /* client and server info must match */
    if (testcookie == NULL || refcookie == NULL) {
        return 0;
    }
    if ((testcookie->openldap_compat != refcookie->openldap_compat ||
         strcmp(testcookie->cookie_client_signature, refcookie->cookie_client_signature) ||
         testcookie->cookie_change_info == -1 ||
         testcookie->cookie_change_info > refcookie->cookie_change_info)) {
        return 0;
    }

    if (refcookie->openldap_compat) {
        if (testcookie->cookie_server_signature != NULL ||
            refcookie->cookie_server_signature != NULL) {
            return 0;
        }
    } else {
        if (strcmp(testcookie->cookie_server_signature, refcookie->cookie_server_signature)) {
            return 0;
        }
    }
    /* could add an additional check if the requested state in client cookie is still
     * available. Accept any state request for now.
     */
    return 1;
}