Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
CVE-2026-2763-POC — Proof-of-concept exploit for CVE-2026-2763, a use-after-free in Mozilla's JavaScript engine, demonstrating a constrained 1-bit write primitive leading to out-of-bounds read/write. | Kitploit
Tools/GitHubGitHub/ppwwiinn/cve-2026-2763-poc
Memory ForensicsVulnerability AnalysisExploitationWeb SecurityBinary Exploitation
GitHubppwwiinn/cve-2026-2763-poc

CVE-2026-2763-POC

Proof-of-concept exploit for CVE-2026-2763, a use-after-free in Mozilla's JavaScript engine, demonstrating a constrained 1-bit write primitive leading to out-of-bounds read/write.

View Repository
2166 months agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

Vulnerability

The vulnerability arises from the implementation of the for-in syntax.

The scope of a for-in loop is represented by a pair of bytecodes: JSOP::Iter and JSOP::EndIter.

root@kitploit:~
CASE(Iter) {
      MOZ_ASSERT(REGS.stackDepth() >= 1);
      HandleValue val = REGS.stackHandleAt(-1);
      JSObject* iter = ValueToIterator(cx, val); // [1]
      if (!iter) {
        goto error;
      }
      REGS.sp[-1].setObject(*iter); // [2]
    }

...

CASE(EndIter) {
      MOZ_ASSERT(REGS.stackDepth() >= 2);
      CloseIterator(&REGS.sp[-2].toObject());
      REGS.sp -= 2; // [3]
    }

At scope entry, ValueToIterator is called at [1] to create a PropertyIteratorObject. The interpreter then stores (pushes) it on the interpreter stack at [2]. When the scope ends, the PropertyIteratorObject is removed at [3].

root@kitploit:~
static PropertyIteratorObject* GetIteratorImpl(JSContext* cx, HandleObject obj,
                                               bool wantIndices,
                                               bool forObjectKeys) {
...
  PropertyIteratorObject* iterobj = CreatePropertyIterator(
      cx, obj, keys, supportsIndices, indicesPtr, cacheableProtoChainLength,
      ownPropertyCount, forObjectKeys); // [1]
  if (!iterobj) {
    return nullptr;
  }
  if (!forObjectKeys) {
    RegisterEnumerator(cx, iterobj->getNativeIterator(), obj); // [2]
  }
...
  return iterobj;
}

static inline void RegisterEnumerator(JSContext* cx, NativeIterator* ni,
                                      HandleObject obj) {
  ni->initObjectBeingIterated(*obj);

  // Register non-escaping native enumerators (for-in) with the current
  // context.
  ni->link(cx->compartment()->enumeratorsAddr());

  MOZ_ASSERT(!ni->isActive());
  ni->markActive();
}

Inside ValueToIterator, object creation is performed as shown above. After allocating the iterator object at [1], it is linked into a global linked list at [2]. At this point, the only reference to the PropertyIteratorObject is held on the interpreter stack.

root@kitploit:~
void js::CloseIterator(JSObject* obj) {
  if (!obj->is<PropertyIteratorObject>()) {
    return;
  }

  // Remove iterator from the active list, which is a stack. The shared iterator
  // used for for-in with null/undefined is immutable and unlinked.

  NativeIterator* ni = obj->as<PropertyIteratorObject>().getNativeIterator();
  if (ni->isEmptyIteratorSingleton()) {
    return;
  }

  ni->unlink();

  MOZ_ASSERT(ni->isActive());
  ni->markInactive();

  ni->clearObjectBeingIterated();

  // Reset the enumerator; it may still be in the cached iterators for
  // this thread and can be reused.
  ni->resetPropertyCursorForReuse();
}

When the scope ends, CloseIterator is invoked and unlinks the iterator from the previously-linked list, preventing any access to the PropertyIteratorObject outside the scope.

However, because yield is not handled correctly, it is possible to execute code outside the loop scope before CloseIterator runs.

In this situation, since the only strong reference to the PropertyIteratorObject is on the interpreter stack, if yield is executed and the returned generator is also not referenced, then when GC runs the PropertyIteratorObject can be collected (freed).

root@kitploit:~
static bool SuppressDeletedPropertyHelper(JSContext* cx, HandleObject obj,
                                          Handle<JSLinearString*> str) {
  NativeIteratorListIter iter(obj->compartment()->enumeratorsAddr());
  while (!iter.done()) {
    NativeIterator* ni = iter.next();
    if (!SuppressDeletedProperty(cx, ni, obj, str)) { // [1]
      return false;
    }
  }

  return true;
}

This function is called when delete is used. It walks the global linked list and updates iterator objects to account for the deleted property. At [1], a callback can be invoked; if this triggers GC, an unreferenced PropertyIteratorObject can be freed. Its finalizer will also free the corresponding ni object, leading to a use-after-free (UAF).


Exploit

root@kitploit:~
static bool SuppressDeletedProperty(JSContext* cx, NativeIterator* ni,
                                    HandleObject obj,
                                    Handle<JSLinearString*> str) {
  ...
  // Check whether id is still to come.
  Rooted<JSLinearString*> idStr(cx);
  IteratorProperty* cursor = ni->nextProperty();
  for (; cursor < ni->propertiesEnd(); ++cursor) {
    idStr = cursor->asString();
    // Common case: both strings are atoms.
    if (idStr->isAtom() && str->isAtom()) {
      if (idStr != str) {
        continue;
      }
    } else {
      if (!EqualStrings(idStr, str)) {
        continue;
      }
    }
    ...
    cursor->markDeleted(); // [1]
    ni->markHasUnvisitedPropertyDeletion();
    return true;
  }

  return true;
}

A NativeIterator can have its size controlled via the number of properties on the target object, and cursor is a pointer to the property slot that will be marked as deleted. Since [1] effectively ORs a single bit, this yields a constrained primitive: a 1-bit write at an arbitrary (8-byte aligned) address.

NativeIterator is not allocated via the GC allocator; instead, it is allocated from js::MallocArena. As a result, you cannot proceed with a typical JS-object spray to place controlled GC-heap objects adjacent to it.

root@kitploit:~
js::UniquePtr<ImmutableScriptData> js::ImmutableScriptData::new_(
    FrontendContext* fc, uint32_t codeLength, uint32_t noteLength,
    uint32_t numResumeOffsets, uint32_t numScopeNotes, uint32_t numTryNotes) {
  auto size = sizeFor(codeLength, noteLength, numResumeOffsets, numScopeNotes,
                      numTryNotes);
  if (!size.isValid()) {
    ReportAllocationOverflow(fc);
    return nullptr;
  }

  // Allocate contiguous raw buffer.
  void* raw = fc->getAllocator()->pod_malloc<uint8_t>(size.value());
  MOZ_ASSERT(uintptr_t(raw) % alignof(ImmutableScriptData) == 0);
  if (!raw) {
    return nullptr;
  }

  // Constuct the ImmutableScriptData. Trailing arrays are uninitialized but
  // GCPtrs are put into a safe state.
  UniquePtr<ImmutableScriptData> result(new (raw) ImmutableScriptData(
      codeLength, noteLength, numResumeOffsets, numScopeNotes, numTryNotes));
  if (!result) {
    return nullptr;
  }

  // Sanity check
  MOZ_ASSERT(result->endOffset() == size.value());

  return result;
}

One useful object allocated in js::MallocArena is ImmutableScriptData, which stores JS bytecode.

root@kitploit:~
static MOZ_ALWAYS_INLINE void InitElemArrayOperation(JSContext* cx,
                                                     jsbytecode* pc,
                                                     Handle<ArrayObject*> arr,
                                                     HandleValue val) {
  MOZ_ASSERT(JSOp(*pc) == JSOp::InitElemArray);

  // The dense elements must have been initialized up to this index. The JIT
  // implementation also depends on this.
  uint32_t index = GET_UINT32(pc); // [1]
  MOZ_ASSERT(index < arr->getDenseCapacity());
  MOZ_ASSERT(index == arr->getDenseInitializedLength());

  // Bump the initialized length even for hole values to ensure the
  // index == initLength invariant holds for later InitElemArray ops.
  arr->setDenseInitializedLength(index + 1); // [2]

  if (val.isMagic(JS_ELEMENTS_HOLE)) {
    arr->initDenseElementHole(index);
  } else {
    arr->initDenseElement(index, val);
  }
}

This is the bytecode handler used when initializing a JS array.

It reads a 4-byte value from the bytecode stream and uses it to set the array’s InitializedLength at [2].

If the 1-bit write is applied to the bytecode at [1], it becomes possible to set InitializedLength beyond the array’s actual capacity. From there, the exploit proceeds by leveraging OOB read/write primitives.

Download Tool