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              
19             use strict;
20 26     26   166 use warnings;
  26         51  
  26         880  
21 26     26   138 # use bytes;
  26         51  
  26         1285  
22             use re 'taint';
23 26     26   142  
  26         47  
  26         1256  
24             use Mail::SpamAssassin;
25 26     26   139 use Mail::SpamAssassin::Locker;
  26         52  
  26         719  
26 26     26   8949 use Mail::SpamAssassin::Util;
  26         70  
  26         769  
27 26     26   155 use Mail::SpamAssassin::Logger;
  26         52  
  26         1375  
28 26     26   129 use File::Spec;
  26         43  
  26         1690  
29 26     26   162 use Time::Local;
  26         38  
  26         751  
30 26     26   122 use Fcntl qw(:DEFAULT :flock);
  26         45  
  26         2172  
31 26     26   143  
  26         56  
  26         12822  
32             our @ISA = qw(Mail::SpamAssassin::Locker);
33              
34             ###########################################################################
35              
36             my $class = shift;
37             my $self = $class->SUPER::new(@_);
38 92     92 0 322 $self;
39 92         607 }
40 92         2530  
41             ###########################################################################
42             # NFS-safe locking (I hope!):
43             # Attempt to create a file lock, using NFS-safe locking techniques.
44             #
45             # Locking code adapted from code by Alexis Rosen <alexis@panix.com>
46             # by Kelsey Cummings <kgc@sonic.net>, with mods by jm and quinlan
47             #
48             # A good implementation of Alexis' code, for reference, is here:
49             # http://mail-index.netbsd.org/netbsd-bugs/1996/04/17/0002.html
50              
51             use constant LOCK_MAX_AGE => 600; # seconds
52              
53 26     26   177 my ($self, $path, $max_retries, $mode) = @_;
  26         51  
  26         30266  
54             my $is_locked = 0;
55             my @stat;
56 36     36 0 141  
57 36         70 $max_retries ||= 30;
58 36         70 $mode ||= "0700";
59             $mode = (oct $mode) & 0666;
60 36   50     110 dbg ("locker: mode is $mode");
61 36   50     108  
62 36         130 my $lock_file = "$path.lock";
63 36         237 my $hname = Mail::SpamAssassin::Util::fq_hostname();
64             my $lock_tmp = Mail::SpamAssassin::Util::untaint_file_path
65 36         99 ($path.".lock.".$hname.".".$$);
66 36         271  
67 36         298 # keep this for unlocking
68             $self->{lock_tmp} = $lock_tmp;
69              
70             my $umask = umask(~$mode);
71 36         215 if (!open(LTMP, ">$lock_tmp")) {
72             umask $umask; # just in case
73 36         289 die "locker: safe_lock: cannot create tmp lockfile $lock_tmp for $lock_file: $!\n";
74 36 50       3433 }
75 0         0 umask $umask;
76 0         0 autoflush LTMP 1;
77             dbg("locker: safe_lock: created $lock_tmp");
78 36         272  
79 36         432 for (my $retries = 0; $retries < $max_retries; $retries++) {
80 36         3480 if ($retries > 0) { $self->jittery_one_second_sleep(); }
81             print LTMP "$hname.$$\n" or warn "Error writing to $lock_tmp: $!";
82 36         180 dbg("locker: safe_lock: trying to get lock on $path with $retries retries");
83 36 50       130 if (link($lock_tmp, $lock_file)) {
  0         0  
84 36 50       1918 dbg("locker: safe_lock: link to $lock_file: link ok");
85 36         318 $is_locked = 1;
86 36 50       1273 last;
87 36         256 }
88 36         69 # link _may_ return false even if the link _is_ created
89 36         113 @stat = lstat($lock_tmp);
90             @stat or warn "locker: error accessing $lock_tmp: $!";
91             if (defined $stat[3] && $stat[3] > 1) {
92 0         0 dbg("locker: safe_lock: link to $lock_file: stat ok");
93 0 0       0 $is_locked = 1;
94 0 0 0     0 last;
95 0         0 }
96 0         0 # check age of lockfile ctime
97 0         0 my $now = ($#stat < 11 ? undef : $stat[10]);
98             @stat = lstat($lock_file);
99             @stat or warn "locker: error accessing $lock_file: $!";
100 0 0       0 my $lock_age = ($#stat < 11 ? undef : $stat[10]);
101 0         0 if (defined($lock_age) && defined($now) && ($now - $lock_age) > LOCK_MAX_AGE)
102 0 0       0 {
103 0 0       0 # we got a stale lock, break it
104 0 0 0     0 dbg("locker: safe_lock: breaking stale $lock_file: age=" .
      0        
105             (defined $lock_age ? $lock_age : "undef") . " now=$now");
106             unlink($lock_file)
107 0 0       0 or warn "locker: safe_lock: unlink of lock file $lock_file failed: $!\n";
108             }
109 0 0       0 }
110              
111             close LTMP or die "error closing $lock_tmp: $!";
112             unlink($lock_tmp)
113             or warn "locker: safe_lock: unlink of temp lock $lock_tmp failed: $!\n";
114 36 50       594  
115 36 50       1184 # record this for safe unlocking
116             if ($is_locked) {
117             @stat = lstat($lock_file);
118             @stat or warn "locker: error accessing $lock_file: $!";
119 36 50       168 my $lock_ctime = ($#stat < 11 ? undef : $stat[10]);
120 36         742  
121 36 50       167 $self->{lock_ctimes} ||= { };
122 36 50       158 $self->{lock_ctimes}->{$path} = $lock_ctime;
123             }
124 36   100     242  
125 36         120 return $is_locked;
126             }
127              
128 36         210 ###########################################################################
129              
130             my ($self, $path) = @_;
131              
132             my $lock_file = "$path.lock";
133             my $lock_tmp = $self->{lock_tmp};
134 36     36 0 120 if (!$lock_tmp) {
135             dbg("locker: safe_unlock: $path.lock never locked");
136 36         126 return;
137 36         92 }
138 36 50       104  
139 0         0 # 1. Build a temp file and stat that to get an idea of what the server
140 0         0 # thinks the current time is (our_tmp.st_ctime). note: do not use time()
141             # directly because the server's clock may be out of sync with the client's.
142              
143             my @stat_ourtmp;
144             if (!defined sysopen(LTMP, $lock_tmp, O_CREAT|O_WRONLY|O_EXCL, 0700)) {
145             warn "locker: safe_unlock: failed to create lock tmpfile $lock_tmp: $!";
146             return;
147 36         79 } else {
148 36 50       3160 autoflush LTMP 1;
149 0         0 print LTMP "\n" or warn "Error writing to $lock_tmp: $!";
150 0         0  
151             if (!(@stat_ourtmp = stat(LTMP)) || (scalar(@stat_ourtmp) < 11)) {
152 36         386 @stat_ourtmp or warn "locker: error accessing $lock_tmp: $!";
153 36 50       3479 warn "locker: safe_unlock: failed to create lock tmpfile $lock_tmp";
154             close LTMP or die "error closing $lock_tmp: $!";
155 36 50 33     753 unlink($lock_tmp)
156 0 0       0 or warn "locker: safe_lock: unlink of lock file failed: $!\n";
157 0         0 return;
158 0 0       0 }
159 0 0       0 }
160            
161 0         0 my $ourtmp_ctime = $stat_ourtmp[10]; # paranoia
162             if (!defined $ourtmp_ctime) {
163             die "locker: safe_unlock: stat failed on $lock_tmp";
164             }
165 36         151  
166 36 50       124 close LTMP or die "error closing $lock_tmp: $!";
167 0         0 unlink($lock_tmp)
168             or warn "locker: safe_lock: unlink of lock file failed: $!\n";
169              
170 36 50       438 # 2. If the ctime hasn't been modified, unlink the file and return. If the
171 36 50       2045 # lock has expired, sleep the usual random interval before returning. If we
172             # didn't sleep, there could be a race if the caller immediately tries to
173             # relock the file.
174              
175             my $lock_ctime = $self->{lock_ctimes}->{$path};
176             if (!defined $lock_ctime) {
177             warn "locker: safe_unlock: no ctime recorded for $lock_file";
178             return;
179 36         186 }
180 36 50       125  
181 0         0 my @stat_lock = lstat($lock_file);
182 0         0 @stat_lock or warn "locker: error accessing $lock_file: $!";
183              
184             my $now_ctime = $stat_lock[10];
185 36         499  
186 36 50       170 if (defined $now_ctime && $now_ctime == $lock_ctime)
187             {
188 36         99 # things are good: the ctimes match so it was our lock
189             unlink($lock_file)
190 36 50 33     219 or warn "locker: safe_unlock: unlink failed: $lock_file\n";
191             dbg("locker: safe_unlock: unlink $lock_file");
192              
193 36 50       1260 if ($ourtmp_ctime >= $lock_ctime + LOCK_MAX_AGE) {
194             # the lock has expired, so sleep a bit; use some randomness
195 36         294 # to avoid race conditions.
196             dbg("locker: safe_unlock: lock expired on $lock_file expired safely; sleeping");
197 36 50       149 my $i; for ($i = 0; $i < 5; $i++) {
198             $self->jittery_one_second_sleep();
199             }
200 0         0 }
201 0         0 return;
  0         0  
202 0         0 }
203              
204             # 4. Either ctime has been modified, or the entire lock file is missing.
205 36         169 # If the lock should still be ours, based on the ctime of the temp
206             # file, warn it was stolen. If not, then our lock is expired and
207             # someone else has grabbed the file, so warn it was lost.
208             if ($ourtmp_ctime < $lock_ctime + LOCK_MAX_AGE) {
209             warn "locker: safe_unlock: lock on $lock_file was stolen";
210             } else {
211             warn "locker: safe_unlock: lock on $lock_file was lost due to expiry";
212 0 0       0 }
213 0         0 }
214              
215 0         0 ###########################################################################
216              
217             my($self, $path) = @_;
218              
219             return unless $path;
220              
221             # this could arguably read the lock and make sure the same process
222 4     4 0 16 # owns it, but this shouldn't, in theory, be an issue.
223             # TODO: in NFS, it definitely may be one :(
224 4 50       15  
225             my $lock_file = "$path.lock";
226             utime time, time, $lock_file;
227              
228             # update the lock_ctimes entry
229             my @stat = lstat($lock_file);
230 4         14 @stat or warn "locker: error accessing $lock_file: $!";
231 4         163  
232             my $lock_ctime = ($#stat < 11 ? undef : $stat[10]);
233             $self->{lock_ctimes}->{$path} = $lock_ctime;
234 4         75  
235 4 50       36 dbg("locker: refresh_lock: refresh $path.lock");
236             }
237 4 50       18  
238 4         17 ###########################################################################
239              
240 4         29 1;