Skip to content

v8-exploitation: builder, tests, some solves - #81

Open
zardus wants to merge 23 commits into
mainfrom
wip/v8
Open

v8-exploitation: builder, tests, some solves#81
zardus wants to merge 23 commits into
mainfrom
wip/v8

Conversation

@zardus

@zardus zardus commented Feb 2, 2026

Copy link
Copy Markdown
Member

This is just porting to this repo. All credit goes to @sinamhdv.

Copilot AI review requested due to automatic review settings February 2, 2026 07:22
@github-actions github-actions Bot added the dangerous Changes critical or security-sensitive files label Feb 2, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6bb008acfe

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

exit 1
fi

cp "$filename" "$tempfile"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Prevent TOCTOU file swap when copying user script

Because /challenge/run is installed setuid-root by common/.init, the -r check happens under the real UID but the subsequent cp runs with effective root. A user can race by swapping a symlink after the readability check and before this cp, causing root to copy a protected file (e.g., /flag) into the temp file that is then chmod 644 and readable in /tmp before removal. This bypasses the intended exploit path and leaks the flag; consider opening the file once (e.g., exec <"$filename" with O_NOFOLLOW) and copying from that fd, or dropping privileges before the copy.

Useful? React with 👍 / 👎.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request ports V8 exploitation challenges from a previous repository to this one, creating a module with 9 progressive V8 JavaScript engine exploitation challenges. The challenges are designed to teach browser exploitation by introducing intentional vulnerabilities through patches to the V8 engine.

Changes:

  • Added module configuration with 9 levels of V8 exploitation challenges
  • Created common infrastructure (Dockerfile, build scripts, catflag utility, run wrapper)
  • Added 9 challenge levels, each with custom V8 patches introducing exploitable vulnerabilities

Reviewed changes

Copilot reviewed 81 out of 83 changed files in this pull request and generated 18 comments.

Show a summary per file
File Description
challenges/v8-exploitation/module.yml Module configuration defining 9 challenge levels with visibility settings and reference materials
challenges/v8-exploitation/common/* Shared infrastructure including V8 build configuration, Docker setup, and execution wrapper
challenges/v8-exploitation/level-1/* Challenge with ArrayRun builtin that executes doubles as shellcode
challenges/v8-exploitation/level-2/* Challenge with arbitrary read/write primitives exposed via GetAddressOf, ArbRead32, ArbWrite32
challenges/v8-exploitation/level-3/* Challenge with GetAddressOf and GetFakeObject primitives for heap manipulation
challenges/v8-exploitation/level-4/* Challenge with ArrayPrototypeSetLength builtin for array length manipulation
challenges/v8-exploitation/level-5/* Challenge with ArrayOffByOne builtin for out-of-bounds access
challenges/v8-exploitation/level-6/* Challenge with ArrayFunctionMap builtin vulnerable to side effects during iteration
challenges/v8-exploitation/level-7/* Challenge with disabled CheckMaps deoptimization for type confusion
challenges/v8-exploitation/level-8/* Challenge with weakened bounds check optimization
challenges/v8-exploitation/level-9/* Challenge with V8 sandbox enabled and memory corruption API exposed

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +82 to +83
+ case Builtin::kArrayRun:
+ return Type::Receiver();

Copilot AI Feb 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar indentation inconsistency with tab-prefixed case statement.

Suggested change
+ case Builtin::kArrayRun:
+ return Type::Receiver();
+ case Builtin::kArrayRun:
+ return Type::Receiver();

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +3
#!/bin/bash -ex
echo 'console.log("ok");' > /tmp/test.js
/challenge/d8 /tmp/test.js 2>&1 | grep -q ok

Copilot AI Feb 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only level-1 has private tests (tests_private directory), while levels 2-9 are missing private tests entirely. Based on the repository pattern where other challenges have both public and private tests, this appears to be incomplete. Private tests typically verify that the challenges can actually be solved and that the exploits work as intended.

Copilot uses AI. Check for mistakes.
Author: [sinamhdv](https://github.com/sinamhdv)

visibility:
start: "2024-08-01T17:00:00-07:00"

Copilot AI Feb 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The visibility start date is set to "2024-08-01T17:00:00-07:00", which is in the past. The current date is February 2, 2026. This appears to be intentional since this is described as "porting" content from the "2024 fall Quarterly Quiz", but it's worth confirming this is the intended start date for this repository.

Copilot uses AI. Check for mistakes.
Comment thread challenges/v8-exploitation/module.yml Outdated
@@ -0,0 +1,51 @@
name: V8 Exploitation
description: |
Welcome to the 2024 fall Quarterly Quiz, and to the world of browser exploitation! In this set of challenges, you can explore Chromium's Javascript engine, V8.

Copilot AI Feb 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The module description references "2024 fall Quarterly Quiz" but this content is being ported in 2026. Consider updating the description to clarify whether this is archived content or if it should reference the current timeframe.

Copilot uses AI. Check for mistakes.
Comment on lines +8 to +9
"src/sandbox/external-pointer-table.h",
+ "src/sandbox/testing.cc",

Copilot AI Feb 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is inconsistent indentation in the patch. Lines 8-9 use tabs while most other lines use spaces. While this is within a patch file (which may preserve the original V8 codebase's formatting), it's worth noting for consistency.

Copilot uses AI. Check for mistakes.
Comment on lines +142 to +307
+ }
+
+ // HeapObjects must be allocated inside the pointer compression cage so their
+ // address relative to the start of the sandbox can be obtained simply by
+ // taking the lowest 32 bits of the absolute address.
+ uint32_t address = static_cast<uint32_t>(HeapObject::cast(*arg).address());
+ args.GetReturnValue().Set(v8::Integer::NewFromUnsigned(isolate, address));
+}
+
+// Sandbox.getSizeOf(object) -> Number
+void SandboxGetSizeOf(const v8::FunctionCallbackInfo<v8::Value>& args) {
+ v8::Isolate* isolate = args.GetIsolate();
+
+ if (args.Length() == 0) {
+ isolate->ThrowError("First argument must be provided");
+ return;
+ }
+
+ Handle<Object> arg = Utils::OpenHandle(*args[0]);
+ if (!arg->IsHeapObject()) {
+ isolate->ThrowError("First argument must be a HeapObject");
+ return;
+ }
+
+ int size = HeapObject::cast(*arg).Size();
+ args.GetReturnValue().Set(v8::Integer::New(isolate, size));
+}
+
+Handle<FunctionTemplateInfo> NewFunctionTemplate(
+ Isolate* isolate, FunctionCallback func,
+ ConstructorBehavior constructor_behavior) {
+ // Use the API functions here as they are more convenient to use.
+ v8::Isolate* api_isolate = reinterpret_cast<v8::Isolate*>(isolate);
+ Local<FunctionTemplate> function_template =
+ FunctionTemplate::New(api_isolate, func, {}, {}, 0, constructor_behavior,
+ SideEffectType::kHasSideEffect);
+ return v8::Utils::OpenHandle(*function_template);
+}
+
+Handle<JSFunction> CreateFunc(Isolate* isolate, FunctionCallback func,
+ Handle<String> name, bool is_constructor) {
+ ConstructorBehavior constructor_behavior = is_constructor
+ ? ConstructorBehavior::kAllow
+ : ConstructorBehavior::kThrow;
+ Handle<FunctionTemplateInfo> function_template =
+ NewFunctionTemplate(isolate, func, constructor_behavior);
+ return ApiNatives::InstantiateFunction(function_template, name)
+ .ToHandleChecked();
+}
+
+void InstallFunc(Isolate* isolate, Handle<JSObject> holder,
+ FunctionCallback func, const char* name, int num_parameters,
+ bool is_constructor) {
+ Factory* factory = isolate->factory();
+ Handle<String> function_name = factory->NewStringFromAsciiChecked(name);
+ Handle<JSFunction> function =
+ CreateFunc(isolate, func, function_name, is_constructor);
+ function->shared().set_length(num_parameters);
+ JSObject::AddProperty(isolate, holder, function_name, function, NONE);
+}
+
+void InstallGetter(Isolate* isolate, Handle<JSObject> object,
+ FunctionCallback func, const char* name) {
+ Factory* factory = isolate->factory();
+ Handle<String> property_name = factory->NewStringFromAsciiChecked(name);
+ Handle<JSFunction> getter = CreateFunc(isolate, func, property_name, false);
+ Handle<Object> setter = factory->null_value();
+ JSObject::DefineAccessor(object, property_name, getter, setter, FROZEN);
+}
+
+void InstallFunction(Isolate* isolate, Handle<JSObject> holder,
+ FunctionCallback func, const char* name,
+ int num_parameters) {
+ InstallFunc(isolate, holder, func, name, num_parameters, false);
+}
+
+void InstallConstructor(Isolate* isolate, Handle<JSObject> holder,
+ FunctionCallback func, const char* name,
+ int num_parameters) {
+ InstallFunc(isolate, holder, func, name, num_parameters, true);
+}
+
+} // namespace
+
+// static
+void MemoryCorruptionApi::Install(Isolate* isolate) {
+ CHECK(GetProcessWideSandbox()->is_initialized());
+
+ Factory* factory = isolate->factory();
+
+ // Create the special Sandbox object that provides read/write access to the
+ // sandbox address space alongside other miscellaneous functionality.
+ Handle<JSObject> sandbox =
+ factory->NewJSObject(isolate->object_function(), AllocationType::kOld);
+
+ InstallGetter(isolate, sandbox, SandboxGetByteLength, "byteLength");
+ InstallConstructor(isolate, sandbox, SandboxMemoryView, "MemoryView", 2);
+ InstallFunction(isolate, sandbox, SandboxGetAddressOf, "getAddressOf", 1);
+ InstallFunction(isolate, sandbox, SandboxGetSizeOf, "getSizeOf", 1);
+

Copilot AI Feb 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The MemoryCorruptionApi exposes a Sandbox object into the JS global scope that provides byteLength, a MemoryView constructor directly wrapping arbitrary slices of the sandbox’s address space in JSArrayBuffers, and helpers like getAddressOf/getSizeOf for heap objects.
This effectively hands untrusted JavaScript direct, mutable access to the sandbox’s raw memory and internal object layout, completely breaking sandbox guarantees and enabling arbitrary memory read/write and native code execution inside the V8 process.
Such low-level inspection and memory access APIs must not be exposed to untrusted code; if needed for testing, they should be compiled out or strictly gated so they cannot be enabled in deployed runtimes.

Copilot uses AI. Check for mistakes.
Comment thread challenges/v8-exploitation/common/run Outdated
rm "$tempfile"
}

su -s /bin/bash -c "$(declare -f run_challenge); run_challenge \"$filename\"" nobody

Copilot AI Feb 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The run wrapper constructs a shell command for su -s /bin/bash -c using the untrusted filename inside a double-quoted string (run_challenge "$filename"), which is expanded by the outer shell and then re-parsed by the inner bash -c. If filename contains shell metacharacters (e.g., embedded quotes and semicolons) and the attacker can create such a file, they can break out of the intended run_challenge invocation and inject additional commands executed by bash as user nobody. To avoid this, avoid passing untrusted data through shell command strings at all (e.g., use su/bash argument arrays or carefully escape/quote filename so that it cannot alter the command structure).

Suggested change
su -s /bin/bash -c "$(declare -f run_challenge); run_challenge \"$filename\"" nobody
func_def="$(declare -f run_challenge)"
cmd="$func_def; run_challenge \"\$1\""
su -s /bin/bash nobody -c "$cmd" -- "$filename"

Copilot uses AI. Check for mistakes.
Comment on lines +8 to +52

+void Shell::GetAddressOf(const v8::FunctionCallbackInfo<v8::Value>& info) {
+ v8::Isolate* isolate = info.GetIsolate();
+
+ if (info.Length() == 0) {
+ isolate->ThrowError("First argument must be provided");
+ return;
+ }
+
+ internal::Handle<internal::Object> arg = Utils::OpenHandle(*info[0]);
+ if (!IsHeapObject(*arg)) {
+ isolate->ThrowError("First argument must be a HeapObject");
+ return;
+ }
+ internal::Tagged<internal::HeapObject> obj = internal::Cast<internal::HeapObject>(*arg);
+
+ uint32_t address = static_cast<uint32_t>(obj->address());
+ info.GetReturnValue().Set(v8::Integer::NewFromUnsigned(isolate, address));
+}
+
+void Shell::GetFakeObject(const v8::FunctionCallbackInfo<v8::Value>& info) {
+ v8::Isolate *isolate = info.GetIsolate();
+ Local<v8::Context> context = isolate->GetCurrentContext();
+
+ if (info.Length() != 1) {
+ isolate->ThrowError("Need exactly one argument");
+ return;
+ }
+
+ Local<v8::Uint32> arg;
+ if (!info[0]->ToUint32(context).ToLocal(&arg)) {
+ isolate->ThrowError("Argument must be a number");
+ return;
+ }
+
+ uint32_t addr = arg->Value();
+
+ internal::PtrComprCageBase cage_base = internal::GetPtrComprCageBase();
+ internal::Address base_addr = internal::V8HeapCompressionScheme::GetPtrComprCageBaseAddress(cage_base);
+ uint64_t full_addr = base_addr + (uint64_t)addr;
+
+ internal::Tagged<internal::HeapObject> obj = internal::HeapObject::FromAddress(full_addr);
+ internal::Isolate *i_isolate = reinterpret_cast<internal::Isolate*>(isolate);
+ internal::Handle<internal::Object> obj_handle(obj, i_isolate);
+ info.GetReturnValue().Set(ToApiHandle<v8::Value>(obj_handle));

Copilot AI Feb 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The GetAddressOf and GetFakeObject helpers added to Shell give JavaScript the ability to obtain raw heap object addresses and synthesize new JS values from arbitrary addresses by reconstructing compressed pointers. This enables direct type confusion and arbitrary memory access from JS (by forging fake objects pointing into controlled memory), breaking V8’s isolation between JS and native memory and enabling native code execution and sensitive data theft from the engine process. These APIs should not be exposed to untrusted scripts and, if needed for testing, should be compiled out or gated so they are unavailable in production challenge/runtime builds.

Copilot uses AI. Check for mistakes.
Comment on lines +8 to +45

+BUILTIN(ArrayOffByOne) {
+ HandleScope scope(isolate);
+ Factory *factory = isolate->factory();
+ Handle<Object> receiver = args.receiver();
+
+ if (!IsJSArray(*receiver) || !HasOnlySimpleReceiverElements(isolate, Cast<JSArray>(*receiver))) {
+ THROW_NEW_ERROR_RETURN_FAILURE(isolate, NewTypeError(MessageTemplate::kPlaceholderOnly,
+ factory->NewStringFromAsciiChecked("Nope")));
+ }
+
+ Handle<JSArray> array = Cast<JSArray>(receiver);
+
+ ElementsKind kind = array->GetElementsKind();
+
+ if (kind != PACKED_DOUBLE_ELEMENTS) {
+ THROW_NEW_ERROR_RETURN_FAILURE(isolate, NewTypeError(MessageTemplate::kPlaceholderOnly,
+ factory->NewStringFromAsciiChecked("Need an array of double numbers")));
+ }
+
+ if (args.length() > 2) {
+ THROW_NEW_ERROR_RETURN_FAILURE(isolate, NewTypeError(MessageTemplate::kPlaceholderOnly,
+ factory->NewStringFromAsciiChecked("Too many arguments")));
+ }
+
+ Handle<FixedDoubleArray> elements(Cast<FixedDoubleArray>(array->elements()), isolate);
+ uint32_t len = static_cast<uint32_t>(Object::NumberValue(array->length()));
+ if (args.length() == 1) { // read mode
+ return *(isolate->factory()->NewNumber(elements->get_scalar(len)));
+ } else { // write mode
+ Handle<Object> value = args.at(1);
+ if (!IsNumber(*value)) {
+ THROW_NEW_ERROR_RETURN_FAILURE(isolate, NewTypeError(MessageTemplate::kPlaceholderOnly,
+ factory->NewStringFromAsciiChecked("Need a number argument")));
+ }
+ double num = static_cast<double>(Object::NumberValue(*value));
+ elements->set(len, num);
+ return ReadOnlyRoots(isolate).undefined_value();

Copilot AI Feb 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ArrayOffByOne builtin deliberately reads and writes element index len of a FixedDoubleArray (elements->get_scalar(len) / elements->set(len, ...)), even though valid indices are 0..len-1. This is a direct out-of-bounds read/write primitive from JavaScript into the array’s backing store, allowing attackers to corrupt adjacent heap memory or read unintended data, which is a classic path to reliable native code execution in the V8 process. Any array accessor must enforce strict bounds checks and never allow JS to access elements[len] without a corresponding allocation guaranteeing that slot exists.

Copilot uses AI. Check for mistakes.
Comment on lines +4 to +31
+++ b/src/compiler/turboshaft/machine-lowering-reducer-inl.h
@@ -2740,7 +2740,7 @@ class MachineLoweringReducer : public Next {
const ZoneRefSet<Map>& maps, CheckMapsFlags flags,
const FeedbackSource& feedback) {
if (maps.is_empty()) {
- __ Deoptimize(frame_state, DeoptimizeReason::kWrongMap, feedback);
+ //__ Deoptimize(frame_state, DeoptimizeReason::kWrongMap, feedback);
return {};
}

@@ -2749,14 +2749,14 @@ class MachineLoweringReducer : public Next {
IF_NOT (LIKELY(CompareMapAgainstMultipleMaps(heap_object_map, maps))) {
// Reloading the map slightly reduces register pressure, and we are on a
// slow path here anyway.
- MigrateInstanceOrDeopt(heap_object, __ LoadMapField(heap_object),
- frame_state, feedback);
- __ DeoptimizeIfNot(__ CompareMaps(heap_object, maps), frame_state,
- DeoptimizeReason::kWrongMap, feedback);
+ //MigrateInstanceOrDeopt(heap_object, __ LoadMapField(heap_object),
+ // frame_state, feedback);
+ //__ DeoptimizeIfNot(__ CompareMaps(heap_object, maps), frame_state,
+ // DeoptimizeReason::kWrongMap, feedback);
}
} else {
- __ DeoptimizeIfNot(__ CompareMaps(heap_object, maps), frame_state,
- DeoptimizeReason::kWrongMap, feedback);
+ //__ DeoptimizeIfNot(__ CompareMaps(heap_object, maps), frame_state,
+ // DeoptimizeReason::kWrongMap, feedback);

Copilot AI Feb 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes in MachineLoweringReducer::CheckMaps comment out all deoptimization and map migration paths when a heap object’s map does not match the expected set, effectively turning failed map checks into silent success. This breaks Turboshaft’s key type and shape assumptions, allowing just-in-time compiled code to treat objects with incompatible layouts as if they had the expected map, which is a classic type confusion primitive that can lead to arbitrary memory reads/writes and native code execution from crafted JavaScript. Map checks in optimized code are fundamental safety guards and must never be disabled or converted into no-ops for untrusted inputs.

Copilot uses AI. Check for mistakes.
zardus and others added 12 commits June 19, 2026 01:09
First challenge in the Maglev compiler exploitation submodule.
Students observe V8's multi-tier JIT pipeline by triggering Maglev
and TurboFan independently, reading %GetOptimizationStatus() bits,
and passing them to WinMaglevObserved().

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Bridge challenge exploiting Maglev's BuildAllocateFastObject missing
double element initialization. Students trigger Maglev compilation of
a function with a double array literal and observe uninitialized
element values.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Prove that Maglev's allocation folding incorrectly spans the super() call
boundary — the root cause of CVE-2024-0517. The WinAllocationFolding helper
checks that objects allocated in a derived constructor's body are folded
(adjacent in memory) with the super() allocation due to a missing
ClearCurrentRawAllocation() call. No maglev-graph-builder.cc patch needed
since the bug exists natively in V8 12.0.

Also fix .init.j2 in maglev-warmup and maglev-1 to use 4755 permissions on
catflag (world-executable with SUID) for reliable test execution.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…-2024-4947 and reorder module.yml

Add remaining Maglev challenges completing the 7-challenge submodule:
- maglev-3: GC-triggered OOB bridge with GetAddressOf helper
- cve-2024-0517: full CVE exploitation on educational V8 revision
- cve-2024-0517-real: CVE on original vulnerable V8 12.0 (no helpers)
- cve-2024-4947: Lazarus Group ITW 0-day, module namespace AccessInfo
  type confusion on V8 12.4

Move Maglev Compiler Exploitation section in module.yml to its correct
position between "JIT Bugs on the V8 Heap" and "Garbage Collection
Internals" per MAGLEV_MODULE_PLAN.md.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Thorough review of all 57 challenges covering inventory, difficulty
progression, learning gaps, quality assessment, and recommendations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dangerous Changes critical or security-sensitive files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants