/* ----------------------------------------------------------------------- * * * Copyright 1998-2008 H. Peter Anvin - All Rights Reserved * Copyright 2009-2010 Intel Corporation; author: H. Peter Anvin * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, Inc., 53 Temple Place Ste 330, * Boston MA 02111-1307, USA; either version 2 of the License, or * (at your option) any later version; incorporated herein by reference. * * ----------------------------------------------------------------------- */ /* * extlinux.c * * Install the syslinux boot block on an fat, ext2/3/4 and btrfs filesystem */ #define _GNU_SOURCE /* Enable everything */ #include /* This is needed to deal with the kernel headers imported into glibc 3.3.3. */ typedef uint64_t u64; #include #include #include #include #include #ifndef __KLIBC__ #include #endif #include #include #include #include #include #include #include #include #include #include #include #include "linuxioctl.h" #include "btrfs.h" #include "fat.h" #include "../version.h" #include "syslxint.h" #include "syslxcom.h" /* common functions shared with extlinux and syslinux */ #include "setadv.h" #include "syslxopt.h" /* unified options */ #ifdef DEBUG # define dprintf printf #else # define dprintf(...) ((void)0) #endif #ifndef EXT2_SUPER_OFFSET #define EXT2_SUPER_OFFSET 1024 #endif /* the btrfs partition first 64K blank area is used to store boot sector and boot image, the boot sector is from 0~512, the boot image starts after */ #define BTRFS_BOOTSECT_AREA 65536 #define BTRFS_EXTLINUX_OFFSET SECTOR_SIZE #define BTRFS_SUBVOL_OPT "subvol=" #define BTRFS_SUBVOL_MAX 256 /* By btrfs specification */ static char subvol[BTRFS_SUBVOL_MAX]; #define BTRFS_ADV_OFFSET (BTRFS_BOOTSECT_AREA - 2 * ADV_SIZE) /* * Get the size of a block device */ uint64_t get_size(int devfd) { uint64_t bytes; uint32_t sects; struct stat st; #ifdef BLKGETSIZE64 if (!ioctl(devfd, BLKGETSIZE64, &bytes)) return bytes; #endif if (!ioctl(devfd, BLKGETSIZE, §s)) return (uint64_t) sects << 9; else if (!fstat(devfd, &st) && st.st_size) return st.st_size; else return 0; } /* * Get device geometry and partition offset */ struct geometry_table { uint64_t bytes; struct hd_geometry g; }; /* Standard floppy disk geometries, plus LS-120. Zipdisk geometry (x/64/32) is the final fallback. I don't know what LS-240 has as its geometry, since I don't have one and don't know anyone that does, and Google wasn't helpful... */ static const struct geometry_table standard_geometries[] = { {360 * 1024, {2, 9, 40, 0}}, {720 * 1024, {2, 9, 80, 0}}, {1200 * 1024, {2, 15, 80, 0}}, {1440 * 1024, {2, 18, 80, 0}}, {1680 * 1024, {2, 21, 80, 0}}, {1722 * 1024, {2, 21, 80, 0}}, {2880 * 1024, {2, 36, 80, 0}}, {3840 * 1024, {2, 48, 80, 0}}, {123264 * 1024, {8, 32, 963, 0}}, /* LS120 */ {0, {0, 0, 0, 0}} }; int get_geometry(int devfd, uint64_t totalbytes, struct hd_geometry *geo) { struct floppy_struct fd_str; struct loop_info li; struct loop_info64 li64; const struct geometry_table *gp; memset(geo, 0, sizeof *geo); if (!ioctl(devfd, HDIO_GETGEO, &geo)) { return 0; } else if (!ioctl(devfd, FDGETPRM, &fd_str)) { geo->heads = fd_str.head; geo->sectors = fd_str.sect; geo->cylinders = fd_str.track; geo->start = 0; return 0; } /* Didn't work. Let's see if this is one of the standard geometries */ for (gp = standard_geometries; gp->bytes; gp++) { if (gp->bytes == totalbytes) { memcpy(geo, &gp->g, sizeof *geo); return 0; } } /* Didn't work either... assign a geometry of 64 heads, 32 sectors; this is what zipdisks use, so this would help if someone has a USB key that they're booting in USB-ZIP mode. */ geo->heads = opt.heads ? : 64; geo->sectors = opt.sectors ? : 32; geo->cylinders = totalbytes / (geo->heads * geo->sectors << SECTOR_SHIFT); geo->start = 0; if (!opt.sectors && !opt.heads) fprintf(stderr, "Warning: unable to obtain device geometry (defaulting to %d heads, %d sectors)\n" " (on hard disks, this is usually harmless.)\n", geo->heads, geo->sectors); /* If this is a loopback device, try to set the start */ if (!ioctl(devfd, LOOP_GET_STATUS64, &li64)) geo->start = li64.lo_offset >> SECTOR_SHIFT; else if (!ioctl(devfd, LOOP_GET_STATUS, &li)) geo->start = (unsigned int)li.lo_offset >> SECTOR_SHIFT; return 1; } /* * Query the device geometry and put it into the boot sector. * Map the file and put the map in the boot sector and file. * Stick the "current directory" inode number into the file. * * Returns the number of modified bytes in the boot file. */ int patch_file_and_bootblock(int fd, const char *dir, int devfd) { struct stat dirst, xdst; struct hd_geometry geo; sector_t *sectp; uint64_t totalbytes, totalsectors; int nsect; struct boot_sector *sbs; char *dirpath, *subpath, *xdirpath, *xsubpath; int rv; dirpath = realpath(dir, NULL); if (!dirpath || stat(dir, &dirst)) { perror("accessing install directory"); exit(255); /* This should never happen */ } if (lstat(dirpath, &xdst) || dirst.st_ino != xdst.st_ino || dirst.st_dev != xdst.st_dev) { perror("realpath returned nonsense"); exit(255); } subpath = strchr(dirpath, '\0'); for (;;) { if (*subpath == '/') { if (subpath > dirpath) { *subpath = '\0'; xsubpath = subpath+1; xdirpath = dirpath; } else { xsubpath = subpath; xdirpath = "/"; } if (lstat(xdirpath, &xdst) || dirst.st_dev != xdst.st_dev) { subpath = strchr(subpath+1, '/'); if (!subpath) subpath = "/"; /* It's the root of the filesystem */ break; } *subpath = '/'; } if (subpath == dirpath) break; subpath--; } /* Now subpath should contain the path relative to the fs base */ dprintf("subpath = %s\n", subpath); totalbytes = get_size(devfd); get_geometry(devfd, totalbytes, &geo); if (opt.heads) geo.heads = opt.heads; if (opt.sectors) geo.sectors = opt.sectors; /* Patch this into a fake FAT superblock. This isn't because FAT is a good format in any way, it's because it lets the early bootstrap share code with the FAT version. */ dprintf("heads = %u, sect = %u\n", geo.heads, geo.sectors); sbs = (struct boot_sector *)syslinux_bootsect; totalsectors = totalbytes >> SECTOR_SHIFT; if (totalsectors >= 65536) { set_16(&sbs->bsSectors, 0); } else { set_16(&sbs->bsSectors, totalsectors); } set_32(&sbs->bsHugeSectors, totalsectors); set_16(&sbs->bsBytesPerSec, SECTOR_SIZE); set_16(&sbs->bsSecPerTrack, geo.sectors); set_16(&sbs->bsHeads, geo.heads); set_32(&sbs->bsHiddenSecs, geo.start); /* Construct the boot file map */ dprintf("directory inode = %lu\n", (unsigned long)dirst.st_ino); nsect = (boot_image_len + SECTOR_SIZE - 1) >> SECTOR_SHIFT; nsect += 2; /* Two sectors for the ADV */ sectp = alloca(sizeof(sector_t) * nsect); if (fs_type == EXT2 || fs_type == VFAT) { if (sectmap(fd, sectp, nsect)) { perror("bmap"); exit(1); } } else if (fs_type == BTRFS) { int i; sector_t *sp = sectp; for (i = 0; i < nsect - 2; i++) *sp++ = BTRFS_EXTLINUX_OFFSET/SECTOR_SIZE + i; for (i = 0; i < 2; i++) *sp++ = BTRFS_ADV_OFFSET/SECTOR_SIZE + i; } /* Create the modified image in memory */ rv = syslinux_patch(sectp, nsect, opt.stupid_mode, opt.raid_mode, subpath, subvol); free(dirpath); return rv; } /* * Install the boot block on the specified device. * Must be run AFTER install_file()! */ int install_bootblock(int fd, const char *device) { struct ext2_super_block sb; struct btrfs_super_block sb2; struct boot_sector sb3; bool ok = false; if (fs_type == EXT2) { if (xpread(fd, &sb, sizeof sb, EXT2_SUPER_OFFSET) != sizeof sb) { perror("reading superblock"); return 1; } if (sb.s_magic == EXT2_SUPER_MAGIC) ok = true; } else if (fs_type == BTRFS) { if (xpread(fd, &sb2, sizeof sb2, BTRFS_SUPER_INFO_OFFSET) != sizeof sb2) { perror("reading superblock"); return 1; } if (sb2.magic == *(u64 *)BTRFS_MAGIC) ok = true; } else if (fs_type == VFAT) { if (xpread(fd, &sb3, sizeof sb3, 0) != sizeof sb3) { perror("reading fat superblock"); return 1; } if (sb3.bsResSectors && sb3.bsFATs && (strstr(sb3.bs16.FileSysType, "FAT") || strstr(sb3.bs32.FileSysType, "FAT"))) ok = true; } if (!ok) { fprintf(stderr, "no fat, ext2/3/4 or btrfs superblock found on %s\n", device); return 1; } if (fs_type == VFAT) { struct boot_sector *sbs = (struct boot_sector *)syslinux_bootsect; if (xpwrite(fd, &sbs->bsHead, bsHeadLen, 0) != bsHeadLen || xpwrite(fd, &sbs->bsCode, bsCodeLen, offsetof(struct boot_sector, bsCode)) != bsCodeLen) { perror("writing fat bootblock"); return 1; } } else { if (xpwrite(fd, syslinux_bootsect, syslinux_bootsect_len, 0) != syslinux_bootsect_len) { perror("writing bootblock"); return 1; } } return 0; } int ext2_fat_install_file(const char *path, int devfd, struct stat *rst) { char *file, *oldfile; int fd = -1, dirfd = -1; int modbytes; int r1, r2; r1 = asprintf(&file, "%s%sldlinux.sys", path, path[0] && path[strlen(path) - 1] == '/' ? "" : "/"); r2 = asprintf(&oldfile, "%s%sextlinux.sys", path, path[0] && path[strlen(path) - 1] == '/' ? "" : "/"); if (r1 < 0 || !file || r2 < 0 || !oldfile) { perror(program); return 1; } dirfd = open(path, O_RDONLY | O_DIRECTORY); if (dirfd < 0) { perror(path); goto bail; } fd = open(file, O_RDONLY); if (fd < 0) { if (errno != ENOENT) { perror(file); goto bail; } } else { clear_attributes(fd); } close(fd); fd = open(file, O_WRONLY | O_TRUNC | O_CREAT | O_SYNC, S_IRUSR | S_IRGRP | S_IROTH); if (fd < 0) { perror(file); goto bail; } /* Write it the first time */ if (xpwrite(fd, boot_image, boot_image_len, 0) != boot_image_len || xpwrite(fd, syslinux_adv, 2 * ADV_SIZE, boot_image_len) != 2 * ADV_SIZE) { fprintf(stderr, "%s: write failure on %s\n", program, file); goto bail; } /* Map the file, and patch the initial sector accordingly */ modbytes = patch_file_and_bootblock(fd, path, devfd); /* Write the patch area again - this relies on the file being overwritten in place! */ if (xpwrite(fd, boot_image, modbytes, 0) != modbytes) { fprintf(stderr, "%s: write failure on %s\n", program, file); goto bail; } /* Attempt to set immutable flag and remove all write access */ /* Only set immutable flag if file is owned by root */ set_attributes(fd); if (fstat(fd, rst)) { perror(file); goto bail; } close(dirfd); close(fd); /* Look if we have the old filename */ fd = open(oldfile, O_RDONLY); if (fd >= 0) { clear_attributes(fd); close(fd); unlink(oldfile); } free(file); free(oldfile); return 0; bail: if (dirfd >= 0) close(dirfd); if (fd >= 0) close(fd); free(file); free(oldfile); return 1; } /* btrfs has to install the ldlinux.sys in the first 64K blank area, which is not managered by btrfs tree, so actually this is not installed as files. since the cow feature of btrfs will move the ldlinux.sys every where */ int btrfs_install_file(const char *path, int devfd, struct stat *rst) { patch_file_and_bootblock(-1, path, devfd); if (xpwrite(devfd, boot_image, boot_image_len, BTRFS_EXTLINUX_OFFSET) != boot_image_len) { perror("writing bootblock"); return 1; } dprintf("write boot_image to 0x%x\n", BTRFS_EXTLINUX_OFFSET); if (xpwrite(devfd, syslinux_adv, 2 * ADV_SIZE, BTRFS_ADV_OFFSET) != 2 * ADV_SIZE) { perror("writing adv"); return 1; } dprintf("write adv to 0x%x\n", BTRFS_ADV_OFFSET); if (stat(path, rst)) { perror(path); return 1; } return 0; } int install_file(const char *path, int devfd, struct stat *rst) { if (fs_type == EXT2 || fs_type == VFAT) return ext2_fat_install_file(path, devfd, rst); else if (fs_type == BTRFS) return btrfs_install_file(path, devfd, rst); return 1; } /* * SYSLINUX installs the string 'SYSLINUX' at offset 3 in the boot * sector; this is consistent with FAT filesystems. Earlier versions * would install the string "EXTLINUX" instead, handle both. */ int already_installed(int devfd) { char buffer[8]; xpread(devfd, buffer, 8, 3); return !memcmp(buffer, "SYSLINUX", 8) || !memcmp(buffer, "EXTLINUX", 8); } #ifdef __KLIBC__ static char devname_buf[64]; static void device_cleanup(void) { unlink(devname_buf); } #endif /* Verify that a device fd and a pathname agree. Return 0 on valid, -1 on error. */ static int validate_device(const char *path, int devfd) { struct stat pst, dst; struct statfs sfs; if (stat(path, &pst) || fstat(devfd, &dst) || statfs(path, &sfs)) return -1; /* btrfs st_dev is not matched with mnt st_rdev, it is a known issue */ if (fs_type == BTRFS && sfs.f_type == BTRFS_SUPER_MAGIC) return 0; return (pst.st_dev == dst.st_rdev) ? 0 : -1; } #ifndef __KLIBC__ static const char *find_device(const char *mtab_file, dev_t dev) { struct mntent *mnt; struct stat dst; FILE *mtab; const char *devname = NULL; bool done; mtab = setmntent(mtab_file, "r"); if (!mtab) return NULL; done = false; while ((mnt = getmntent(mtab))) { /* btrfs st_dev is not matched with mnt st_rdev, it is a known issue */ switch (fs_type) { case BTRFS: if (!strcmp(mnt->mnt_type, "btrfs") && !stat(mnt->mnt_dir, &dst) && dst.st_dev == dev) { char *opt = strstr(mnt->mnt_opts, BTRFS_SUBVOL_OPT); if (opt) { if (!subvol[0]) { char *tmp; strcpy(subvol, opt + sizeof(BTRFS_SUBVOL_OPT) - 1); tmp = strchr(subvol, 32); if (tmp) *tmp = '\0'; } break; /* should break and let upper layer try again */ } else done = true; } break; case EXT2: if ((!strcmp(mnt->mnt_type, "ext2") || !strcmp(mnt->mnt_type, "ext3") || !strcmp(mnt->mnt_type, "ext4")) && !stat(mnt->mnt_fsname, &dst) && dst.st_rdev == dev) { done = true; break; } case VFAT: if ((!strcmp(mnt->mnt_type, "vfat")) && !stat(mnt->mnt_fsname, &dst) && dst.st_rdev == dev) { done = true; break; } case NONE: break; } if (done) { devname = strdup(mnt->mnt_fsname); break; } } endmntent(mtab); return devname; } #endif static const char *get_devname(const char *path) { const char *devname = NULL; struct stat st; struct statfs sfs; if (stat(path, &st) || !S_ISDIR(st.st_mode)) { fprintf(stderr, "%s: Not a directory: %s\n", program, path); return devname; } if (statfs(path, &sfs)) { fprintf(stderr, "%s: statfs %s: %s\n", program, path, strerror(errno)); return devname; } #ifdef __KLIBC__ /* klibc doesn't have getmntent and friends; instead, just create a new device with the appropriate device type */ snprintf(devname_buf, sizeof devname_buf, "/tmp/dev-%u:%u", major(st.st_dev), minor(st.st_dev)); if (mknod(devname_buf, S_IFBLK | 0600, st.st_dev)) { fprintf(stderr, "%s: cannot create device %s\n", program, devname); return devname; } atexit(device_cleanup); /* unlink the device node on exit */ devname = devname_buf; #else /* check /etc/mtab first, since btrfs subvol info is only in here */ devname = find_device("/etc/mtab", st.st_dev); if (subvol[0] && !devname) { /* we just find it is a btrfs subvol */ char parent[256]; char *tmp; strcpy(parent, path); tmp = strrchr(parent, '/'); if (tmp) { *tmp = '\0'; fprintf(stderr, "%s is subvol, try its parent dir %s\n", path, parent); devname = get_devname(parent); } else devname = NULL; } if (!devname) { /* Didn't find it in /etc/mtab, try /proc/mounts */ devname = find_device("/proc/mounts", st.st_dev); } if (!devname) { fprintf(stderr, "%s: cannot find device for path %s\n", program, path); return devname; } fprintf(stderr, "%s is device %s\n", path, devname); #endif return devname; } static int open_device(const char *path, struct stat *st, const char **_devname) { int devfd; const char *devname = NULL; struct statfs sfs; if (st) if (stat(path, st) || !S_ISDIR(st->st_mode)) { fprintf(stderr, "%s: Not a directory: %s\n", program, path); return -1; } if (statfs(path, &sfs)) { fprintf(stderr, "%s: statfs %s: %s\n", program, path, strerror(errno)); return -1; } if (sfs.f_type == EXT2_SUPER_MAGIC) fs_type = EXT2; else if (sfs.f_type == BTRFS_SUPER_MAGIC) fs_type = BTRFS; else if (sfs.f_type == MSDOS_SUPER_MAGIC) fs_type = VFAT; if (!fs_type) { fprintf(stderr, "%s: not a fat, ext2/3/4 or btrfs filesystem: %s\n", program, path); return -1; } devfd = -1; devname = get_devname(path); if (_devname) *_devname = devname; if ((devfd = open(devname, O_RDWR | O_SYNC)) < 0) { fprintf(stderr, "%s: cannot open device %s\n", program, devname); return -1; } /* Verify that the device we opened is the device intended */ if (validate_device(path, devfd)) { fprintf(stderr, "%s: path %s doesn't match device %s\n", program, path, devname); close(devfd); return -1; } return devfd; } static int btrfs_read_adv(int devfd) { if (xpread(devfd, syslinux_adv, 2 * ADV_SIZE, BTRFS_ADV_OFFSET) != 2 * ADV_SIZE) return -1; return syslinux_validate_adv(syslinux_adv) ? 1 : 0; } static int ext_read_adv(const char *path, int devfd, const char **namep) { int err; const char *name; if (fs_type == BTRFS) { /* btrfs "ldlinux.sys" is in 64k blank area */ return btrfs_read_adv(devfd); } else { err = read_adv(path, name = "ldlinux.sys"); if (err == 2) /* ldlinux.sys does not exist */ err = read_adv(path, name = "extlinux.sys"); if (namep) *namep = name; return err; } } static int ext_write_adv(const char *path, const char *cfg, int devfd) { if (fs_type == BTRFS) { /* btrfs "ldlinux.sys" is in 64k blank area */ if (xpwrite(devfd, syslinux_adv, 2 * ADV_SIZE, BTRFS_ADV_OFFSET) != 2 * ADV_SIZE) { perror("writing adv"); return 1; } return 0; } return write_adv(path, cfg); } int install_loader(const char *path, int update_only) { struct stat st, fst; int devfd, rv; const char *devname; devfd = open_device(path, &st, &devname); if (devfd < 0) return 1; if (update_only && !already_installed(devfd)) { fprintf(stderr, "%s: no previous syslinux boot sector found\n", program); close(devfd); return 1; } /* Read a pre-existing ADV, if already installed */ if (opt.reset_adv) { syslinux_reset_adv(syslinux_adv); } else if (ext_read_adv(path, devfd, NULL) < 0) { close(devfd); return 1; } if (modify_adv() < 0) { close(devfd); return 1; } /* Install ldlinux.sys */ if (install_file(path, devfd, &fst)) { close(devfd); return 1; } if (fst.st_dev != st.st_dev) { fprintf(stderr, "%s: file system changed under us - aborting!\n", program); close(devfd); return 1; } sync(); rv = install_bootblock(devfd, devname); close(devfd); sync(); return rv; } /* * Modify the ADV of an existing installation */ int modify_existing_adv(const char *path) { const char *filename; int devfd; devfd = open_device(path, NULL, NULL); if (devfd < 0) return 1; if (opt.reset_adv) syslinux_reset_adv(syslinux_adv); else if (ext_read_adv(path, devfd, &filename) < 0) { close(devfd); return 1; } if (modify_adv() < 0) { close(devfd); return 1; } if (ext_write_adv(path, filename, devfd) < 0) { close(devfd); return 1; } close(devfd); return 0; } int main(int argc, char *argv[]) { parse_options(argc, argv, MODE_EXTLINUX); if (!opt.directory || opt.install_mbr || opt.activate_partition) usage(EX_USAGE, 0); if (opt.update_only == -1) { if (opt.reset_adv || opt.set_once || opt.menu_save) return modify_existing_adv(opt.directory); else usage(EX_USAGE, MODE_EXTLINUX); } return install_loader(opt.directory, opt.update_only); }