Fri, 16 Jan 2015 18:13:44 +0100
Integrate suggestion from review to improve consistency with existing code.
michael@0 | 1 | /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
michael@0 | 2 | /* vim: se cin sw=2 ts=2 et : */ |
michael@0 | 3 | /* This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 4 | * License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 5 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 6 | |
michael@0 | 7 | #include "nsDownloadScanner.h" |
michael@0 | 8 | #include <comcat.h> |
michael@0 | 9 | #include <process.h> |
michael@0 | 10 | #include "nsDownloadManager.h" |
michael@0 | 11 | #include "nsIXULAppInfo.h" |
michael@0 | 12 | #include "nsXULAppAPI.h" |
michael@0 | 13 | #include "nsIPrefService.h" |
michael@0 | 14 | #include "nsNetUtil.h" |
michael@0 | 15 | #include "nsDeque.h" |
michael@0 | 16 | #include "nsIFileURL.h" |
michael@0 | 17 | #include "nsIPrefBranch.h" |
michael@0 | 18 | #include "nsXPCOMCIDInternal.h" |
michael@0 | 19 | |
michael@0 | 20 | /** |
michael@0 | 21 | * Code overview |
michael@0 | 22 | * |
michael@0 | 23 | * Download scanner attempts to make use of one of two different virus |
michael@0 | 24 | * scanning interfaces available on Windows - IOfficeAntiVirus (Windows |
michael@0 | 25 | * 95/NT 4 and IE 5) and IAttachmentExecute (XPSP2 and up). The latter |
michael@0 | 26 | * interface supports calling IOfficeAntiVirus internally, while also |
michael@0 | 27 | * adding support for XPSP2+ ADS forks which define security related |
michael@0 | 28 | * prompting on downloaded content. |
michael@0 | 29 | * |
michael@0 | 30 | * Both interfaces are synchronous and can take a while, so it is not a |
michael@0 | 31 | * good idea to call either from the main thread. Some antivirus scanners can |
michael@0 | 32 | * take a long time to scan or the call might block while the scanner shows |
michael@0 | 33 | * its UI so if the user were to download many files that finished around the |
michael@0 | 34 | * same time, they would have to wait a while if the scanning were done on |
michael@0 | 35 | * exactly one other thread. Since the overhead of creating a thread is |
michael@0 | 36 | * relatively small compared to the time it takes to download a file and scan |
michael@0 | 37 | * it, a new thread is spawned for each download that is to be scanned. Since |
michael@0 | 38 | * most of the mozilla codebase is not threadsafe, all the information needed |
michael@0 | 39 | * for the scanner is gathered in the main thread in nsDownloadScanner::Scan::Start. |
michael@0 | 40 | * The only function of nsDownloadScanner::Scan which is invoked on another |
michael@0 | 41 | * thread is DoScan. |
michael@0 | 42 | * |
michael@0 | 43 | * Watchdog overview |
michael@0 | 44 | * |
michael@0 | 45 | * The watchdog is used internally by the scanner. It maintains a queue of |
michael@0 | 46 | * current download scans. In a separate thread, it dequeues the oldest scan |
michael@0 | 47 | * and waits on that scan's thread with a timeout given by WATCHDOG_TIMEOUT |
michael@0 | 48 | * (default is 30 seconds). If the wait times out, then the watchdog notifies |
michael@0 | 49 | * the Scan that it has timed out. If the scan really has timed out, then the |
michael@0 | 50 | * Scan object will dispatch its run method to the main thread; this will |
michael@0 | 51 | * release the watchdog thread's addref on the Scan. If it has not timed out |
michael@0 | 52 | * (i.e. the Scan just finished in time), then the watchdog dispatches a |
michael@0 | 53 | * ReleaseDispatcher to release its ref of the Scan on the main thread. |
michael@0 | 54 | * |
michael@0 | 55 | * In order to minimize execution time, there are two events used to notify the |
michael@0 | 56 | * watchdog thread of a non-empty queue and a quit event. Every blocking wait |
michael@0 | 57 | * that the watchdog thread does waits on the quit event; this lets the thread |
michael@0 | 58 | * quickly exit when shutting down. Also, the download scan queue will be empty |
michael@0 | 59 | * most of the time; rather than use a spin loop, a simple event is triggered |
michael@0 | 60 | * by the main thread when a new scan is added to an empty queue. When the |
michael@0 | 61 | * watchdog thread knows that it has run out of elements in the queue, it will |
michael@0 | 62 | * wait on the new item event. |
michael@0 | 63 | * |
michael@0 | 64 | * Memory/resource leaks due to timeout: |
michael@0 | 65 | * In the event of a timeout, the thread must remain alive; terminating it may |
michael@0 | 66 | * very well cause the antivirus scanner to crash or be put into an |
michael@0 | 67 | * inconsistent state; COM resources may also not be cleaned up. The downside |
michael@0 | 68 | * is that we need to leave the thread running; suspending it may lead to a |
michael@0 | 69 | * deadlock. Because the scan call may be ongoing, it may be dependent on the |
michael@0 | 70 | * memory referenced by the MSOAVINFO structure, so we cannot free mName, mPath |
michael@0 | 71 | * or mOrigin; this means that we cannot free the Scan object since doing so |
michael@0 | 72 | * will deallocate that memory. Note that mDownload is set to null upon timeout |
michael@0 | 73 | * or completion, so the download itself is never leaked. If the scan does |
michael@0 | 74 | * eventually complete, then the all the memory and resources will be freed. |
michael@0 | 75 | * It is possible, however extremely rare, that in the event of a timeout, the |
michael@0 | 76 | * mStateSync critical section will leak its event; this will happen only if |
michael@0 | 77 | * the scanning thread, watchdog thread or main thread try to enter the |
michael@0 | 78 | * critical section when one of the others is already in it. |
michael@0 | 79 | * |
michael@0 | 80 | * Reasoning for CheckAndSetState - there exists a race condition between the time when |
michael@0 | 81 | * either the timeout or normal scan sets the state and when Scan::Run is |
michael@0 | 82 | * executed on the main thread. Ex: mStatus could be set by Scan::DoScan* which |
michael@0 | 83 | * then queues a dispatch on the main thread. Before that dispatch is executed, |
michael@0 | 84 | * the timeout code fires and sets mStatus to AVSCAN_TIMEDOUT which then queues |
michael@0 | 85 | * its dispatch to the main thread (the same function as DoScan*). Both |
michael@0 | 86 | * dispatches run and both try to set the download state to AVSCAN_TIMEDOUT |
michael@0 | 87 | * which is incorrect. |
michael@0 | 88 | * |
michael@0 | 89 | * There are 5 possible outcomes of the virus scan: |
michael@0 | 90 | * AVSCAN_GOOD => the file is clean |
michael@0 | 91 | * AVSCAN_BAD => the file has a virus |
michael@0 | 92 | * AVSCAN_UGLY => the file had a virus, but it was cleaned |
michael@0 | 93 | * AVSCAN_FAILED => something else went wrong with the virus scanner. |
michael@0 | 94 | * AVSCAN_TIMEDOUT => the scan (thread setup + execution) took too long |
michael@0 | 95 | * |
michael@0 | 96 | * Both the good and ugly states leave the user with a benign file, so they |
michael@0 | 97 | * transition to the finished state. Bad files are sent to the blocked state. |
michael@0 | 98 | * The failed and timedout states transition to finished downloads. |
michael@0 | 99 | * |
michael@0 | 100 | * Possible Future enhancements: |
michael@0 | 101 | * * Create an interface for scanning files in general |
michael@0 | 102 | * * Make this a service |
michael@0 | 103 | * * Get antivirus scanner status via WMI/registry |
michael@0 | 104 | */ |
michael@0 | 105 | |
michael@0 | 106 | // IAttachementExecute supports user definable settings for certain |
michael@0 | 107 | // security related prompts. This defines a general GUID for use in |
michael@0 | 108 | // all projects. Individual projects can define an individual guid |
michael@0 | 109 | // if they want to. |
michael@0 | 110 | #ifndef MOZ_VIRUS_SCANNER_PROMPT_GUID |
michael@0 | 111 | #define MOZ_VIRUS_SCANNER_PROMPT_GUID \ |
michael@0 | 112 | { 0xb50563d1, 0x16b6, 0x43c2, { 0xa6, 0x6a, 0xfa, 0xe6, 0xd2, 0x11, 0xf2, \ |
michael@0 | 113 | 0xea } } |
michael@0 | 114 | #endif |
michael@0 | 115 | static const GUID GUID_MozillaVirusScannerPromptGeneric = |
michael@0 | 116 | MOZ_VIRUS_SCANNER_PROMPT_GUID; |
michael@0 | 117 | |
michael@0 | 118 | // Initial timeout is 30 seconds |
michael@0 | 119 | #define WATCHDOG_TIMEOUT (30*PR_USEC_PER_SEC) |
michael@0 | 120 | |
michael@0 | 121 | // Maximum length for URI's passed into IAE |
michael@0 | 122 | #define MAX_IAEURILENGTH 1683 |
michael@0 | 123 | |
michael@0 | 124 | class nsDownloadScannerWatchdog |
michael@0 | 125 | { |
michael@0 | 126 | typedef nsDownloadScanner::Scan Scan; |
michael@0 | 127 | public: |
michael@0 | 128 | nsDownloadScannerWatchdog(); |
michael@0 | 129 | ~nsDownloadScannerWatchdog(); |
michael@0 | 130 | |
michael@0 | 131 | nsresult Init(); |
michael@0 | 132 | nsresult Shutdown(); |
michael@0 | 133 | |
michael@0 | 134 | void Watch(Scan *scan); |
michael@0 | 135 | private: |
michael@0 | 136 | static unsigned int __stdcall WatchdogThread(void *p); |
michael@0 | 137 | CRITICAL_SECTION mQueueSync; |
michael@0 | 138 | nsDeque mScanQueue; |
michael@0 | 139 | HANDLE mThread; |
michael@0 | 140 | HANDLE mNewItemEvent; |
michael@0 | 141 | HANDLE mQuitEvent; |
michael@0 | 142 | }; |
michael@0 | 143 | |
michael@0 | 144 | nsDownloadScanner::nsDownloadScanner() : |
michael@0 | 145 | mAESExists(false) |
michael@0 | 146 | { |
michael@0 | 147 | } |
michael@0 | 148 | |
michael@0 | 149 | // This destructor appeases the compiler; it would otherwise complain about an |
michael@0 | 150 | // incomplete type for nsDownloadWatchdog in the instantiation of |
michael@0 | 151 | // nsAutoPtr::~nsAutoPtr |
michael@0 | 152 | // Plus, it's a handy location to call nsDownloadScannerWatchdog::Shutdown from |
michael@0 | 153 | nsDownloadScanner::~nsDownloadScanner() { |
michael@0 | 154 | if (mWatchdog) |
michael@0 | 155 | (void)mWatchdog->Shutdown(); |
michael@0 | 156 | } |
michael@0 | 157 | |
michael@0 | 158 | nsresult |
michael@0 | 159 | nsDownloadScanner::Init() |
michael@0 | 160 | { |
michael@0 | 161 | // This CoInitialize/CoUninitialize pattern seems to be common in the Mozilla |
michael@0 | 162 | // codebase. All other COM calls/objects are made on different threads. |
michael@0 | 163 | nsresult rv = NS_OK; |
michael@0 | 164 | CoInitialize(nullptr); |
michael@0 | 165 | |
michael@0 | 166 | if (!IsAESAvailable()) { |
michael@0 | 167 | CoUninitialize(); |
michael@0 | 168 | return NS_ERROR_NOT_AVAILABLE; |
michael@0 | 169 | } |
michael@0 | 170 | |
michael@0 | 171 | mAESExists = true; |
michael@0 | 172 | |
michael@0 | 173 | // Initialize scanning |
michael@0 | 174 | mWatchdog = new nsDownloadScannerWatchdog(); |
michael@0 | 175 | if (mWatchdog) { |
michael@0 | 176 | rv = mWatchdog->Init(); |
michael@0 | 177 | if (FAILED(rv)) |
michael@0 | 178 | mWatchdog = nullptr; |
michael@0 | 179 | } else { |
michael@0 | 180 | rv = NS_ERROR_OUT_OF_MEMORY; |
michael@0 | 181 | } |
michael@0 | 182 | |
michael@0 | 183 | if (NS_FAILED(rv)) |
michael@0 | 184 | return rv; |
michael@0 | 185 | |
michael@0 | 186 | return rv; |
michael@0 | 187 | } |
michael@0 | 188 | |
michael@0 | 189 | bool |
michael@0 | 190 | nsDownloadScanner::IsAESAvailable() |
michael@0 | 191 | { |
michael@0 | 192 | // Try to instantiate IAE to see if it's available. |
michael@0 | 193 | nsRefPtr<IAttachmentExecute> ae; |
michael@0 | 194 | HRESULT hr; |
michael@0 | 195 | hr = CoCreateInstance(CLSID_AttachmentServices, nullptr, CLSCTX_INPROC, |
michael@0 | 196 | IID_IAttachmentExecute, getter_AddRefs(ae)); |
michael@0 | 197 | if (FAILED(hr)) { |
michael@0 | 198 | NS_WARNING("Could not instantiate attachment execution service\n"); |
michael@0 | 199 | return false; |
michael@0 | 200 | } |
michael@0 | 201 | return true; |
michael@0 | 202 | } |
michael@0 | 203 | |
michael@0 | 204 | // If IAttachementExecute is available, use the CheckPolicy call to find out |
michael@0 | 205 | // if this download should be prevented due to Security Zone Policy settings. |
michael@0 | 206 | AVCheckPolicyState |
michael@0 | 207 | nsDownloadScanner::CheckPolicy(nsIURI *aSource, nsIURI *aTarget) |
michael@0 | 208 | { |
michael@0 | 209 | nsresult rv; |
michael@0 | 210 | |
michael@0 | 211 | if (!mAESExists || !aSource || !aTarget) |
michael@0 | 212 | return AVPOLICY_DOWNLOAD; |
michael@0 | 213 | |
michael@0 | 214 | nsAutoCString source; |
michael@0 | 215 | rv = aSource->GetSpec(source); |
michael@0 | 216 | if (NS_FAILED(rv)) |
michael@0 | 217 | return AVPOLICY_DOWNLOAD; |
michael@0 | 218 | |
michael@0 | 219 | nsCOMPtr<nsIFileURL> fileUrl(do_QueryInterface(aTarget)); |
michael@0 | 220 | if (!fileUrl) |
michael@0 | 221 | return AVPOLICY_DOWNLOAD; |
michael@0 | 222 | |
michael@0 | 223 | nsCOMPtr<nsIFile> theFile; |
michael@0 | 224 | nsAutoString aFileName; |
michael@0 | 225 | if (NS_FAILED(fileUrl->GetFile(getter_AddRefs(theFile))) || |
michael@0 | 226 | NS_FAILED(theFile->GetLeafName(aFileName))) |
michael@0 | 227 | return AVPOLICY_DOWNLOAD; |
michael@0 | 228 | |
michael@0 | 229 | // IAttachementExecute prohibits src data: schemes by default but we |
michael@0 | 230 | // support them. If this is a data src, skip off doing a policy check. |
michael@0 | 231 | // (The file will still be scanned once it lands on the local system.) |
michael@0 | 232 | bool isDataScheme(false); |
michael@0 | 233 | nsCOMPtr<nsIURI> innerURI = NS_GetInnermostURI(aSource); |
michael@0 | 234 | if (innerURI) |
michael@0 | 235 | (void)innerURI->SchemeIs("data", &isDataScheme); |
michael@0 | 236 | if (isDataScheme) |
michael@0 | 237 | return AVPOLICY_DOWNLOAD; |
michael@0 | 238 | |
michael@0 | 239 | nsRefPtr<IAttachmentExecute> ae; |
michael@0 | 240 | HRESULT hr; |
michael@0 | 241 | hr = CoCreateInstance(CLSID_AttachmentServices, nullptr, CLSCTX_INPROC, |
michael@0 | 242 | IID_IAttachmentExecute, getter_AddRefs(ae)); |
michael@0 | 243 | if (FAILED(hr)) |
michael@0 | 244 | return AVPOLICY_DOWNLOAD; |
michael@0 | 245 | |
michael@0 | 246 | (void)ae->SetClientGuid(GUID_MozillaVirusScannerPromptGeneric); |
michael@0 | 247 | (void)ae->SetSource(NS_ConvertUTF8toUTF16(source).get()); |
michael@0 | 248 | (void)ae->SetFileName(aFileName.get()); |
michael@0 | 249 | |
michael@0 | 250 | // Any failure means the file download/exec will be blocked by the system. |
michael@0 | 251 | // S_OK or S_FALSE imply it's ok. |
michael@0 | 252 | hr = ae->CheckPolicy(); |
michael@0 | 253 | |
michael@0 | 254 | if (hr == S_OK) |
michael@0 | 255 | return AVPOLICY_DOWNLOAD; |
michael@0 | 256 | |
michael@0 | 257 | if (hr == S_FALSE) |
michael@0 | 258 | return AVPOLICY_PROMPT; |
michael@0 | 259 | |
michael@0 | 260 | if (hr == E_INVALIDARG) |
michael@0 | 261 | return AVPOLICY_PROMPT; |
michael@0 | 262 | |
michael@0 | 263 | return AVPOLICY_BLOCKED; |
michael@0 | 264 | } |
michael@0 | 265 | |
michael@0 | 266 | #ifndef THREAD_MODE_BACKGROUND_BEGIN |
michael@0 | 267 | #define THREAD_MODE_BACKGROUND_BEGIN 0x00010000 |
michael@0 | 268 | #endif |
michael@0 | 269 | |
michael@0 | 270 | #ifndef THREAD_MODE_BACKGROUND_END |
michael@0 | 271 | #define THREAD_MODE_BACKGROUND_END 0x00020000 |
michael@0 | 272 | #endif |
michael@0 | 273 | |
michael@0 | 274 | unsigned int __stdcall |
michael@0 | 275 | nsDownloadScanner::ScannerThreadFunction(void *p) |
michael@0 | 276 | { |
michael@0 | 277 | HANDLE currentThread = GetCurrentThread(); |
michael@0 | 278 | NS_ASSERTION(!NS_IsMainThread(), "Antivirus scan should not be run on the main thread"); |
michael@0 | 279 | nsDownloadScanner::Scan *scan = static_cast<nsDownloadScanner::Scan*>(p); |
michael@0 | 280 | if (!SetThreadPriority(currentThread, THREAD_MODE_BACKGROUND_BEGIN)) |
michael@0 | 281 | (void)SetThreadPriority(currentThread, THREAD_PRIORITY_IDLE); |
michael@0 | 282 | scan->DoScan(); |
michael@0 | 283 | (void)SetThreadPriority(currentThread, THREAD_MODE_BACKGROUND_END); |
michael@0 | 284 | _endthreadex(0); |
michael@0 | 285 | return 0; |
michael@0 | 286 | } |
michael@0 | 287 | |
michael@0 | 288 | // The sole purpose of this class is to release an object on the main thread |
michael@0 | 289 | // It assumes that its creator will addref it and it will release itself on |
michael@0 | 290 | // the main thread too |
michael@0 | 291 | class ReleaseDispatcher : public nsRunnable { |
michael@0 | 292 | public: |
michael@0 | 293 | ReleaseDispatcher(nsISupports *ptr) |
michael@0 | 294 | : mPtr(ptr) {} |
michael@0 | 295 | NS_IMETHOD Run(); |
michael@0 | 296 | private: |
michael@0 | 297 | nsISupports *mPtr; |
michael@0 | 298 | }; |
michael@0 | 299 | |
michael@0 | 300 | nsresult ReleaseDispatcher::Run() { |
michael@0 | 301 | NS_ASSERTION(NS_IsMainThread(), "Antivirus scan release dispatch should be run on the main thread"); |
michael@0 | 302 | NS_RELEASE(mPtr); |
michael@0 | 303 | NS_RELEASE_THIS(); |
michael@0 | 304 | return NS_OK; |
michael@0 | 305 | } |
michael@0 | 306 | |
michael@0 | 307 | nsDownloadScanner::Scan::Scan(nsDownloadScanner *scanner, nsDownload *download) |
michael@0 | 308 | : mDLScanner(scanner), mThread(nullptr), |
michael@0 | 309 | mDownload(download), mStatus(AVSCAN_NOTSTARTED), |
michael@0 | 310 | mSkipSource(false) |
michael@0 | 311 | { |
michael@0 | 312 | InitializeCriticalSection(&mStateSync); |
michael@0 | 313 | } |
michael@0 | 314 | |
michael@0 | 315 | nsDownloadScanner::Scan::~Scan() { |
michael@0 | 316 | DeleteCriticalSection(&mStateSync); |
michael@0 | 317 | } |
michael@0 | 318 | |
michael@0 | 319 | nsresult |
michael@0 | 320 | nsDownloadScanner::Scan::Start() |
michael@0 | 321 | { |
michael@0 | 322 | mStartTime = PR_Now(); |
michael@0 | 323 | |
michael@0 | 324 | mThread = (HANDLE)_beginthreadex(nullptr, 0, ScannerThreadFunction, |
michael@0 | 325 | this, CREATE_SUSPENDED, nullptr); |
michael@0 | 326 | if (!mThread) |
michael@0 | 327 | return NS_ERROR_OUT_OF_MEMORY; |
michael@0 | 328 | |
michael@0 | 329 | nsresult rv = NS_OK; |
michael@0 | 330 | |
michael@0 | 331 | // Get the path to the file on disk |
michael@0 | 332 | nsCOMPtr<nsIFile> file; |
michael@0 | 333 | rv = mDownload->GetTargetFile(getter_AddRefs(file)); |
michael@0 | 334 | NS_ENSURE_SUCCESS(rv, rv); |
michael@0 | 335 | rv = file->GetPath(mPath); |
michael@0 | 336 | NS_ENSURE_SUCCESS(rv, rv); |
michael@0 | 337 | |
michael@0 | 338 | // Grab the app name |
michael@0 | 339 | nsCOMPtr<nsIXULAppInfo> appinfo = |
michael@0 | 340 | do_GetService(XULAPPINFO_SERVICE_CONTRACTID, &rv); |
michael@0 | 341 | NS_ENSURE_SUCCESS(rv, rv); |
michael@0 | 342 | |
michael@0 | 343 | nsAutoCString name; |
michael@0 | 344 | rv = appinfo->GetName(name); |
michael@0 | 345 | NS_ENSURE_SUCCESS(rv, rv); |
michael@0 | 346 | CopyUTF8toUTF16(name, mName); |
michael@0 | 347 | |
michael@0 | 348 | // Get the origin |
michael@0 | 349 | nsCOMPtr<nsIURI> uri; |
michael@0 | 350 | rv = mDownload->GetSource(getter_AddRefs(uri)); |
michael@0 | 351 | NS_ENSURE_SUCCESS(rv, rv); |
michael@0 | 352 | |
michael@0 | 353 | nsAutoCString origin; |
michael@0 | 354 | rv = uri->GetSpec(origin); |
michael@0 | 355 | NS_ENSURE_SUCCESS(rv, rv); |
michael@0 | 356 | |
michael@0 | 357 | // Certain virus interfaces do not like extremely long uris. |
michael@0 | 358 | // Chop off the path and cgi data and just pass the base domain. |
michael@0 | 359 | if (origin.Length() > MAX_IAEURILENGTH) { |
michael@0 | 360 | rv = uri->GetPrePath(origin); |
michael@0 | 361 | NS_ENSURE_SUCCESS(rv, rv); |
michael@0 | 362 | } |
michael@0 | 363 | |
michael@0 | 364 | CopyUTF8toUTF16(origin, mOrigin); |
michael@0 | 365 | |
michael@0 | 366 | // We count https/ftp/http as an http download |
michael@0 | 367 | bool isHttp(false), isFtp(false), isHttps(false); |
michael@0 | 368 | nsCOMPtr<nsIURI> innerURI = NS_GetInnermostURI(uri); |
michael@0 | 369 | if (!innerURI) innerURI = uri; |
michael@0 | 370 | (void)innerURI->SchemeIs("http", &isHttp); |
michael@0 | 371 | (void)innerURI->SchemeIs("ftp", &isFtp); |
michael@0 | 372 | (void)innerURI->SchemeIs("https", &isHttps); |
michael@0 | 373 | mIsHttpDownload = isHttp || isFtp || isHttps; |
michael@0 | 374 | |
michael@0 | 375 | // IAttachementExecute prohibits src data: schemes by default but we |
michael@0 | 376 | // support them. Mark the download if it's a data scheme, so we |
michael@0 | 377 | // can skip off supplying the src to IAttachementExecute when we scan |
michael@0 | 378 | // the resulting file. |
michael@0 | 379 | (void)innerURI->SchemeIs("data", &mSkipSource); |
michael@0 | 380 | |
michael@0 | 381 | // ResumeThread returns the previous suspend count |
michael@0 | 382 | if (1 != ::ResumeThread(mThread)) { |
michael@0 | 383 | CloseHandle(mThread); |
michael@0 | 384 | return NS_ERROR_UNEXPECTED; |
michael@0 | 385 | } |
michael@0 | 386 | return NS_OK; |
michael@0 | 387 | } |
michael@0 | 388 | |
michael@0 | 389 | nsresult |
michael@0 | 390 | nsDownloadScanner::Scan::Run() |
michael@0 | 391 | { |
michael@0 | 392 | NS_ASSERTION(NS_IsMainThread(), "Antivirus scan dispatch should be run on the main thread"); |
michael@0 | 393 | |
michael@0 | 394 | // Cleanup our thread |
michael@0 | 395 | if (mStatus != AVSCAN_TIMEDOUT) |
michael@0 | 396 | WaitForSingleObject(mThread, INFINITE); |
michael@0 | 397 | CloseHandle(mThread); |
michael@0 | 398 | |
michael@0 | 399 | DownloadState downloadState = 0; |
michael@0 | 400 | EnterCriticalSection(&mStateSync); |
michael@0 | 401 | switch (mStatus) { |
michael@0 | 402 | case AVSCAN_BAD: |
michael@0 | 403 | downloadState = nsIDownloadManager::DOWNLOAD_DIRTY; |
michael@0 | 404 | break; |
michael@0 | 405 | default: |
michael@0 | 406 | case AVSCAN_FAILED: |
michael@0 | 407 | case AVSCAN_GOOD: |
michael@0 | 408 | case AVSCAN_UGLY: |
michael@0 | 409 | case AVSCAN_TIMEDOUT: |
michael@0 | 410 | downloadState = nsIDownloadManager::DOWNLOAD_FINISHED; |
michael@0 | 411 | break; |
michael@0 | 412 | } |
michael@0 | 413 | LeaveCriticalSection(&mStateSync); |
michael@0 | 414 | // Download will be null if we already timed out |
michael@0 | 415 | if (mDownload) |
michael@0 | 416 | (void)mDownload->SetState(downloadState); |
michael@0 | 417 | |
michael@0 | 418 | // Clean up some other variables |
michael@0 | 419 | // In the event of a timeout, our destructor won't be called |
michael@0 | 420 | mDownload = nullptr; |
michael@0 | 421 | |
michael@0 | 422 | NS_RELEASE_THIS(); |
michael@0 | 423 | return NS_OK; |
michael@0 | 424 | } |
michael@0 | 425 | |
michael@0 | 426 | static DWORD |
michael@0 | 427 | ExceptionFilterFunction(DWORD exceptionCode) { |
michael@0 | 428 | switch(exceptionCode) { |
michael@0 | 429 | case EXCEPTION_ACCESS_VIOLATION: |
michael@0 | 430 | case EXCEPTION_ILLEGAL_INSTRUCTION: |
michael@0 | 431 | case EXCEPTION_IN_PAGE_ERROR: |
michael@0 | 432 | case EXCEPTION_PRIV_INSTRUCTION: |
michael@0 | 433 | case EXCEPTION_STACK_OVERFLOW: |
michael@0 | 434 | return EXCEPTION_EXECUTE_HANDLER; |
michael@0 | 435 | default: |
michael@0 | 436 | return EXCEPTION_CONTINUE_SEARCH; |
michael@0 | 437 | } |
michael@0 | 438 | } |
michael@0 | 439 | |
michael@0 | 440 | bool |
michael@0 | 441 | nsDownloadScanner::Scan::DoScanAES() |
michael@0 | 442 | { |
michael@0 | 443 | // This warning is for the destructor of ae which will not be invoked in the |
michael@0 | 444 | // event of a win32 exception |
michael@0 | 445 | #pragma warning(disable: 4509) |
michael@0 | 446 | HRESULT hr; |
michael@0 | 447 | nsRefPtr<IAttachmentExecute> ae; |
michael@0 | 448 | MOZ_SEH_TRY { |
michael@0 | 449 | hr = CoCreateInstance(CLSID_AttachmentServices, nullptr, CLSCTX_ALL, |
michael@0 | 450 | IID_IAttachmentExecute, getter_AddRefs(ae)); |
michael@0 | 451 | } MOZ_SEH_EXCEPT(ExceptionFilterFunction(GetExceptionCode())) { |
michael@0 | 452 | return CheckAndSetState(AVSCAN_NOTSTARTED,AVSCAN_FAILED); |
michael@0 | 453 | } |
michael@0 | 454 | |
michael@0 | 455 | // If we (somehow) already timed out, then don't bother scanning |
michael@0 | 456 | if (CheckAndSetState(AVSCAN_SCANNING, AVSCAN_NOTSTARTED)) { |
michael@0 | 457 | AVScanState newState; |
michael@0 | 458 | if (SUCCEEDED(hr)) { |
michael@0 | 459 | bool gotException = false; |
michael@0 | 460 | MOZ_SEH_TRY { |
michael@0 | 461 | (void)ae->SetClientGuid(GUID_MozillaVirusScannerPromptGeneric); |
michael@0 | 462 | (void)ae->SetLocalPath(mPath.get()); |
michael@0 | 463 | // Provide the src for everything but data: schemes. |
michael@0 | 464 | if (!mSkipSource) |
michael@0 | 465 | (void)ae->SetSource(mOrigin.get()); |
michael@0 | 466 | |
michael@0 | 467 | // Save() will invoke the scanner |
michael@0 | 468 | hr = ae->Save(); |
michael@0 | 469 | } MOZ_SEH_EXCEPT(ExceptionFilterFunction(GetExceptionCode())) { |
michael@0 | 470 | gotException = true; |
michael@0 | 471 | } |
michael@0 | 472 | |
michael@0 | 473 | MOZ_SEH_TRY { |
michael@0 | 474 | ae = nullptr; |
michael@0 | 475 | } MOZ_SEH_EXCEPT(ExceptionFilterFunction(GetExceptionCode())) { |
michael@0 | 476 | gotException = true; |
michael@0 | 477 | } |
michael@0 | 478 | |
michael@0 | 479 | if(gotException) { |
michael@0 | 480 | newState = AVSCAN_FAILED; |
michael@0 | 481 | } |
michael@0 | 482 | else if (SUCCEEDED(hr)) { // Passed the scan |
michael@0 | 483 | newState = AVSCAN_GOOD; |
michael@0 | 484 | } |
michael@0 | 485 | else if (HRESULT_CODE(hr) == ERROR_FILE_NOT_FOUND) { |
michael@0 | 486 | NS_WARNING("Downloaded file disappeared before it could be scanned"); |
michael@0 | 487 | newState = AVSCAN_FAILED; |
michael@0 | 488 | } |
michael@0 | 489 | else if (hr == E_INVALIDARG) { |
michael@0 | 490 | NS_WARNING("IAttachementExecute returned invalid argument error"); |
michael@0 | 491 | newState = AVSCAN_FAILED; |
michael@0 | 492 | } |
michael@0 | 493 | else { |
michael@0 | 494 | newState = AVSCAN_UGLY; |
michael@0 | 495 | } |
michael@0 | 496 | } |
michael@0 | 497 | else { |
michael@0 | 498 | newState = AVSCAN_FAILED; |
michael@0 | 499 | } |
michael@0 | 500 | return CheckAndSetState(newState, AVSCAN_SCANNING); |
michael@0 | 501 | } |
michael@0 | 502 | return false; |
michael@0 | 503 | } |
michael@0 | 504 | #pragma warning(default: 4509) |
michael@0 | 505 | |
michael@0 | 506 | void |
michael@0 | 507 | nsDownloadScanner::Scan::DoScan() |
michael@0 | 508 | { |
michael@0 | 509 | CoInitialize(nullptr); |
michael@0 | 510 | |
michael@0 | 511 | if (DoScanAES()) { |
michael@0 | 512 | // We need to do a few more things on the main thread |
michael@0 | 513 | NS_DispatchToMainThread(this); |
michael@0 | 514 | } else { |
michael@0 | 515 | // We timed out, so just release |
michael@0 | 516 | ReleaseDispatcher* releaser = new ReleaseDispatcher(this); |
michael@0 | 517 | if(releaser) { |
michael@0 | 518 | NS_ADDREF(releaser); |
michael@0 | 519 | NS_DispatchToMainThread(releaser); |
michael@0 | 520 | } |
michael@0 | 521 | } |
michael@0 | 522 | |
michael@0 | 523 | MOZ_SEH_TRY { |
michael@0 | 524 | CoUninitialize(); |
michael@0 | 525 | } MOZ_SEH_EXCEPT(ExceptionFilterFunction(GetExceptionCode())) { |
michael@0 | 526 | // Not much we can do at this point... |
michael@0 | 527 | } |
michael@0 | 528 | } |
michael@0 | 529 | |
michael@0 | 530 | HANDLE |
michael@0 | 531 | nsDownloadScanner::Scan::GetWaitableThreadHandle() const |
michael@0 | 532 | { |
michael@0 | 533 | HANDLE targetHandle = INVALID_HANDLE_VALUE; |
michael@0 | 534 | (void)DuplicateHandle(GetCurrentProcess(), mThread, |
michael@0 | 535 | GetCurrentProcess(), &targetHandle, |
michael@0 | 536 | SYNCHRONIZE, // Only allow clients to wait on this handle |
michael@0 | 537 | FALSE, // cannot be inherited by child processes |
michael@0 | 538 | 0); |
michael@0 | 539 | return targetHandle; |
michael@0 | 540 | } |
michael@0 | 541 | |
michael@0 | 542 | bool |
michael@0 | 543 | nsDownloadScanner::Scan::NotifyTimeout() |
michael@0 | 544 | { |
michael@0 | 545 | bool didTimeout = CheckAndSetState(AVSCAN_TIMEDOUT, AVSCAN_SCANNING) || |
michael@0 | 546 | CheckAndSetState(AVSCAN_TIMEDOUT, AVSCAN_NOTSTARTED); |
michael@0 | 547 | if (didTimeout) { |
michael@0 | 548 | // We need to do a few more things on the main thread |
michael@0 | 549 | NS_DispatchToMainThread(this); |
michael@0 | 550 | } |
michael@0 | 551 | return didTimeout; |
michael@0 | 552 | } |
michael@0 | 553 | |
michael@0 | 554 | bool |
michael@0 | 555 | nsDownloadScanner::Scan::CheckAndSetState(AVScanState newState, AVScanState expectedState) { |
michael@0 | 556 | bool gotExpectedState = false; |
michael@0 | 557 | EnterCriticalSection(&mStateSync); |
michael@0 | 558 | if(gotExpectedState = (mStatus == expectedState)) |
michael@0 | 559 | mStatus = newState; |
michael@0 | 560 | LeaveCriticalSection(&mStateSync); |
michael@0 | 561 | return gotExpectedState; |
michael@0 | 562 | } |
michael@0 | 563 | |
michael@0 | 564 | nsresult |
michael@0 | 565 | nsDownloadScanner::ScanDownload(nsDownload *download) |
michael@0 | 566 | { |
michael@0 | 567 | if (!mAESExists) |
michael@0 | 568 | return NS_ERROR_NOT_AVAILABLE; |
michael@0 | 569 | |
michael@0 | 570 | // No ref ptr, see comment below |
michael@0 | 571 | Scan *scan = new Scan(this, download); |
michael@0 | 572 | if (!scan) |
michael@0 | 573 | return NS_ERROR_OUT_OF_MEMORY; |
michael@0 | 574 | |
michael@0 | 575 | NS_ADDREF(scan); |
michael@0 | 576 | |
michael@0 | 577 | nsresult rv = scan->Start(); |
michael@0 | 578 | |
michael@0 | 579 | // Note that we only release upon error. On success, the scan is passed off |
michael@0 | 580 | // to a new thread. It is eventually released in Scan::Run on the main thread. |
michael@0 | 581 | if (NS_FAILED(rv)) |
michael@0 | 582 | NS_RELEASE(scan); |
michael@0 | 583 | else |
michael@0 | 584 | // Notify the watchdog |
michael@0 | 585 | mWatchdog->Watch(scan); |
michael@0 | 586 | |
michael@0 | 587 | return rv; |
michael@0 | 588 | } |
michael@0 | 589 | |
michael@0 | 590 | nsDownloadScannerWatchdog::nsDownloadScannerWatchdog() |
michael@0 | 591 | : mNewItemEvent(nullptr), mQuitEvent(nullptr) { |
michael@0 | 592 | InitializeCriticalSection(&mQueueSync); |
michael@0 | 593 | } |
michael@0 | 594 | nsDownloadScannerWatchdog::~nsDownloadScannerWatchdog() { |
michael@0 | 595 | DeleteCriticalSection(&mQueueSync); |
michael@0 | 596 | } |
michael@0 | 597 | |
michael@0 | 598 | nsresult |
michael@0 | 599 | nsDownloadScannerWatchdog::Init() { |
michael@0 | 600 | // Both events are auto-reset |
michael@0 | 601 | mNewItemEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr); |
michael@0 | 602 | if (INVALID_HANDLE_VALUE == mNewItemEvent) |
michael@0 | 603 | return NS_ERROR_OUT_OF_MEMORY; |
michael@0 | 604 | mQuitEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr); |
michael@0 | 605 | if (INVALID_HANDLE_VALUE == mQuitEvent) { |
michael@0 | 606 | (void)CloseHandle(mNewItemEvent); |
michael@0 | 607 | return NS_ERROR_OUT_OF_MEMORY; |
michael@0 | 608 | } |
michael@0 | 609 | |
michael@0 | 610 | // This thread is always running, however it will be asleep |
michael@0 | 611 | // for most of the dlmgr's lifetime |
michael@0 | 612 | mThread = (HANDLE)_beginthreadex(nullptr, 0, WatchdogThread, |
michael@0 | 613 | this, 0, nullptr); |
michael@0 | 614 | if (!mThread) { |
michael@0 | 615 | (void)CloseHandle(mNewItemEvent); |
michael@0 | 616 | (void)CloseHandle(mQuitEvent); |
michael@0 | 617 | return NS_ERROR_OUT_OF_MEMORY; |
michael@0 | 618 | } |
michael@0 | 619 | |
michael@0 | 620 | return NS_OK; |
michael@0 | 621 | } |
michael@0 | 622 | |
michael@0 | 623 | nsresult |
michael@0 | 624 | nsDownloadScannerWatchdog::Shutdown() { |
michael@0 | 625 | // Tell the watchdog thread to quite |
michael@0 | 626 | (void)SetEvent(mQuitEvent); |
michael@0 | 627 | (void)WaitForSingleObject(mThread, INFINITE); |
michael@0 | 628 | (void)CloseHandle(mThread); |
michael@0 | 629 | // Manually clear and release the queued scans |
michael@0 | 630 | while (mScanQueue.GetSize() != 0) { |
michael@0 | 631 | Scan *scan = reinterpret_cast<Scan*>(mScanQueue.Pop()); |
michael@0 | 632 | NS_RELEASE(scan); |
michael@0 | 633 | } |
michael@0 | 634 | (void)CloseHandle(mNewItemEvent); |
michael@0 | 635 | (void)CloseHandle(mQuitEvent); |
michael@0 | 636 | return NS_OK; |
michael@0 | 637 | } |
michael@0 | 638 | |
michael@0 | 639 | void |
michael@0 | 640 | nsDownloadScannerWatchdog::Watch(Scan *scan) { |
michael@0 | 641 | bool wasEmpty; |
michael@0 | 642 | // Note that there is no release in this method |
michael@0 | 643 | // The scan will be released by the watchdog ALWAYS on the main thread |
michael@0 | 644 | // when either the watchdog thread processes the scan or the watchdog |
michael@0 | 645 | // is shut down |
michael@0 | 646 | NS_ADDREF(scan); |
michael@0 | 647 | EnterCriticalSection(&mQueueSync); |
michael@0 | 648 | wasEmpty = mScanQueue.GetSize()==0; |
michael@0 | 649 | mScanQueue.Push(scan); |
michael@0 | 650 | LeaveCriticalSection(&mQueueSync); |
michael@0 | 651 | // If the queue was empty, then the watchdog thread is/will be asleep |
michael@0 | 652 | if (wasEmpty) |
michael@0 | 653 | (void)SetEvent(mNewItemEvent); |
michael@0 | 654 | } |
michael@0 | 655 | |
michael@0 | 656 | unsigned int |
michael@0 | 657 | __stdcall |
michael@0 | 658 | nsDownloadScannerWatchdog::WatchdogThread(void *p) { |
michael@0 | 659 | NS_ASSERTION(!NS_IsMainThread(), "Antivirus scan watchdog should not be run on the main thread"); |
michael@0 | 660 | nsDownloadScannerWatchdog *watchdog = (nsDownloadScannerWatchdog*)p; |
michael@0 | 661 | HANDLE waitHandles[3] = {watchdog->mNewItemEvent, watchdog->mQuitEvent, INVALID_HANDLE_VALUE}; |
michael@0 | 662 | DWORD waitStatus; |
michael@0 | 663 | DWORD queueItemsLeft = 0; |
michael@0 | 664 | // Loop until quit event or error |
michael@0 | 665 | while (0 != queueItemsLeft || |
michael@0 | 666 | (WAIT_OBJECT_0 + 1) != |
michael@0 | 667 | (waitStatus = |
michael@0 | 668 | WaitForMultipleObjects(2, waitHandles, FALSE, INFINITE)) && |
michael@0 | 669 | waitStatus != WAIT_FAILED) { |
michael@0 | 670 | Scan *scan = nullptr; |
michael@0 | 671 | PRTime startTime, expectedEndTime, now; |
michael@0 | 672 | DWORD waitTime; |
michael@0 | 673 | |
michael@0 | 674 | // Pop scan from queue |
michael@0 | 675 | EnterCriticalSection(&watchdog->mQueueSync); |
michael@0 | 676 | scan = reinterpret_cast<Scan*>(watchdog->mScanQueue.Pop()); |
michael@0 | 677 | queueItemsLeft = watchdog->mScanQueue.GetSize(); |
michael@0 | 678 | LeaveCriticalSection(&watchdog->mQueueSync); |
michael@0 | 679 | |
michael@0 | 680 | // Calculate expected end time |
michael@0 | 681 | startTime = scan->GetStartTime(); |
michael@0 | 682 | expectedEndTime = WATCHDOG_TIMEOUT + startTime; |
michael@0 | 683 | now = PR_Now(); |
michael@0 | 684 | // PRTime is not guaranteed to be a signed integral type (afaik), but |
michael@0 | 685 | // currently it is |
michael@0 | 686 | if (now > expectedEndTime) { |
michael@0 | 687 | waitTime = 0; |
michael@0 | 688 | } else { |
michael@0 | 689 | // This is a positive value, and we know that it will not overflow |
michael@0 | 690 | // (bounded by WATCHDOG_TIMEOUT) |
michael@0 | 691 | // waitTime is in milliseconds, nspr uses microseconds |
michael@0 | 692 | waitTime = static_cast<DWORD>((expectedEndTime - now)/PR_USEC_PER_MSEC); |
michael@0 | 693 | } |
michael@0 | 694 | HANDLE hThread = waitHandles[2] = scan->GetWaitableThreadHandle(); |
michael@0 | 695 | |
michael@0 | 696 | // Wait for the thread (obj 1) or quit event (obj 0) |
michael@0 | 697 | waitStatus = WaitForMultipleObjects(2, (waitHandles+1), FALSE, waitTime); |
michael@0 | 698 | CloseHandle(hThread); |
michael@0 | 699 | |
michael@0 | 700 | ReleaseDispatcher* releaser = new ReleaseDispatcher(scan); |
michael@0 | 701 | if(!releaser) |
michael@0 | 702 | continue; |
michael@0 | 703 | NS_ADDREF(releaser); |
michael@0 | 704 | // Got quit event or error |
michael@0 | 705 | if (waitStatus == WAIT_FAILED || waitStatus == WAIT_OBJECT_0) { |
michael@0 | 706 | NS_DispatchToMainThread(releaser); |
michael@0 | 707 | break; |
michael@0 | 708 | // Thread exited normally |
michael@0 | 709 | } else if (waitStatus == (WAIT_OBJECT_0+1)) { |
michael@0 | 710 | NS_DispatchToMainThread(releaser); |
michael@0 | 711 | continue; |
michael@0 | 712 | // Timeout case |
michael@0 | 713 | } else { |
michael@0 | 714 | NS_ASSERTION(waitStatus == WAIT_TIMEOUT, "Unexpected wait status in dlmgr watchdog thread"); |
michael@0 | 715 | if (!scan->NotifyTimeout()) { |
michael@0 | 716 | // If we didn't time out, then release the thread |
michael@0 | 717 | NS_DispatchToMainThread(releaser); |
michael@0 | 718 | } else { |
michael@0 | 719 | // NotifyTimeout did a dispatch which will release the scan, so we |
michael@0 | 720 | // don't need to release the scan |
michael@0 | 721 | NS_RELEASE(releaser); |
michael@0 | 722 | } |
michael@0 | 723 | } |
michael@0 | 724 | } |
michael@0 | 725 | _endthreadex(0); |
michael@0 | 726 | return 0; |
michael@0 | 727 | } |