Problem/Motivation

Small bug in the core "node" module's node.module

Current code:

function _node_access_rebuild_batch_operation(&$context) {
....
  while ($row = db_fetch_array($result)) {
    $loaded_node = node_load($row['nid'], NULL, TRUE);
    // To preserve database integrity, only aquire grants if the node
    // loads successfully.
    if (!empty($loaded_node)) {
      node_access_acquire_grants($loaded_node);
    }
    $context['sandbox']['progress']++;
    $context['sandbox']['current_node'] = $loaded_node->nid;
  }
...
}

The last line in this while loop should read: $context['sandbox']['current_node'] = $row['nid'];

As if the loaded is empty, $loaded_node->nid will be empty too causing an infinite loop in the batch operation, as it takes an empty value for $context['sandbox']['current_node'] as meaning it has not yet started the rebuild and starts all over again.

Steps to reproduce

Not sure

Proposed resolution

Based on patch #42. Loop through the $nids vs loaded $nodes and check if $nid is in the $node array first.

Remaining tasks

Maybe test = @catch mentioned in #46 this may not be possible

User interface changes

NA

API changes

NA

Data model changes

NA

Release notes snippet

NA

Comments

Anonymous’s picture

Status: Needs review » Active

We can only review patch files. Since there is no file marking as active.

btopro’s picture

+1 this fix, I've encountered the same error and it was resolved by changing that one line.

coltrane’s picture

Status: Active » Needs review
StatusFileSize
new675 bytes

I believe this fixes a problem that almost has had me pulling my hair out. I've rebuilt permissions a hundred times before and never had it stall or consistently keep saying 'The content access permissions have not been properly rebuilt.' Disabling and uninstalling modules hasn't fixed it on the site suffering from this problem so I dove into the batch process. It's easy to see that if not all nodes are processed in _node_access_rebuild_batch_operation() than the batch process reports it as unfinished and node_access_needs_rebuild(FALSE) is never called. What's difficult for me to deduce at this moment is why the node_load() may not return a full node object. However, since it's just wrong to use $loaded_node->nid in the case it might not be an actual node object we really should be using the $row['nid'].

coltrane’s picture

This is likely a problem in HEAD as well http://api.drupal.org/api/function/_node_access_rebuild_batch_operation/7 because if $node is empty then $context['sandbox']['current_node'] won't record the processed node id.

Edit: Spoke with Dave Reid on irc about this and node_load_multiple() doesn't return any invalid nodes so this bug probably won't occur in 7.

yched’s picture

Version: 6.x-dev » 7.x-dev
StatusFileSize
new1.53 KB

The fix makes complete sense, nice catch. I guess such cases, where there is a record in the node table but node_load() fails, can happen with deleted node types.

D7 can be affected too, though : if *no* valid node is found within the nid range, then $context['sandbox']['current_node'] won't be updated, and infinite loop ensues.

So, patch in #3 is RTBC for D6, but this will need to be fixed in D7 first. Here's a patch, needs review.

Status: Needs review » Needs work

The last submitted patch failed testing.

martin_q’s picture

Status: Needs work » Needs review

This bug has just started to affect my D6 site. I can't see why the patch shouldn't work, the #3 patch works on my D6 site, and I can't see any errors in the automated testing results for #5. Can someone who knows more than me (that doesn't narrow it down much) review this manually and see if it's RTBC?

Thanks.

PaulMagrath’s picture

StatusFileSize
new1.07 KB

Patch is failing because in Drupal 7 they have changed the for loop into a for each loop:

  $nids = db_query_range("SELECT nid FROM {node} WHERE nid > :nid ORDER BY nid ASC", 0, $limit, array(':nid' => $context['sandbox']['current_node']))->fetchCol();
  $nodes = node_load_multiple($nids, array(), TRUE);
  foreach ($nodes as $node) {
    // To preserve database integrity, only acquire grants if the node
    // loads successfully.
    if (!empty($node)) {
      node_access_acquire_grants($node);
    }
    $context['sandbox']['progress']++;
    $context['sandbox']['current_node'] = $node->nid;
  }

This should fix it:

  $nids = db_query_range("SELECT nid FROM {node} WHERE nid > :nid ORDER BY nid ASC", 0, $limit, array(':nid' => $context['sandbox']['current_node']))->fetchCol();
  $nodes = node_load_multiple($nids, array(), TRUE);
  foreach ($nodes as $nid => $node) {
    // To preserve database integrity, only acquire grants if the node
    // loads successfully.
    if (!empty($node)) {
      node_access_acquire_grants($node);
    }
    $context['sandbox']['progress']++;
    $context['sandbox']['current_node'] = $nid;
  }

I've attached a patch file for testing:

tobiasb’s picture

#8 where is the different?

PaulMagrath’s picture

tobiasb:

The difference is that instead of getting the nid from the loaded node, you use the nid from the array. The nid from the array will always be valid whereas the nid from the loaded node will be undefined if the loading of the node fails for any reason.

Ainur’s picture

Had the same problem as PaulMagrath, changing $context['sandbox']['current_node'] = $loaded_node->nid to $context['sandbox']['current_node'] = $row['nid']; worked well for me.

Re-test of nodebug.patch from comment #8 was requested by moshe weitzman.

moshe weitzman’s picture

Status: Needs review » Reviewed & tested by the community

Code looks good. More robust. Bot is happy.

webchick’s picture

Status: Reviewed & tested by the community » Needs review

Interesting. So over at #471080: Trigger watchdog when node rebuild permissions fails, which is essentially the same fix, chx threatens to won't fix due to it only masking the issue.

The big question in my mind is whether or not this will still pick it up when a node does indeed fail to rebuild permissions, due to corrupt data or similar. If it does, then this is good to go.

Could someone test this to make sure (doing something like manually inserting an invalid uid into the node table's uid column will do it)? Even better, write automated tests for the rebuild permissions functionality to prove this, and/or point out where in core they already exist?

PaulMagrath’s picture

#8: nodebug.patch queued for re-testing.

PaulMagrath’s picture

Status: Needs review » Reviewed & tested by the community

I can understand chx's point of view. Fact is though that entries in the database are going to get corrupt sometimes either from hard drive failure, db admin's fiddling with database table contents directly or from people misusing the APIs. Drupal shouldn't fail like this when that happens. This is a small change for a significant increase in the robustness of the code of the function.

To answer the big question in your mind: there is no logging when a node fails to rebuild currently and this doesn't add it. That is being discussed currently over at #471080 I believe. This issue is for the fix itself only.

I've tested this fix to make sure it works and it has been tested and found to work by others as well. I agree that automated tests would be better and if you have the time, it'd be great if you get around to writing some!

In the meantime, I'm going to move this issue back to "reviewed & tested by the community". That reflects its current status and maturity. The lack of automated tests such as you describe is not directly relevant IMHO and shouldn't block this issue being fixed in HEAD.

tacituseu’s picture

StatusFileSize
new736 bytes

I think the problem lies in other part of this function, there's a race condition between:
$context['sandbox']['max'] = db_query('SELECT COUNT(DISTINCT nid) FROM {node}')->fetchField();
and
$nids = db_query_range("SELECT nid FROM {node} WHERE nid > :nid ORDER BY nid ASC", 0, $limit, array(':nid' => $context['sandbox']['current_node']))->fetchCol();
if at some point between context initialization and last iteration of second query node gets deleted or added, following code will result in an infinite loop:

  if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
    $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
  }
tacituseu’s picture

Status: Reviewed & tested by the community » Needs review
StatusFileSize
new851 bytes

Takes into consideration that DrupalDefaultEntityController removes any invalid ids from the array (returned) and fixes potential progress bar overflow.

Encarte’s picture

subscribing

dave reid’s picture

Related for Drupal 6 only (already fixed in D7+): #600836: Batch API never terminates if you set $context['finished'] > 1

mstrelan’s picture

To me it doesn't make sense that we run $context['sandbox']['progress']++; inside of foreach ($nodes as $nid => $node) { }, because it is never guaranteed that count($nodes) is equal to count($nids);, yet the batch process is based on $nids, not on $nodes.

In my case I'm working with a D5 database I've inherited and I'm upgrading it to D7 and adding domain access. There are about 70 nodes which don't have any revisions, don't ask me how this is possible, but rebuilding permissions stops at 99%.

Two possible solutions.

1. Change the foreach loop to go through the nids, not the nodes.

foreach ($nids as $nid) {
  if (isset($nodes[$nid])) {
    // ....
  }
  $context['sandbox']['progress']++;
  $context['sandbox']['current_node'] = $nid;  
}

2. Replace $context['sandbox']['progress']++; with $context['sandbox']['progress'] += count($nids); outside of the foreach loop.

Anonymous’s picture

Version: 7.x-dev » 8.x-dev
Status: Needs review » Needs work

The patch will need to be 8.x based first.

xjm’s picture

Component: node.module » node system
Issue summary: View changes

(Merging "node system" and "node.module" components for 8.x; disregard.)

damienmckenna’s picture

damienmckenna’s picture

Per tacituseu's comment #17, this issue occurs when the 'max' number of nodes at the start of the batch process falls out of sync with the per-iteration count from the second query. I've seen this happen on large sites that required the access rebuild on production – because the process took so long, other events were triggered that caused other nodes to be fixed separately, thus the running total never reached the 'max'.

David_Rothstein’s picture

Version: 8.0.x-dev » 8.1.x-dev

Drupal 8.0.6 was released on April 6 and is the final bugfix release for the Drupal 8.0.x series. Drupal 8.0.x will not receive any further development aside from security fixes. Drupal 8.1.0-rc1 is now available and sites should prepare to update to 8.1.0.

Bug reports should be targeted against the 8.1.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.2.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.1.x-dev » 8.2.x-dev

Drupal 8.1.9 was released on September 7 and is the final bugfix release for the Drupal 8.1.x series. Drupal 8.1.x will not receive any further development aside from security fixes. Drupal 8.2.0-rc1 is now available and sites should prepare to upgrade to 8.2.0.

Bug reports should be targeted against the 8.2.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.2.x-dev » 8.3.x-dev

Drupal 8.2.6 was released on February 1, 2017 and is the final full bugfix release for the Drupal 8.2.x series. Drupal 8.2.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.3.0 on April 5, 2017. (Drupal 8.3.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.3.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.6 was released on August 2, 2017 and is the final full bugfix release for the Drupal 8.3.x series. Drupal 8.3.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.4.0 on October 4, 2017. (Drupal 8.4.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.4.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.4 was released on January 3, 2018 and is the final full bugfix release for the Drupal 8.4.x series. Drupal 8.4.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.5.0 on March 7, 2018. (Drupal 8.5.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.5.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.6 was released on August 1, 2018 and is the final bugfix release for the Drupal 8.5.x series. Drupal 8.5.x will not receive any further development aside from security fixes. Sites should prepare to update to 8.6.0 on September 5, 2018. (Drupal 8.6.0-rc1 is available for testing.)

Bug reports should be targeted against the 8.6.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.6.x-dev » 8.8.x-dev

Drupal 8.6.x will not receive any further development aside from security fixes. Bug reports should be targeted against the 8.8.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.9.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.7 was released on June 3, 2020 and is the final full bugfix release for the Drupal 8.8.x series. Drupal 8.8.x will not receive any further development aside from security fixes. Sites should prepare to update to Drupal 8.9.0 or Drupal 9.0.0 for ongoing support.

Bug reports should be targeted against the 8.9.x-dev branch from now on, and new development or disruptive changes should be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

pameeela’s picture

Not sure whether this is still valid, but the referenced code has changed so the IS needs an update.

damienmckenna’s picture

Version: 8.9.x-dev » 9.3.x-dev

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

mstrelan’s picture

Status: Needs work » Needs review
Issue tags: +Needs tests
StatusFileSize
new864 bytes

This came up as the daily triage target for Bug Smash and Lendude pinged me since I encountered this 11 years ago ...

I've attached a patch against 10.1.x that implements my suggestion in #22. Haven't got around to writing a test for this though since it doesn't seem straight forward to replicate.

FWIW when this was originally written in 6.x the sandbox progress was incremented for each nid loaded from the db, whereas in 7.x this changed to be incremented for each node that was successfully loaded. The patch changes it back to nids.

6.x:

while ($row = db_fetch_array($result)) {
  $loaded_node = node_load($row['nid'], NULL, TRUE);
  // To preserve database integrity, only aquire grants if the node
  // loads successfully.
  if (!empty($loaded_node)) {
    node_access_acquire_grants($loaded_node);
  }
  $context['sandbox']['progress']++;
  $context['sandbox']['current_node'] = $loaded_node->nid;
}

7.x:

$nids = db_query_range("SELECT nid FROM {node} WHERE nid > :nid ORDER BY nid ASC", 0, $limit, array(':nid' => $context['sandbox']['current_node']))->fetchCol();
$nodes = node_load_multiple($nids, array(), TRUE);
foreach ($nodes as $nid => $node) {
  // To preserve database integrity, only acquire grants if the node
  // loads successfully.
  if (!empty($node)) {
    node_access_acquire_grants($node);
  }
  $context['sandbox']['progress']++;
  $context['sandbox']['current_node'] = $nid;
}
mstrelan’s picture

StatusFileSize
new1.4 KB

Fixed phpstan baseline (in before the CCF fixers arrive)

ameymudras’s picture

Status: Needs review » Needs work

I did a quick test and was able to get this working with the patch.

I have a suggestion regarding the code, rather than checking the nid below

-    if (!empty($node)) {
+    if (!empty($nodes[$nid])) {
+      $node = $nodes[$nid];

Why don't we use

    if ($node instanceof NodeInterface) {

Also we need supporting tests for this use case

mstrelan’s picture

@ameymudras the main reason is that we're iterating through a list of $nids and confirming that a corresponding node exists in the $nodes array, so $node doesn't exist at this point. There are plenty of other ways to refactor this but I think this minimizes the changes as much as possible.

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

catch’s picture

Priority: Normal » Major
Status: Needs work » Needs review

I think this is actually major. Not sure how we'd write tests for it since I think it requires a race condition or similar? Also phpstan is finding the existing bug in the logic.

#42 looks good so moving back to needs review.

smustgrave’s picture

Issue summary: View changes
Status: Needs review » Reviewed & tested by the community
Issue tags: -Needs issue summary update

Wow 2008!

Updated the issue summary best I could on patch #42

Leaving the tests tag just in case but noted in the IS this may not be possible.

Applied patch #42 and ran rebuild from admin/reports/status/rebuild which completed without failure.

Change makes sense to me.

quietone’s picture

I'm triaging RTBC issues. I read the IS and skimmed comments. It is very helpful that the proposed resolution identifies the patch being referred to. Although, the sentence is rather cryptic and I didn't understand it until I read the patch. I didn't find any unanswered questions or other work to do.

Leaving at RTBC.

kim.pepper’s picture

+++ b/core/modules/node/node.module
@@ -1174,10 +1174,11 @@ function _node_access_rebuild_batch_operation(&$context) {
+    if (!empty($nodes[$nid])) {

Should this be isset() instead?

mstrelan’s picture

@kim.pepper probably, but it doesn't make a difference to this issue, as we will still increment the progress whether the node is empty or not.

  • catch committed 0dd82d68 on 10.1.x
    Issue #315302 by mstrelan, tacituseu, PaulMagrath, coltrane, yched,...
catch’s picture

Version: 11.x-dev » 10.1.x-dev
Status: Reviewed & tested by the community » Fixed
Issue tags: -Needs tests

Going to go ahead and commit this given it's been more or less the same fix since 2009 and has been RTBC multiple times since then.

Once again I don't see how this is testable since it depends on a race condition being triggered during node access rebuild, however it's also a good example of phpstan finding a (very old) bug.

We could open a follow-up for the empty vs. isset change, also think it's probably time to look at a NodeAccessUpdater class similar to the config update helper to try to modernize some of this and make it a bit less tied to batch.

  • catch committed c19a53a0 on 11.x
    Issue #315302 by mstrelan, tacituseu, PaulMagrath, coltrane, yched,...

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.