-
Notifications
You must be signed in to change notification settings - Fork 9.4k
Description
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'sentity_idon 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 STOREProposed 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\Fullprocesses products in batches, so$entityIdsis never null)
Steps to reproduce
- Install Magento 2.4.8-p3 with sample data
- Create a second store view
- 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 != 0row)
- Run:
bin/magento indexer:reindex catalog_product_attribute - 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
Type
Projects
Status