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