Skip to content

Incorrect EAV reindex: disabled product indexed when enabled on another store view (multistore) #40518

@kdivan

Description

@kdivan

Preconditions and environment

Preconditions

  • Magento Open Source 2.4.8-p3 (fresh install with sample data, no third-party modules)
  • PHP 8.4
  • MySQL 8.4
  • 2 store views

Setup

First, retrieve the attribute IDs for your installation (they may vary):

SELECT attribute_id, attribute_code
FROM eav_attribute
WHERE attribute_code IN ('status', 'visibility')
AND entity_type_id = 4;

In the examples below, we use {STATUS_ID} and {VISIBILITY_ID} as placeholders for these values.

Configure product 24-MB01 as follows:

Scope Status Visibility
Default (store 0) Disabled Catalog, Search
Store View 1 Disabled (inherits default) Catalog, Search (store-level override)
Store View 2 Enabled (store-level override) Catalog, Search (inherits default)

Resulting EAV data:

-- Status values (attribute_id = {STATUS_ID})
SELECT store_id, value FROM catalog_product_entity_int
WHERE entity_id = {PRODUCT_ID} AND attribute_id = {STATUS_ID};
-- store_id=0, value=2 (Disabled)
-- store_id=2, value=1 (Enabled)

-- Visibility values (attribute_id = {VISIBILITY_ID})
SELECT store_id, value FROM catalog_product_entity_int
WHERE entity_id = {PRODUCT_ID} AND attribute_id = {VISIBILITY_ID};
-- store_id=0, value=4 (Catalog, Search)
-- store_id=1, value=4 (Catalog, Search)

Root Cause

The bug is in Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\Source::_prepareSelectIndex().

When $entityIds is not null, a $selectWithoutDefaultStore query is built and UNIONed with the main query. This second query has an incorrect JOIN condition for the status check:

Buggy code (line 268):

->joinLeft(
    ['d2s' => $this->getTable('catalog_product_entity_int')],
    "d2s.store_id != 0 AND d2s.attribute_id = d2d.attribute_id AND " .
    "d2s.{$productIdField} = d2d.{$productIdField}",
    []
)

The d2s.store_id != 0 matches the status attribute from any non-default store. If the product is Enabled on any store, COALESCE(d2s.value, d2d.value) evaluates to 1 (Enabled), causing the product to
be indexed for all stores where it has a visibility row — regardless of whether the product is actually Disabled on that specific store.

The main query in the same method correctly uses d2s.store_id = s.store_id (line
199
):

->joinLeft(
    ['d2s' => $this->getTable('catalog_product_entity_int')],
    "d2s.store_id = s.store_id AND d2s.attribute_id = d2d.attribute_id AND " .
    "d2s.{$productIdField} = d2d.{$productIdField}",
    []
)

Why full reindex is also affected

The $selectWithoutDefaultStore query is built whenever $entityIds !== null. Even full reindex is affected because
Action\Full::execute() processes products in batches, calling
reindexEntities($batchOfIds) per batch. Since $entityIds is never null in practice, the buggy query is always built — for both full and partial (cron) reindex.

SQL proof of the bug

Note: Replace {STATUS_ID}, {VISIBILITY_ID} and {PRODUCT_ID} with the actual attribute IDs and the product's entity_id on your installation.

-- Simulates the buggy selectWithoutDefaultStore JOIN
-- For store 1 (DISABLED), it picks up ENABLED status from store 2
SELECT
    wd.store_id AS indexing_for_store,
    d2s.store_id AS status_taken_from_store,
    d2s.value AS status_value,
    CASE WHEN wd.store_id != d2s.store_id THEN 'WRONG STORE' ELSE 'OK' END AS problem
FROM catalog_product_entity_int wd
JOIN catalog_product_entity cpe ON cpe.entity_id = wd.entity_id
LEFT JOIN catalog_product_entity_int d2d
    ON d2d.store_id = 0 AND d2d.entity_id = wd.entity_id AND d2d.attribute_id = {STATUS_ID}
LEFT JOIN catalog_product_entity_int d2s
    ON d2s.store_id != 0  -- BUG: matches ANY store
    AND d2s.attribute_id = d2d.attribute_id
    AND d2s.entity_id = d2d.entity_id
WHERE cpe.entity_id = {PRODUCT_ID}
    AND wd.attribute_id = {VISIBILITY_ID}
    AND wd.store_id = 1
    AND COALESCE(d2s.value, d2d.value) = 1;

-- Result:
-- indexing_for_store=1, status_taken_from_store=2, status_value=1, problem=WRONG STORE

Proposed Fix

Change d2s.store_id != 0 to d2s.store_id = wd.store_id in the $selectWithoutDefaultStore query to match the main query's behavior:

--- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/Source.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/Source.php
@@ -265,7 +265,7 @@
             )->joinLeft(
                 ['d2s' => $this->getTable('catalog_product_entity_int')],
-                "d2s.store_id != 0 AND d2s.attribute_id = d2d.attribute_id AND " .
+                "d2s.store_id = wd.store_id AND d2s.attribute_id = d2d.attribute_id AND " .
                 "d2s.{$productIdField} = d2d.{$productIdField}",
                 []
             )

Impact

  • Affected versions: 2.4.8-p3 (confirmed), likely all 2.4.x versions (the buggy code has never been modified since introduction)
  • Trigger condition: Multistore setup + product Enabled on at least one store and Disabled on another + store-level visibility override exists on a disabled store
  • Consequence: Disabled products appear in layered navigation filters and attribute-filtered catalog queries for stores where they should be hidden
  • No workaround: Full reindex is also affected (Action\Full processes products in batches, so $entityIds is never null)

Steps to reproduce

  1. Install Magento 2.4.8-p3 with sample data
  2. Create a second store view
  3. For product 24-MB01:
    • Set default scope status to Disabled
    • Set Store View 2 status to Enabled (store-level override)
    • Set Store View 1 visibility to Catalog, Search (store-level override, to create a store_id != 0 row)
  4. Run: bin/magento indexer:reindex catalog_product_attribute
  5. Check catalog_product_index_eav

Expected result

Product should only be indexed for Store View 2 (where it is Enabled).

+--------+----------+-----+
| entity | store_id | val |
+--------+----------+-----+
|      1 |        2 |   4 |  ← Correct: Enabled on store 2
+--------+----------+-----+

Actual result

Product is incorrectly indexed for both Store View 1 and Store View 2, even though it is Disabled on Store View 1.

+--------+----------+-----+
| entity | store_id | val |
+--------+----------+-----+
|      1 |        1 |   4 |  ← BUG: Disabled on store 1 but indexed
|      1 |        2 |   4 |  ← Correct
+--------+----------+-----+

Additional information

No response

Release note

No response

Triage and priority

  • Severity: S0 - Affects critical data or functionality and leaves users without workaround.
  • Severity: S1 - Affects critical data or functionality and forces users to employ a workaround.
  • Severity: S2 - Affects non-critical data or functionality and forces users to employ a workaround.
  • Severity: S3 - Affects non-critical data or functionality and does not force users to employ a workaround.
  • Severity: S4 - Affects aesthetics, professional look and feel, “quality” or “usability”.

Metadata

Metadata

Assignees

Labels

Issue: needs updateAdditional information is require, waiting for responseReported on 2.4.8-p3Indicates original Magento version for the Issue report.

Type

No type

Projects

Status

Needs Update

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions