Skip to content
This repository was archived by the owner on Jul 11, 2026. It is now read-only.
This repository was archived by the owner on Jul 11, 2026. It is now read-only.

[Bug]: ELUSoC_2026 — Shop purchase controller has a TOCTOU race condition that allows overselling limited-stock items #258

Description

@Babin123456

Problem

The purchaseItem controller in shopController.js checks item.stock === 0 and then later decrements item.stock in two separate database operations. This is a classic Time-Of-Check / Time-Of-Use (TOCTOU) race condition: under concurrent purchases of the same limited-stock item, multiple requests can pass the stock check simultaneously and all proceed to decrement, resulting in the stock going negative and the item being sold more times than intended.

Current Behavior

// Check (read):
if (item.stock === 0) { return res.status(400)... }

// ... many operations later ...

// Use (write):
item.soldCount += 1;
if (item.stock > 0) { item.stock -= 1; }
await item.save();

If two users purchase the last unit of a stock-1 item simultaneously:

  • Both requests read stock = 1 → both pass the check
  • Both create a Purchase document and debit points
  • Both decrement stock → stock ends at -1

Why This Improvement Is Needed

  • Corrupts stock data, making inventory tracking unreliable.
  • Members can receive items that were already sold to someone else.
  • The bug is silent — no error is thrown and no log is generated.
  • This is a data integrity issue affecting the core reward system.

Proposed Solution

Replace the two-step check+decrement with a single atomic findOneAndUpdate call using MongoDB's $gt operator as a conditional guard:

const updatedItem = await ShopItem.findOneAndUpdate(
  { _id: item._id, stock: { $gt: 0 } },  // only match if stock > 0
  { $inc: { soldCount: 1, stock: -1 } },
  { new: true },
);

if (!updatedItem) {
  // another concurrent request claimed the last unit — refund points
  return res.status(400).json({ message: "Item sold out — please try again" });
}

This is an atomic read-modify-write at the database level, making the race condition impossible.

Expected Outcome

  • Limited-stock items can never be oversold, regardless of concurrent load.
  • If two requests race for the last unit, exactly one succeeds and the other receives a clear error with a points refund.
  • soldCount always accurately reflects the number of successful purchases.

Additional Notes

  • Items with stock = -1 (unlimited) are unaffected.
  • The fix adds a best-effort points refund when the race is lost, protecting the member's balance.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions