blob: ad238e6b9f9e97311292cca38d12ee31b3aede3e [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hadoop.io;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.fs.permission.FsPermission;
import org.apache.hadoop.io.nativeio.Errno;
import org.apache.hadoop.io.nativeio.NativeIO;
import org.apache.hadoop.io.nativeio.NativeIOException;
import org.apache.hadoop.io.nativeio.NativeIO.POSIX.Stat;
import org.apache.hadoop.security.UserGroupInformation;
import com.google.common.annotations.VisibleForTesting;
/**
* This class provides secure APIs for opening and creating files on the local
* disk. The main issue this class tries to handle is that of symlink traversal.
* <br/>
* An example of such an attack is:
* <ol>
* <li> Malicious user removes his task's syslog file, and puts a link to the
* jobToken file of a target user.</li>
* <li> Malicious user tries to open the syslog file via the servlet on the
* tasktracker.</li>
* <li> The tasktracker is unaware of the symlink, and simply streams the contents
* of the jobToken file. The malicious user can now access potentially sensitive
* map outputs, etc. of the target user's job.</li>
* </ol>
* A similar attack is possible involving task log truncation, but in that case
* due to an insecure write to a file.
* <br/>
*/
public class SecureIOUtils {
/**
* Ensure that we are set up to run with the appropriate native support code.
* If security is disabled, and the support code is unavailable, this class
* still tries its best to be secure, but is vulnerable to some race condition
* attacks.
*
* If security is enabled but the support code is unavailable, throws a
* RuntimeException since we don't want to run insecurely.
*/
static {
boolean shouldBeSecure = UserGroupInformation.isSecurityEnabled();
boolean canBeSecure = NativeIO.isAvailable();
if (!canBeSecure && shouldBeSecure) {
throw new RuntimeException(
"Secure IO is not possible without native code extensions.");
}
// Pre-cache an instance of the raw FileSystem since we sometimes
// do secure IO in a shutdown hook, where this call could fail.
try {
rawFilesystem = FileSystem.getLocal(new Configuration()).getRaw();
} catch (IOException ie) {
throw new RuntimeException(
"Couldn't obtain an instance of RawLocalFileSystem.");
}
// SecureIO just skips security checks in the case that security is
// disabled
skipSecurity = !canBeSecure;
}
private final static boolean skipSecurity;
private final static FileSystem rawFilesystem;
/**
* Open the given File for random read access, verifying the expected user/
* group constraints if security is enabled.
*
* Note that this function provides no additional security checks if hadoop
* security is disabled, since doing the checks would be too expensive when
* native libraries are not available.
*
* @param f file that we are trying to open
* @param mode mode in which we want to open the random access file
* @param expectedOwner the expected user owner for the file
* @param expectedGroup the expected group owner for the file
* @throws IOException if an IO error occurred or if the user/group does
* not match when security is enabled.
*/
public static RandomAccessFile openForRandomRead(File f,
String mode, String expectedOwner, String expectedGroup)
throws IOException {
if (!UserGroupInformation.isSecurityEnabled()) {
return new RandomAccessFile(f, mode);
}
return forceSecureOpenForRandomRead(f, mode, expectedOwner, expectedGroup);
}
/**
* Same as openForRandomRead except that it will run even if security is off.
* This is used by unit tests.
*/
@VisibleForTesting
protected static RandomAccessFile forceSecureOpenForRandomRead(File f,
String mode, String expectedOwner, String expectedGroup)
throws IOException {
RandomAccessFile raf = new RandomAccessFile(f, mode);
boolean success = false;
try {
Stat stat = NativeIO.POSIX.getFstat(raf.getFD());
checkStat(f, stat.getOwner(), stat.getGroup(), expectedOwner,
expectedGroup);
success = true;
return raf;
} finally {
if (!success) {
raf.close();
}
}
}
/**
* Opens the {@link FSDataInputStream} on the requested file on local file
* system, verifying the expected user/group constraints if security is
* enabled.
* @param file absolute path of the file
* @param expectedOwner the expected user owner for the file
* @param expectedGroup the expected group owner for the file
* @throws IOException if an IO Error occurred or the user/group does not
* match if security is enabled
*/
public static FSDataInputStream openFSDataInputStream(File file,
String expectedOwner, String expectedGroup) throws IOException {
if (!UserGroupInformation.isSecurityEnabled()) {
return rawFilesystem.open(new Path(file.getAbsolutePath()));
}
return forceSecureOpenFSDataInputStream(file, expectedOwner, expectedGroup);
}
/**
* Same as openFSDataInputStream except that it will run even if security is
* off. This is used by unit tests.
*/
@VisibleForTesting
protected static FSDataInputStream forceSecureOpenFSDataInputStream(
File file,
String expectedOwner, String expectedGroup) throws IOException {
final FSDataInputStream in =
rawFilesystem.open(new Path(file.getAbsolutePath()));
boolean success = false;
try {
Stat stat = NativeIO.POSIX.getFstat(in.getFileDescriptor());
checkStat(file, stat.getOwner(), stat.getGroup(), expectedOwner,
expectedGroup);
success = true;
return in;
} finally {
if (!success) {
in.close();
}
}
}
/**
* Open the given File for read access, verifying the expected user/group
* constraints if security is enabled.
*
* Note that this function provides no additional checks if Hadoop
* security is disabled, since doing the checks would be too expensive
* when native libraries are not available.
*
* @param f the file that we are trying to open
* @param expectedOwner the expected user owner for the file
* @param expectedGroup the expected group owner for the file
* @throws IOException if an IO Error occurred, or security is enabled and
* the user/group does not match
*/
public static FileInputStream openForRead(File f, String expectedOwner,
String expectedGroup) throws IOException {
if (!UserGroupInformation.isSecurityEnabled()) {
return new FileInputStream(f);
}
return forceSecureOpenForRead(f, expectedOwner, expectedGroup);
}
/**
* Same as openForRead() except that it will run even if security is off.
* This is used by unit tests.
*/
@VisibleForTesting
protected static FileInputStream forceSecureOpenForRead(File f, String expectedOwner,
String expectedGroup) throws IOException {
FileInputStream fis = new FileInputStream(f);
boolean success = false;
try {
Stat stat = NativeIO.POSIX.getFstat(fis.getFD());
checkStat(f, stat.getOwner(), stat.getGroup(), expectedOwner,
expectedGroup);
success = true;
return fis;
} finally {
if (!success) {
fis.close();
}
}
}
private static FileOutputStream insecureCreateForWrite(File f,
int permissions) throws IOException {
// If we can't do real security, do a racy exists check followed by an
// open and chmod
if (f.exists()) {
throw new AlreadyExistsException("File " + f + " already exists");
}
FileOutputStream fos = new FileOutputStream(f);
boolean success = false;
try {
rawFilesystem.setPermission(new Path(f.getAbsolutePath()),
new FsPermission((short)permissions));
success = true;
return fos;
} finally {
if (!success) {
fos.close();
}
}
}
/**
* Open the specified File for write access, ensuring that it does not exist.
* @param f the file that we want to create
* @param permissions we want to have on the file (if security is enabled)
*
* @throws AlreadyExistsException if the file already exists
* @throws IOException if any other error occurred
*/
public static FileOutputStream createForWrite(File f, int permissions)
throws IOException {
if (skipSecurity) {
return insecureCreateForWrite(f, permissions);
} else {
return NativeIO.getCreateForWriteFileOutputStream(f, permissions);
}
}
private static void checkStat(File f, String owner, String group,
String expectedOwner,
String expectedGroup) throws IOException {
boolean success = true;
if (expectedOwner != null &&
!expectedOwner.equals(owner)) {
if (Path.WINDOWS) {
UserGroupInformation ugi =
UserGroupInformation.createRemoteUser(expectedOwner);
final String adminsGroupString = "Administrators";
success = owner.equals(adminsGroupString)
&& ugi.getGroups().contains(adminsGroupString);
} else {
success = false;
}
}
if (!success) {
throw new IOException(
"Owner '" + owner + "' for path " + f + " did not match " +
"expected owner '" + expectedOwner + "'");
}
}
/**
* Signals that an attempt to create a file at a given pathname has failed
* because another file already existed at that path.
*/
public static class AlreadyExistsException extends IOException {
private static final long serialVersionUID = 1L;
public AlreadyExistsException(String msg) {
super(msg);
}
public AlreadyExistsException(Throwable cause) {
super(cause);
}
}
}