Skip to content

Commit e37ca83

Browse files
DeepDiver1975claude
andcommitted
fix(integrity): clear stale per app results and use the local cache tier
storeResults() writes one cache entry per checked scope in addition to the entry holding the combined results, but cleanResults() only removed the latter. The per app entries had no TTL either, so a verdict about an app was cached forever and kept being served through getVerifiedAppsFromCache() even after the app had been repaired or replaced. cleanResults() now clears the whole prefix - ICache::clear() is prefix scoped in every backend - and the entries expire. The results describe the files on disk of the host that produced them, so they also move to the host local cache tier. getResults() already falls back to appconfig, so a check run through occ stays visible to the web requests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com>
1 parent 4cbb897 commit e37ca83

3 files changed

Lines changed: 102 additions & 38 deletions

File tree

changelog/unreleased/41735

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
Bugfix: Clear stale integrity check results when rescanning
2+
3+
The code integrity checker stores one cache entry per checked scope, but a
4+
rescan only removed the entry holding the combined results. The per app entries
5+
had no expiry either, so a verdict about an app was cached indefinitely and was
6+
served even after the app had been repaired or replaced. Rescanning now clears
7+
all of them, the entries expire, and the results are kept in the host local
8+
cache tier - they describe the files on disk of one host and are of no use to
9+
another.
10+
11+
https://github.com/owncloud/core/pull/41735

lib/private/IntegrityCheck/Checker.php

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,14 @@
5555
*/
5656
class Checker implements OnDiskHasher {
5757
public const CACHE_KEY = 'oc.integritycheck.checker';
58+
59+
/**
60+
* The results describe the files on disk of this instance, so cached entries
61+
* have to expire for a node that never runs a check itself to notice a
62+
* repaired or replaced installation.
63+
*/
64+
public const CACHE_TTL = 24 * 3600;
65+
5866
/** @var EnvironmentHelper */
5967
private $environmentHelper;
6068
/** @var AppLocator */
@@ -104,7 +112,9 @@ public function __construct(
104112
$this->fileAccessHelper = $fileAccessHelper;
105113
$this->appLocator = $appLocator;
106114
$this->config = $config;
107-
$this->cache = $cacheFactory ? $cacheFactory->create(self::CACHE_KEY) : new \OC\Memcache\NullCache();
115+
$this->cache = $cacheFactory
116+
? \OC\Memcache\LocalCacheFactory::create($cacheFactory, self::CACHE_KEY)
117+
: new \OC\Memcache\NullCache();
108118
$this->appManager = $appManager;
109119
$this->tempManager = $tempManager;
110120
$this->verifier = $verifier;
@@ -400,17 +410,21 @@ private function storeResults($scope, array $result) {
400410

401411
$this->setAppValue(self::CACHE_KEY, \json_encode($resultArray));
402412
//Set cache for each app
403-
$this->cache->set($scope, \json_encode($resultArray));
404-
$this->cache->set(self::CACHE_KEY, \json_encode($resultArray));
413+
$this->cache->set($scope, \json_encode($resultArray), self::CACHE_TTL);
414+
$this->cache->set(self::CACHE_KEY, \json_encode($resultArray), self::CACHE_TTL);
405415
}
406416

407417
/**
418+
* Clean previous results for a proper rescanning. Otherwise a stale verdict
419+
* would be served instead of the one the rescan is about to produce.
408420
*
409-
* Clean previous results for a proper rescanning. Otherwise
421+
* storeResults() writes one entry per scope in addition to CACHE_KEY, so the
422+
* whole prefix is cleared - removing CACHE_KEY alone left every per app entry
423+
* behind.
410424
*/
411425
private function cleanResults() {
412426
$this->deleteAppValue(self::CACHE_KEY);
413-
$this->cache->remove(self::CACHE_KEY);
427+
$this->cache->clear();
414428
}
415429

416430
/**

tests/lib/IntegrityCheck/CheckerTest.php

Lines changed: 72 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,12 @@
2727
use OC\IntegrityCheck\Helpers\FileAccessHelper;
2828
use OC\IntegrityCheck\Verifier\Verifier;
2929
use OC\IntegrityCheck\Verifier\VerificationResult;
30+
use OC\Memcache\ArrayCache;
3031
use OC\Memcache\NullCache;
3132
use OC\Memcache\Redis;
3233
use OCP\App\IAppManager;
33-
use OCP\ICacheFactory;
3434
use OCP\IConfig;
35+
use Test\Memcache\FixedCacheFactory;
3536
use Test\TestCase;
3637

3738
/**
@@ -48,7 +49,7 @@ class CheckerTest extends TestCase {
4849
private $fileAccessHelper;
4950
/** @var IConfig | \PHPUnit\Framework\MockObject\MockObject */
5051
private $config;
51-
/** @var ICacheFactory | \PHPUnit\Framework\MockObject\MockObject */
52+
/** @var FixedCacheFactory */
5253
private $cacheFactory;
5354
/** @var IAppManager | \PHPUnit\Framework\MockObject\MockObject */
5455
private $appManager;
@@ -61,7 +62,7 @@ public function setUp(): void {
6162
$this->fileAccessHelper = $this->createMock(FileAccessHelper::class);
6263
$this->appLocator = $this->createMock(AppLocator::class);
6364
$this->config = $this->createMock(IConfig::class);
64-
$this->cacheFactory = $this->createMock(ICacheFactory::class);
65+
$this->cacheFactory = new FixedCacheFactory(new NullCache());
6566
$this->appManager = $this->createMock(IAppManager::class);
6667
$this->verifier = $this->createMock(Verifier::class);
6768

@@ -87,12 +88,6 @@ public function setUp(): void {
8788
->method('getAllApps')
8889
->willReturn([]);
8990

90-
$this->cacheFactory
91-
->expects($this->any())
92-
->method('create')
93-
->with('oc.integritycheck.checker')
94-
->willReturn(new NullCache());
95-
9691
$this->checker = new Checker(
9792
$this->environmentHelper,
9893
$this->fileAccessHelper,
@@ -103,6 +98,69 @@ public function setUp(): void {
10398
\OC::$server->getTempManager(),
10499
$this->verifier
105100
);
101+
102+
$this->assertSame([Checker::CACHE_KEY], $this->cacheFactory->getRequestedPrefixes());
103+
}
104+
105+
/**
106+
* The results describe the files on disk of this host, so they belong in the
107+
* host local cache tier - not in a distributed one where another host could
108+
* hand back a verdict about an installation it cannot see.
109+
*/
110+
public function testUsesTheLocalCacheTier() {
111+
$cacheFactory = $this->createMock(FixedCacheFactory::class);
112+
$cacheFactory->expects($this->once())
113+
->method('createLocal')
114+
->with(Checker::CACHE_KEY)
115+
->willReturn(new NullCache());
116+
$cacheFactory->expects($this->never())->method('createDistributed');
117+
$cacheFactory->expects($this->never())->method('create');
118+
119+
new Checker(
120+
$this->environmentHelper,
121+
$this->fileAccessHelper,
122+
$this->appLocator,
123+
$this->config,
124+
$cacheFactory,
125+
$this->appManager,
126+
\OC::$server->getTempManager(),
127+
$this->verifier
128+
);
129+
}
130+
131+
/**
132+
* storeResults() writes one entry per scope next to CACHE_KEY, and a rescan
133+
* has to invalidate all of them - removing CACHE_KEY alone left every per app
134+
* verdict cached forever.
135+
*/
136+
public function testRescanningDropsThePerAppResults() {
137+
$cache = new ArrayCache();
138+
$checker = new Checker(
139+
$this->environmentHelper,
140+
$this->fileAccessHelper,
141+
$this->appLocator,
142+
$this->config,
143+
new FixedCacheFactory($cache),
144+
$this->appManager,
145+
\OC::$server->getTempManager(),
146+
$this->verifier
147+
);
148+
149+
$this->environmentHelper->method('getChannel')->willReturn('stable');
150+
$this->environmentHelper->method('getServerRoot')->willReturn(\OC::$SERVERROOT);
151+
$this->verifier->method('verify')->willReturn(VerificationResult::passed());
152+
153+
$cache->set('SomeApp', '{"SomeApp":[]}');
154+
$cache->set('SomeOtherApp', '{"SomeOtherApp":[]}');
155+
$cache->set(Checker::CACHE_KEY, '{"SomeApp":[]}');
156+
157+
$checker->runInstanceVerification();
158+
159+
// only the results of this run are left - the verification passed, so
160+
// there is nothing to report
161+
$this->assertNull($cache->get('SomeApp'));
162+
$this->assertNull($cache->get('SomeOtherApp'));
163+
$this->assertSame('[]', $cache->get(Checker::CACHE_KEY));
106164
}
107165

108166
public function testIgnoredAppSignatureWithoutSignatureData() {
@@ -630,12 +688,7 @@ public function testVerifyCachedAppSignatureCheck() {
630688
$redisObj->method('get')
631689
->with('SomeApp')
632690
->willReturn('[]');
633-
$cacheFactory = $this->createMock(ICacheFactory::class);
634-
$cacheFactory
635-
->expects($this->any())
636-
->method('create')
637-
->with('oc.integritycheck.checker')
638-
->will($this->returnValue($redisObj));
691+
$cacheFactory = new FixedCacheFactory($redisObj);
639692
$checker = new Checker(
640693
$this->environmentHelper,
641694
$this->fileAccessHelper,
@@ -654,12 +707,7 @@ public function testAppNotCachedSignatureCheck() {
654707
$redisObj->method('get')
655708
->with('SomeApp')
656709
->willReturn(null);
657-
$cacheFactory = $this->createMock(ICacheFactory::class);
658-
$cacheFactory
659-
->expects($this->any())
660-
->method('create')
661-
->with('oc.integritycheck.checker')
662-
->will($this->returnValue($redisObj));
710+
$cacheFactory = new FixedCacheFactory($redisObj);
663711
$checker = new Checker(
664712
$this->environmentHelper,
665713
$this->fileAccessHelper,
@@ -702,10 +750,7 @@ public function testHasPassedCheckWithExceptionResult() {
702750
]
703751
]));
704752

705-
$cacheFactory = $this->createMock(ICacheFactory::class);
706-
$cacheFactory->expects($this->any())
707-
->method('create')
708-
->willReturn(new NullCache());
753+
$cacheFactory = new FixedCacheFactory(new NullCache());
709754

710755
$checker = new Checker(
711756
$this->environmentHelper,
@@ -732,10 +777,7 @@ public function testHasPassedCheckWithEmptyResults() {
732777
->with('core', 'oc.integritycheck.checker', '{}')
733778
->willReturn('{}');
734779

735-
$cacheFactory = $this->createMock(ICacheFactory::class);
736-
$cacheFactory->expects($this->any())
737-
->method('create')
738-
->willReturn(new NullCache());
780+
$cacheFactory = new FixedCacheFactory(new NullCache());
739781

740782
$checker = new Checker(
741783
$this->environmentHelper,
@@ -766,10 +808,7 @@ public function testHasPassedCheckWithFileMissing() {
766808
]
767809
]));
768810

769-
$cacheFactory = $this->createMock(ICacheFactory::class);
770-
$cacheFactory->expects($this->any())
771-
->method('create')
772-
->willReturn(new NullCache());
811+
$cacheFactory = new FixedCacheFactory(new NullCache());
773812

774813
$checker = new Checker(
775814
$this->environmentHelper,

0 commit comments

Comments
 (0)