/* ----------------------------------------------------------------------- * * * Copyright 1998-2008 H. Peter Anvin - All Rights Reserved * * 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 extlinux boot block on an ext2/3 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 /* Floppy geometry */ #include /* Hard disk geometry */ #define statfs _kernel_statfs /* HACK to deal with broken 2.4 distros */ #include /* FIGETBSZ, FIBMAP */ #undef statfs #include "ext2_fs.h" #include "../version.h" #include "syslxint.h" #ifdef DEBUG # define dprintf printf #else # define dprintf(...) ((void)0) #endif /* Global option handling */ const char *program; /* These are the options we can set and their values */ struct my_options { unsigned int sectors; unsigned int heads; int raid_mode; int reset_adv; const char *set_once; } opt = { .sectors = 0,.heads = 0,.raid_mode = 0,.reset_adv = 0,.set_once = NULL,}; static void __attribute__ ((noreturn)) usage(int rv) { fprintf(stderr, "Usage: %s [options] directory\n" " --install -i Install over the current bootsector\n" " --update -U Update a previous EXTLINUX installation\n" " --zip -z Force zipdrive geometry (-H 64 -S 32)\n" " --sectors=# -S Force the number of sectors per track\n" " --heads=# -H Force number of heads\n" " --raid -r Fall back to the next device on boot failure\n" " --once=... -o Execute a command once upon boot\n" " --clear-once -O Clear the boot-once command\n" " --reset-adv Reset auxilliary data\n" "\n" " Note: geometry is determined at boot time for devices which\n" " are considered hard disks by the BIOS. Unfortunately, this is\n" " not possible for devices which are considered floppy disks,\n" " which includes zipdisks and LS-120 superfloppies.\n" "\n" " The -z option is useful for USB devices which are considered\n" " hard disks by some BIOSes and zipdrives by other BIOSes.\n", program); exit(rv); } enum long_only_opt { OPT_NONE, OPT_RESET_ADV, }; static const struct option long_options[] = { {"install", 0, NULL, 'i'}, {"update", 0, NULL, 'U'}, {"zipdrive", 0, NULL, 'z'}, {"sectors", 1, NULL, 'S'}, {"heads", 1, NULL, 'H'}, {"raid-mode", 0, NULL, 'r'}, {"version", 0, NULL, 'v'}, {"help", 0, NULL, 'h'}, {"once", 1, NULL, 'o'}, {"clear-once", 0, NULL, 'O'}, {"reset-adv", 0, NULL, OPT_RESET_ADV}, {0, 0, 0, 0} }; static const char short_options[] = "iUuzS:H:rvho:O"; #if defined(__linux__) && !defined(BLKGETSIZE64) /* This takes a u64, but the size field says size_t. Someone screwed big. */ # define BLKGETSIZE64 _IOR(0x12,114,size_t) #endif #define LDLINUX_MAGIC 0x3eb202fe enum bs_offsets { bsJump = 0x00, bsOemName = 0x03, bsBytesPerSec = 0x0b, bsSecPerClust = 0x0d, bsResSectors = 0x0e, bsFATs = 0x10, bsRootDirEnts = 0x11, bsSectors = 0x13, bsMedia = 0x15, bsFATsecs = 0x16, bsSecPerTrack = 0x18, bsHeads = 0x1a, bsHiddenSecs = 0x1c, bsHugeSectors = 0x20, /* FAT12/16 only */ bs16DriveNumber = 0x24, bs16Reserved1 = 0x25, bs16BootSignature = 0x26, bs16VolumeID = 0x27, bs16VolumeLabel = 0x2b, bs16FileSysType = 0x36, bs16Code = 0x3e, /* FAT32 only */ bs32FATSz32 = 36, bs32ExtFlags = 40, bs32FSVer = 42, bs32RootClus = 44, bs32FSInfo = 48, bs32BkBootSec = 50, bs32Reserved = 52, bs32DriveNumber = 64, bs32Reserved1 = 65, bs32BootSignature = 66, bs32VolumeID = 67, bs32VolumeLabel = 71, bs32FileSysType = 82, bs32Code = 90, bsSignature = 0x1fe }; #define bsHead bsJump #define bsHeadLen (bsOemName-bsHead) #define bsCode bs32Code /* The common safe choice */ #define bsCodeLen (bsSignature-bs32Code) #ifndef EXT2_SUPER_OFFSET #define EXT2_SUPER_OFFSET 1024 #endif /* * Boot block */ extern unsigned char extlinux_bootsect[]; extern unsigned int extlinux_bootsect_len; #define boot_block extlinux_bootsect #define boot_block_len extlinux_bootsect_len /* * Image file */ extern unsigned char extlinux_image[]; extern unsigned int extlinux_image_len; #define boot_image extlinux_image #define boot_image_len extlinux_image_len /* * Common abort function */ void __attribute__ ((noreturn)) die(const char *msg) { fputs(msg, stderr); exit(1); } /* * read/write wrapper functions */ ssize_t xpread(int fd, void *buf, size_t count, off_t offset) { char *bufp = (char *)buf; ssize_t rv; ssize_t done = 0; while (count) { rv = pread(fd, bufp, count, offset); if (rv == 0) { die("short read"); } else if (rv == -1) { if (errno == EINTR) { continue; } else { die(strerror(errno)); } } else { bufp += rv; offset += rv; done += rv; count -= rv; } } return done; } ssize_t xpwrite(int fd, const void *buf, size_t count, off_t offset) { const char *bufp = (const char *)buf; ssize_t rv; ssize_t done = 0; while (count) { rv = pwrite(fd, bufp, count, offset); if (rv == 0) { die("short write"); } else if (rv == -1) { if (errno == EINTR) { continue; } else { die(strerror(errno)); } } else { bufp += rv; offset += rv; done += rv; count -= rv; } } return done; } /* * Produce file map */ int sectmap(int fd, uint32_t * sectors, int nsectors) { unsigned int blksize, blk, nblk; unsigned int i; /* Get block size */ if (ioctl(fd, FIGETBSZ, &blksize)) return -1; /* Number of sectors per block */ blksize >>= SECTOR_SHIFT; nblk = 0; while (nsectors) { blk = nblk++; dprintf("querying block %u\n", blk); if (ioctl(fd, FIBMAP, &blk)) return -1; blk *= blksize; for (i = 0; i < blksize; i++) { if (!nsectors) return 0; dprintf("Sector: %10u\n", blk); *sectors++ = blk++; nsectors--; } } return 0; } /* * 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; 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); 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. */ void patch_file_and_bootblock(int fd, int dirfd, int devfd) { struct stat dirst; struct hd_geometry geo; uint32_t *sectp; uint64_t totalbytes, totalsectors; int nsect; unsigned char *p, *patcharea; int i, dw; uint32_t csum; if (fstat(dirfd, &dirst)) { perror("fstat dirfd"); exit(255); /* This should never happen */ } 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); totalsectors = totalbytes >> SECTOR_SHIFT; if (totalsectors >= 65536) { set_16(boot_block + bsSectors, 0); } else { set_16(boot_block + bsSectors, totalsectors); } set_32(boot_block + bsHugeSectors, totalsectors); set_16(boot_block + bsBytesPerSec, SECTOR_SIZE); set_16(boot_block + bsSecPerTrack, geo.sectors); set_16(boot_block + bsHeads, geo.heads); set_32(boot_block + bsHiddenSecs, geo.start); /* If we're in RAID mode then patch the appropriate instruction; either way write the proper boot signature */ i = get_16(boot_block + 0x1FE); if (opt.raid_mode) set_16(boot_block + i, 0x18CD); /* INT 18h */ set_16(boot_block + 0x1FE, 0xAA55); /* Construct the boot file */ 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(uint32_t) * nsect); if (sectmap(fd, sectp, nsect)) { perror("bmap"); exit(1); } /* First sector need pointer in boot sector */ set_32(boot_block + 0x1F8, *sectp++); nsect--; /* Search for LDLINUX_MAGIC to find the patch area */ for (p = boot_image; get_32(p) != LDLINUX_MAGIC; p += 4) ; patcharea = p + 8; /* Set up the totals */ dw = boot_image_len >> 2; /* COMPLETE dwords, excluding ADV */ set_16(patcharea, dw); set_16(patcharea + 2, nsect); /* Not including the first sector, but including the ADV */ set_32(patcharea + 8, dirst.st_ino); /* "Current" directory */ /* Set the sector pointers */ p = patcharea + 12; memset(p, 0, 64 * 4); while (nsect--) { set_32(p, *sectp++); p += 4; } /* Now produce a checksum */ set_32(patcharea + 4, 0); csum = LDLINUX_MAGIC; for (i = 0, p = boot_image; i < dw; i++, p += 4) csum -= get_32(p); /* Negative checksum */ set_32(patcharea + 4, csum); } /* * Read the ADV from an existing instance, or initialize if invalid. * Returns -1 on fatal errors, 0 if ADV is okay, and 1 if no valid * ADV was found. */ int read_adv(const char *path) { char *file; int fd = -1; struct stat st; int err = 0; asprintf(&file, "%s%sextlinux.sys", path, path[0] && path[strlen(path) - 1] == '/' ? "" : "/"); if (!file) { perror(program); return -1; } fd = open(file, O_RDONLY); if (fd < 0) { if (errno != ENOENT) { err = -1; } else { syslinux_reset_adv(syslinux_adv); } } else if (fstat(fd, &st)) { err = -1; } else if (st.st_size < 2 * ADV_SIZE) { /* Too small to be useful */ syslinux_reset_adv(syslinux_adv); err = 0; /* Nothing to read... */ } else if (xpread(fd, syslinux_adv, 2 * ADV_SIZE, st.st_size - 2 * ADV_SIZE) != 2 * ADV_SIZE) { err = -1; } else { /* We got it... maybe? */ err = syslinux_validate_adv(syslinux_adv) ? 1 : 0; } if (err < 0) perror(file); if (fd >= 0) close(fd); if (file) free(file); return err; } /* * Update the ADV in an existing installation. */ int write_adv(const char *path) { unsigned char advtmp[2 * ADV_SIZE]; char *file; int fd = -1; struct stat st, xst; int err = 0; int flags, nflags; asprintf(&file, "%s%sextlinux.sys", path, path[0] && path[strlen(path) - 1] == '/' ? "" : "/"); if (!file) { perror(program); return -1; } fd = open(file, O_RDONLY); if (fd < 0) { err = -1; } else if (fstat(fd, &st)) { err = -1; } else if (st.st_size < 2 * ADV_SIZE) { /* Too small to be useful */ err = -2; } else if (xpread(fd, advtmp, 2 * ADV_SIZE, st.st_size - 2 * ADV_SIZE) != 2 * ADV_SIZE) { err = -1; } else { /* We got it... maybe? */ err = syslinux_validate_adv(advtmp) ? -2 : 0; if (!err) { /* Got a good one, write our own ADV here */ if (!ioctl(fd, EXT2_IOC_GETFLAGS, &flags)) { nflags = flags & ~EXT2_IMMUTABLE_FL; if (nflags != flags) ioctl(fd, EXT2_IOC_SETFLAGS, &nflags); } if (!(st.st_mode & S_IWUSR)) fchmod(fd, st.st_mode | S_IWUSR); /* Need to re-open read-write */ close(fd); fd = open(file, O_RDWR | O_SYNC); if (fd < 0) { err = -1; } else if (fstat(fd, &xst) || xst.st_ino != st.st_ino || xst.st_dev != st.st_dev || xst.st_size != st.st_size) { fprintf(stderr, "%s: race condition on write\n", file); err = -2; } /* Write our own version ... */ if (xpwrite(fd, syslinux_adv, 2 * ADV_SIZE, st.st_size - 2 * ADV_SIZE) != 2 * ADV_SIZE) { err = -1; } sync(); if (!(st.st_mode & S_IWUSR)) fchmod(fd, st.st_mode); if (nflags != flags) ioctl(fd, EXT2_IOC_SETFLAGS, &flags); } } if (err == -2) fprintf(stderr, "%s: cannot write auxilliary data (need --update)?\n", file); else if (err == -1) perror(file); if (fd >= 0) close(fd); if (file) free(file); return err; } /* * Make any user-specified ADV modifications */ int modify_adv(void) { int rv = 0; if (opt.set_once) { if (syslinux_setadv(ADV_BOOTONCE, strlen(opt.set_once), opt.set_once)) { fprintf(stderr, "%s: not enough space for boot-once command\n", program); rv = -1; } } 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; if (xpread(fd, &sb, sizeof sb, EXT2_SUPER_OFFSET) != sizeof sb) { perror("reading superblock"); return 1; } if (sb.s_magic != EXT2_SUPER_MAGIC) { fprintf(stderr, "no ext2/ext3 superblock found on %s\n", device); return 1; } if (xpwrite(fd, boot_block, boot_block_len, 0) != boot_block_len) { perror("writing bootblock"); return 1; } return 0; } int install_file(const char *path, int devfd, struct stat *rst) { char *file; int fd = -1, dirfd = -1, flags; struct stat st; asprintf(&file, "%s%sextlinux.sys", path, path[0] && path[strlen(path) - 1] == '/' ? "" : "/"); if (!file) { 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 { /* If file exist, remove the immutable flag and set u+w mode */ if (!ioctl(fd, EXT2_IOC_GETFLAGS, &flags)) { flags &= ~EXT2_IMMUTABLE_FL; ioctl(fd, EXT2_IOC_SETFLAGS, &flags); } if (!fstat(fd, &st)) { fchmod(fd, st.st_mode | S_IWUSR); } } 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 */ patch_file_and_bootblock(fd, dirfd, devfd); /* Write the first sector again - this relies on the file being overwritten in place! */ if (xpwrite(fd, boot_image, SECTOR_SIZE, 0) != SECTOR_SIZE) { 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 */ if (!fstat(fd, &st)) { fchmod(fd, st.st_mode & (S_IRUSR | S_IRGRP | S_IROTH)); if (st.st_uid == 0 && !ioctl(fd, EXT2_IOC_GETFLAGS, &flags)) { flags |= EXT2_IMMUTABLE_FL; ioctl(fd, EXT2_IOC_SETFLAGS, &flags); } } if (fstat(fd, rst)) { perror(file); goto bail; } close(dirfd); close(fd); return 0; bail: if (dirfd >= 0) close(dirfd); if (fd >= 0) close(fd); return 1; } /* EXTLINUX installs the string 'EXTLINUX' at offset 3 in the boot sector; this is consistent with FAT filesystems. */ int already_installed(int devfd) { char buffer[8]; xpread(devfd, buffer, 8, 3); return !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; if (stat(path, &pst) || fstat(devfd, &dst)) return -1; 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; mtab = setmntent(mtab_file, "r"); if (!mtab) return NULL; while ((mnt = getmntent(mtab))) { if ((!strcmp(mnt->mnt_type, "ext2") || !strcmp(mnt->mnt_type, "ext3")) && !stat(mnt->mnt_fsname, &dst) && dst.st_rdev == dev) { devname = strdup(mnt->mnt_fsname); break; } } endmntent(mtab); return devname; } #endif int install_loader(const char *path, int update_only) { struct stat st, fst; int devfd, rv; const char *devname = NULL; struct statfs sfs; 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) { fprintf(stderr, "%s: not an ext2/ext3 filesystem: %s\n", program, path); return 1; } devfd = -1; #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 1; } atexit(device_cleanup); /* unlink the device node on exit */ devname = devname_buf; #else devname = find_device("/proc/mounts", st.st_dev); if (!devname) { /* Didn't find it in /proc/mounts, try /etc/mtab */ devname = find_device("/etc/mtab", st.st_dev); } if (!devname) { fprintf(stderr, "%s: cannot find device for path %s\n", program, path); return 1; } fprintf(stderr, "%s is device %s\n", path, devname); #endif 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); return 1; } if (update_only && !already_installed(devfd)) { fprintf(stderr, "%s: no previous extlinux boot sector found\n", program); return 1; } /* Read a pre-existing ADV, if already installed */ if (opt.reset_adv) syslinux_reset_adv(syslinux_adv); else if (read_adv(path) < 0) return 1; if (modify_adv() < 0) return 1; /* Install extlinux.sys */ if (install_file(path, devfd, &fst)) return 1; if (fst.st_dev != st.st_dev) { fprintf(stderr, "%s: file system changed under us - aborting!\n", program); 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) { if (opt.reset_adv) syslinux_reset_adv(syslinux_adv); else if (read_adv(path) < 0) return 1; if (modify_adv() < 0) return 1; if (write_adv(path) < 0) return 1; return 0; } int main(int argc, char *argv[]) { int o; const char *directory; int update_only = -1; program = argv[0]; while ((o = getopt_long(argc, argv, short_options, long_options, NULL)) != EOF) { switch (o) { case 'z': opt.heads = 64; opt.sectors = 32; break; case 'S': opt.sectors = strtoul(optarg, NULL, 0); if (opt.sectors < 1 || opt.sectors > 63) { fprintf(stderr, "%s: invalid number of sectors: %u (must be 1-63)\n", program, opt.sectors); exit(EX_USAGE); } break; case 'H': opt.heads = strtoul(optarg, NULL, 0); if (opt.heads < 1 || opt.heads > 256) { fprintf(stderr, "%s: invalid number of heads: %u (must be 1-256)\n", program, opt.heads); exit(EX_USAGE); } break; case 'r': opt.raid_mode = 1; break; case 'i': update_only = 0; break; case 'u': case 'U': update_only = 1; break; case 'h': usage(0); break; case 'o': opt.set_once = optarg; break; case 'O': opt.set_once = ""; break; case OPT_RESET_ADV: opt.reset_adv = 1; break; case 'v': fputs("extlinux " VERSION_STR " Copyright 1994-" YEAR_STR " H. Peter Anvin \n", stderr); exit(0); default: usage(EX_USAGE); } } directory = argv[optind]; if (!directory) usage(EX_USAGE); if (update_only == -1) { if (opt.reset_adv || opt.set_once) { return modify_existing_adv(directory); } else { usage(EX_USAGE); } } return install_loader(directory, update_only); }