001/**
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.io;
019
020import java.io.File;
021import java.io.FileDescriptor;
022import java.io.FileInputStream;
023import java.io.FileOutputStream;
024import java.io.IOException;
025
026import org.apache.hadoop.conf.Configuration;
027import org.apache.hadoop.fs.FileStatus;
028import org.apache.hadoop.fs.FileSystem;
029import org.apache.hadoop.fs.Path;
030import org.apache.hadoop.fs.permission.FsPermission;
031import org.apache.hadoop.io.nativeio.Errno;
032import org.apache.hadoop.io.nativeio.NativeIO;
033import org.apache.hadoop.io.nativeio.NativeIOException;
034import org.apache.hadoop.io.nativeio.NativeIO.Stat;
035import org.apache.hadoop.security.UserGroupInformation;
036
037/**
038 * This class provides secure APIs for opening and creating files on the local
039 * disk. The main issue this class tries to handle is that of symlink traversal.
040 * <br/>
041 * An example of such an attack is:
042 * <ol>
043 * <li> Malicious user removes his task's syslog file, and puts a link to the
044 * jobToken file of a target user.</li>
045 * <li> Malicious user tries to open the syslog file via the servlet on the
046 * tasktracker.</li>
047 * <li> The tasktracker is unaware of the symlink, and simply streams the contents
048 * of the jobToken file. The malicious user can now access potentially sensitive
049 * map outputs, etc. of the target user's job.</li>
050 * </ol>
051 * A similar attack is possible involving task log truncation, but in that case
052 * due to an insecure write to a file.
053 * <br/>
054 */
055public class SecureIOUtils {
056
057  /**
058   * Ensure that we are set up to run with the appropriate native support code.
059   * If security is disabled, and the support code is unavailable, this class
060   * still tries its best to be secure, but is vulnerable to some race condition
061   * attacks.
062   *
063   * If security is enabled but the support code is unavailable, throws a
064   * RuntimeException since we don't want to run insecurely.
065   */
066  static {
067    boolean shouldBeSecure = UserGroupInformation.isSecurityEnabled();
068    boolean canBeSecure = NativeIO.isAvailable();
069
070    if (!canBeSecure && shouldBeSecure) {
071      throw new RuntimeException(
072        "Secure IO is not possible without native code extensions.");
073    }
074
075    // Pre-cache an instance of the raw FileSystem since we sometimes
076    // do secure IO in a shutdown hook, where this call could fail.
077    try {
078      rawFilesystem = FileSystem.getLocal(new Configuration()).getRaw();
079    } catch (IOException ie) {
080      throw new RuntimeException(
081      "Couldn't obtain an instance of RawLocalFileSystem.");
082    }
083
084    // SecureIO just skips security checks in the case that security is
085    // disabled
086    skipSecurity = !canBeSecure;
087  }
088
089  private final static boolean skipSecurity;
090  private final static FileSystem rawFilesystem;
091
092  /**
093   * Open the given File for read access, verifying the expected user/group
094   * constraints if security is enabled.
095   *
096   * Note that this function provides no additional checks if Hadoop
097   * security is disabled, since doing the checks would be too expensive
098   * when native libraries are not available.
099   *
100   * @param f the file that we are trying to open
101   * @param expectedOwner the expected user owner for the file
102   * @param expectedGroup the expected group owner for the file
103   * @throws IOException if an IO Error occurred, or security is enabled and
104   * the user/group does not match
105   */
106  public static FileInputStream openForRead(File f, String expectedOwner, 
107      String expectedGroup) throws IOException {
108    if (!UserGroupInformation.isSecurityEnabled()) {
109      return new FileInputStream(f);
110    }
111    return forceSecureOpenForRead(f, expectedOwner, expectedGroup);
112  }
113
114  /**
115   * Same as openForRead() except that it will run even if security is off.
116   * This is used by unit tests.
117   */
118  static FileInputStream forceSecureOpenForRead(File f, String expectedOwner,
119      String expectedGroup) throws IOException {
120
121    FileInputStream fis = new FileInputStream(f);
122    boolean success = false;
123    try {
124      Stat stat = NativeIO.fstat(fis.getFD());
125      checkStat(f, stat.getOwner(), stat.getGroup(), expectedOwner,
126          expectedGroup);
127      success = true;
128      return fis;
129    } finally {
130      if (!success) {
131        fis.close();
132      }
133    }
134  }
135
136  private static FileOutputStream insecureCreateForWrite(File f,
137      int permissions) throws IOException {
138    // If we can't do real security, do a racy exists check followed by an
139    // open and chmod
140    if (f.exists()) {
141      throw new AlreadyExistsException("File " + f + " already exists");
142    }
143    FileOutputStream fos = new FileOutputStream(f);
144    boolean success = false;
145    try {
146      rawFilesystem.setPermission(new Path(f.getAbsolutePath()),
147        new FsPermission((short)permissions));
148      success = true;
149      return fos;
150    } finally {
151      if (!success) {
152        fos.close();
153      }
154    }
155  }
156
157  /**
158   * Open the specified File for write access, ensuring that it does not exist.
159   * @param f the file that we want to create
160   * @param permissions we want to have on the file (if security is enabled)
161   *
162   * @throws AlreadyExistsException if the file already exists
163   * @throws IOException if any other error occurred
164   */
165  public static FileOutputStream createForWrite(File f, int permissions)
166  throws IOException {
167    if (skipSecurity) {
168      return insecureCreateForWrite(f, permissions);
169    } else {
170      // Use the native wrapper around open(2)
171      try {
172        FileDescriptor fd = NativeIO.open(f.getAbsolutePath(),
173          NativeIO.O_WRONLY | NativeIO.O_CREAT | NativeIO.O_EXCL,
174          permissions);
175        return new FileOutputStream(fd);
176      } catch (NativeIOException nioe) {
177        if (nioe.getErrno() == Errno.EEXIST) {
178          throw new AlreadyExistsException(nioe);
179        }
180        throw nioe;
181      }
182    }
183  }
184
185  private static void checkStat(File f, String owner, String group, 
186      String expectedOwner, 
187      String expectedGroup) throws IOException {
188    if (expectedOwner != null &&
189        !expectedOwner.equals(owner)) {
190      throw new IOException(
191        "Owner '" + owner + "' for path " + f + " did not match " +
192        "expected owner '" + expectedOwner + "'");
193    }
194    if (expectedGroup != null &&
195        !expectedGroup.equals(group)) {
196      throw new IOException(
197        "Group '" + group + "' for path " + f + " did not match " +
198        "expected group '" + expectedGroup + "'");
199    }
200  }
201
202  /**
203   * Signals that an attempt to create a file at a given pathname has failed
204   * because another file already existed at that path.
205   */
206  public static class AlreadyExistsException extends IOException {
207    private static final long serialVersionUID = 1L;
208
209    public AlreadyExistsException(String msg) {
210      super(msg);
211    }
212
213    public AlreadyExistsException(Throwable cause) {
214      super(cause);
215    }
216  }
217}