ClamAV broken pipe
A while ago we upgraded ClamAV at $JOB from an fpm package of
dubious-quality to an upstream Ubuntu package. In doing so we found that one
of our applications, which submits data to clamd
over a socket, had
stopped working and was logging Broken pipe
errors.
The following text is a paraphrase of a commit message to one of our internal repositories. It seems that this problem still isn’t well represented by Google-sauce, so hopefully this will be useful to others.
Debugging
After some fiddling a colleague found that we could reproduce this on the command line by streaming content in from a file:
dcarley@preview-asset-master:~$ echo foo > foo
dcarley@preview-asset-master:~$ clamdscan --stream foo
ERROR: Can't send to clamd: Broken pipe
/home/dcarley/foo: no reply from clamd
...
In the midst of testing some theories I restarted the service and suddenly it started working again. I hypothesised that it might be a race condition created by the upgrade process. However I wasn’t able to reproduce problem by performing the same upgrade on a Vagrant VM.
Like many a desperate sysadmin I reached for strace
to inspect the clamd
process while re-running the clamdscan
command, which revealed:
dcarley@preview-asset-master:~$ sudo strace -vfp $(pgrep clamd)
...
[pid 5279] open("/tmp/user/0/clamav-e41b103da0dd009d881b8f4777da7902", O_RDWR|O_CREAT|O_EXCL|O_TRUNC, 0700) = -1 EACCES (Permission denied)
I rewound a little and read up on what ClamAV was actually doing. When
clamd
receives a stream instead of being passed an open file handle, which
is the default behaviour, it buffers the contents to a temporary file and
then reads it back as it’s own leisure. However the /tmp/user/0
directory
that it’s trying to write to is owned by root:root
and the clamd
process
running as clamav:clamav
doesn’t have permission.
It turns out that those /tmp/user/${UID}
directories are created and two
environment variables are set ($TMP
and $TMPDIR
) by PAM’s pam_tmpdir
for new sessions. If TMPDIR
is set then clamd
will use that as it’s
temporary directory. Because we run Puppet from cron
as root
, that
pam_tmpdir
module gets invoked each time and sets the environment
variables, which are passed over to clamd
when Puppet calls
/etc/init.d/clamav-daemon start
.
Reproduction
The reason I wasn’t able to reproduce the same behaviour in a local VM is
that Vagrant’s provisioner for Puppet wraps the command with sudo
, which
doesn’t initiate a new “session” and the default setting of env_reset
prevents the environment variables being passed on from the calling session.
dcarley@preview-asset-master:~$ id -u
2918
dcarley@preview-asset-master:~$ env | grep TMP
TMPDIR=/tmp/user/2918
TMP=/tmp/user/2918
dcarley@preview-asset-master:~$ sudo env | grep TMP
In a similar vein, the reason that restarting the service fixed it was
because I used service clamav-daemon restart
. Even when not using sudo
this wraps the init script with exec env -i
which scrubs all environment
variables from the calling shell.
This is one reason it’s preferable to use service foo start
instead of
/etc/init.d/foo start
. However the Puppet service type on
Debian/Ubuntu, which inherits from a simpler
init provider, doesn’t do this.
The fix
The simplest and most robust workaround for us was to hardset the temporary
directory in clamd.conf(5)
. Setting this ensures that $TMPDIR
never
takes precedence:
TemporaryDirectory /tmp
AppArmor
It’s worth noting that we also experienced similar symptoms during the package upgrade due to AppArmor. Our old dubious-package didn’t contain any AppArmor profiles, but the Ubuntu package does out-of-the-box. These are mostly sensible defaults and you certainly shouldn’t disable them.
The distro defaults for clamd
are stored in
/etc/apparmor.d/usr.sbin.clamd
. You can append to these using
/etc/apparmor.d/local/usr.sbin.clamd
. For example:
# Site-specific additions and overrides for usr.sbin.clamd.
# For more details, please see /etc/apparmor.d/local/README.
# Permit reads from local data in /srv
/srv/** r,