gmw-probe.c 13 KB
Newer Older
1 2 3 4
/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
 *
 * Copyright (C) 2015 Richard Hughes <richard@hughsie.com>
 *
5
 * SPDX-License-Identifier: GPL-2.0+
6 7 8 9 10 11 12
 */

#include "config.h"

#include <errno.h>
#include <fcntl.h>
#include <glib/gi18n.h>
13
#include <glib/gprintf.h>
14 15 16 17 18 19 20 21 22 23 24 25 26
#include <glib/gstdio.h>
#include <gudev/gudev.h>
#include <linux/fs.h>
#include <linux/usbdevice_fs.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#include "gmw-cleanup.h"

27 28 29
#define ONE_BLOCK		0x8000
#define ONE_MB			0x100000
#define ONE_GB			0x40000000
30 31

typedef struct {
32 33 34 35 36
	guint8			*data_old;
	guint8			*data_random;
	gssize			 bytes_read;
	gssize			 bytes_wrote;
	guint64			 address;
37
	guint64			 offset;
38
	gboolean		 valid;
39 40 41
} GmwProbeBlock;

typedef struct {
42 43 44 45 46
	guint64			 disk_size;
	int			 fd;
	gchar			*block_dev;
	GPtrArray		*data_save;
	GUdevDevice		*udev_device;
47 48
} GmwProbeDevice;

Bastien Nocera's avatar
Bastien Nocera committed
49 50
static guint32 seed = 0;

51 52 53 54
#define GMW_ERROR		1
#define GMW_ERROR_FAILED	0
#define GMW_ERROR_IS_FAKE	1

55 56 57 58
/* The number of chunks of data to read and write to
 * verify integrity */
#define NUM_CHUNKS 256

59 60 61
static guint8 *
gmw_probe_get_random_data (guint len)
{
Richard Hughes's avatar
Richard Hughes committed
62 63
	guint8 *data = g_new (guint8, len);
	for (guint i = 0; i < len; i++)
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
		data[i] = g_random_int_range ('a', 'z');
	return data;
}

static void
gmw_probe_block_free (GmwProbeBlock *item)
{
	g_free (item->data_old);
	g_free (item->data_random);
	g_free (item);
}

static gboolean
gmw_probe_device_reset (GmwProbeDevice *dev, GError **error)
{
	int fd;
	const gchar *devnode;

	/* just reset device */
	devnode = g_udev_device_get_device_file (dev->udev_device);
84
	g_debug ("Resetting %s", devnode);
85 86 87 88 89
	fd = g_open (devnode, O_WRONLY | O_NONBLOCK);
	if (fd < 0) {
		g_set_error (error,
			     GMW_ERROR,
			     GMW_ERROR_FAILED,
90
			     "Failed to open %s", devnode);
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112
		return FALSE;
	}
	if (ioctl (fd, USBDEVFS_RESET) != 0) {
		g_set_error (error,
			     GMW_ERROR,
			     GMW_ERROR_FAILED,
			     "Failed to reset device");
		close (fd);
		return FALSE;
	}
	close (fd);
	return TRUE;
}

static gboolean
gmw_probe_device_open (GmwProbeDevice *dev, GError **error)
{
	dev->fd = g_open (dev->block_dev, O_RDWR | O_SYNC);
	if (dev->fd < 0) {
		g_set_error (error,
			     GMW_ERROR,
			     GMW_ERROR_FAILED,
113
			     "Failed to open %s", dev->block_dev);
114 115 116 117
		return FALSE;
	}

	/* do not use the OS cache */
118 119 120 121 122 123
	if (posix_fadvise (dev->fd, 0, 0,
			   POSIX_FADV_DONTNEED |
			   POSIX_FADV_RANDOM |
			   POSIX_FADV_NOREUSE) != 0) {
		g_warning ("Unable to call fadvise on %s", dev->block_dev);
	}
124 125 126 127 128 129 130 131
	return TRUE;
}

static void
gmw_probe_device_free (GmwProbeDevice *dev)
{
	if (dev->fd > 0)
		close (dev->fd);
132 133
	if (dev->udev_device != NULL)
		g_object_unref (dev->udev_device);
134 135 136 137 138 139 140 141 142
	g_free (dev->block_dev);
	g_ptr_array_unref (dev->data_save);
	g_free (dev);
}

static gsize
gmw_probe_device_read (GmwProbeDevice *dev, guint64 addr, guint8 *buf, gssize len)
{
	gsize bytes_read;
143 144
	if (lseek (dev->fd, addr, SEEK_SET) < 0)
		return 0;
145
	bytes_read = read (dev->fd, buf, len);
146 147
	g_debug ("read %" G_GSIZE_FORMAT " @ %" G_GUINT64_FORMAT "MB",
		 bytes_read, addr / ONE_MB);
148 149 150 151 152 153 154
	return bytes_read;
}

static gsize
gmw_probe_device_write (GmwProbeDevice *dev, guint64 addr, const guint8 *buf, gssize len)
{
	gsize bytes_written;
155 156
	if (lseek (dev->fd, addr, SEEK_SET) < 0)
		return 0;
157
	bytes_written = write (dev->fd, buf, len);
158 159
	g_debug ("wrote %" G_GSIZE_FORMAT " @ %" G_GUINT64_FORMAT "MB",
		 bytes_written, addr / ONE_MB);
160 161 162 163 164 165 166 167
	return bytes_written;
}

static gboolean
gmw_probe_device_data_save (GmwProbeDevice *dev,
			    GCancellable *cancellable,
			    GError **error)
{
168
	/* aim for roughtly the same number of chunks for all device sizes */
169
	guint64 chunk_size = dev->disk_size / NUM_CHUNKS;
170 171
	g_debug ("using chunk size of %" G_GUINT64_FORMAT "MB",
		 chunk_size / ONE_MB);
172
	for (guint i = 1; i < NUM_CHUNKS; i++) {
Richard Hughes's avatar
Richard Hughes committed
173
		GmwProbeBlock *item = g_new0 (GmwProbeBlock, 1);
174
		item->valid = TRUE;
175
		item->offset = g_random_int_range (1, 0xff);
176
		item->address = i * chunk_size;
177 178 179 180 181 182 183
		item->data_old = g_new0 (guint8, ONE_BLOCK);
		if (item->address >= dev->disk_size) {
			gmw_probe_block_free (item);
			break;
		}
		item->data_random = gmw_probe_get_random_data (ONE_BLOCK);
		item->bytes_read = gmw_probe_device_read (dev,
184 185
							  item->address +
							  item->offset,
186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201
							  item->data_old,
							  ONE_BLOCK);
		g_ptr_array_add (dev->data_save, item);
		if (item->bytes_read != ONE_BLOCK)
			break;
		if (g_cancellable_set_error_if_cancelled (cancellable, error))
			return FALSE;
	}
	return TRUE;
}

static gboolean
gmw_probe_device_data_set_dummy (GmwProbeDevice *dev,
				 GCancellable *cancellable,
				 GError **error)
{
Richard Hughes's avatar
Richard Hughes committed
202 203
	for (guint i = 0; i < dev->data_save->len; i++) {
		GmwProbeBlock *item = g_ptr_array_index (dev->data_save, i);
204
		item->bytes_wrote = gmw_probe_device_write (dev,
205 206
							    item->address +
							    item->offset,
207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224
							    item->data_random,
							    ONE_BLOCK);

		if (item->bytes_wrote != ONE_BLOCK)
			break;
		if (g_cancellable_set_error_if_cancelled (cancellable, error))
			return FALSE;
	}

	return TRUE;
}

static gboolean
gmw_probe_device_data_verify (GmwProbeDevice *dev,
			      GCancellable *cancellable,
			      GError **error)
{
	guint i;
Richard Hughes's avatar
Richard Hughes committed
225
	g_autofree guint8 *wbuf2 = g_new (guint8, ONE_BLOCK + 0xff);
226
	for (i = 0; i < dev->data_save->len; i++) {
Richard Hughes's avatar
Richard Hughes committed
227
		GmwProbeBlock *item = g_ptr_array_index (dev->data_save, i);
228 229 230

		/* use a random offset to confuse drives that are just saving
		 * the address and data in some phantom FAT */
Richard Hughes's avatar
Richard Hughes committed
231
		guint32 offset = g_random_int_range (1, 0xff);
232
		item->bytes_read = gmw_probe_device_read (dev,
233 234
							  item->address +
							  item->offset - offset,
235 236 237 238 239 240
							  wbuf2,
							  ONE_BLOCK + offset);
		if (item->bytes_read != ONE_BLOCK + offset) {
			g_set_error (error,
				     GMW_ERROR,
				     GMW_ERROR_FAILED,
Bastien Nocera's avatar
Bastien Nocera committed
241 242
				     "Failed to read data (seed: %u)",
				     seed);
243 244 245 246 247 248 249 250 251 252 253 254 255 256 257
			return FALSE;
		}
		item->valid = memcmp (item->data_random,
				      wbuf2 + offset,
				      ONE_BLOCK) == 0;
		if (g_cancellable_set_error_if_cancelled (cancellable, error))
			return FALSE;

		/* optimize; we don't need to check any more */
		if (!item->valid)
			break;
	}

	/* if we aborted early, the rest of the drive is junk */
	for (i = i; i < dev->data_save->len; i++) {
Richard Hughes's avatar
Richard Hughes committed
258
		GmwProbeBlock *item = g_ptr_array_index (dev->data_save, i);
259 260 261 262 263 264 265 266 267 268 269
		item->valid = FALSE;
	}

	return TRUE;
}

static gboolean
gmw_probe_device_data_restore (GmwProbeDevice *dev,
			       GCancellable *cancellable,
			       GError **error)
{
Richard Hughes's avatar
Richard Hughes committed
270 271
	for (guint i = 0; i < dev->data_save->len; i++) {
		GmwProbeBlock *item = g_ptr_array_index (dev->data_save, i);
272 273 274
		if (!item->valid)
			continue;
		item->bytes_wrote = gmw_probe_device_write (dev,
275 276
							    item->address +
							    item->offset,
277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306
							    item->data_old,
							    ONE_BLOCK);
		if (item->bytes_wrote != ONE_BLOCK)
			break;
		if (g_cancellable_set_error_if_cancelled (cancellable, error))
			return FALSE;
	}

	return TRUE;
}

static gboolean
gmw_probe_scan_device (GmwProbeDevice *dev, GCancellable *cancellable, GError **error)
{
	/* open block device */
	if (!gmw_probe_device_open (dev, error))
		return FALSE;

	/* get reported size */
	if (ioctl (dev->fd, BLKGETSIZE64, &dev->disk_size) != 0) {
		g_set_error (error,
			     GMW_ERROR,
			     GMW_ERROR_FAILED,
			     "Failed to get reported size");
		return FALSE;
	}
	if (dev->disk_size == 0) {
		g_set_error_literal (error,
				     GMW_ERROR,
				     GMW_ERROR_FAILED,
307
				     "Disk capacity reported as zero");
308 309
		return FALSE;
	}
310
	if (dev->disk_size > 0x4000000000llu) {
311 312 313 314
		g_set_error (error,
			     GMW_ERROR,
			     GMW_ERROR_FAILED,
			     "Disk capacity reported as invalid: %"
315 316
			     G_GUINT64_FORMAT "GB",
			     dev->disk_size / ONE_GB);
317 318
		return FALSE;
	}
319 320
	g_debug ("Disk reports to be %" G_GUINT64_FORMAT "MB in size",
		 dev->disk_size / ONE_MB);
321 322 323 324 325 326 327 328 329 330 331 332

	/* save data that's there already */
	if (!gmw_probe_device_data_save (dev, cancellable, error))
		return FALSE;

	/* write 32k or random to every 32Mb */
	if (!gmw_probe_device_data_set_dummy (dev, cancellable, error)) {
		gmw_probe_device_data_restore (dev, cancellable, NULL);
		return FALSE;
	}

	/* sanity check for really broken devices */
Richard Hughes's avatar
Richard Hughes committed
333 334
	for (guint i = 0; i < dev->data_save->len; i++) {
		GmwProbeBlock *item = g_ptr_array_index (dev->data_save, i);
335 336 337 338
		if (item->bytes_read != item->bytes_wrote) {
			g_set_error (error,
				     GMW_ERROR,
				     GMW_ERROR_FAILED,
339
				     "Failed to write len at %" G_GUINT64_FORMAT "MB",
340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367
				     item->address / ONE_MB);
			gmw_probe_device_data_restore (dev, cancellable, NULL);
			return FALSE;
		}
	}

	/* reset device */
	if (!gmw_probe_device_reset (dev, error)) {
		gmw_probe_device_data_restore (dev, cancellable, NULL);
		return FALSE;
	}

	/* wait for block drive to reappear */
	close (dev->fd);
	if (!gmw_probe_device_open (dev, error))
		return FALSE;

	/* read each chunk in again */
	if (!gmw_probe_device_data_verify (dev, cancellable, error)) {
		gmw_probe_device_data_restore (dev, cancellable, NULL);
		return FALSE;
	}

	/* write back original data */
	if (!gmw_probe_device_data_restore (dev, cancellable, error))
		return FALSE;

	/* get results */
Richard Hughes's avatar
Richard Hughes committed
368 369
	for (guint i = 0; i < dev->data_save->len; i++) {
		GmwProbeBlock *item = g_ptr_array_index (dev->data_save, i);
370 371 372 373
		if (!item->valid) {
			g_set_error (error,
				     GMW_ERROR,
				     GMW_ERROR_IS_FAKE,
Bastien Nocera's avatar
Bastien Nocera committed
374 375 376
				     "Failed to verify data at %" G_GUINT64_FORMAT "MB  (seed: %u)",
				     item->address / ONE_MB,
				     seed);
377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402
			return FALSE;
		}
	}
	return TRUE;
}

static gboolean
gmw_probe_use_device (GUdevClient *udev_client,
		      const gchar *block_dev,
		      GCancellable *cancellable,
		      GError **error)
{
	_cleanup_object_unref_ GUdevDevice *udev_device = NULL;
	GmwProbeDevice *dev;

	/* create worker object */
	dev = g_new0 (GmwProbeDevice, 1);
	dev->block_dev = g_strdup (block_dev);
	dev->data_save = g_ptr_array_new_with_free_func ((GDestroyNotify) gmw_probe_block_free);

	/* find udev device */
	udev_device = g_udev_client_query_by_device_file (udev_client, block_dev);
	if (udev_device == NULL) {
		g_set_error (error,
			     GMW_ERROR,
			     GMW_ERROR_FAILED,
403
			     "Failed to find %s", block_dev);
404 405 406 407 408 409 410
		gmw_probe_device_free (dev);
		return FALSE;
	}
	dev->udev_device = g_udev_device_get_parent_with_subsystem (udev_device,
								    "usb",
								    "usb_device");
	if (dev->udev_device == NULL) {
411 412 413 414
		g_set_error_literal (error,
				     GMW_ERROR,
				     GMW_ERROR_FAILED,
				     "Not a USB device");
415 416 417 418 419 420 421 422 423 424 425 426 427 428 429
		gmw_probe_device_free (dev);
		return FALSE;
	}

	/* actually do the scanning now */
	if (!gmw_probe_scan_device (dev, cancellable, error)) {
		gmw_probe_device_free (dev);
		return FALSE;
	}

	/* success */
	gmw_probe_device_free (dev);
	return TRUE;
}

430 431 432 433 434 435 436 437
static gboolean
gmw_probe_is_block_device_valid (const gchar *block_device)
{
	/* dev prefix */
	if (!g_str_has_prefix (block_device, "/dev/"))
		return FALSE;

	/* has no partition number */
Richard Hughes's avatar
Richard Hughes committed
438
	for (guint i = 5; block_device[i] != '\0'; i++) {
439 440 441 442 443 444 445 446 447 448
		if (g_ascii_isdigit (block_device[i]))
			return FALSE;
	}
	return TRUE;
}


static gboolean
gmw_probe_is_block_device_mounted (const gchar *block_device)
{
449
	g_autofree gchar *data = NULL;
450 451 452 453 454
	if (!g_file_get_contents ("/etc/mtab", &data, NULL, NULL))
		return FALSE;
	return g_strrstr (data, block_device) != NULL;
}

455 456 457 458
int
main (int argc, char **argv)
{
	const gchar *subsystems[] = { "usb", NULL };
459
	gboolean verbose = FALSE;
460
	_cleanup_object_unref_ GUdevClient *udev_client = NULL;
461
	g_autoptr(GCancellable) cancellable = NULL;
Richard Hughes's avatar
Richard Hughes committed
462 463
	g_autoptr(GError) error = NULL;
	g_autoptr(GOptionContext) context = NULL;
464 465 466 467 468

	const GOptionEntry options[] = {
		{ "verbose", 'v', 0, G_OPTION_ARG_NONE, &verbose,
			/* TRANSLATORS: command line option */
			_("Show extra debugging information"), NULL },
Bastien Nocera's avatar
Bastien Nocera committed
469 470 471
		{ "seed", 's', 0, G_OPTION_ARG_INT, &seed,
			/* TRANSLATORS: command line option */
			_("Random seed for predictability"), NULL },
472 473 474
		{ NULL}
	};

Bastien Nocera's avatar
Bastien Nocera committed
475 476 477 478
	if (seed == 0)
		seed = g_random_int_range (0, G_MAXINT);
	g_random_set_seed (seed);
	g_debug ("Using %u as a random seed", seed);
479 480 481 482 483 484

	/* TRANSLATORS: A program to copy the LiveUSB image onto USB hardware */
	context = g_option_context_new (NULL);
	g_option_context_add_main_entries (context, options, NULL);
	if (!g_option_context_parse (context, &argc, &argv, &error)) {
		g_print ("Failed to parse command line: %s\n", error->message);
Richard Hughes's avatar
Richard Hughes committed
485
		return EXIT_FAILURE;
486 487 488 489 490
	}

	if (verbose)
		g_setenv ("G_MESSAGES_DEBUG", "all", TRUE);

491 492
	/* valid arguments */
	if (argc != 2 || !gmw_probe_is_block_device_valid (argv[1])) {
493
		g_print ("Block device required as argument\n");
Richard Hughes's avatar
Richard Hughes committed
494
		return EXIT_FAILURE;
495 496
	}

497 498 499
	/* already mounted */
	if (gmw_probe_is_block_device_mounted (argv[1])) {
		g_print ("Partition mounted from block device\n");
Richard Hughes's avatar
Richard Hughes committed
500
		return EXIT_FAILURE;
501 502
	}

503
	/* probe device */
504 505
	cancellable = g_cancellable_new ();
	udev_client = g_udev_client_new (subsystems);
506 507 508 509 510 511
	if (!gmw_probe_use_device (udev_client, argv[1], cancellable, &error)) {
		if (g_error_matches (error, GMW_ERROR, GMW_ERROR_IS_FAKE)) {
			g_print ("Device is FAKE: %s\n", error->message);
		} else {
			g_print ("Failed to scan device: %s\n", error->message);
		}
Richard Hughes's avatar
Richard Hughes committed
512
		return EXIT_FAILURE;
513 514
	}
	g_print ("Device is GOOD\n");
Richard Hughes's avatar
Richard Hughes committed
515
	return EXIT_SUCCESS;
516
}