1 /*
2 * PROJECT: ReactOS AutoChk
3 * LICENSE: GPL-2.0+ (https://spdx.org/licenses/GPL-2.0+)
4 * PURPOSE: FileSystem checker in Native mode.
5 * COPYRIGHT: Copyright 2002-2018 Eric Kohl
6 * Copyright 2006-2018 Aleksey Bragin
7 * Copyright 2006-2018 Hervé Poussineau
8 * Copyright 2008-2018 Pierre Schweitzer
9 * Copyright 2018 Hermes Belusca-Maito
10 */
11
12 /* INCLUDES *****************************************************************/
13
14 #include <stdio.h>
15
16 #define WIN32_NO_STATUS
17 #include <windef.h>
18 #include <winbase.h>
19 #include <ntddkbd.h>
20
21 #define NTOS_MODE_USER
22 #include <ndk/exfuncs.h>
23 #include <ndk/iofuncs.h>
24 #include <ndk/obfuncs.h>
25 #include <ndk/psfuncs.h>
26 #include <ndk/rtlfuncs.h>
27 #include <fmifs/fmifs.h>
28
29 #include <fslib/vfatlib.h>
30 #include <fslib/vfatxlib.h>
31 #include <fslib/ntfslib.h>
32 #include <fslib/btrfslib.h>
33 #include <fslib/ext2lib.h>
34 #include <fslib/cdfslib.h>
35
36 #define NDEBUG
37 #include <debug.h>
38
39 /* DEFINES ******************************************************************/
40
41 typedef struct _FILESYSTEM_CHKDSK
42 {
43 WCHAR Name[10];
44 PULIB_CHKDSK ChkdskFunc;
45 } FILESYSTEM_CHKDSK, *PFILESYSTEM_CHKDSK;
46
47 FILESYSTEM_CHKDSK FileSystems[] =
48 {
49 { L"FAT", VfatChkdsk },
50 { L"FAT32", VfatChkdsk },
51 { L"FATX" , VfatxChkdsk },
52 { L"NTFS", NtfsChkdsk },
53 { L"BTRFS", BtrfsChkdsk },
54 { L"EXT2", Ext2Chkdsk },
55 { L"EXT3", Ext2Chkdsk },
56 { L"EXT4", Ext2Chkdsk },
57 { L"CDFS", CdfsChkdsk },
58 };
59
60 HANDLE KeyboardHandle = NULL;
61
62 /* FUNCTIONS ****************************************************************/
63
64 //
65 // FMIFS function
66 //
67
68 static INT
PrintString(char * fmt,...)69 PrintString(char* fmt,...)
70 {
71 INT Len;
72 char buffer[512];
73 va_list ap;
74 UNICODE_STRING UnicodeString;
75 ANSI_STRING AnsiString;
76
77 va_start(ap, fmt);
78 Len = vsprintf(buffer, fmt, ap);
79 va_end(ap);
80
81 RtlInitAnsiString(&AnsiString, buffer);
82 RtlAnsiStringToUnicodeString(&UnicodeString,
83 &AnsiString,
84 TRUE);
85 NtDisplayString(&UnicodeString);
86 RtlFreeUnicodeString(&UnicodeString);
87
88 return Len;
89 }
90
91 static VOID
EraseLine(IN INT LineLength)92 EraseLine(
93 IN INT LineLength)
94 {
95 INT Len;
96 UNICODE_STRING UnicodeString;
97 WCHAR buffer[512];
98
99 if (LineLength <= 0)
100 return;
101
102 /* Go to the beginning of the line */
103 RtlInitUnicodeString(&UnicodeString, L"\r");
104 NtDisplayString(&UnicodeString);
105
106 /* Fill the buffer chunk with spaces */
107 Len = min(LineLength, ARRAYSIZE(buffer));
108 while (Len > 0) buffer[--Len] = L' ';
109
110 RtlInitEmptyUnicodeString(&UnicodeString, buffer, sizeof(buffer));
111 while (LineLength > 0)
112 {
113 /* Display the buffer */
114 Len = min(LineLength, ARRAYSIZE(buffer));
115 LineLength -= Len;
116 UnicodeString.Length = Len * sizeof(WCHAR);
117 NtDisplayString(&UnicodeString);
118 }
119
120 /* Go to the beginning of the line */
121 RtlInitUnicodeString(&UnicodeString, L"\r");
122 NtDisplayString(&UnicodeString);
123 }
124
125 static NTSTATUS
OpenKeyboard(OUT PHANDLE KeyboardHandle)126 OpenKeyboard(
127 OUT PHANDLE KeyboardHandle)
128 {
129 OBJECT_ATTRIBUTES ObjectAttributes;
130 IO_STATUS_BLOCK IoStatusBlock;
131 UNICODE_STRING KeyboardName = RTL_CONSTANT_STRING(L"\\Device\\KeyboardClass0");
132
133 /* Just open the class driver */
134 InitializeObjectAttributes(&ObjectAttributes,
135 &KeyboardName,
136 0,
137 NULL,
138 NULL);
139 return NtOpenFile(KeyboardHandle,
140 FILE_ALL_ACCESS,
141 &ObjectAttributes,
142 &IoStatusBlock,
143 FILE_OPEN,
144 0);
145 }
146
147 static NTSTATUS
WaitForKeyboard(IN HANDLE KeyboardHandle,IN LONG TimeOut)148 WaitForKeyboard(
149 IN HANDLE KeyboardHandle,
150 IN LONG TimeOut)
151 {
152 NTSTATUS Status;
153 IO_STATUS_BLOCK IoStatusBlock;
154 LARGE_INTEGER Offset, Timeout;
155 KEYBOARD_INPUT_DATA InputData;
156 INT Len = 0;
157
158 /* Skip the wait if there is no timeout */
159 if (TimeOut <= 0)
160 return STATUS_TIMEOUT;
161
162 /* Attempt to read a down key-press from the keyboard */
163 do
164 {
165 Offset.QuadPart = 0;
166 Status = NtReadFile(KeyboardHandle,
167 NULL,
168 NULL,
169 NULL,
170 &IoStatusBlock,
171 &InputData,
172 sizeof(KEYBOARD_INPUT_DATA),
173 &Offset,
174 NULL);
175 if (!NT_SUCCESS(Status))
176 {
177 /* Something failed, bail out */
178 return Status;
179 }
180 if ((Status != STATUS_PENDING) && !(InputData.Flags & KEY_BREAK))
181 {
182 /* We have a down key-press, return */
183 return Status;
184 }
185 } while (Status != STATUS_PENDING);
186
187 /* Perform the countdown of TimeOut seconds */
188 Status = STATUS_TIMEOUT;
189 while (TimeOut > 0)
190 {
191 /*
192 * Display the countdown string.
193 * Note that we only do a carriage return to go back to the
194 * beginning of the line to possibly erase it. Note also that
195 * we display a trailing space to erase any last character
196 * when the string length decreases.
197 */
198 Len = PrintString("Press any key within %d second(s) to cancel and resume startup. \r", TimeOut);
199
200 /* Decrease the countdown */
201 --TimeOut;
202
203 /* Wait one second for a key press */
204 Timeout.QuadPart = -10000000;
205 Status = NtWaitForSingleObject(KeyboardHandle, FALSE, &Timeout);
206 if (Status != STATUS_TIMEOUT)
207 break;
208 }
209
210 /* Erase the countdown string */
211 EraseLine(Len);
212
213 if (Status == STATUS_TIMEOUT)
214 {
215 /* The user did not press any key, cancel the read */
216 NtCancelIoFile(KeyboardHandle, &IoStatusBlock);
217 }
218 else
219 {
220 /* Otherwise, return some status */
221 Status = IoStatusBlock.Status;
222 }
223
224 return Status;
225 }
226
227 static NTSTATUS
GetFileSystem(IN PUNICODE_STRING VolumePathU,IN OUT PWSTR FileSystemName,IN SIZE_T FileSystemNameSize)228 GetFileSystem(
229 IN PUNICODE_STRING VolumePathU,
230 IN OUT PWSTR FileSystemName,
231 IN SIZE_T FileSystemNameSize)
232 {
233 NTSTATUS Status;
234 OBJECT_ATTRIBUTES ObjectAttributes;
235 HANDLE FileHandle;
236 IO_STATUS_BLOCK IoStatusBlock;
237 PFILE_FS_ATTRIBUTE_INFORMATION FileFsAttribute;
238 UCHAR Buffer[sizeof(FILE_FS_ATTRIBUTE_INFORMATION) + MAX_PATH * sizeof(WCHAR)];
239
240 FileFsAttribute = (PFILE_FS_ATTRIBUTE_INFORMATION)Buffer;
241
242 InitializeObjectAttributes(&ObjectAttributes,
243 VolumePathU,
244 OBJ_CASE_INSENSITIVE,
245 NULL,
246 NULL);
247
248 Status = NtCreateFile(&FileHandle,
249 FILE_GENERIC_READ,
250 &ObjectAttributes,
251 &IoStatusBlock,
252 NULL,
253 0,
254 FILE_SHARE_READ | FILE_SHARE_WRITE,
255 FILE_OPEN,
256 0,
257 NULL,
258 0);
259 if (!NT_SUCCESS(Status))
260 {
261 DPRINT1("Could not open volume '%wZ' to obtain its file system, Status 0x%08lx\n",
262 VolumePathU, Status);
263 return Status;
264 }
265
266 Status = NtQueryVolumeInformationFile(FileHandle,
267 &IoStatusBlock,
268 FileFsAttribute,
269 sizeof(Buffer),
270 FileFsAttributeInformation);
271 NtClose(FileHandle);
272
273 if (!NT_SUCCESS(Status))
274 {
275 DPRINT1("NtQueryVolumeInformationFile() failed, Status 0x%08lx\n", Status);
276 return Status;
277 }
278
279 if (FileSystemNameSize >= FileFsAttribute->FileSystemNameLength + sizeof(WCHAR))
280 {
281 RtlCopyMemory(FileSystemName,
282 FileFsAttribute->FileSystemName,
283 FileFsAttribute->FileSystemNameLength);
284 FileSystemName[FileFsAttribute->FileSystemNameLength / sizeof(WCHAR)] = UNICODE_NULL;
285 }
286 else
287 {
288 return STATUS_BUFFER_TOO_SMALL;
289 }
290
291 return STATUS_SUCCESS;
292 }
293
294 /* This is based on SysInternals' ChkDsk application */
295 static BOOLEAN
296 NTAPI
ChkdskCallback(IN CALLBACKCOMMAND Command,IN ULONG Modifier,IN PVOID Argument)297 ChkdskCallback(
298 IN CALLBACKCOMMAND Command,
299 IN ULONG Modifier,
300 IN PVOID Argument)
301 {
302 PDWORD Percent;
303 PBOOLEAN Status;
304 PTEXTOUTPUT Output;
305
306 //
307 // We get other types of commands,
308 // but we don't have to pay attention to them
309 //
310 switch(Command)
311 {
312 case UNKNOWN2:
313 DPRINT("UNKNOWN2\n");
314 break;
315
316 case UNKNOWN3:
317 DPRINT("UNKNOWN3\n");
318 break;
319
320 case UNKNOWN4:
321 DPRINT("UNKNOWN4\n");
322 break;
323
324 case UNKNOWN5:
325 DPRINT("UNKNOWN5\n");
326 break;
327
328 case UNKNOWN9:
329 DPRINT("UNKNOWN9\n");
330 break;
331
332 case UNKNOWNA:
333 DPRINT("UNKNOWNA\n");
334 break;
335
336 case UNKNOWNC:
337 DPRINT("UNKNOWNC\n");
338 break;
339
340 case UNKNOWND:
341 DPRINT("UNKNOWND\n");
342 break;
343
344 case INSUFFICIENTRIGHTS:
345 DPRINT("INSUFFICIENTRIGHTS\n");
346 break;
347
348 case FSNOTSUPPORTED:
349 DPRINT("FSNOTSUPPORTED\n");
350 break;
351
352 case VOLUMEINUSE:
353 DPRINT("VOLUMEINUSE\n");
354 break;
355
356 case STRUCTUREPROGRESS:
357 DPRINT("STRUCTUREPROGRESS\n");
358 break;
359
360 case DONEWITHSTRUCTURE:
361 DPRINT("DONEWITHSTRUCTURE\n");
362 break;
363
364 case CLUSTERSIZETOOSMALL:
365 DPRINT("CLUSTERSIZETOOSMALL\n");
366 break;
367
368 case PROGRESS:
369 Percent = (PDWORD) Argument;
370 PrintString("%d percent completed.\r", *Percent);
371 break;
372
373 case OUTPUT:
374 Output = (PTEXTOUTPUT) Argument;
375 PrintString("%s", Output->Output);
376 break;
377
378 case DONE:
379 Status = (PBOOLEAN)Argument;
380 if (*Status == FALSE)
381 {
382 PrintString("The file system check was unable to complete successfully.\r\n\r\n");
383 // Error = TRUE;
384 }
385 break;
386 }
387 return TRUE;
388 }
389
390 static NTSTATUS
CheckVolume(IN PCWSTR VolumePath,IN LONG TimeOut,IN BOOLEAN CheckOnlyIfDirty)391 CheckVolume(
392 IN PCWSTR VolumePath,
393 IN LONG TimeOut,
394 IN BOOLEAN CheckOnlyIfDirty)
395 {
396 NTSTATUS Status;
397 BOOLEAN Success;
398 PCWSTR DisplayName;
399 UNICODE_STRING VolumePathU;
400 ULONG Count;
401 WCHAR FileSystem[128];
402
403 RtlInitUnicodeString(&VolumePathU, VolumePath);
404
405 /* Get a drive string for display purposes only */
406 if (wcslen(VolumePath) == 6 &&
407 VolumePath[0] == L'\\' &&
408 VolumePath[1] == L'?' &&
409 VolumePath[2] == L'?' &&
410 VolumePath[3] == L'\\' &&
411 VolumePath[5] == L':')
412 {
413 /* DOS drive */
414 DisplayName = &VolumePath[4];
415 }
416 else
417 {
418 DisplayName = VolumePath;
419 }
420
421 DPRINT1("AUTOCHK: Checking %wZ\n", &VolumePathU);
422 PrintString("Verifying the file system on %S\r\n", DisplayName);
423
424 /* Get the file system */
425 Status = GetFileSystem(&VolumePathU,
426 FileSystem,
427 sizeof(FileSystem));
428 if (!NT_SUCCESS(Status))
429 {
430 DPRINT1("GetFileSystem() failed, Status 0x%08lx\n", Status);
431 PrintString(" Unable to detect the file system of volume %S\r\n", DisplayName);
432 goto Quit;
433 }
434
435 PrintString(" The file system type is %S.\r\n\r\n", FileSystem);
436
437 /* Find a suitable file system provider */
438 for (Count = 0; Count < RTL_NUMBER_OF(FileSystems); ++Count)
439 {
440 if (wcscmp(FileSystem, FileSystems[Count].Name) == 0)
441 break;
442 }
443 if (Count >= RTL_NUMBER_OF(FileSystems))
444 {
445 DPRINT1("File system %S not supported\n", FileSystem);
446 PrintString(" Unable to verify the volume. The %S file system is not supported.\r\n", FileSystem);
447 Status = STATUS_DLL_NOT_FOUND;
448 goto Quit;
449 }
450
451 /* Check whether the volume is dirty */
452 Status = STATUS_SUCCESS;
453 Success = FileSystems[Count].ChkdskFunc(&VolumePathU,
454 ChkdskCallback,
455 FALSE, // FixErrors
456 TRUE, // Verbose
457 TRUE, // CheckOnlyIfDirty
458 FALSE, // ScanDrive
459 NULL,
460 NULL,
461 NULL,
462 NULL,
463 (PULONG)&Status);
464
465 /* Perform the check either when the volume is dirty or a check is forced */
466 if (Success && ((Status == STATUS_DISK_CORRUPT_ERROR) || !CheckOnlyIfDirty))
467 {
468 /* Let the user decide whether to repair */
469 if (Status == STATUS_DISK_CORRUPT_ERROR)
470 {
471 PrintString("The file system on volume %S needs to be checked for problems.\r\n", DisplayName);
472 PrintString("You may cancel this check, but it is recommended that you continue.\r\n");
473 }
474 else
475 {
476 PrintString("A volume check has been scheduled.\r\n");
477 }
478
479 if (!KeyboardHandle || WaitForKeyboard(KeyboardHandle, TimeOut) == STATUS_TIMEOUT)
480 {
481 PrintString("The system will now check the file system.\r\n\r\n");
482 Status = STATUS_SUCCESS;
483 Success = FileSystems[Count].ChkdskFunc(&VolumePathU,
484 ChkdskCallback,
485 TRUE, // FixErrors
486 TRUE, // Verbose
487 CheckOnlyIfDirty,
488 FALSE, // ScanDrive
489 NULL,
490 NULL,
491 NULL,
492 NULL,
493 (PULONG)&Status);
494 }
495 else
496 {
497 PrintString("The file system check has been skipped.\r\n");
498 }
499 }
500
501 Quit:
502 PrintString("\r\n\r\n");
503 return Status;
504 }
505
506 static VOID
QueryTimeout(IN OUT PLONG TimeOut)507 QueryTimeout(
508 IN OUT PLONG TimeOut)
509 {
510 RTL_QUERY_REGISTRY_TABLE QueryTable[2];
511
512 RtlZeroMemory(QueryTable, sizeof(QueryTable));
513 QueryTable[0].Flags = RTL_QUERY_REGISTRY_DIRECT;
514 QueryTable[0].Name = L"AutoChkTimeOut";
515 QueryTable[0].EntryContext = TimeOut;
516
517 RtlQueryRegistryValues(RTL_REGISTRY_CONTROL, L"Session Manager", QueryTable, NULL, NULL);
518 /* See: https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/autochk */
519 *TimeOut = min(max(*TimeOut, 0), 259200);
520 }
521
522 INT
523 __cdecl
_main(IN INT argc,IN PCHAR argv[],IN PCHAR envp[],IN ULONG DebugFlag)524 _main(
525 IN INT argc,
526 IN PCHAR argv[],
527 IN PCHAR envp[],
528 IN ULONG DebugFlag)
529 {
530 NTSTATUS Status;
531 LONG TimeOut;
532 ULONG i;
533 BOOLEAN CheckAllVolumes = FALSE;
534 BOOLEAN CheckOnlyIfDirty = TRUE;
535 PROCESS_DEVICEMAP_INFORMATION DeviceMap;
536 PCHAR SkipDrives = NULL;
537 ANSI_STRING VolumePathA;
538 UNICODE_STRING VolumePathU;
539 WCHAR VolumePath[128] = L"";
540
541 /*
542 * Parse the command-line: optional command switches,
543 * then the NT volume name (or drive letter) to be analysed.
544 * If "*" is passed this means that we check all the volumes.
545 */
546 if (argc <= 1)
547 {
548 /* Only one parameter (the program name), bail out */
549 return 1;
550 }
551 for (i = 1; i < argc; ++i)
552 {
553 if (argv[i][0] == '/' || argv[i][0] == '-')
554 {
555 DPRINT("Parameter %d: %s\n", i, argv[i]);
556 switch (toupper(argv[i][1]))
557 {
558 case 'K': /* List of drive letters to skip */
559 {
560 /* Check for the separator */
561 if (argv[i][2] != ':')
562 goto Default;
563
564 SkipDrives = &argv[i][3];
565 break;
566 }
567
568 case 'R': /* Repair the errors, implies /P */
569 case 'P': /* Check even if not dirty */
570 {
571 if (argv[i][2] != ANSI_NULL)
572 goto Default;
573
574 CheckOnlyIfDirty = FALSE;
575 break;
576 }
577
578 default: Default:
579 DPRINT1("Unknown switch %d: %s\n", i, argv[i]);
580 break;
581 }
582 }
583 else
584 {
585 DPRINT("Parameter %d - Volume specification: %s\n", i, argv[i]);
586 if (strcmp(argv[i], "*") == 0)
587 {
588 CheckAllVolumes = TRUE;
589 }
590 else
591 {
592 RtlInitEmptyUnicodeString(&VolumePathU,
593 VolumePath,
594 sizeof(VolumePath));
595 RtlInitAnsiString(&VolumePathA, argv[i]);
596 Status = RtlAnsiStringToUnicodeString(&VolumePathU,
597 &VolumePathA,
598 FALSE);
599 }
600
601 /* Stop the parsing now */
602 break;
603 }
604 }
605
606 /*
607 * FIXME: We should probably use here the mount manager to be
608 * able to check volumes which don't have a drive letter.
609 */
610 Status = NtQueryInformationProcess(NtCurrentProcess(),
611 ProcessDeviceMap,
612 &DeviceMap.Query,
613 sizeof(DeviceMap.Query),
614 NULL);
615 if (!NT_SUCCESS(Status))
616 {
617 DPRINT1("NtQueryInformationProcess() failed, Status 0x%08lx\n", Status);
618 return 1;
619 }
620
621 /* Filter out the skipped drives from the map */
622 if (SkipDrives && *SkipDrives)
623 {
624 DPRINT1("Skipping drives:");
625 while (*SkipDrives)
626 {
627 #if DBG
628 DbgPrint(" %c:", *SkipDrives);
629 #endif
630 /* Retrieve the index and filter the drive out */
631 i = toupper(*SkipDrives) - 'A';
632 if (0 <= i && i <= 'Z'-'A')
633 DeviceMap.Query.DriveMap &= ~(1 << i);
634
635 /* Go to the next drive letter */
636 ++SkipDrives;
637 }
638 DbgPrint("\n");
639 }
640
641 /* Query the timeout */
642 TimeOut = 3;
643 QueryTimeout(&TimeOut);
644
645 /* Open the keyboard */
646 Status = OpenKeyboard(&KeyboardHandle);
647 if (!NT_SUCCESS(Status))
648 {
649 DPRINT1("OpenKeyboard() failed, Status 0x%08lx\n", Status);
650 /* Ignore keyboard interaction */
651 }
652
653 if (CheckAllVolumes)
654 {
655 /* Enumerate and check each of the available volumes */
656 for (i = 0; i <= 'Z'-'A'; i++)
657 {
658 if ((DeviceMap.Query.DriveMap & (1 << i)) &&
659 (DeviceMap.Query.DriveType[i] == DOSDEVICE_DRIVE_FIXED))
660 {
661 swprintf(VolumePath, L"\\??\\%c:", L'A' + i);
662 CheckVolume(VolumePath, TimeOut, CheckOnlyIfDirty);
663 }
664 }
665 }
666 else
667 {
668 /* Retrieve our index and analyse the volume */
669 if (wcslen(VolumePath) == 6 &&
670 VolumePath[0] == L'\\' &&
671 VolumePath[1] == L'?' &&
672 VolumePath[2] == L'?' &&
673 VolumePath[3] == L'\\' &&
674 VolumePath[5] == L':')
675 {
676 i = toupper(VolumePath[4]) - 'A';
677 if ((DeviceMap.Query.DriveMap & (1 << i)) &&
678 (DeviceMap.Query.DriveType[i] == DOSDEVICE_DRIVE_FIXED))
679 {
680 CheckVolume(VolumePath, TimeOut, CheckOnlyIfDirty);
681 }
682 }
683 else
684 {
685 /* Just perform the check on the specified volume */
686 // TODO: Check volume type using NtQueryVolumeInformationFile(FileFsDeviceInformation)
687 CheckVolume(VolumePath, TimeOut, CheckOnlyIfDirty);
688 }
689 }
690
691 /* Close the keyboard */
692 if (KeyboardHandle)
693 NtClose(KeyboardHandle);
694
695 // PrintString("Done\r\n\r\n");
696 return 0;
697 }
698
699 /* EOF */
700