* syscalls.cc (rename): Check oldpath and newpath for trailing dir

separators, require them to be existing directories if so.  Check
	for a request to change only the case of the filename.  Check paths
	for case insensitve equality only once.  Handle renaming a directory
	to another, existing directory by unlinking the destination directory
	first.  If newpath points to an existing file with R/O attribute set,
	try to unset R/O attribute first.  Augment hardlink test by not
	checking directories.  If renaming fails with STATUS_ACCESS_DENIED,
	try to unlink existing destination filename and try renaming again.
	Drop useless test for non-empty directory.  Always close fh at the
	end of the function.
This commit is contained in:
Corinna Vinschen 2007-08-10 11:16:27 +00:00
parent a680a53258
commit 349fba0cb4
2 changed files with 127 additions and 31 deletions

View File

@ -1,3 +1,17 @@
2007-08-10 Corinna Vinschen <corinna@vinschen.de>
* syscalls.cc (rename): Check oldpath and newpath for trailing dir
separators, require them to be existing directories if so. Check
for a request to change only the case of the filename. Check paths
for case insensitve equality only once. Handle renaming a directory
to another, existing directory by unlinking the destination directory
first. If newpath points to an existing file with R/O attribute set,
try to unset R/O attribute first. Augment hardlink test by not
checking directories. If renaming fails with STATUS_ACCESS_DENIED,
try to unlink existing destination filename and try renaming again.
Drop useless test for non-empty directory. Always close fh at the
end of the function.
2007-08-09 Ernie Coskrey <Ernie.Coskrey@steeleye.com> 2007-08-09 Ernie Coskrey <Ernie.Coskrey@steeleye.com>
* gendef (sigbe): Reset "incyg" while the stack lock is active to avoid * gendef (sigbe): Reset "incyg" while the stack lock is active to avoid

View File

@ -1350,11 +1350,14 @@ extern "C" int
rename (const char *oldpath, const char *newpath) rename (const char *oldpath, const char *newpath)
{ {
int res = -1; int res = -1;
char *oldbuf, *newbuf;
path_conv oldpc, newpc, new2pc, *dstpc, *removepc = NULL; path_conv oldpc, newpc, new2pc, *dstpc, *removepc = NULL;
bool old_dir_requested = false, new_dir_requested = false;
bool old_explicit_suffix = false, new_explicit_suffix = false; bool old_explicit_suffix = false, new_explicit_suffix = false;
size_t olen, nlen; size_t olen, nlen;
bool equal_path;
NTSTATUS status; NTSTATUS status;
HANDLE fh, nfh; HANDLE fh = NULL, nfh;
OBJECT_ATTRIBUTES attr; OBJECT_ATTRIBUTES attr;
IO_STATUS_BLOCK io; IO_STATUS_BLOCK io;
ULONG size; ULONG size;
@ -1372,6 +1375,18 @@ rename (const char *oldpath, const char *newpath)
goto out; goto out;
} }
/* A trailing slash requires that the pathname points to an existing
directory. If it's not, it's a ENOTDIR condition. The same goes
for newpath a bit further down this function. */
olen = strlen (oldpath);
if (isdirsep (oldpath[olen - 1]))
{
stpcpy (oldbuf = (char *) alloca (olen + 1), oldpath);
while (olen > 0 && isdirsep (oldbuf[olen - 1]))
oldbuf[--olen] = '\0';
oldpath = oldbuf;
old_dir_requested = true;
}
oldpc.check (oldpath, PC_SYM_NOFOLLOW, stat_suffixes); oldpc.check (oldpath, PC_SYM_NOFOLLOW, stat_suffixes);
if (oldpc.error) if (oldpc.error)
{ {
@ -1388,12 +1403,25 @@ rename (const char *oldpath, const char *newpath)
set_errno (EROFS); set_errno (EROFS);
goto out; goto out;
} }
olen = strlen (oldpath); if (old_dir_requested && !oldpc.isdir ())
{
set_errno (ENOTDIR);
goto out;
}
if (oldpc.known_suffix if (oldpc.known_suffix
&& (strcasematch (oldpath + olen - 4, ".lnk") && (strcasematch (oldpath + olen - 4, ".lnk")
|| strcasematch (oldpath + olen - 4, ".exe"))) || strcasematch (oldpath + olen - 4, ".exe")))
old_explicit_suffix = true; old_explicit_suffix = true;
nlen = strlen (newpath);
if (isdirsep (newpath[nlen - 1]))
{
stpcpy (newbuf = (char *) alloca (nlen + 1), newpath);
while (nlen > 0 && isdirsep (newbuf[nlen - 1]))
newbuf[--nlen] = '\0';
newpath = newbuf;
new_dir_requested = true;
}
newpc.check (newpath, PC_SYM_NOFOLLOW, stat_suffixes); newpc.check (newpath, PC_SYM_NOFOLLOW, stat_suffixes);
if (newpc.error) if (newpc.error)
{ {
@ -1405,13 +1433,32 @@ rename (const char *oldpath, const char *newpath)
set_errno (EROFS); set_errno (EROFS);
goto out; goto out;
} }
nlen = strlen (newpath); if (new_dir_requested && !newpc.isdir ())
{
set_errno (ENOTDIR);
goto out;
}
if (newpc.known_suffix if (newpc.known_suffix
&& (strcasematch (newpath + nlen - 4, ".lnk") && (strcasematch (newpath + nlen - 4, ".lnk")
|| strcasematch (newpath + nlen - 4, ".exe"))) || strcasematch (newpath + nlen - 4, ".exe")))
new_explicit_suffix = true; new_explicit_suffix = true;
if (oldpc.isdir ()) /* This test is necessary in almost every case, so just do it once here. */
equal_path = RtlEqualUnicodeString (oldpc.get_nt_native_path (),
newpc.get_nt_native_path (),
TRUE);
/* First check if oldpath and newpath only differ by case. If so, it's
just a request to change the case of the filename. By simply setting
the file attributes to INVALID_FILE_ATTRIBUTES (which translates to
"file doesn't exist"), all later tests are skipped. */
if (newpc.exists ()
&& equal_path
&& !RtlEqualUnicodeString (oldpc.get_nt_native_path (),
newpc.get_nt_native_path (),
FALSE))
newpc.file_attributes (INVALID_FILE_ATTRIBUTES);
else if (oldpc.isdir ())
{ {
if (newpc.exists () && !newpc.isdir ()) if (newpc.exists () && !newpc.isdir ())
{ {
@ -1433,10 +1480,7 @@ rename (const char *oldpath, const char *newpath)
} }
else if (!newpc.exists ()) else if (!newpc.exists ())
{ {
if (RtlEqualUnicodeString (oldpc.get_nt_native_path (), if (equal_path && old_explicit_suffix != new_explicit_suffix)
newpc.get_nt_native_path (),
TRUE)
&& old_explicit_suffix != new_explicit_suffix)
{ {
newpc.check (newpath, PC_SYM_NOFOLLOW); newpc.check (newpath, PC_SYM_NOFOLLOW);
if (RtlEqualUnicodeString (oldpc.get_nt_native_path (), if (RtlEqualUnicodeString (oldpc.get_nt_native_path (),
@ -1464,10 +1508,7 @@ rename (const char *oldpath, const char *newpath)
} }
else else
{ {
if (RtlEqualUnicodeString (oldpc.get_nt_native_path (), if (equal_path && old_explicit_suffix != new_explicit_suffix)
newpc.get_nt_native_path (),
TRUE)
&& old_explicit_suffix != new_explicit_suffix)
{ {
newpc.check (newpath, PC_SYM_NOFOLLOW); newpc.check (newpath, PC_SYM_NOFOLLOW);
if (RtlEqualUnicodeString (oldpc.get_nt_native_path (), if (RtlEqualUnicodeString (oldpc.get_nt_native_path (),
@ -1524,6 +1565,52 @@ rename (const char *oldpath, const char *newpath)
__seterrno_from_nt_status (status); __seterrno_from_nt_status (status);
goto out; goto out;
} }
/* Renaming a dir to another, existing dir fails always, even if
ReplaceIfExists is set to TRUE and the existing dir is empty. So
we have to remove the destination dir first. This also covers the
case that the destination directory is not empty. In that case,
unlink_nt returns with STATUS_DIRECTORY_NOT_EMPTY. */
if (dstpc->isdir ())
{
status = unlink_nt (*dstpc);
if (!NT_SUCCESS (status))
{
__seterrno_from_nt_status (status);
goto out;
}
}
/* You can't copy a file if the destination exists and has the R/O
attribute set. Remove the R/O attribute first. */
else if (dstpc->has_attribute (FILE_ATTRIBUTE_READONLY))
{
status = NtOpenFile (&nfh, FILE_WRITE_ATTRIBUTES,
dstpc->get_object_attr (attr, sec_none_nih),
&io, FILE_SHARE_VALID_FLAGS,
FILE_OPEN_FOR_BACKUP_INTENT
| (dstpc->is_rep_symlink ()
? FILE_OPEN_REPARSE_POINT : 0));
if (!NT_SUCCESS (status))
{
__seterrno_from_nt_status (status);
goto out;
}
FILE_BASIC_INFORMATION fbi;
fbi.CreationTime.QuadPart = fbi.LastAccessTime.QuadPart =
fbi.LastWriteTime.QuadPart = fbi.ChangeTime.QuadPart = 0LL;
fbi.FileAttributes = (dstpc->file_attributes ()
& ~FILE_ATTRIBUTE_READONLY)
?: FILE_ATTRIBUTE_NORMAL;
status = NtSetInformationFile (nfh, &io, &fbi, sizeof fbi,
FileBasicInformation);
NtClose (nfh);
if (!NT_SUCCESS (status))
{
__seterrno_from_nt_status (status);
goto out;
}
}
/* SUSv3: If the old argument and the new argument resolve to the same /* SUSv3: If the old argument and the new argument resolve to the same
existing file, rename() shall return successfully and perform no existing file, rename() shall return successfully and perform no
other action. other action.
@ -1532,6 +1619,7 @@ rename (const char *oldpath, const char *newpath)
file ids. If so, it tests for identical volume serial numbers, If so, file ids. If so, it tests for identical volume serial numbers, If so,
oldpath and newpath refer to the same file. */ oldpath and newpath refer to the same file. */
if ((removepc || dstpc->exists ()) if ((removepc || dstpc->exists ())
&& !oldpc.isdir ()
&& NT_SUCCESS (NtQueryInformationFile (fh, &io, &ofsi, sizeof ofsi, && NT_SUCCESS (NtQueryInformationFile (fh, &io, &ofsi, sizeof ofsi,
FileStandardInformation)) FileStandardInformation))
&& ofsi.NumberOfLinks > 1 && ofsi.NumberOfLinks > 1
@ -1562,7 +1650,6 @@ rename (const char *oldpath, const char *newpath)
{ {
debug_printf ("%s and %s are the same file", oldpath, newpath); debug_printf ("%s and %s are the same file", oldpath, newpath);
NtClose (nfh); NtClose (nfh);
NtClose (fh);
res = 0; res = 0;
goto out; goto out;
} }
@ -1577,7 +1664,16 @@ rename (const char *oldpath, const char *newpath)
memcpy (&pfri->FileName, dstpc->get_nt_native_path ()->Buffer, memcpy (&pfri->FileName, dstpc->get_nt_native_path ()->Buffer,
pfri->FileNameLength); pfri->FileNameLength);
status = NtSetInformationFile (fh, &io, pfri, size, FileRenameInformation); status = NtSetInformationFile (fh, &io, pfri, size, FileRenameInformation);
NtClose (fh); /* This happens if the access rights don't allow deleting the destination.
Even if the handle to the original file is opened with BACKUP
and/or RECOVERY, these flags don't apply to the destination of the
rename operation. So, a privileged user can't rename a file to an
existing file, if the permissions of the existing file aren't right.
Like directories, we have to handle this separately by removing the
destination before renaming. */
if (status == STATUS_ACCESS_DENIED && dstpc->exists () && !dstpc->isdir ()
&& NT_SUCCESS (status = unlink_nt (*dstpc)))
status = NtSetInformationFile (fh, &io, pfri, size, FileRenameInformation);
if (NT_SUCCESS (status)) if (NT_SUCCESS (status))
{ {
if (removepc) if (removepc)
@ -1585,25 +1681,11 @@ rename (const char *oldpath, const char *newpath)
res = 0; res = 0;
} }
else else
{ __seterrno_from_nt_status (status);
/* Check in case of STATUS_ACCESS_DENIED and pc.isdir(),
whether we tried to rename to an existing non-empty dir.
In this case we have to set errno to EEXIST. */
if (status == STATUS_ACCESS_DENIED && dstpc->isdir ()
&& NT_SUCCESS (NtOpenFile (&nfh, FILE_LIST_DIRECTORY | SYNCHRONIZE,
dstpc->get_object_attr (attr, sec_none_nih),
&io, FILE_SHARE_VALID_FLAGS,
FILE_OPEN_FOR_BACKUP_INTENT
| FILE_SYNCHRONOUS_IO_NONALERT)))
{
if (check_dir_not_empty (nfh) == STATUS_DIRECTORY_NOT_EMPTY)
status = STATUS_DIRECTORY_NOT_EMPTY;
NtClose (nfh);
}
__seterrno_from_nt_status (status);
}
out: out:
if (fh)
NtClose (fh);
syscall_printf ("%d = rename (%s, %s)", res, oldpath, newpath); syscall_printf ("%d = rename (%s, %s)", res, oldpath, newpath);
return res; return res;
} }