Context
docker-dev has no ClamAV support today. There is no compose service, no client binary in any PHP image, and no antivirus_clamav wiring in php/includes/config-after.php. Developers who need to work on or test antivirus behaviour currently have to stand up clamd themselves.
The goal is a clamav container that developers opt into with tup clamav, and site config that wires itself up automatically whenever that container is running — matching how maildev and the binary paths are already handled in config-after.php.
Constraints
Two obvious approaches do not work:
tcpsocket is a dead end. server/lib/antivirus/clamav/classes/scanner.php::is_configured() only returns true for commandline and unixsocket; the tcpsocket branch falls through to return false. \core\antivirus\manager::get_enabled() filters on is_configured(), so a TCP-configured plugin is silently never invoked.
- Path-passing methods need the file visible to
clamd. The unixsocket method sends nSCAN <path>. The primary web upload path scans $_FILES[...]['tmp_name'], which lives in the PHP container's /tmp — not in the shared totara-data volume. Making that work would require sharing /tmp across all PHP containers.
--fdpass solves it. Totara adds --fdpass automatically when basename($pathtoclam) === 'clamdscan'. Over a unix socket this passes the open file descriptor via SCM_RIGHTS, so clamd reads the file through the kernel regardless of mount namespace or permissions. This is the intended deployment shape and needs no path sharing. (--fdpass is unavailable over TCP, which is the other reason TCP is out.)
- There is no whitelist problem, at least.
\core\command\executable::get_whitelist() adds get_config('antivirus_clamav', 'pathtoclam') with web execution allowed, so nothing extra is needed there. The template already sets $CFG->preventexecpath = false.
clamav/clamav on Docker Hub is amd64-only (verified via docker manifest inspect on latest, latest_base, 1.4, 1.5). The PR template requires ARM64 compatibility, so we build our own multiarch image using the existing CI machinery.
Assumptions
Scan method: commandline + clamdscan --fdpass over a shared unix socket. Costs two PHP Dockerfile edits and an image rebuild, but avoids sharing /tmp and works for every scan path.
Image source: build our own multiarch ghcr.io/totara/docker-dev-clamav.
Config behaviour: auto-enable when the container is up, with clamfailureonupload => 'donothing' so a still-starting clamd never blocks a dev upload.
Scope
ClamAV support is limited to PHP 8.4 and 8.5. Only php/php84/Dockerfile and php/php85/Dockerfile gain the client, and only the four services php-8.4, php-8.4-debug, php-8.5, and php-8.5-debug in compose/php.yml gain the socket mount. compose/php-legacy.yml needs no change — it contains no 8.x services.
The file_exists() gate in config-after.php scopes itself for free: on 7.3–8.3 containers the socket volume is not mounted, so the socket never appears and the antivirus block stays inert. No version check is needed.
Context
docker-dev has no ClamAV support today. There is no compose service, no client binary in any PHP image, and no antivirus_clamav wiring in php/includes/config-after.php. Developers who need to work on or test antivirus behaviour currently have to stand up clamd themselves.
The goal is a clamav container that developers opt into with tup clamav, and site config that wires itself up automatically whenever that container is running — matching how maildev and the binary paths are already handled in config-after.php.
Constraints
Two obvious approaches do not work:
tcpsocketis a dead end.server/lib/antivirus/clamav/classes/scanner.php::is_configured()only returns true for commandline and unixsocket; the tcpsocket branch falls through to return false.\core\antivirus\manager::get_enabled()filters onis_configured(), so a TCP-configured plugin is silently never invoked.clamd. The unixsocket method sendsnSCAN <path>. The primary web upload path scans$_FILES[...]['tmp_name'], which lives in the PHP container's/tmp— not in the shared totara-data volume. Making that work would require sharing /tmp across all PHP containers.--fdpasssolves it. Totara adds--fdpassautomatically whenbasename($pathtoclam) === 'clamdscan'. Over a unix socket this passes the open file descriptor viaSCM_RIGHTS, soclamdreads the file through the kernel regardless of mount namespace or permissions. This is the intended deployment shape and needs no path sharing. (--fdpassis unavailable over TCP, which is the other reason TCP is out.)\core\command\executable::get_whitelist()addsget_config('antivirus_clamav', 'pathtoclam')with web execution allowed, so nothing extra is needed there. The template already sets$CFG->preventexecpath = false.clamav/clamavon Docker Hub is amd64-only (verified via docker manifest inspect on latest, latest_base, 1.4, 1.5). The PR template requires ARM64 compatibility, so we build our own multiarch image using the existing CI machinery.Assumptions
Scan method: commandline +
clamdscan --fdpassover a shared unix socket. Costs two PHP Dockerfile edits and an image rebuild, but avoids sharing /tmp and works for every scan path.Image source: build our own multiarch ghcr.io/totara/docker-dev-clamav.
Config behaviour: auto-enable when the container is up, with clamfailureonupload => 'donothing' so a still-starting clamd never blocks a dev upload.
Scope
ClamAV support is limited to PHP 8.4 and 8.5. Only php/php84/Dockerfile and php/php85/Dockerfile gain the client, and only the four services php-8.4, php-8.4-debug, php-8.5, and php-8.5-debug in compose/php.yml gain the socket mount. compose/php-legacy.yml needs no change — it contains no 8.x services.
The file_exists() gate in config-after.php scopes itself for free: on 7.3–8.3 containers the socket volume is not mounted, so the socket never appears and the antivirus block stays inert. No version check is needed.