src: add new option --link to preserve hard links; use with care

This commit is contained in:
Markus F.X.J. Oberhumer 2023-09-04 22:34:21 +02:00
parent 62dbf8485f
commit 4f6320d3aa
8 changed files with 181 additions and 82 deletions

1
NEWS
View File

@ -4,6 +4,7 @@ User visible changes for UPX
Changes in 4.2.0 (XX XXX 2023):
* bug fixes - see https://github.com/upx/upx/milestone/13
* new option '--link' to preserve hard-links (Unix only; use with care)
* add support for NO_COLOR env var; see https://no-color.org/
Changes in 4.1.0 (08 Aug 2023):

View File

@ -724,7 +724,7 @@ extern const char *progname;
bool main_set_exit_code(int ec);
int main_get_options(int argc, char **argv);
void main_get_envoptions();
int upx_main(int argc, char *argv[]);
int upx_main(int argc, char *argv[]) may_throw;
// msg.cpp
void printSetNl(int need_nl) noexcept;
@ -741,8 +741,8 @@ void infoHeader();
void infoWriting(const char *what, upx_int64_t size);
// work.cpp
void do_one_file(const char *iname, char *oname);
int do_files(int i, int argc, char *argv[]);
void do_one_file(const char *iname, char *oname) may_throw;
int do_files(int i, int argc, char *argv[]) may_throw;
// help.cpp
extern const char gitrev[];

View File

@ -33,6 +33,7 @@
**************************************************************************/
/*static*/ void FileBase::chmod(const char *name, int mode) {
assert(name != nullptr && name[0] != 0);
#if HAVE_CHMOD
if (::chmod(name, mode) != 0)
throwIOException(name, errno);
@ -51,9 +52,16 @@
throwIOException("rename error", errno);
}
/*static*/ void FileBase::unlink(const char *name) {
if (::unlink(name) != 0)
/*static*/ bool FileBase::unlink(const char *name, bool check) {
assert(name != nullptr && name[0] != 0);
bool success = ::unlink(name) == 0;
#if HAVE_CHMOD
if (!success)
success = (::chmod(name, 0666) == 0 && ::unlink(name) == 0);
#endif
if (check && !success)
throwIOException(name, errno);
return success;
}
/*************************************************************************
@ -122,14 +130,14 @@ upx_off_t FileBase::seek(upx_off_t off, int whence) {
if (off < 0)
throwIOException("bad seek 2");
off += _offset;
}
if (whence == SEEK_END) {
} else if (whence == SEEK_END) {
if (off > 0)
throwIOException("bad seek 3");
off += _offset + _length;
whence = SEEK_SET;
}
// SEEK_CUR falls through to here
} else if (whence == SEEK_CUR) {
} else
throwInternalError("bad seek: whence");
upx_off_t l = ::lseek(_fd, off, whence);
if (l < 0)
throwIOException("seek error", errno);

View File

@ -50,9 +50,9 @@ public:
public:
// static file-related util functions; will throw on error
static void chmod(const char *name, int mode);
static void rename(const char *old_, const char *new_);
static void unlink(const char *name);
static void chmod(const char *name, int mode) may_throw;
static void rename(const char *old_, const char *new_) may_throw;
static bool unlink(const char *name, bool check = true) may_throw;
protected:
bool do_sopen();

View File

@ -185,7 +185,6 @@ void show_help(int verbose) {
con_fprintf(f,
" -q be quiet -v be verbose\n"
" -oFILE write output to 'FILE'\n"
//" -f force overwrite of output files and compression of suspicious files\n"
" -f force compression of suspicious files\n"
"%s%s"
, (verbose == 0) ? " -k keep backup files\n" : ""
@ -222,6 +221,19 @@ void show_help(int verbose) {
" --overlay=skip don't compress a file with an overlay\n"
"\n");
fg = con_fg(f, FG_YELLOW);
con_fprintf(f, "File system options:\n");
fg = con_fg(f, fg);
con_fprintf(f,
" --force-overwrite force overwrite of output files\n"
#if defined(__unix__) && !defined(__MSYS2__)
" --link preserve hard links (Unix only) [USE WITH CARE]\n"
" --no-link do not preserve hard links but rename files [default]\n"
#endif
" --no-mode do not preserve file mode (aka permissions)\n"
" --no-owner do not preserve file ownership\n"
" --no-time do not preserve file timestamp\n"
"\n");
fg = con_fg(f, FG_YELLOW);
con_fprintf(f, "Options for djgpp2/coff:\n");
fg = con_fg(f, fg);
con_fprintf(f,

View File

@ -160,7 +160,7 @@ static void check_not_both(bool e1, bool e2, const char *c1, const char *c2) {
}
}
static void check_options(int i, int argc) {
static void check_and_update_options(int i, int argc) {
assert(i <= argc);
if (opt->cmd != CMD_COMPRESS) {
@ -179,6 +179,7 @@ static void check_options(int i, int argc) {
opt->overlay = opt->COPY_OVERLAY;
check_not_both(opt->exact, opt->overlay == opt->STRIP_OVERLAY, "--exact", "--overlay=strip");
check_not_both(opt->force_overwrite, opt->preserve_link, "--force-overwrite", "--link");
// set default backup option
if (opt->backup < 0)
@ -198,6 +199,13 @@ static void check_options(int i, int argc) {
e_usage();
}
}
#if defined(__unix__) && !defined(__MSYS2__)
#else
// preserve_link is currently silently ignored on non-Unix platforms
// (we may revisit this decision later if there is some actual use-case)
opt->preserve_link = false;
#endif
}
/*************************************************************************
@ -521,6 +529,14 @@ static int do_option(int optc, const char *arg) {
case 519:
opt->no_env = true;
break;
case 530:
// NOTE: only use "preserve_link" if you really need it, e.g. it can fail
// with ETXTBSY and other unexpected errors; renaming files is much safer
opt->preserve_link = true;
break;
case 531:
opt->preserve_link = false;
break;
case 526:
opt->preserve_mode = false;
break;
@ -797,8 +813,10 @@ int main_get_options(int argc, char **argv) {
{"force", 0, N, 'f'}, // force overwrite of output files
{"force-compress", 0, N, 'f'}, // and compression of suspicious files
{"force-overwrite", 0x90, N, 529}, // force overwrite of output files
{"link", 0x90, N, 530}, // preserve hard link
{"info", 0, N, 'i'}, // info mode
{"no-env", 0x10, N, 519}, // no environment var
{"no-link", 0x90, N, 531}, // do not preserve hard link [default]
{"no-mode", 0x10, N, 526}, // do not preserve mode (permissions)
{"no-owner", 0x10, N, 527}, // do not preserve ownership
{"no-progress", 0, N, 516}, // no progress bar
@ -1147,7 +1165,7 @@ static void first_options(int argc, char **argv) {
// main entry point
**************************************************************************/
int upx_main(int argc, char *argv[]) {
int upx_main(int argc, char *argv[]) may_throw {
int i;
static char default_argv0[] = "upx";
assert(argc >= 1); // sanity check
@ -1251,7 +1269,7 @@ int upx_main(int argc, char *argv[]) {
if (argc == 1)
e_help();
set_term(stderr);
check_options(i, argc);
check_and_update_options(i, argc);
int num_files = argc - i;
if (num_files < 1) {
if (opt->verbose >= 2)

View File

@ -81,6 +81,7 @@ struct Options final {
bool no_env;
bool no_progress;
const char *output_name;
bool preserve_link;
bool preserve_mode;
bool preserve_ownership;
bool preserve_timestamp;

View File

@ -34,6 +34,7 @@
#include "file.h"
#include "packmast.h"
#include "ui.h"
#include "util/membuffer.h"
#if (ACC_OS_DOS32) && defined(__DJGPP__)
#define USE_FTIME 1
@ -50,16 +51,94 @@
#define SH_DENYWR (-1)
#endif
/*************************************************************************
// util
**************************************************************************/
// ignore errors in some cases and silence __attribute__((__warn_unused_result__))
#define IGNORE_ERROR(var) ACC_UNUSED(var)
enum WronlyOpenMode { WOM_MUST_EXIST_TRUNCATE, WOM_MUST_CREATE, WOM_CREATE_OR_TRUNCATE };
static constexpr int get_wronly_open_flags(WronlyOpenMode mode) noexcept {
constexpr int flags = O_WRONLY | O_BINARY;
if (mode == WOM_MUST_EXIST_TRUNCATE)
return flags | O_TRUNC; // will cause an error if file does not exist
if (mode == WOM_MUST_CREATE)
return flags | O_CREAT | O_EXCL; // will cause an error if file already exists
// create if not exists, otherwise truncate
return flags | O_CREAT | O_TRUNC;
}
static void copy_file_contents(const char *iname, const char *oname, WronlyOpenMode mode)
may_throw {
InputFile fi;
fi.sopen(iname, O_RDONLY | O_BINARY, SH_DENYWR);
fi.seek(0, SEEK_SET);
int flags = get_wronly_open_flags(mode);
int shmode = SH_DENYWR;
int omode = 0600; // affected by umask; ignored unless O_CREAT
OutputFile fo;
fo.sopen(oname, flags, shmode, omode);
fo.seek(0, SEEK_SET);
MemBuffer buf(256 * 1024 * 1024);
for (;;) {
size_t bytes = fi.read(buf, buf.getSize());
if (bytes == 0)
break;
fo.write(buf, bytes);
}
fi.closex();
fo.closex();
}
static void copy_file_attributes(const struct stat *st, const char *oname, bool preserve_mode,
bool preserve_ownership, bool preserve_timestamp) noexcept {
#if USE_UTIME
// copy time stamp
if (preserve_timestamp) {
struct utimbuf u;
u.actime = st->st_atime;
u.modtime = st->st_mtime;
int r = utime(oname, &u);
IGNORE_ERROR(r);
}
#endif
#if HAVE_CHOWN
// copy the group ownership
if (preserve_ownership) {
int r = chown(oname, -1, st->st_gid);
IGNORE_ERROR(r);
}
#endif
#if HAVE_CHMOD
// copy permissions
if (preserve_mode) {
int r = chmod(oname, st->st_mode);
IGNORE_ERROR(r);
}
#endif
#if HAVE_CHOWN
// copy the user ownership
if (preserve_ownership) {
int r = chown(oname, st->st_uid, -1);
IGNORE_ERROR(r);
}
#endif
// maybe unused
UNUSED(oname);
UNUSED(preserve_mode);
UNUSED(preserve_ownership);
UNUSED(preserve_timestamp);
}
/*************************************************************************
// process one file
**************************************************************************/
void do_one_file(const char *iname, char *oname) {
void do_one_file(const char *const iname, char *const oname) may_throw {
int r;
struct stat st;
struct stat st; // stat of iname
mem_clear(&st);
#if HAVE_LSTAT
r = lstat(iname, &st);
@ -99,6 +178,7 @@ void do_one_file(const char *iname, char *oname) {
throwIOException("file is write protected -- skipped");
}
// open input file
InputFile fi;
fi.sopen(iname, O_RDONLY | O_BINARY, SH_DENYWR);
@ -113,6 +193,7 @@ void do_one_file(const char *iname, char *oname) {
// open output file
OutputFile fo;
bool copy_timestamp_only = false;
if (opt->cmd == CMD_COMPRESS || opt->cmd == CMD_DECOMPRESS) {
if (opt->to_stdout) {
if (!fo.openStdout(1, opt->force ? true : false))
@ -121,33 +202,30 @@ void do_one_file(const char *iname, char *oname) {
char tname[ACC_FN_PATH_MAX + 1];
if (opt->output_name) {
strcpy(tname, opt->output_name);
if (opt->force_overwrite || opt->force >= 2) {
#if HAVE_CHMOD
r = chmod(tname, 0777);
IGNORE_ERROR(r);
#endif
r = unlink(tname);
IGNORE_ERROR(r);
}
if ((opt->force_overwrite || opt->force >= 2) && !opt->preserve_link)
FileBase::unlink(tname, false);
} else {
if (!maketempname(tname, sizeof(tname), iname, ".upx"))
throwIOException("could not create a temporary file name");
}
int flags = O_CREAT | O_WRONLY | O_BINARY;
if (opt->force_overwrite || opt->force)
flags |= O_TRUNC;
else
flags |= O_EXCL;
int flags = get_wronly_open_flags(WOM_MUST_CREATE);
if (opt->output_name && opt->preserve_link) {
flags = get_wronly_open_flags(WOM_CREATE_OR_TRUNCATE);
if (file_exists(opt->output_name)) {
flags = get_wronly_open_flags(WOM_MUST_EXIST_TRUNCATE);
copy_timestamp_only = true;
}
} else if (opt->force_overwrite || opt->force)
flags = get_wronly_open_flags(WOM_CREATE_OR_TRUNCATE);
int shmode = SH_DENYWR;
#if (ACC_ARCH_M68K && ACC_OS_TOS && ACC_CC_GNUC) && defined(__MINT__)
// TODO later: check current mintlib if this hack is still needed
flags |= O_TRUNC;
shmode = O_DENYRW;
#endif
// cannot rely on open() because of umask
// int omode = st.st_mode | 0600;
int omode = 0600;
if (!opt->preserve_mode)
omode = 0666;
int omode = opt->preserve_mode ? 0600 : 0666; // affected by umask; only for O_CREAT
fo.sopen(tname, flags, shmode, omode);
// open succeeded - now set oname[]
strcpy(oname, tname);
@ -187,59 +265,45 @@ void do_one_file(const char *iname, char *oname) {
fo.closex();
fi.closex();
// rename or delete files
// rename or copy files
// NOTE: only use "preserve_link" if you really need it, e.g. it can fail
// with ETXTBSY and other unexpected errors; renaming files is much safer
if (oname[0] && !opt->output_name) {
// both iname and oname do exist; rename oname to iname
if (opt->backup) {
char bakname[ACC_FN_PATH_MAX + 1];
if (!makebakname(bakname, sizeof(bakname), iname))
throwIOException("could not create a backup file name");
FileBase::rename(iname, bakname);
if (opt->preserve_link) {
copy_file_contents(iname, bakname, WOM_MUST_CREATE);
copy_file_attributes(&st, bakname, true, true, true);
copy_file_contents(oname, iname, WOM_MUST_EXIST_TRUNCATE);
FileBase::unlink(oname);
copy_timestamp_only = true;
} else {
FileBase::rename(iname, bakname);
FileBase::rename(oname, iname);
}
} else if (opt->preserve_link) {
copy_file_contents(oname, iname, WOM_MUST_EXIST_TRUNCATE);
FileBase::unlink(oname);
copy_timestamp_only = true;
} else {
#if HAVE_CHMOD
r = chmod(iname, 0777);
IGNORE_ERROR(r);
#endif
FileBase::unlink(iname);
FileBase::rename(oname, iname);
}
FileBase::rename(oname, iname);
// now iname is the new packed/unpacked file and oname does not exist any longer
}
// copy file attributes
if (oname[0]) {
oname[0] = 0; // done with oname
const char *name = opt->output_name ? opt->output_name : iname;
UNUSED(name);
#if USE_UTIME
// copy time stamp
if (opt->preserve_timestamp) {
struct utimbuf u;
u.actime = st.st_atime;
u.modtime = st.st_mtime;
r = utime(name, &u);
IGNORE_ERROR(r);
}
#endif
#if HAVE_CHOWN
// copy the group ownership
if (opt->preserve_ownership) {
r = chown(name, -1, st.st_gid);
IGNORE_ERROR(r);
}
#endif
#if HAVE_CHMOD
// copy permissions
if (opt->preserve_mode) {
r = chmod(name, st.st_mode);
IGNORE_ERROR(r);
}
#endif
#if HAVE_CHOWN
// copy the user ownership
if (opt->preserve_ownership) {
r = chown(name, st.st_uid, -1);
IGNORE_ERROR(r);
}
#endif
if (copy_timestamp_only)
copy_file_attributes(&st, name, false, false, opt->preserve_timestamp);
else
copy_file_attributes(&st, name, opt->preserve_mode, opt->preserve_ownership,
opt->preserve_timestamp);
}
UiPacker::uiConfirmUpdate();
@ -251,17 +315,12 @@ void do_one_file(const char *iname, char *oname) {
static void unlink_ofile(char *oname) noexcept {
if (oname && oname[0]) {
#if HAVE_CHMOD
int r;
r = chmod(oname, 0777);
IGNORE_ERROR(r);
#endif
if (unlink(oname) == 0)
oname[0] = 0; // done with oname
FileBase::unlink(oname, false);
oname[0] = 0; // done with oname
}
}
int do_files(int i, int argc, char *argv[]) {
int do_files(int i, int argc, char *argv[]) may_throw {
upx_compiler_sanity_check();
if (opt->verbose >= 1) {
show_header();
@ -271,7 +330,7 @@ int do_files(int i, int argc, char *argv[]) {
for (; i < argc; i++) {
infoHeader();
const char *iname = argv[i];
const char *const iname = argv[i];
char oname[ACC_FN_PATH_MAX + 1];
oname[0] = 0;