File Coverage

blib/lib/Mail/SpamAssassin/Locker/UnixNFSSafe.pm
Criterion Covered Total %
statement 101 136 74.2
branch 24 72 33.3
condition 6 21 28.5
subroutine 15 15 100.0
pod 0 4 0.0
total 146 248 58.8


line stmt bran cond sub pod time code
1             # <@LICENSE>
2             # Licensed to the Apache Software Foundation (ASF) under one or more
3             # contributor license agreements. See the NOTICE file distributed with
4             # this work for additional information regarding copyright ownership.
5             # The ASF licenses this file to you under the Apache License, Version 2.0
6             # (the "License"); you may not use this file except in compliance with
7             # the License. You may obtain a copy of the License at:
8             #
9             # http://www.apache.org/licenses/LICENSE-2.0
10             #
11             # Unless required by applicable law or agreed to in writing, software
12             # distributed under the License is distributed on an "AS IS" BASIS,
13             # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14             # See the License for the specific language governing permissions and
15             # limitations under the License.
16             # </@LICENSE>
17              
18             package Mail::SpamAssassin::Locker::UnixNFSSafe;
19              
20 25     25   183 use strict;
  25         47  
  25         927  
21 25     25   147 use warnings;
  25         47  
  25         1179  
22             # use bytes;
23 25     25   166 use re 'taint';
  25         67  
  25         1412  
24              
25 25     25   168 use Mail::SpamAssassin;
  25         55  
  25         812  
26 25     25   8558 use Mail::SpamAssassin::Locker;
  25         62  
  25         852  
27 25     25   205 use Mail::SpamAssassin::Util;
  25         45  
  25         1304  
28 25     25   151 use Mail::SpamAssassin::Logger;
  25         46  
  25         2233  
29 25     25   162 use File::Spec;
  25         44  
  25         866  
30 25     25   172 use Time::Local;
  25         44  
  25         2110  
31 25     25   166 use Fcntl qw(:DEFAULT :flock);
  25         66  
  25         13927  
32              
33             our @ISA = qw(Mail::SpamAssassin::Locker);
34              
35             ###########################################################################
36              
37             sub new {
38 91     91 0 307 my $class = shift;
39 91         743 my $self = $class->SUPER::new(@_);
40 91         2903 $self;
41             }
42              
43             ###########################################################################
44             # NFS-safe locking (I hope!):
45             # Attempt to create a file lock, using NFS-safe locking techniques.
46             #
47             # Locking code adapted from code by Alexis Rosen <alexis@panix.com>
48             # by Kelsey Cummings <kgc@sonic.net>, with mods by jm and quinlan
49             #
50             # A good implementation of Alexis' code, for reference, is here:
51             # http://mail-index.netbsd.org/netbsd-bugs/1996/04/17/0002.html
52              
53 25     25   208 use constant LOCK_MAX_AGE => 600; # seconds
  25         48  
  25         33608  
54              
55             sub safe_lock {
56 36     36 0 151 my ($self, $path, $max_retries, $mode) = @_;
57 36         82 my $is_locked = 0;
58 36         73 my @stat;
59              
60 36   50     122 $max_retries ||= 30;
61 36   50     101 $mode ||= "0700";
62 36         120 $mode = (oct $mode) & 0666;
63 36         196 dbg ("locker: mode is $mode");
64              
65 36         105 my $lock_file = "$path.lock";
66 36         174 my $hname = Mail::SpamAssassin::Util::fq_hostname();
67 36         253 my $lock_tmp = Mail::SpamAssassin::Util::untaint_file_path
68             ($path.".lock.".$hname.".".$$);
69              
70             # keep this for unlocking
71 36         214 $self->{lock_tmp} = $lock_tmp;
72              
73 36         280 my $umask = umask(~$mode);
74 36 50       2791 if (!open(LTMP, ">$lock_tmp")) {
75 0         0 umask $umask; # just in case
76 0         0 die "locker: safe_lock: cannot create tmp lockfile $lock_tmp for $lock_file: $!\n";
77             }
78 36         272 umask $umask;
79 36         403 autoflush LTMP 1;
80 36         2115 dbg("locker: safe_lock: created $lock_tmp");
81              
82 36         176 for (my $retries = 0; $retries < $max_retries; $retries++) {
83 36 50       117 if ($retries > 0) { $self->jittery_one_second_sleep(); }
  0         0  
84 36 50       1510 print LTMP "$hname.$$\n" or warn "Error writing to $lock_tmp: $!";
85 36         346 dbg("locker: safe_lock: trying to get lock on $path with $retries retries");
86 36 50       1367 if (link($lock_tmp, $lock_file)) {
87 36         284 dbg("locker: safe_lock: link to $lock_file: link ok");
88 36         83 $is_locked = 1;
89 36         94 last;
90             }
91             # link _may_ return false even if the link _is_ created
92 0         0 @stat = lstat($lock_tmp);
93 0 0       0 @stat or warn "locker: error accessing $lock_tmp: $!";
94 0 0 0     0 if (defined $stat[3] && $stat[3] > 1) {
95 0         0 dbg("locker: safe_lock: link to $lock_file: stat ok");
96 0         0 $is_locked = 1;
97 0         0 last;
98             }
99             # check age of lockfile ctime
100 0 0       0 my $now = ($#stat < 11 ? undef : $stat[10]);
101 0         0 @stat = lstat($lock_file);
102 0 0       0 @stat or warn "locker: error accessing $lock_file: $!";
103 0 0       0 my $lock_age = ($#stat < 11 ? undef : $stat[10]);
104 0 0 0     0 if (defined($lock_age) && defined($now) && ($now - $lock_age) > LOCK_MAX_AGE)
      0        
105             {
106             # we got a stale lock, break it
107 0 0       0 dbg("locker: safe_lock: breaking stale $lock_file: age=" .
108             (defined $lock_age ? $lock_age : "undef") . " now=$now");
109 0 0       0 unlink($lock_file)
110             or warn "locker: safe_lock: unlink of lock file $lock_file failed: $!\n";
111             }
112             }
113              
114 36 50       864 close LTMP or die "error closing $lock_tmp: $!";
115 36 50       1227 unlink($lock_tmp)
116             or warn "locker: safe_lock: unlink of temp lock $lock_tmp failed: $!\n";
117              
118             # record this for safe unlocking
119 36 50       146 if ($is_locked) {
120 36         652 @stat = lstat($lock_file);
121 36 50       162 @stat or warn "locker: error accessing $lock_file: $!";
122 36 50       156 my $lock_ctime = ($#stat < 11 ? undef : $stat[10]);
123              
124 36   100     235 $self->{lock_ctimes} ||= { };
125 36         106 $self->{lock_ctimes}->{$path} = $lock_ctime;
126             }
127              
128 36         243 return $is_locked;
129             }
130              
131             ###########################################################################
132              
133             sub safe_unlock {
134 36     36 0 115 my ($self, $path) = @_;
135              
136 36         105 my $lock_file = "$path.lock";
137 36         91 my $lock_tmp = $self->{lock_tmp};
138 36 50       121 if (!$lock_tmp) {
139 0         0 dbg("locker: safe_unlock: $path.lock never locked");
140 0         0 return;
141             }
142              
143             # 1. Build a temp file and stat that to get an idea of what the server
144             # thinks the current time is (our_tmp.st_ctime). note: do not use time()
145             # directly because the server's clock may be out of sync with the client's.
146              
147 36         76 my @stat_ourtmp;
148 36 50       2617 if (!defined sysopen(LTMP, $lock_tmp, O_CREAT|O_WRONLY|O_EXCL, 0700)) {
149 0         0 warn "locker: safe_unlock: failed to create lock tmpfile $lock_tmp: $!";
150 0         0 return;
151             } else {
152 36         325 autoflush LTMP 1;
153 36 50       3261 print LTMP "\n" or warn "Error writing to $lock_tmp: $!";
154              
155 36 50 33     752 if (!(@stat_ourtmp = stat(LTMP)) || (scalar(@stat_ourtmp) < 11)) {
156 0 0       0 @stat_ourtmp or warn "locker: error accessing $lock_tmp: $!";
157 0         0 warn "locker: safe_unlock: failed to create lock tmpfile $lock_tmp";
158 0 0       0 close LTMP or die "error closing $lock_tmp: $!";
159 0 0       0 unlink($lock_tmp)
160             or warn "locker: safe_lock: unlink of lock file failed: $!\n";
161 0         0 return;
162             }
163             }
164            
165 36         141 my $ourtmp_ctime = $stat_ourtmp[10]; # paranoia
166 36 50       120 if (!defined $ourtmp_ctime) {
167 0         0 die "locker: safe_unlock: stat failed on $lock_tmp";
168             }
169              
170 36 50       408 close LTMP or die "error closing $lock_tmp: $!";
171 36 50       1894 unlink($lock_tmp)
172             or warn "locker: safe_lock: unlink of lock file failed: $!\n";
173              
174             # 2. If the ctime hasn't been modified, unlink the file and return. If the
175             # lock has expired, sleep the usual random interval before returning. If we
176             # didn't sleep, there could be a race if the caller immediately tries to
177             # relock the file.
178              
179 36         172 my $lock_ctime = $self->{lock_ctimes}->{$path};
180 36 50       106 if (!defined $lock_ctime) {
181 0         0 warn "locker: safe_unlock: no ctime recorded for $lock_file";
182 0         0 return;
183             }
184              
185 36         591 my @stat_lock = lstat($lock_file);
186 36 50       162 @stat_lock or warn "locker: error accessing $lock_file: $!";
187              
188 36         90 my $now_ctime = $stat_lock[10];
189              
190 36 50 33     225 if (defined $now_ctime && $now_ctime == $lock_ctime)
191             {
192             # things are good: the ctimes match so it was our lock
193 36 50       1321 unlink($lock_file)
194             or warn "locker: safe_unlock: unlink failed: $lock_file\n";
195 36         309 dbg("locker: safe_unlock: unlink $lock_file");
196              
197 36 50       134 if ($ourtmp_ctime >= $lock_ctime + LOCK_MAX_AGE) {
198             # the lock has expired, so sleep a bit; use some randomness
199             # to avoid race conditions.
200 0         0 dbg("locker: safe_unlock: lock expired on $lock_file expired safely; sleeping");
201 0         0 my $i; for ($i = 0; $i < 5; $i++) {
  0         0  
202 0         0 $self->jittery_one_second_sleep();
203             }
204             }
205 36         196 return;
206             }
207              
208             # 4. Either ctime has been modified, or the entire lock file is missing.
209             # If the lock should still be ours, based on the ctime of the temp
210             # file, warn it was stolen. If not, then our lock is expired and
211             # someone else has grabbed the file, so warn it was lost.
212 0 0       0 if ($ourtmp_ctime < $lock_ctime + LOCK_MAX_AGE) {
213 0         0 warn "locker: safe_unlock: lock on $lock_file was stolen";
214             } else {
215 0         0 warn "locker: safe_unlock: lock on $lock_file was lost due to expiry";
216             }
217             }
218              
219             ###########################################################################
220              
221             sub refresh_lock {
222 4     4 0 15 my($self, $path) = @_;
223              
224 4 50       18 return unless $path;
225              
226             # this could arguably read the lock and make sure the same process
227             # owns it, but this shouldn't, in theory, be an issue.
228             # TODO: in NFS, it definitely may be one :(
229              
230 4         15 my $lock_file = "$path.lock";
231 4         117 utime time, time, $lock_file;
232              
233             # update the lock_ctimes entry
234 4         72 my @stat = lstat($lock_file);
235 4 50       24 @stat or warn "locker: error accessing $lock_file: $!";
236              
237 4 50       20 my $lock_ctime = ($#stat < 11 ? undef : $stat[10]);
238 4         14 $self->{lock_ctimes}->{$path} = $lock_ctime;
239              
240 4         32 dbg("locker: refresh_lock: refresh $path.lock");
241             }
242              
243             ###########################################################################
244              
245             1;