/*! \file boolexp.cpp * \brief Boolean Expressions for \@locks * * The functions here evaluate and organize boolean expressions used in * locks. Lock evaluation contains one of server's the three parsers. The * other two parsers are for functions (see eval.cpp) and commands (see * command.cpp). */ #include "copyright.h" #include "autoconf.h" #include "config.h" #include "externs.h" static bool parsing_internal = false; // Bound parser recursion. parse_boolexp_E/T/F/L are mutually recursive on // attacker-controlled @lock input, with no depth limit of their own — the // existing mudconf.lock_nest_lim guards only the *evaluator's* indirect-lock // recursion. A deeply nested lock such as "!!!!...#1" or "((((...#1" therefore // overflows the C stack and crashes the server. Real locks nest a handful of // levels; this cap is far beyond any legitimate use yet well within the stack // (parse_boolexp_F's scratch buffer is heap-allocated, below, so each frame is // small). Tripping it yields TRUE_BOOLEXP, the same sentinel the parser already // returns for any malformed lock. // static constexpr int LOCK_PARSE_MAX_DEPTH = 1024; static thread_local int s_parse_depth = 0; namespace { class ParseDepthGuard { public: ParseDepthGuard() { ++s_parse_depth; } ~ParseDepthGuard() { --s_parse_depth; } }; } /* --------------------------------------------------------------------------- * check_attr: indicate if attribute ATTR on player passes key when checked by * the object lockobj */ static bool check_attr(dbref player, dbref lockobj, ATTR *attr, UTF8 *key) { dbref aowner; int aflags; bool bCheck = false; LBuf buff = LBuf_Adopt(atr_pget(player, attr->number, &aowner, &aflags)); if (attr->number == A_LENTER) { // We can see enterlocks... else we'd break zones. // bCheck = true; } else if (See_attr(lockobj, player, attr)) { bCheck = true; } else if (attr->number == A_NAME) { bCheck = true; } if ( bCheck && !wild_match(key, buff)) { bCheck = false; } return bCheck; } bool eval_boolexp(dbref player, dbref thing, dbref from, BOOLEXP *b) { if (b == TRUE_BOOLEXP) { return true; } dbref aowner, obj, source; int aflags; UTF8 *buff, *bp; UTF8 *key; ATTR *a; bool bCheck, c; switch (b->type) { case BOOLEXP_AND: return eval_boolexp(player, thing, from, b->sub1) && eval_boolexp(player, thing, from, b->sub2); case BOOLEXP_OR: return eval_boolexp(player, thing, from, b->sub1) || eval_boolexp(player, thing, from, b->sub2); case BOOLEXP_NOT: return !eval_boolexp(player, thing, from, b->sub1); case BOOLEXP_INDIR: // BOOLEXP_INDIR (i.e. @) is a unary operation which is replaced at // evaluation time by the lock of the object whose number is the // argument of the operation. // mudstate.lock_nest_lev++; if (mudstate.lock_nest_lev >= mudconf.lock_nest_lim) { if (mudstate.bStandAlone) { Log.WriteString(T("Lock exceeded recursion limit." ENDLINE)); } else { STARTLOG(LOG_BUGS, "BUG", "LOCK"); log_name_and_loc(player); log_text(T(": Lock exceeded recursion limit.")); ENDLOG; notify(player, M_("Sorry, broken lock!")); } mudstate.lock_nest_lev--; return false; } if ( b->sub1->type != BOOLEXP_CONST || b->sub1->thing < 0) { if (mudstate.bStandAlone) { Log.WriteString(T("Broken lock." ENDLINE)); } else { STARTLOG(LOG_BUGS, T("BUG"), T("LOCK")); log_name_and_loc(player); buff = alloc_mbuf("eval_boolexp.LOG.indir"); mux_sprintf(buff, MBUF_SIZE, T(": Lock had bad indirection (%c, type %d)"), INDIR_TOKEN, b->sub1->type); log_text(buff); free_mbuf(buff); ENDLOG; notify(player, M_("Sorry, broken lock!")); } mudstate.lock_nest_lev--; return false; } { LBuf lkey = LBuf_Adopt(atr_get("boolexp.130", b->sub1->thing, b->thing, &aowner, &aflags)); c = eval_boolexp_atr(player, b->sub1->thing, from, lkey); } mudstate.lock_nest_lev--; return c; case BOOLEXP_CONST: return b->thing == player || member(b->thing, Contents(player)); case BOOLEXP_ATR: a = atr_num(b->thing); if (!a) { // No such attribute. // return false; } // First check the object itself, then its contents. // if (check_attr(player, from, a, reinterpret_cast(b->sub1))) { return true; } DOLIST(obj, Contents(player)) { if (check_attr(obj, from, a, reinterpret_cast(b->sub1))) { return true; } } return false; case BOOLEXP_EVAL: a = atr_num(b->thing); if ( !a || alarm_clock.alarmed) { // No such attribute. // return false; } source = from; { LBuf lbuff = LBuf_Adopt(atr_pget(from, a->number, &aowner, &aflags)); if (!lbuff.get() || !*lbuff.get()) { lbuff = LBuf_Adopt(atr_pget(thing, a->number, &aowner, &aflags)); source = thing; } bCheck = false; if ( a->number == A_NAME || a->number == A_LENTER || bCanReadAttr(source, source, a, false)) { bCheck = true; } if (bCheck) { if ((aflags & AF_NOEVAL) || NoEval(source)) { bCheck = !string_compare(lbuff, reinterpret_cast(b->sub1)); } else { reg_ref** preserve = PushRegisters(MAX_GLOBAL_REGS); save_global_regs(preserve); LBuf buff2 = LBuf_Src("eval_boolexp"); bp = buff2.get(); mux_exec(lbuff, LBUF_SIZE-1, buff2, &bp, source, player, player, AttrTrace(aflags, EV_FIGNORE|EV_EVAL|EV_FCHECK|EV_TOP), nullptr, 0); *bp = '\0'; restore_global_regs(preserve); PopRegisters(preserve, MAX_GLOBAL_REGS); bCheck = !string_compare(buff2, reinterpret_cast(b->sub1)); } } } return bCheck; case BOOLEXP_IS: // If an object check, do that. // if (b->sub1->type == BOOLEXP_CONST) { return (b->sub1->thing == player); } // Nope, do an attribute check // a = atr_num(b->sub1->thing); if (!a) { return false; } return check_attr(player, from, a, reinterpret_cast((b->sub1)->sub1)); case BOOLEXP_CARRY: // If an object check, do that // if (b->sub1->type == BOOLEXP_CONST) { return member(b->sub1->thing, Contents(player)); } // Nope, do an attribute check // a = atr_num(b->sub1->thing); if (!a) { return false; } DOLIST(obj, Contents(player)) { if (check_attr(obj, from, a, reinterpret_cast((b->sub1)->sub1))) { return true; } } return false; case BOOLEXP_OWNER: return (Owner(b->sub1->thing) == Owner(player)); default: // Bad type // mux_assert(0); return false; } } bool eval_boolexp_atr(dbref player, dbref thing, dbref from, UTF8 *key) { bool ret_value; BOOLEXP *b = parse_boolexp(player, key, true); if (b == nullptr) { ret_value = true; } else { ret_value = eval_boolexp(player, thing, from, b); free_boolexp(b); } return ret_value; } // If the parser returns TRUE_BOOLEXP, you lose // TRUE_BOOLEXP cannot be typed in by the user; use @unlock instead // static const char *parsebuf; thread_local char parsestore[LBUF_SIZE]; static dbref parse_player; static void skip_whitespace(void) { while (mux_isspace(*parsebuf)) { parsebuf++; } } // Defined below. // static BOOLEXP *parse_boolexp_E(void); static BOOLEXP *test_atr(UTF8 *s) { int anum; boolexp_type locktype; LBuf buff = LBuf_Src("test_atr"); mux_strncpy(buff, s, LBUF_SIZE-1); for (s = buff.get(); *s && (*s != ':') && (*s != '/'); s++) { } if (!*s) { return TRUE_BOOLEXP; } if (*s == '/') { locktype = BOOLEXP_EVAL; } else { locktype = BOOLEXP_ATR; } *s++ = '\0'; // See if left side is valid attribute. Access to attr is checked on eval // Also allow numeric references to attributes. It can't hurt us, and lets // us import stuff that stores attr locks by number instead of by name. // ATTR *attrib = atr_str(buff); if (!attrib) { UTF8 *s1; // Only #1 can lock on numbers // if (!God(parse_player)) { return TRUE_BOOLEXP; } for (s1 = buff; mux_isdigit(*s1); s1++) { } if (*s1) { return TRUE_BOOLEXP; } anum = mux_atoi64(buff); if (anum <= 0) { return TRUE_BOOLEXP; } } else { anum = attrib->number; } // made it now make the parse tree node // auto b = alloc_bool("test_str"); b->type = locktype; b->thing = static_cast(anum); b->sub1 = reinterpret_cast(StringClone(s)); return b; } // L -> (E); L -> object identifier // static BOOLEXP *parse_boolexp_L(void) { BOOLEXP *b; UTF8 *p; MSTATE mstate; skip_whitespace(); switch (*parsebuf) { case '(': parsebuf++; b = parse_boolexp_E(); skip_whitespace(); if ( b == TRUE_BOOLEXP || *parsebuf++ != ')') { free_boolexp(b); return TRUE_BOOLEXP; } break; default: // Must have hit an object ref. Load the name into our buffer. // LBuf buf = LBuf_Src("parse_boolexp_L"); p = buf.get(); while ( *parsebuf && *parsebuf != AND_TOKEN && *parsebuf != OR_TOKEN && *parsebuf != ')' && p < buf.get() + LBUF_SIZE) { *p++ = *parsebuf++; } // Strip trailing whitespace. // *p-- = '\0'; while (mux_isspace(*p)) { *p-- = '\0'; } // Check for an attribute. // if ((b = test_atr(buf)) != nullptr) { return (b); } b = alloc_bool("parse_boolexp_L"); b->type = BOOLEXP_CONST; // do the match. // if (!mudstate.bStandAlone) { // If we are parsing a boolexp that was a stored lock then we // know that object refs are all dbrefs, so we skip the // expensive match code. // if (parsing_internal) { if (buf[0] != '#') { free_bool(b); return TRUE_BOOLEXP; } b->thing = mux_atoi64(&buf[1]); if (!Good_dbref(b->thing)) { free_bool(b); return TRUE_BOOLEXP; } } else { save_match_state(&mstate); init_match(parse_player, buf, TYPE_THING); match_everything(MAT_EXIT_PARENTS); b->thing = match_result(); restore_match_state(&mstate); } if (b->thing == NOTHING) { notify(parse_player, tprintf(M_("I don’t see %s here."), buf.get())); free_bool(b); return TRUE_BOOLEXP; } if (b->thing == AMBIGUOUS) { notify(parse_player, tprintf(M_("I don’t know which %s you mean!"), buf.get())); free_bool(b); return TRUE_BOOLEXP; } } else { // Had better be # or we're hosed. // if (buf[0] != '#') { free_bool(b); return TRUE_BOOLEXP; } b->thing = mux_atoi64(&buf[1]); if (b->thing < 0) { free_bool(b); return TRUE_BOOLEXP; } } } return b; } // F -> !F; F -> @L; F -> =L; F -> +L; F -> $L // The argument L must be type BOOLEXP_CONST // static BOOLEXP *parse_boolexp_F(void) { // Every parser recursion cycle (NOT's F->F, AND's T->F, OR's E->T->F, // parens' L->E->T->F) passes through here, so bounding depth at this one // point bounds all of them. // ParseDepthGuard depth_guard; if (s_parse_depth > LOCK_PARSE_MAX_DEPTH) { return TRUE_BOOLEXP; } BOOLEXP *b2; skip_whitespace(); switch (*parsebuf) { case NOT_TOKEN: parsebuf++; b2 = alloc_bool("parse_boolexp_F.not"); b2->type = BOOLEXP_NOT; if ((b2->sub1 = parse_boolexp_F()) == TRUE_BOOLEXP) { free_boolexp(b2); return (TRUE_BOOLEXP); } else { return (b2); } // NOTREACHED // case INDIR_TOKEN: parsebuf++; b2 = alloc_bool("parse_boolexp_F.indir"); b2->type = BOOLEXP_INDIR; b2->thing = A_LOCK; // Scan ahead for '/' (lock name separator) before any operator. // We need to find it before parse_boolexp_L() consumes it. // { const char *slash = nullptr; int depth = 0; for (const char *scan = parsebuf; *scan; scan++) { if (*scan == '(') { depth++; } else if (*scan == ')') { if (depth > 0) { depth--; } else { break; } } else if ( depth == 0 && ( *scan == AND_TOKEN || *scan == OR_TOKEN)) { break; } else if ( depth == 0 && *scan == '/') { slash = scan; break; } } if (slash) { // Copy the object-ref portion into a local buffer and // parse it separately, then extract and look up the lock // name that follows the '/'. // size_t objlen = static_cast(slash - parsebuf); // Heap-allocated (not a 32 KB stack array) so that deep parser // recursion does not balloon the stack frame. LBuf objbuf_lbuf = LBuf_Src("parse_boolexp_F.objbuf"); char *objbuf = reinterpret_cast(objbuf_lbuf.get()); if (objlen >= LBUF_SIZE) { objlen = LBUF_SIZE - 1; } memcpy(objbuf, parsebuf, objlen); objbuf[objlen] = '\0'; const char *saved = parsebuf; parsebuf = objbuf; b2->sub1 = parse_boolexp_L(); parsebuf = saved; // Advance past the object ref and the '/'. // const char *lockname_start = slash + 1; const char *lockname_end = lockname_start; while ( *lockname_end && *lockname_end != AND_TOKEN && *lockname_end != OR_TOKEN && *lockname_end != ')') { lockname_end++; } // Strip trailing whitespace from the lock name. // const char *lockname_trim = lockname_end; while ( lockname_trim > lockname_start && mux_isspace(*(lockname_trim - 1))) { lockname_trim--; } char lockname[SBUF_SIZE]; size_t lnlen = static_cast(lockname_trim - lockname_start); if (lnlen >= sizeof(lockname)) { lnlen = sizeof(lockname) - 1; } memcpy(lockname, lockname_start, lnlen); lockname[lnlen] = '\0'; parsebuf = lockname_end; // Look up the lock name. // int lock_attr; if (!search_nametab(parse_player, lock_sw, reinterpret_cast(lockname), &lock_attr)) { if (!mudstate.bStandAlone) { notify(parse_player, M_("Unknown lock type.")); } free_boolexp(b2); return TRUE_BOOLEXP; } b2->thing = static_cast(lock_attr); } else { b2->sub1 = parse_boolexp_L(); } } if ((b2->sub1) == TRUE_BOOLEXP) { free_boolexp(b2); return (TRUE_BOOLEXP); } else if ((b2->sub1->type) != BOOLEXP_CONST) { free_boolexp(b2); return (TRUE_BOOLEXP); } else { return (b2); } // NOTREACHED // case IS_TOKEN: parsebuf++; b2 = alloc_bool("parse_boolexp_F.is"); b2->type = BOOLEXP_IS; b2->sub1 = parse_boolexp_L(); if (b2->sub1 == TRUE_BOOLEXP) { free_boolexp(b2); return (TRUE_BOOLEXP); } else if ( b2->sub1->type != BOOLEXP_CONST && b2->sub1->type != BOOLEXP_ATR) { free_boolexp(b2); return TRUE_BOOLEXP; } else { return (b2); } // NOTREACHED // case CARRY_TOKEN: parsebuf++; b2 = alloc_bool("parse_boolexp_F.carry"); b2->type = BOOLEXP_CARRY; b2->sub1 = parse_boolexp_L(); if (b2->sub1 == TRUE_BOOLEXP) { free_boolexp(b2); return TRUE_BOOLEXP; } else if ( b2->sub1->type != BOOLEXP_CONST && b2->sub1->type != BOOLEXP_ATR) { free_boolexp(b2); return TRUE_BOOLEXP; } else { return b2; } // NOTREACHED // case OWNER_TOKEN: parsebuf++; b2 = alloc_bool("parse_boolexp_F.owner"); b2->type = BOOLEXP_OWNER; b2->sub1 = parse_boolexp_L(); if (b2->sub1 == TRUE_BOOLEXP) { free_boolexp(b2); return TRUE_BOOLEXP; } else if (b2->sub1->type != BOOLEXP_CONST) { free_boolexp(b2); return TRUE_BOOLEXP; } else { return b2; } // NOTREACHED // default: return parse_boolexp_L(); } } // T -> F; T -> F & T // static BOOLEXP *parse_boolexp_T(void) { BOOLEXP *b; if ((b = parse_boolexp_F()) != TRUE_BOOLEXP) { skip_whitespace(); if (*parsebuf == AND_TOKEN) { parsebuf++; const auto b2 = alloc_bool("parse_boolexp_T"); b2->type = BOOLEXP_AND; b2->sub1 = b; if ((b2->sub2 = parse_boolexp_T()) == TRUE_BOOLEXP) { free_boolexp(b2); return TRUE_BOOLEXP; } b = b2; } } return b; } // E -> T; E -> T | E // static BOOLEXP *parse_boolexp_E(void) { BOOLEXP *b, *b2; if ((b = parse_boolexp_T()) != TRUE_BOOLEXP) { skip_whitespace(); if (*parsebuf == OR_TOKEN) { parsebuf++; b2 = alloc_bool("parse_boolexp_E"); b2->type = BOOLEXP_OR; b2->sub1 = b; if ((b2->sub2 = parse_boolexp_E()) == TRUE_BOOLEXP) { free_boolexp(b2); return TRUE_BOOLEXP; } b = b2; } } return b; } BOOLEXP *parse_boolexp(dbref player, const UTF8 *buf, bool internal) { if ( nullptr == buf || '\0' == buf[0]) { return TRUE_BOOLEXP; } size_t n = strlen(reinterpret_cast(buf)); if (n > sizeof(parsestore)-1) { // Truncate oversize keys. Always write an explicit NUL — memcpy of // n+1 from a longer source does not leave parsestore terminated and // the recursive-descent scanner would walk off the buffer. // n = sizeof(parsestore)-1; } memcpy(parsestore, buf, n); parsestore[n] = '\0'; parsebuf = parsestore; parse_player = player; s_parse_depth = 0; if (!mudstate.bStandAlone) { parsing_internal = internal; } return parse_boolexp_E(); }