/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- * vim: set ts=8 sts=2 et sw=2 tw=80: * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* * JS atom table. */ #include "vm/JSAtom-inl.h" #include "mozilla/ArrayUtils.h" #include "mozilla/EndianUtils.h" #include "mozilla/RangedPtr.h" #include "mozilla/Unused.h" #include #include #include "jstypes.h" #include "gc/GC.h" #include "gc/Marking.h" #include "gc/MaybeRooted.h" #include "js/CharacterEncoding.h" #include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* #include "js/Symbol.h" #include "util/Text.h" #include "vm/JSContext.h" #include "vm/SymbolType.h" #include "vm/Xdr.h" #include "gc/AtomMarking-inl.h" #include "vm/JSContext-inl.h" #include "vm/JSObject-inl.h" #include "vm/Realm-inl.h" #include "vm/StringType-inl.h" using namespace js; using mozilla::Maybe; using mozilla::Nothing; using mozilla::RangedPtr; template extern void InflateUTF8CharsToBufferAndTerminate(const InputCharsT src, CharT* dst, size_t dstLen, JS::SmallestEncoding encoding); template extern bool UTF8OrWTF8EqualsChars(const CharsT utf8, const CharT* chars); template extern bool GetUTF8AtomizationData(JSContext* cx, const InputCharsT utf8, size_t* outlen, JS::SmallestEncoding* encoding, HashNumber* hashNum); struct js::AtomHasher::Lookup { union { const JS::Latin1Char* latin1Chars; const char16_t* twoByteChars; LittleEndianChars littleEndianChars; const char* utf8Bytes; }; enum { TwoByteChar, LittleEndianTwoByte, Latin1, UTF8, WTF8 } type; size_t length; size_t byteLength; const JSAtom* atom; /* Optional. */ JS::AutoCheckCannotGC nogc; HashNumber hash; MOZ_ALWAYS_INLINE Lookup(const char* utf8Bytes, size_t byteLen, size_t length, HashNumber hash) : utf8Bytes(utf8Bytes), type(UTF8), length(length), byteLength(byteLen), atom(nullptr), hash(hash) {} MOZ_ALWAYS_INLINE Lookup(const char16_t* chars, size_t length) : twoByteChars(chars), type(TwoByteChar), length(length), atom(nullptr), hash(mozilla::HashString(chars, length)) {} MOZ_ALWAYS_INLINE Lookup(const JS::Latin1Char* chars, size_t length) : latin1Chars(chars), type(Latin1), length(length), atom(nullptr), hash(mozilla::HashString(chars, length)) {} MOZ_ALWAYS_INLINE Lookup(HashNumber hash, const char16_t* chars, size_t length) : twoByteChars(chars), type(TwoByteChar), length(length), atom(nullptr), hash(hash) { MOZ_ASSERT(hash == mozilla::HashString(chars, length)); } MOZ_ALWAYS_INLINE Lookup(HashNumber hash, const JS::Latin1Char* chars, size_t length) : latin1Chars(chars), type(Latin1), length(length), atom(nullptr), hash(hash) { MOZ_ASSERT(hash == mozilla::HashString(chars, length)); } inline explicit Lookup(const JSAtom* atom) : type(atom->hasLatin1Chars() ? Latin1 : TwoByteChar), length(atom->length()), atom(atom), hash(atom->hash()) { if (type == Latin1) { latin1Chars = atom->latin1Chars(nogc); MOZ_ASSERT(mozilla::HashString(latin1Chars, length) == hash); } else { MOZ_ASSERT(type == TwoByteChar); twoByteChars = atom->twoByteChars(nogc); MOZ_ASSERT(mozilla::HashString(twoByteChars, length) == hash); } } MOZ_ALWAYS_INLINE Lookup(LittleEndianChars chars, size_t length) : littleEndianChars(chars), type(LittleEndianTwoByte), length(length), atom(nullptr), hash(mozilla::HashStringKnownLength(chars, length)) {} }; inline HashNumber js::AtomHasher::hash(const Lookup& l) { return l.hash; } MOZ_ALWAYS_INLINE bool js::AtomHasher::match(const AtomStateEntry& entry, const Lookup& lookup) { JSAtom* key = entry.asPtrUnbarriered(); if (lookup.atom) { return lookup.atom == key; } if (key->length() != lookup.length || key->hash() != lookup.hash) { return false; } auto EqualsLittleEndianChars = [&lookup](auto keyChars) { for (size_t i = 0, len = lookup.length; i < len; i++) { if (keyChars[i] != lookup.littleEndianChars[i]) { return false; } } return true; }; if (key->hasLatin1Chars()) { const Latin1Char* keyChars = key->latin1Chars(lookup.nogc); switch (lookup.type) { case Lookup::Latin1: return mozilla::ArrayEqual(keyChars, lookup.latin1Chars, lookup.length); case Lookup::TwoByteChar: return EqualChars(keyChars, lookup.twoByteChars, lookup.length); case Lookup::LittleEndianTwoByte: return EqualsLittleEndianChars(keyChars); case Lookup::UTF8: { JS::UTF8Chars utf8(lookup.utf8Bytes, lookup.byteLength); return UTF8OrWTF8EqualsChars(utf8, keyChars); } case Lookup::WTF8: { JS::WTF8Chars wtf8(lookup.utf8Bytes, lookup.byteLength); return UTF8OrWTF8EqualsChars(wtf8, keyChars); } } } const char16_t* keyChars = key->twoByteChars(lookup.nogc); switch (lookup.type) { case Lookup::Latin1: return EqualChars(lookup.latin1Chars, keyChars, lookup.length); case Lookup::TwoByteChar: return mozilla::ArrayEqual(keyChars, lookup.twoByteChars, lookup.length); case Lookup::LittleEndianTwoByte: return EqualsLittleEndianChars(keyChars); case Lookup::UTF8: { JS::UTF8Chars utf8(lookup.utf8Bytes, lookup.byteLength); return UTF8OrWTF8EqualsChars(utf8, keyChars); } case Lookup::WTF8: { JS::WTF8Chars wtf8(lookup.utf8Bytes, lookup.byteLength); return UTF8OrWTF8EqualsChars(wtf8, keyChars); } } MOZ_ASSERT_UNREACHABLE("AtomHasher::match unknown type"); return false; } inline JSAtom* js::AtomStateEntry::asPtr(JSContext* cx) const { JSAtom* atom = asPtrUnbarriered(); if (!cx->isHelperThreadContext()) { gc::ReadBarrier(atom); } return atom; } UniqueChars js::AtomToPrintableString(JSContext* cx, JSAtom* atom) { return QuoteString(cx, atom); } #define DEFINE_PROTO_STRING(name, clasp) const char js_##name##_str[] = #name; JS_FOR_EACH_PROTOTYPE(DEFINE_PROTO_STRING) #undef DEFINE_PROTO_STRING #define CONST_CHAR_STR(idpart, id, text) const char js_##idpart##_str[] = text; FOR_EACH_COMMON_PROPERTYNAME(CONST_CHAR_STR) #undef CONST_CHAR_STR // Use a low initial capacity for the permanent atoms table to avoid penalizing // runtimes that create a small number of atoms. static const uint32_t JS_PERMANENT_ATOM_SIZE = 64; MOZ_ALWAYS_INLINE AtomSet::Ptr js::FrozenAtomSet::readonlyThreadsafeLookup( const AtomSet::Lookup& l) const { return mSet->readonlyThreadsafeLookup(l); } struct CommonNameInfo { const char* str; size_t length; }; bool JSRuntime::initializeAtoms(JSContext* cx) { MOZ_ASSERT(!atoms_); MOZ_ASSERT(!permanentAtomsDuringInit_); MOZ_ASSERT(!permanentAtoms_); if (parentRuntime) { permanentAtoms_ = parentRuntime->permanentAtoms_; staticStrings = parentRuntime->staticStrings; commonNames = parentRuntime->commonNames; emptyString = parentRuntime->emptyString; wellKnownSymbols = parentRuntime->wellKnownSymbols; atoms_ = js_new(); if (!atoms_) { return false; } return atoms_->init(); } permanentAtomsDuringInit_ = js_new(JS_PERMANENT_ATOM_SIZE); if (!permanentAtomsDuringInit_) { return false; } staticStrings = js_new(); if (!staticStrings || !staticStrings->init(cx)) { return false; } static const CommonNameInfo cachedNames[] = { #define COMMON_NAME_INFO(idpart, id, text) \ {js_##idpart##_str, sizeof(text) - 1}, FOR_EACH_COMMON_PROPERTYNAME(COMMON_NAME_INFO) #undef COMMON_NAME_INFO #define COMMON_NAME_INFO(name, clasp) {js_##name##_str, sizeof(#name) - 1}, JS_FOR_EACH_PROTOTYPE(COMMON_NAME_INFO) #undef COMMON_NAME_INFO #define COMMON_NAME_INFO(name) {#name, sizeof(#name) - 1}, JS_FOR_EACH_WELL_KNOWN_SYMBOL(COMMON_NAME_INFO) #undef COMMON_NAME_INFO #define COMMON_NAME_INFO(name) {"Symbol." #name, sizeof("Symbol." #name) - 1}, JS_FOR_EACH_WELL_KNOWN_SYMBOL(COMMON_NAME_INFO) #undef COMMON_NAME_INFO }; commonNames = js_new(); if (!commonNames) { return false; } ImmutablePropertyNamePtr* names = reinterpret_cast(commonNames.ref()); for (const auto& cachedName : cachedNames) { JSAtom* atom = Atomize(cx, cachedName.str, cachedName.length, PinAtom); if (!atom) { return false; } names->init(atom->asPropertyName()); names++; } MOZ_ASSERT(uintptr_t(names) == uintptr_t(commonNames + 1)); emptyString = commonNames->empty; // Create the well-known symbols. auto wks = js_new(); if (!wks) { return false; } // Prevent GC until we have fully initialized the well known symbols table. // Faster than zeroing the array and null checking during every GC. gc::AutoSuppressGC nogc(cx); ImmutablePropertyNamePtr* descriptions = commonNames->wellKnownSymbolDescriptions(); ImmutableSymbolPtr* symbols = reinterpret_cast(wks); for (size_t i = 0; i < JS::WellKnownSymbolLimit; i++) { HandlePropertyName description = descriptions[i]; JS::Symbol* symbol = JS::Symbol::new_(cx, JS::SymbolCode(i), description); if (!symbol) { ReportOutOfMemory(cx); return false; } symbols[i].init(symbol); } wellKnownSymbols = wks; return true; } void JSRuntime::finishAtoms() { js_delete(atoms_.ref()); if (!parentRuntime) { js_delete(permanentAtomsDuringInit_.ref()); js_delete(permanentAtoms_.ref()); js_delete(staticStrings.ref()); js_delete(commonNames.ref()); js_delete(wellKnownSymbols.ref()); } atoms_ = nullptr; permanentAtomsDuringInit_ = nullptr; permanentAtoms_ = nullptr; staticStrings = nullptr; commonNames = nullptr; wellKnownSymbols = nullptr; emptyString = nullptr; } class AtomsTable::AutoLock { Mutex* lock = nullptr; public: MOZ_ALWAYS_INLINE explicit AutoLock(JSRuntime* rt, Mutex& aLock) { if (rt->hasHelperThreadZones()) { lock = &aLock; lock->lock(); } } MOZ_ALWAYS_INLINE ~AutoLock() { if (lock) { lock->unlock(); } } }; AtomsTable::Partition::Partition(uint32_t index) : lock( MutexId{mutexid::AtomsTable.name, mutexid::AtomsTable.order + index}), atoms(InitialTableSize), atomsAddedWhileSweeping(nullptr) {} AtomsTable::Partition::~Partition() { MOZ_ASSERT(!atomsAddedWhileSweeping); } AtomsTable::~AtomsTable() { for (size_t i = 0; i < PartitionCount; i++) { js_delete(partitions[i]); } } bool AtomsTable::init() { for (size_t i = 0; i < PartitionCount; i++) { partitions[i] = js_new(i); if (!partitions[i]) { return false; } } return true; } void AtomsTable::lockAll() { MOZ_ASSERT(!allPartitionsLocked); for (size_t i = 0; i < PartitionCount; i++) { partitions[i]->lock.lock(); } #ifdef DEBUG allPartitionsLocked = true; #endif } void AtomsTable::unlockAll() { MOZ_ASSERT(allPartitionsLocked); for (size_t i = 0; i < PartitionCount; i++) { partitions[PartitionCount - i - 1]->lock.unlock(); } #ifdef DEBUG allPartitionsLocked = false; #endif } MOZ_ALWAYS_INLINE size_t AtomsTable::getPartitionIndex(const AtomHasher::Lookup& lookup) { size_t index = lookup.hash >> (32 - PartitionShift); MOZ_ASSERT(index < PartitionCount); return index; } inline void AtomsTable::tracePinnedAtomsInSet(JSTracer* trc, AtomSet& atoms) { for (auto r = atoms.all(); !r.empty(); r.popFront()) { const AtomStateEntry& entry = r.front(); MOZ_DIAGNOSTIC_ASSERT(entry.asPtrUnbarriered()); if (entry.isPinned()) { JSAtom* atom = entry.asPtrUnbarriered(); TraceRoot(trc, &atom, "interned_atom"); MOZ_ASSERT(entry.asPtrUnbarriered() == atom); } } } void AtomsTable::tracePinnedAtoms(JSTracer* trc, const AutoAccessAtomsZone& access) { for (size_t i = 0; i < PartitionCount; i++) { Partition& part = *partitions[i]; tracePinnedAtomsInSet(trc, part.atoms); if (part.atomsAddedWhileSweeping) { tracePinnedAtomsInSet(trc, *part.atomsAddedWhileSweeping); } } } void js::TraceAtoms(JSTracer* trc, const AutoAccessAtomsZone& access) { JSRuntime* rt = trc->runtime(); if (rt->permanentAtomsPopulated()) { rt->atoms().tracePinnedAtoms(trc, access); } } static void TracePermanentAtoms(JSTracer* trc, AtomSet::Range atoms) { for (; !atoms.empty(); atoms.popFront()) { const AtomStateEntry& entry = atoms.front(); JSAtom* atom = entry.asPtrUnbarriered(); MOZ_ASSERT(atom->isPermanentAtom()); TraceProcessGlobalRoot(trc, atom, "permanent atom"); } } void JSRuntime::tracePermanentAtoms(JSTracer* trc) { // Permanent atoms only need to be traced in the runtime which owns them. if (parentRuntime) { return; } // Static strings are not included in the permanent atoms table. if (staticStrings) { staticStrings->trace(trc); } if (permanentAtomsDuringInit_) { TracePermanentAtoms(trc, permanentAtomsDuringInit_->all()); } if (permanentAtoms_) { TracePermanentAtoms(trc, permanentAtoms_->all()); } } void js::TraceWellKnownSymbols(JSTracer* trc) { JSRuntime* rt = trc->runtime(); if (rt->parentRuntime) { return; } if (WellKnownSymbols* wks = rt->wellKnownSymbols) { for (size_t i = 0; i < JS::WellKnownSymbolLimit; i++) { TraceProcessGlobalRoot(trc, wks->get(i).get(), "well_known_symbol"); } } } void AtomsTable::traceWeak(JSTracer* trc) { JSRuntime* rt = trc->runtime(); for (size_t i = 0; i < PartitionCount; i++) { AutoLock lock(rt, partitions[i]->lock); AtomSet& atoms = partitions[i]->atoms; for (AtomSet::Enum e(atoms); !e.empty(); e.popFront()) { JSAtom* atom = e.front().asPtrUnbarriered(); MOZ_DIAGNOSTIC_ASSERT(atom); if (!TraceManuallyBarrieredWeakEdge(trc, &atom, "AtomsTable::partitions::atoms")) { e.removeFront(); } else { MOZ_ASSERT(atom == e.front().asPtrUnbarriered()); } } } } AtomsTable::SweepIterator::SweepIterator(AtomsTable& atoms) : atoms(atoms), partitionIndex(0) { startSweepingPartition(); settle(); } inline void AtomsTable::SweepIterator::startSweepingPartition() { MOZ_ASSERT(atoms.partitions[partitionIndex]->atomsAddedWhileSweeping); atomsIter.emplace(atoms.partitions[partitionIndex]->atoms); } inline void AtomsTable::SweepIterator::finishSweepingPartition() { atomsIter.reset(); atoms.mergeAtomsAddedWhileSweeping(*atoms.partitions[partitionIndex]); } inline void AtomsTable::SweepIterator::settle() { MOZ_ASSERT(!empty()); while (atomsIter->empty()) { finishSweepingPartition(); partitionIndex++; if (empty()) { return; } startSweepingPartition(); } } inline bool AtomsTable::SweepIterator::empty() const { return partitionIndex == PartitionCount; } inline AtomStateEntry AtomsTable::SweepIterator::front() const { MOZ_ASSERT(!empty()); return atomsIter->front(); } inline void AtomsTable::SweepIterator::removeFront() { MOZ_ASSERT(!empty()); return atomsIter->removeFront(); } inline void AtomsTable::SweepIterator::popFront() { MOZ_ASSERT(!empty()); atomsIter->popFront(); settle(); } bool AtomsTable::startIncrementalSweep() { MOZ_ASSERT(JS::RuntimeHeapIsCollecting()); bool ok = true; for (size_t i = 0; i < PartitionCount; i++) { auto& part = *partitions[i]; auto newAtoms = js_new(); if (!newAtoms) { ok = false; break; } MOZ_ASSERT(!part.atomsAddedWhileSweeping); part.atomsAddedWhileSweeping = newAtoms; } if (!ok) { for (size_t i = 0; i < PartitionCount; i++) { auto& part = *partitions[i]; js_delete(part.atomsAddedWhileSweeping); part.atomsAddedWhileSweeping = nullptr; } } return ok; } void AtomsTable::mergeAtomsAddedWhileSweeping(Partition& part) { // Add atoms that were added to the secondary table while we were sweeping // the main table. AutoEnterOOMUnsafeRegion oomUnsafe; auto newAtoms = part.atomsAddedWhileSweeping; part.atomsAddedWhileSweeping = nullptr; for (auto r = newAtoms->all(); !r.empty(); r.popFront()) { if (!part.atoms.putNew(AtomHasher::Lookup(r.front().asPtrUnbarriered()), r.front())) { oomUnsafe.crash("Adding atom from secondary table after sweep"); } } js_delete(newAtoms); } bool AtomsTable::sweepIncrementally(SweepIterator& atomsToSweep, SliceBudget& budget) { // Sweep the table incrementally until we run out of work or budget. while (!atomsToSweep.empty()) { budget.step(); if (budget.isOverBudget()) { return false; } AtomStateEntry entry = atomsToSweep.front(); JSAtom* atom = entry.asPtrUnbarriered(); MOZ_DIAGNOSTIC_ASSERT(atom); if (IsAboutToBeFinalizedUnbarriered(&atom)) { MOZ_ASSERT(!entry.isPinned()); atomsToSweep.removeFront(); } else { MOZ_ASSERT(atom == entry.asPtrUnbarriered()); } atomsToSweep.popFront(); } for (size_t i = 0; i < PartitionCount; i++) { MOZ_ASSERT(!partitions[i]->atomsAddedWhileSweeping); } return true; } size_t AtomsTable::sizeOfIncludingThis( mozilla::MallocSizeOf mallocSizeOf) const { size_t size = sizeof(AtomsTable); for (size_t i = 0; i < PartitionCount; i++) { size += sizeof(Partition); size += partitions[i]->atoms.shallowSizeOfExcludingThis(mallocSizeOf); } return size; } bool JSRuntime::initMainAtomsTables(JSContext* cx) { MOZ_ASSERT(!parentRuntime); MOZ_ASSERT(!permanentAtomsPopulated()); // The permanent atoms table has now been populated. permanentAtoms_ = js_new(permanentAtomsDuringInit_); // Takes ownership. permanentAtomsDuringInit_ = nullptr; // Initialize the main atoms table. MOZ_ASSERT(!atoms_); atoms_ = js_new(); return atoms_ && atoms_->init(); } template static MOZ_ALWAYS_INLINE JSAtom* AtomizeAndCopyCharsFromLookup( JSContext* cx, Chars chars, size_t length, const AtomHasher::Lookup& lookup, PinningBehavior pin, const Maybe& indexValue); template >> static MOZ_ALWAYS_INLINE JSAtom* AtomizeAndCopyCharsFromLookup( JSContext* cx, CharT* chars, size_t length, const AtomHasher::Lookup& lookup, PinningBehavior pin, const Maybe& indexValue) { return AtomizeAndCopyCharsFromLookup(cx, const_cast(chars), length, lookup, pin, indexValue); } template static MOZ_NEVER_INLINE JSAtom* PermanentlyAtomizeAndCopyChars( JSContext* cx, Maybe& zonePtr, Chars chars, size_t length, const Maybe& indexValue, const AtomHasher::Lookup& lookup); template >> static JSAtom* PermanentlyAtomizeAndCopyChars( JSContext* cx, Maybe& zonePtr, CharT* chars, size_t length, const Maybe& indexValue, const AtomHasher::Lookup& lookup) { return PermanentlyAtomizeAndCopyChars( cx, zonePtr, const_cast(chars), length, indexValue, lookup); } template static MOZ_ALWAYS_INLINE JSAtom* AtomizeAndCopyCharsFromLookup( JSContext* cx, Chars chars, size_t length, const AtomHasher::Lookup& lookup, PinningBehavior pin, const Maybe& indexValue) { // Try the per-Zone cache first. If we find the atom there we can avoid the // atoms lock, the markAtom call, and the multiple HashSet lookups below. // We don't use the per-Zone cache if we want a pinned atom: handling that // is more complicated and pinning atoms is relatively uncommon. Zone* zone = cx->zone(); Maybe zonePtr; if (MOZ_LIKELY(zone && pin == DoNotPinAtom)) { zonePtr.emplace(zone->atomCache().lookupForAdd(lookup)); if (zonePtr.ref()) { // The cache is purged on GC so if we're in the middle of an // incremental GC we should have barriered the atom when we put // it in the cache. JSAtom* atom = zonePtr.ref()->asPtrUnbarriered(); MOZ_ASSERT(AtomIsMarked(zone, atom)); return atom; } } // This function can be called during initialization, while the permanent // atoms table is being created. In this case all atoms created are added to // the permanent atoms table. if (!cx->permanentAtomsPopulated()) { return PermanentlyAtomizeAndCopyChars(cx, zonePtr, chars, length, indexValue, lookup); } AtomSet::Ptr pp = cx->permanentAtoms().readonlyThreadsafeLookup(lookup); if (pp) { JSAtom* atom = pp->asPtr(cx); if (zonePtr && MOZ_UNLIKELY(!zone->atomCache().add( *zonePtr, AtomStateEntry(atom, false)))) { ReportOutOfMemory(cx); return nullptr; } return atom; } // Validate the length before taking an atoms partition lock, as throwing an // exception here may reenter this code. if (MOZ_UNLIKELY(!JSString::validateLength(cx, length))) { return nullptr; } JSAtom* atom = cx->atoms().atomizeAndCopyChars(cx, chars, length, pin, indexValue, lookup); if (!atom) { return nullptr; } if (MOZ_UNLIKELY(!cx->atomMarking().inlinedMarkAtomFallible(cx, atom))) { ReportOutOfMemory(cx); return nullptr; } if (zonePtr && MOZ_UNLIKELY(!zone->atomCache().add( *zonePtr, AtomStateEntry(atom, false)))) { ReportOutOfMemory(cx); return nullptr; } return atom; } template static MOZ_ALWAYS_INLINE JSAtom* AllocateNewAtom( JSContext* cx, Chars chars, size_t length, const Maybe& indexValue, const AtomHasher::Lookup& lookup); template >> static MOZ_ALWAYS_INLINE JSAtom* AllocateNewAtom( JSContext* cx, CharT* chars, size_t length, const Maybe& indexValue, const AtomHasher::Lookup& lookup) { return AllocateNewAtom(cx, const_cast(chars), length, indexValue, lookup); } template MOZ_ALWAYS_INLINE JSAtom* AtomsTable::atomizeAndCopyChars( JSContext* cx, Chars chars, size_t length, PinningBehavior pin, const Maybe& indexValue, const AtomHasher::Lookup& lookup) { Partition& part = *partitions[getPartitionIndex(lookup)]; AutoLock lock(cx->runtime(), part.lock); AtomSet& atoms = part.atoms; AtomSet* atomsAddedWhileSweeping = part.atomsAddedWhileSweeping; AtomSet::AddPtr p; if (!atomsAddedWhileSweeping) { p = atoms.lookupForAdd(lookup); } else { // We're currently sweeping the main atoms table and all new atoms will // be added to a secondary table. Check this first. p = atomsAddedWhileSweeping->lookupForAdd(lookup); // If that fails check the main table but check if any atom found there // is dead. if (!p) { if (AtomSet::AddPtr p2 = atoms.lookupForAdd(lookup)) { JSAtom* atom = p2->asPtrUnbarriered(); if (!IsAboutToBeFinalizedUnbarriered(&atom)) { p = p2; } } } } if (p) { JSAtom* atom = p->asPtr(cx); if (pin && !p->isPinned()) { p->setPinned(true); } return atom; } JSAtom* atom = AllocateNewAtom(cx, chars, length, indexValue, lookup); if (!atom) { return nullptr; } // We have held the lock since looking up p, and the operations we've done // since then can't GC; therefore the atoms table has not been modified and // p is still valid. AtomSet* addSet = part.atomsAddedWhileSweeping ? part.atomsAddedWhileSweeping : &atoms; if (MOZ_UNLIKELY(!addSet->add(p, AtomStateEntry(atom, bool(pin))))) { ReportOutOfMemory(cx); /* SystemAllocPolicy does not report OOM. */ return nullptr; } return atom; } /* |chars| must not point into an inline or short string. */ template static MOZ_ALWAYS_INLINE JSAtom* AtomizeAndCopyChars( JSContext* cx, const CharT* chars, size_t length, PinningBehavior pin, const Maybe& indexValue) { if (JSAtom* s = cx->staticStrings().lookup(chars, length)) { return s; } AtomHasher::Lookup lookup(chars, length); return AtomizeAndCopyCharsFromLookup(cx, chars, length, lookup, pin, indexValue); } template static MOZ_NEVER_INLINE JSAtom* PermanentlyAtomizeAndCopyChars( JSContext* cx, Maybe& zonePtr, Chars chars, size_t length, const Maybe& indexValue, const AtomHasher::Lookup& lookup) { MOZ_ASSERT(!cx->permanentAtomsPopulated()); MOZ_ASSERT(CurrentThreadCanAccessRuntime(cx->runtime())); JSRuntime* rt = cx->runtime(); AtomSet& atoms = *rt->permanentAtomsDuringInit(); AtomSet::AddPtr p = atoms.lookupForAdd(lookup); if (p) { return p->asPtr(cx); } JSAtom* atom = AllocateNewAtom(cx, chars, length, indexValue, lookup); if (!atom) { return nullptr; } atom->morphIntoPermanentAtom(); // We are single threaded at this point, and the operations we've done since // then can't GC; therefore the atoms table has not been modified and p is // still valid. if (!atoms.add(p, AtomStateEntry(atom, true))) { ReportOutOfMemory(cx); /* SystemAllocPolicy does not report OOM. */ return nullptr; } if (zonePtr && MOZ_UNLIKELY(!cx->zone()->atomCache().add( *zonePtr, AtomStateEntry(atom, false)))) { ReportOutOfMemory(cx); return nullptr; } return atom; } template struct AtomizeUTF8OrWTF8CharsWrapper { CharsT utf8; JS::SmallestEncoding encoding; AtomizeUTF8OrWTF8CharsWrapper(const CharsT& chars, JS::SmallestEncoding minEncode) : utf8(chars), encoding(minEncode) {} }; // MakeLinearStringForAtomization has 4 variants. // This is used by Latin1Char and char16_t. template static MOZ_ALWAYS_INLINE JSLinearString* MakeLinearStringForAtomization( JSContext* cx, const CharT* chars, size_t length) { return NewStringCopyN(cx, chars, length, gc::TenuredHeap); } // MakeLinearStringForAtomization has one further variant -- a non-template // overload accepting LittleEndianChars. static MOZ_ALWAYS_INLINE JSLinearString* MakeLinearStringForAtomization( JSContext* cx, LittleEndianChars chars, size_t length) { return NewStringFromLittleEndianNoGC(cx, chars, length, gc::TenuredHeap); } template static MOZ_ALWAYS_INLINE JSLinearString* MakeUTF8AtomHelper( JSContext* cx, const WrapperT* chars, size_t length) { if (JSInlineString::lengthFits(length)) { CharT* storage; JSInlineString* str = AllocateInlineString(cx, length, &storage, gc::TenuredHeap); if (!str) { return nullptr; } InflateUTF8CharsToBufferAndTerminate(chars->utf8, storage, length, chars->encoding); return str; } // MakeAtomUTF8Helper is called from deep in the Atomization path, which // expects functions to fail gracefully with nullptr on OOM, without throwing. // // Flat strings are null-terminated. Leave room with length + 1 UniquePtr newStr( js_pod_arena_malloc(js::StringBufferArena, length + 1)); if (!newStr) { return nullptr; } InflateUTF8CharsToBufferAndTerminate(chars->utf8, newStr.get(), length, chars->encoding); return JSLinearString::new_(cx, std::move(newStr), length, gc::TenuredHeap); } // Another 2 variants of MakeLinearStringForAtomization. // This is used by AtomizeUTF8OrWTF8CharsWrapper with UTF8Chars or WTF8Chars. template /* static */ MOZ_ALWAYS_INLINE JSLinearString* MakeLinearStringForAtomization( JSContext* cx, const AtomizeUTF8OrWTF8CharsWrapper* chars, size_t length) { if (length == 0) { return cx->emptyString(); } if (chars->encoding == JS::SmallestEncoding::UTF16) { return MakeUTF8AtomHelper(cx, chars, length); } return MakeUTF8AtomHelper(cx, chars, length); } template static MOZ_ALWAYS_INLINE JSAtom* AllocateNewAtom( JSContext* cx, Chars chars, size_t length, const Maybe& indexValue, const AtomHasher::Lookup& lookup) { AutoAllocInAtomsZone ac(cx); JSLinearString* linear = MakeLinearStringForAtomization(cx, chars, length); if (!linear) { // Grudgingly forgo last-ditch GC. The alternative would be to release // the lock, manually GC here, and retry from the top. ReportOutOfMemory(cx); return nullptr; } JSAtom* atom = linear->morphAtomizedStringIntoAtom(lookup.hash); MOZ_ASSERT(atom->hash() == lookup.hash); if (indexValue) { atom->maybeInitializeIndex(*indexValue, true); } return atom; } JSAtom* js::AtomizeString(JSContext* cx, JSString* str, js::PinningBehavior pin /* = js::DoNotPinAtom */) { if (str->isAtom()) { JSAtom& atom = str->asAtom(); /* N.B. static atoms are effectively always interned. */ if (pin == PinAtom && !atom.isPermanentAtom()) { cx->runtime()->atoms().maybePinExistingAtom(cx, &atom); } return &atom; } JSLinearString* linear = str->ensureLinear(cx); if (!linear) { return nullptr; } if (cx->isMainThreadContext() && pin == DoNotPinAtom) { if (JSAtom* atom = cx->caches().stringToAtomCache.lookup(linear)) { return atom; } } Maybe indexValue; if (str->hasIndexValue()) { indexValue.emplace(str->getIndexValue()); } JS::AutoCheckCannotGC nogc; JSAtom* atom = linear->hasLatin1Chars() ? AtomizeAndCopyChars(cx, linear->latin1Chars(nogc), linear->length(), pin, indexValue) : AtomizeAndCopyChars(cx, linear->twoByteChars(nogc), linear->length(), pin, indexValue); if (!atom) { return nullptr; } if (cx->isMainThreadContext() && pin == DoNotPinAtom) { cx->caches().stringToAtomCache.maybePut(linear, atom); } return atom; } bool js::AtomIsPinned(JSContext* cx, JSAtom* atom) { JSRuntime* rt = cx->runtime(); return rt->atoms().atomIsPinned(rt, atom); } bool AtomsTable::atomIsPinned(JSRuntime* rt, JSAtom* atom) { MOZ_ASSERT(atom); if (atom->isPermanentAtom()) { return true; } AtomHasher::Lookup lookup(atom); AtomsTable::Partition& part = *partitions[getPartitionIndex(lookup)]; AtomsTable::AutoLock lock(rt, part.lock); AtomSet::Ptr p = part.atoms.lookup(lookup); if (!p && part.atomsAddedWhileSweeping) { p = part.atomsAddedWhileSweeping->lookup(lookup); } MOZ_ASSERT(p); // Non-permanent atoms must exist in atoms table. MOZ_ASSERT(p->asPtrUnbarriered() == atom); return p->isPinned(); } void AtomsTable::maybePinExistingAtom(JSContext* cx, JSAtom* atom) { MOZ_ASSERT(atom); MOZ_ASSERT(!atom->isPermanentAtom()); AtomHasher::Lookup lookup(atom); AtomsTable::Partition& part = *partitions[getPartitionIndex(lookup)]; AtomsTable::AutoLock lock(cx->runtime(), part.lock); AtomSet::Ptr p = part.atoms.lookup(lookup); if (!p && part.atomsAddedWhileSweeping) { p = part.atomsAddedWhileSweeping->lookup(lookup); } MOZ_ASSERT(p); // Non-permanent atoms must exist in atoms table. MOZ_ASSERT(p->asPtrUnbarriered() == atom); p->setPinned(true); } JSAtom* js::Atomize(JSContext* cx, const char* bytes, size_t length, PinningBehavior pin, const Maybe& indexValue) { const Latin1Char* chars = reinterpret_cast(bytes); return AtomizeAndCopyChars(cx, chars, length, pin, indexValue); } template JSAtom* js::AtomizeChars(JSContext* cx, const CharT* chars, size_t length, PinningBehavior pin) { return AtomizeAndCopyChars(cx, chars, length, pin, Nothing()); } template JSAtom* js::AtomizeChars(JSContext* cx, const Latin1Char* chars, size_t length, PinningBehavior pin); template JSAtom* js::AtomizeChars(JSContext* cx, const char16_t* chars, size_t length, PinningBehavior pin); /* |chars| must not point into an inline or short string. */ template JSAtom* js::AtomizeChars(JSContext* cx, HashNumber hash, const CharT* chars, size_t length) { if (JSAtom* s = cx->staticStrings().lookup(chars, length)) { return s; } AtomHasher::Lookup lookup(hash, chars, length); return AtomizeAndCopyCharsFromLookup( cx, chars, length, lookup, PinningBehavior::DoNotPinAtom, Nothing()); } template JSAtom* js::AtomizeChars(JSContext* cx, HashNumber hash, const Latin1Char* chars, size_t length); template JSAtom* js::AtomizeChars(JSContext* cx, HashNumber hash, const char16_t* chars, size_t length); template JSAtom* AtomizeUTF8OrWTF8Chars(JSContext* cx, const char* utf8Chars, size_t utf8ByteLength) { { // Permanent atoms,|JSRuntime::atoms_|, and static strings are disjoint // sets. |AtomizeAndCopyCharsFromLookup| only consults the first two sets, // so we must map any static strings ourselves. See bug 1575947. StaticStrings& statics = cx->staticStrings(); // Handle all pure-ASCII UTF-8 static strings. if (JSAtom* s = statics.lookup(utf8Chars, utf8ByteLength)) { return s; } // The only non-ASCII static strings are the single-code point strings // U+0080 through U+00FF, encoded as // // 0b1100'00xx 0b10xx'xxxx // // where the encoded code point is the concatenation of the 'x' bits -- and // where the highest 'x' bit is necessarily 1 (because U+0080 through U+00FF // all contain an 0x80 bit). if (utf8ByteLength == 2) { auto first = static_cast(utf8Chars[0]); if ((first & 0b1111'1110) == 0b1100'0010) { auto second = static_cast(utf8Chars[1]); if (mozilla::IsTrailingUnit(mozilla::Utf8Unit(second))) { uint8_t unit = static_cast(first << 6) | (second & 0b0011'1111); MOZ_ASSERT(StaticStrings::hasUnit(unit)); return statics.getUnit(unit); } } // Fallthrough code handles the cases where the two units aren't a Latin-1 // code point or are invalid. } } size_t length; HashNumber hash; JS::SmallestEncoding forCopy; CharsT utf8(utf8Chars, utf8ByteLength); if (!GetUTF8AtomizationData(cx, utf8, &length, &forCopy, &hash)) { return nullptr; } AtomizeUTF8OrWTF8CharsWrapper chars(utf8, forCopy); AtomHasher::Lookup lookup(utf8Chars, utf8ByteLength, length, hash); if (std::is_same_v) { lookup.type = AtomHasher::Lookup::WTF8; } return AtomizeAndCopyCharsFromLookup(cx, &chars, length, lookup, DoNotPinAtom, Nothing()); } JSAtom* js::AtomizeUTF8Chars(JSContext* cx, const char* utf8Chars, size_t utf8ByteLength) { return AtomizeUTF8OrWTF8Chars(cx, utf8Chars, utf8ByteLength); } JSAtom* js::AtomizeWTF8Chars(JSContext* cx, const char* wtf8Chars, size_t wtf8ByteLength) { return AtomizeUTF8OrWTF8Chars(cx, wtf8Chars, wtf8ByteLength); } bool js::IndexToIdSlow(JSContext* cx, uint32_t index, MutableHandleId idp) { MOZ_ASSERT(index > JSID_INT_MAX); char16_t buf[UINT32_CHAR_BUFFER_LENGTH]; RangedPtr end(std::end(buf), buf, std::end(buf)); RangedPtr start = BackfillIndexInCharBuffer(index, end); JSAtom* atom = AtomizeChars(cx, start.get(), end - start); if (!atom) { return false; } idp.set(JS::PropertyKey::fromNonIntAtom(atom)); return true; } template static JSAtom* ToAtomSlow( JSContext* cx, typename MaybeRooted::HandleType arg) { MOZ_ASSERT(!arg.isString()); Value v = arg; if (!v.isPrimitive()) { MOZ_ASSERT(!cx->isHelperThreadContext()); if (!allowGC) { return nullptr; } RootedValue v2(cx, v); if (!ToPrimitive(cx, JSTYPE_STRING, &v2)) { return nullptr; } v = v2; } if (v.isString()) { JSAtom* atom = AtomizeString(cx, v.toString()); if (!allowGC && !atom) { cx->recoverFromOutOfMemory(); } return atom; } if (v.isInt32()) { JSAtom* atom = Int32ToAtom(cx, v.toInt32()); if (!allowGC && !atom) { cx->recoverFromOutOfMemory(); } return atom; } if (v.isDouble()) { JSAtom* atom = NumberToAtom(cx, v.toDouble()); if (!allowGC && !atom) { cx->recoverFromOutOfMemory(); } return atom; } if (v.isBoolean()) { return v.toBoolean() ? cx->names().true_ : cx->names().false_; } if (v.isNull()) { return cx->names().null; } if (v.isSymbol()) { MOZ_ASSERT(!cx->isHelperThreadContext()); if (allowGC) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_SYMBOL_TO_STRING); } return nullptr; } if (v.isBigInt()) { RootedBigInt i(cx, v.toBigInt()); JSAtom* atom = BigIntToAtom(cx, i); if (!allowGC && !atom) { cx->recoverFromOutOfMemory(); } return atom; } MOZ_ASSERT(v.isUndefined()); return cx->names().undefined; } template JSAtom* js::ToAtom(JSContext* cx, typename MaybeRooted::HandleType v) { if (!v.isString()) { return ToAtomSlow(cx, v); } JSString* str = v.toString(); if (str->isAtom()) { return &str->asAtom(); } JSAtom* atom = AtomizeString(cx, str); if (!atom && !allowGC) { MOZ_ASSERT_IF(!cx->isHelperThreadContext(), cx->isThrowingOutOfMemory()); cx->recoverFromOutOfMemory(); } return atom; } template JSAtom* js::ToAtom(JSContext* cx, HandleValue v); template JSAtom* js::ToAtom(JSContext* cx, const Value& v); static JSAtom* AtomizeLittleEndianTwoByteChars(JSContext* cx, const uint8_t* leTwoByte, size_t length) { LittleEndianChars chars(leTwoByte); if (JSAtom* s = cx->staticStrings().lookup(chars, length)) { return s; } AtomHasher::Lookup lookup(chars, length); return AtomizeAndCopyCharsFromLookup(cx, chars, length, lookup, DoNotPinAtom, Nothing()); } template XDRResult js::XDRAtomOrNull(XDRState* xdr, MutableHandleAtom atomp) { uint8_t isNull = false; if (mode == XDR_ENCODE) { if (!atomp) { isNull = true; } } MOZ_TRY(xdr->codeUint8(&isNull)); if (!isNull) { MOZ_TRY(XDRAtom(xdr, atomp)); } else if (mode == XDR_DECODE) { atomp.set(nullptr); } return Ok(); } template XDRResult js::XDRAtomOrNull(XDRState* xdr, MutableHandleAtom atomp); template XDRResult js::XDRAtomOrNull(XDRState* xdr, MutableHandleAtom atomp); template static XDRResult XDRAtomIndex(XDRState* xdr, uint32_t* index) { return xdr->codeUint32(index); } template XDRResult js::XDRAtom(XDRState* xdr, MutableHandleAtom atomp) { if (!xdr->hasAtomMap() && !xdr->hasAtomTable()) { return XDRAtomData(xdr, atomp); } if (mode == XDR_ENCODE) { MOZ_ASSERT(xdr->hasAtomMap()); // Atom contents are encoded in a separate buffer, which is joined to the // final result in XDRIncrementalEncoder::linearize. References to atoms // are encoded as indices into the atom stream. uint32_t atomIndex; XDRAtomMap::AddPtr p = xdr->atomMap().lookupForAdd(atomp.get()); if (p) { atomIndex = p->value(); } else { xdr->switchToAtomBuf(); MOZ_TRY(XDRAtomData(xdr, atomp)); xdr->switchToMainBuf(); atomIndex = xdr->natoms(); xdr->natoms() += 1; if (!xdr->atomMap().add(p, atomp.get(), atomIndex)) { return xdr->fail(JS::TranscodeResult_Throw); } } MOZ_TRY(XDRAtomIndex(xdr, &atomIndex)); return Ok(); } MOZ_ASSERT(mode == XDR_DECODE && xdr->hasAtomTable()); uint32_t atomIndex; MOZ_TRY(XDRAtomIndex(xdr, &atomIndex)); if (atomIndex >= xdr->atomTable().length()) { return xdr->fail(JS::TranscodeResult_Failure_BadDecode); } JSAtom* atom = xdr->atomTable()[atomIndex]; atomp.set(atom); return Ok(); } template XDRResult js::XDRAtom(XDRState* xdr, MutableHandleAtom atomp); template XDRResult js::XDRAtom(XDRState* xdr, MutableHandleAtom atomp); template XDRResult js::XDRAtomData(XDRState* xdr, MutableHandleAtom atomp) { bool latin1 = false; uint32_t length = 0; uint32_t lengthAndEncoding = 0; if (mode == XDR_ENCODE) { JS::AutoCheckCannotGC nogc; static_assert(JSString::MAX_LENGTH <= INT32_MAX, "String length must fit in 31 bits"); latin1 = atomp->hasLatin1Chars(); length = atomp->length(); lengthAndEncoding = (length << 1) | uint32_t(latin1); MOZ_TRY(xdr->codeUint32(&lengthAndEncoding)); if (latin1) { return xdr->codeChars( const_cast(atomp->latin1Chars(nogc)), length); } return xdr->codeChars(const_cast(atomp->twoByteChars(nogc)), length); } MOZ_ASSERT(mode == XDR_DECODE); /* Avoid JSString allocation for already existing atoms. See bug 321985. */ JSContext* cx = xdr->cx(); JSAtom* atom = nullptr; MOZ_TRY(xdr->codeUint32(&lengthAndEncoding)); length = lengthAndEncoding >> 1; latin1 = lengthAndEncoding & 0x1; if (latin1) { const Latin1Char* chars = nullptr; if (length) { const uint8_t* ptr; size_t nbyte = length * sizeof(Latin1Char); MOZ_TRY(xdr->readData(&ptr, nbyte)); chars = reinterpret_cast(ptr); } atom = AtomizeChars(cx, chars, length); } else { const uint8_t* twoByteCharsLE = nullptr; if (length) { size_t nbyte = length * sizeof(char16_t); MOZ_TRY(xdr->readData(&twoByteCharsLE, nbyte)); } atom = AtomizeLittleEndianTwoByteChars(cx, twoByteCharsLE, length); } if (!atom) { return xdr->fail(JS::TranscodeResult_Throw); } atomp.set(atom); return Ok(); } template XDRResult js::XDRAtomData(XDRState* xdr, MutableHandleAtom atomp); template XDRResult js::XDRAtomData(XDRState* xdr, MutableHandleAtom atomp); Handle js::ClassName(JSProtoKey key, JSContext* cx) { return ClassName(key, cx->names()); } js::AutoLockAllAtoms::AutoLockAllAtoms(JSRuntime* rt) : runtime(rt) { MOZ_ASSERT(CurrentThreadCanAccessRuntime(runtime)); if (runtime->hasHelperThreadZones()) { runtime->atoms().lockAll(); } } js::AutoLockAllAtoms::~AutoLockAllAtoms() { MOZ_ASSERT(CurrentThreadCanAccessRuntime(runtime)); if (runtime->hasHelperThreadZones()) { runtime->atoms().unlockAll(); } }