Commit 2fb5da81 authored by Fabian Vogt's avatar Fabian Vogt Committed by Alexander Saoutkin

Rewrite absolute symlinks to stay inside their origin

Absolute symlinks have a different meaning when accessed though the regular
filesystem, they don't anchor to the mounted URL anymore:
sftp://server/tmp/symlink -> /home/user/
/mnt/sftp/server/tmp/symlink -> /home/user/
Rewrite absolute symlinks when reading or writing them to keep the target
consistent:
/mnt/sftp/server/tmp/symlink -> /mnt/sftp/server/home/user/

It is necessary that KIOFuseVFS is aware of the mountpoint now, to be able to
deal with absolute paths. This is not ideal as it doesn't account for bind
mounts or symlinks.

Use readlink in fileopstest to get the raw link target instead of an absolute
path.

BUG: 422090
parent 83233ea8
......@@ -19,6 +19,7 @@
#include <QDateTime>
#include <QDebug>
#include <QDir>
#include <QVersionNumber>
#include <KIO/ListJob>
......@@ -185,12 +186,15 @@ bool KIOFuseVFS::start(struct fuse_args &args, const QString& mountpoint)
m_fuseConnInfoOpts.reset(fuse_parse_conn_info_opts(&args));
m_fuseSession = fuse_session_new(&args, &fuse_ll_ops, sizeof(fuse_ll_ops), this);
m_mountpoint = QDir::cleanPath(mountpoint); // Remove redundant slashes
if(!m_mountpoint.endsWith(QLatin1Char('/')))
m_mountpoint += QLatin1Char('/');
if(!m_fuseSession)
return false;
if(!setupSignalHandlers()
|| fuse_session_mount(m_fuseSession, mountpoint.toUtf8().data()) != 0)
|| fuse_session_mount(m_fuseSession, m_mountpoint.toUtf8().data()) != 0)
{
stop();
return false;
......@@ -307,19 +311,28 @@ void KIOFuseVFS::mountUrl(QUrl url, std::function<void (const QString &, int)> c
}
// The file exists, try to mount it
QUrl originUrl = url;
if(originUrl.path().startsWith(QLatin1Char('/')))
originUrl.setPath(QStringLiteral("/"));
else
originUrl.setPath({});
auto pathElements = url.path().split(QLatin1Char('/'));
pathElements.removeAll({});
return findAndCreateOrigin(originUrl, pathElements, callback);
return findAndCreateOrigin(originOfUrl(url), pathElements, callback);
});
}
QStringList KIOFuseVFS::mapUrlToVfs(QUrl url)
{
// Build the path where it will appear in the VFS
auto targetPathComponents = QStringList{url.scheme(), url.authority()};
targetPathComponents.append(url.path().split(QLatin1Char('/')));
// Strip empty path elements, for instance in
// "file:///home/foo"
// ^ V
// "ftp://user@host/dir/ectory/"
targetPathComponents.removeAll({});
return targetPathComponents;
}
void KIOFuseVFS::findAndCreateOrigin(QUrl url, QStringList pathElements, std::function<void (const QString &, int)> callback)
{
QUrl urlWithoutPassword = url;
......@@ -343,15 +356,7 @@ void KIOFuseVFS::findAndCreateOrigin(QUrl url, QStringList pathElements, std::fu
qDebug(KIOFUSE_LOG) << "Origin found at" << urlWithoutPassword;
// Build the path where it will appear in the VFS
auto targetPathComponents = QStringList{url.scheme(), url.authority()};
targetPathComponents.append(url.path().split(QLatin1Char('/')));
// Strip empty path elements, for instance in
// "file:///home/foo"
// ^ V
// "ftp://user@host/dir/ectory/"
targetPathComponents.removeAll({});
auto targetPathComponents = mapUrlToVfs(url);
if(std::any_of(targetPathComponents.begin(), targetPathComponents.end(),
[](const QString &s) { return !isValidFilename(s); }))
......@@ -684,7 +689,20 @@ void KIOFuseVFS::readlink(fuse_req_t req, fuse_ino_t ino)
that->awaitAttrRefreshed(node, [=](int error) {
Q_UNUSED(error); // Just send the old target...
fuse_reply_readlink(req, symlinkNode->m_target.toUtf8().data());
QString target = symlinkNode->m_target;
// Convert an absolute link to be absolute within its origin
if(QDir::isAbsolutePath(target))
{
target = target.mid(1); // Strip the initial /
QUrl origin = that->originOfUrl(that->remoteUrl(symlinkNode));
origin = addPathElements(origin, target.split(QLatin1Char('/')));
target = that->m_mountpoint + that->mapUrlToVfs(origin).join(QLatin1Char('/'));
qCDebug(KIOFUSE_LOG) << "Detected reading of absolute symlink" << symlinkNode->m_target << "at" << that->virtualPath(symlinkNode) << ", rewritten to" << target;
}
fuse_reply_readlink(req, target.toUtf8().data());
});
}
......@@ -866,9 +884,33 @@ void KIOFuseVFS::symlink(fuse_req_t req, const char *link, fuse_ino_t parent, co
return;
}
QString target = QString::fromUtf8(link);
// Convert an absolute link within the VFS to an absolute link on the origin
// (inverse of readlink)
if(QDir::isAbsolutePath(target) && target.startsWith(that->m_mountpoint))
{
QUrl remoteSourceUrl = that->remoteUrl(remote),
// QDir::relativePath would mangle the path, we want to keep it as-is
remoteTargetUrl = that->localPathToRemoteUrl(target.mid(that->m_mountpoint.length()));
if(remoteTargetUrl.isValid()
&& remoteSourceUrl.scheme() == remoteTargetUrl.scheme()
&& remoteSourceUrl.authority() == remoteTargetUrl.authority())
{
target = remoteTargetUrl.path();
if(!target.startsWith(QLatin1Char('/')))
target.prepend(QLatin1Char('/'));
qCDebug(KIOFUSE_LOG) << "Detected creation of absolute symlink, rewritten to" << target;
}
else
qCWarning(KIOFUSE_LOG) << "Creation of absolute symlink to other origin";
}
auto namestr = QString::fromUtf8(name);
auto url = addPathElements(that->remoteUrl(node), {namestr});
auto *job = KIO::symlink(QString::fromUtf8(link), url);
auto *job = KIO::symlink(target, url);
that->connect(job, &KIO::SimpleJob::finished, [=] {
if(job->error())
{
......@@ -2231,6 +2273,17 @@ void KIOFuseVFS::awaitChildMounted(const std::shared_ptr<KIOFuseRemoteDirNode> &
});
}
QUrl KIOFuseVFS::originOfUrl(QUrl url)
{
QUrl originUrl = url;
if(originUrl.path().startsWith(QLatin1Char('/')))
originUrl.setPath(QStringLiteral("/"));
else
originUrl.setPath({});
return originUrl;
}
bool KIOFuseVFS::setupSignalHandlers()
{
// Create required socketpair for custom signal handling
......
......@@ -149,6 +149,10 @@ private:
/** Invokes callback on error on when the child node was fetched and created/updated. */
void awaitChildMounted(const std::shared_ptr<KIOFuseRemoteDirNode> &node, const QString name, std::function<void(const std::shared_ptr<KIOFuseNode>&, int)> callback);
/** Returns the URL pointing to the origin of the linked resource, i.e. path set to / or empty. */
QUrl originOfUrl(QUrl url);
/** Returns the path elements where the URL url gets mapped to in this VFS. */
QStringList mapUrlToVfs(QUrl url);
/** Stats url. If successful, returns the path where url + pathElements is reachable in callback.
* If it failed, it moves one part of pathElements to url and tries again, recursively. */
void findAndCreateOrigin(QUrl url, QStringList pathElements, std::function<void(const QString&, int)> callback);
......@@ -169,6 +173,8 @@ private:
std::unique_ptr<struct fuse_conn_info_opts, decltype(&free)> m_fuseConnInfoOpts{nullptr, &free};
/** Fuse bookkeeping. */
std::unique_ptr<QSocketNotifier> m_fuseNotifier;
/** Path where this VFS is currently mounted at, with trailing '/'. */
QString m_mountpoint;
/** Fds of paired sockets.
* Used in conjunction with socket notifier to allow handling signals with the Qt event loop. **/
......
......@@ -44,6 +44,7 @@ private Q_SLOTS:
void testSymlinkRefresh();
void testTypeRefresh();
void testDirSymlink();
void testSymlinkRewrite();
#ifdef WASTE_DISK_SPACE
void testReadWrite4GBFile();
#endif // WASTE_DISK_SPACE
......@@ -51,6 +52,10 @@ private Q_SLOTS:
private:
QDateTime roundDownToSecond(QDateTime dt);
bool forceNodeTimeout();
/** Unlike QFileInfo::symLinkTarget, which returns absolute paths only,
* this returns the raw link content. On failure or truncation, a null
* QString is returned instead. */
QString readlink(QString symlink);
org::kde::KIOFuse::VFS m_kiofuse_iface{QStringLiteral("org.kde.KIOFuse"),
QStringLiteral("/org/kde/KIOFuse"),
......@@ -762,16 +767,16 @@ void FileOpsTest::testSymlinkRefresh()
QVERIFY(mirrorDir.exists());
// Create a symlink
QCOMPARE(symlink("/oldtarget", localDir.filePath(QStringLiteral("symlink")).toUtf8().data()), 0);
QCOMPARE(QFile::symLinkTarget(mirrorDir.filePath(QStringLiteral("symlink"))), QStringLiteral("/oldtarget"));
QCOMPARE(symlink("oldtarget", localDir.filePath(QStringLiteral("symlink")).toUtf8().data()), 0);
QCOMPARE(readlink(mirrorDir.filePath(QStringLiteral("symlink"))), QStringLiteral("oldtarget"));
// Change the symlink
QVERIFY(QFile::remove(localDir.filePath((QStringLiteral("symlink")))));
QCOMPARE(symlink("/newtarget", localDir.filePath(QStringLiteral("symlink")).toUtf8().data()), 0);
QCOMPARE(symlink("newtarget", localDir.filePath(QStringLiteral("symlink")).toUtf8().data()), 0);
QVERIFY(forceNodeTimeout());
QCOMPARE(QFile::symLinkTarget(mirrorDir.filePath(QStringLiteral("symlink"))), QStringLiteral("/newtarget"));
QCOMPARE(readlink(mirrorDir.filePath(QStringLiteral("symlink"))), QStringLiteral("newtarget"));
}
void FileOpsTest::testTypeRefresh()
......@@ -828,6 +833,9 @@ void FileOpsTest::testDirSymlink()
QVERIFY(mirrorDir.mkpath(QStringLiteral("realdir/child")));
QCOMPARE(symlink("realdir/../realdir",
qPrintable(mirrorDir.filePath(QStringLiteral("linktodir")))), 0);
// Verify that the link is saved as-is
QCOMPARE(readlink(mirrorDir.filePath(QStringLiteral("linktodir"))),
QStringLiteral("realdir/../realdir"));
// Verify that it was correctly created everywhere
QVERIFY(mirrorDir.exists(QStringLiteral("linktodir/child")));
......@@ -846,6 +854,55 @@ void FileOpsTest::testDirSymlink()
QVERIFY(!mountReply.isError());
}
void FileOpsTest::testSymlinkRewrite()
{
QTemporaryDir localTmpDir;
QVERIFY(localTmpDir.isValid());
QDir localDir(localTmpDir.path());
// Mount the temporary dir
QString reply = m_kiofuse_iface.mountUrl(QStringLiteral("file://%1").arg(localDir.path())).value();
QVERIFY(!reply.isEmpty());
QDir mirrorDir(reply);
QVERIFY(mirrorDir.exists());
// Create a symlink /mnt/file/.../symlink -> /mnt/file/.../somedir/../somefile.
// This is to test that even if the target does not exist and is some convoluted
// path, it is still rewritten correctly.
QCOMPARE(symlink(qPrintable(mirrorDir.filePath(QStringLiteral("somedir/../somefile"))),
qPrintable(mirrorDir.filePath(QStringLiteral("symlink")))), 0);
// Verify that it can be read back as-is on the mount
QCOMPARE(readlink(mirrorDir.filePath(QStringLiteral("symlink"))),
mirrorDir.filePath(QStringLiteral("somedir/../somefile")));
// Verify that it's absolute on the local side
QCOMPARE(readlink(localDir.filePath(QStringLiteral("symlink"))),
localDir.filePath(QStringLiteral("somedir/../somefile")));
// Mount something with a different origin
QString outerpath = QFINDTESTDATA(QStringLiteral("data/outerarchive.tar.gz"));
reply = m_kiofuse_iface.mountUrl(QStringLiteral("tar://%1/outerarchive/").arg(outerpath)).value();
QVERIFY(!reply.isEmpty());
QDir archiveDir(reply);
QVERIFY(archiveDir.exists());
// Create a symlink /mnt/file/.../symlink -> /mnt/tar/.../outerarchive/somewhere
// which can't be rewritten properly
QCOMPARE(symlink(qPrintable(archiveDir.filePath(QStringLiteral("somewhere"))),
qPrintable(mirrorDir.filePath(QStringLiteral("symlink2")))), 0);
// Verify that it didn't get rewritten during write
QCOMPARE(readlink(localDir.filePath(QStringLiteral("symlink2"))),
archiveDir.filePath(QStringLiteral("somewhere")));
// If rewriting fails, it'll keep it as-is, but the next read will rewrite
// it in the other direction again, i.e. /mnt/file/mnt/tar/...
reply = m_kiofuse_iface.mountUrl(QStringLiteral("file://%1").arg(archiveDir.path())).value();
QVERIFY(!reply.isEmpty());
QCOMPARE(readlink(mirrorDir.filePath(QStringLiteral("symlink2"))),
QDir(reply).filePath(QStringLiteral("somewhere")));
}
#ifdef WASTE_DISK_SPACE
void FileOpsTest::testReadWrite4GBFile()
{
......@@ -896,6 +953,16 @@ bool FileOpsTest::forceNodeTimeout()
return !reply.isError();
}
QString FileOpsTest::readlink(QString symlink)
{
char buf[PATH_MAX];
int len = ::readlink(qPrintable(symlink), buf, sizeof(buf));
if(len < 0 || len >= int(sizeof(buf))) // Failed or truncated?
return {}; // Return a null QString
return QString::fromLocal8Bit(buf, len);
}
QTEST_GUILESS_MAIN(FileOpsTest)
#include "fileopstest.moc"
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment