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;
025import java.io.RandomAccessFile;
026
027import org.apache.hadoop.conf.Configuration;
028import org.apache.hadoop.fs.FSDataInputStream;
029import org.apache.hadoop.fs.FileSystem;
030import org.apache.hadoop.fs.Path;
031import org.apache.hadoop.fs.permission.FsPermission;
032import org.apache.hadoop.io.nativeio.Errno;
033import org.apache.hadoop.io.nativeio.NativeIO;
034import org.apache.hadoop.io.nativeio.NativeIOException;
035import org.apache.hadoop.io.nativeio.NativeIO.POSIX.Stat;
036import org.apache.hadoop.security.UserGroupInformation;
037
038import com.google.common.annotations.VisibleForTesting;
039
040/**
041 * This class provides secure APIs for opening and creating files on the local
042 * disk. The main issue this class tries to handle is that of symlink traversal.
043 * <br/>
044 * An example of such an attack is:
045 * <ol>
046 * <li> Malicious user removes his task's syslog file, and puts a link to the
047 * jobToken file of a target user.</li>
048 * <li> Malicious user tries to open the syslog file via the servlet on the
049 * tasktracker.</li>
050 * <li> The tasktracker is unaware of the symlink, and simply streams the contents
051 * of the jobToken file. The malicious user can now access potentially sensitive
052 * map outputs, etc. of the target user's job.</li>
053 * </ol>
054 * A similar attack is possible involving task log truncation, but in that case
055 * due to an insecure write to a file.
056 * <br/>
057 */
058public class SecureIOUtils {
059
060  /**
061   * Ensure that we are set up to run with the appropriate native support code.
062   * If security is disabled, and the support code is unavailable, this class
063   * still tries its best to be secure, but is vulnerable to some race condition
064   * attacks.
065   *
066   * If security is enabled but the support code is unavailable, throws a
067   * RuntimeException since we don't want to run insecurely.
068   */
069  static {
070    boolean shouldBeSecure = UserGroupInformation.isSecurityEnabled();
071    boolean canBeSecure = NativeIO.isAvailable();
072
073    if (!canBeSecure && shouldBeSecure) {
074      throw new RuntimeException(
075        "Secure IO is not possible without native code extensions.");
076    }
077
078    // Pre-cache an instance of the raw FileSystem since we sometimes
079    // do secure IO in a shutdown hook, where this call could fail.
080    try {
081      rawFilesystem = FileSystem.getLocal(new Configuration()).getRaw();
082    } catch (IOException ie) {
083      throw new RuntimeException(
084      "Couldn't obtain an instance of RawLocalFileSystem.");
085    }
086
087    // SecureIO just skips security checks in the case that security is
088    // disabled
089    skipSecurity = !canBeSecure;
090  }
091
092  private final static boolean skipSecurity;
093  private final static FileSystem rawFilesystem;
094
095  /**
096   * Open the given File for random read access, verifying the expected user/
097   * group constraints if security is enabled.
098   * 
099   * Note that this function provides no additional security checks if hadoop
100   * security is disabled, since doing the checks would be too expensive when
101   * native libraries are not available.
102   * 
103   * @param f file that we are trying to open
104   * @param mode mode in which we want to open the random access file
105   * @param expectedOwner the expected user owner for the file
106   * @param expectedGroup the expected group owner for the file
107   * @throws IOException if an IO error occurred or if the user/group does
108   * not match when security is enabled.
109   */
110  public static RandomAccessFile openForRandomRead(File f,
111      String mode, String expectedOwner, String expectedGroup)
112      throws IOException {
113    if (!UserGroupInformation.isSecurityEnabled()) {
114      return new RandomAccessFile(f, mode);
115    }
116    return forceSecureOpenForRandomRead(f, mode, expectedOwner, expectedGroup);
117  }
118
119  /**
120   * Same as openForRandomRead except that it will run even if security is off.
121   * This is used by unit tests.
122   */
123  @VisibleForTesting
124  protected static RandomAccessFile forceSecureOpenForRandomRead(File f,
125      String mode, String expectedOwner, String expectedGroup)
126      throws IOException {
127    RandomAccessFile raf = new RandomAccessFile(f, mode);
128    boolean success = false;
129    try {
130      Stat stat = NativeIO.POSIX.getFstat(raf.getFD());
131      checkStat(f, stat.getOwner(), stat.getGroup(), expectedOwner,
132          expectedGroup);
133      success = true;
134      return raf;
135    } finally {
136      if (!success) {
137        raf.close();
138      }
139    }
140  }
141
142  /**
143   * Opens the {@link FSDataInputStream} on the requested file on local file
144   * system, verifying the expected user/group constraints if security is
145   * enabled.
146   * @param file absolute path of the file
147   * @param expectedOwner the expected user owner for the file
148   * @param expectedGroup the expected group owner for the file
149   * @throws IOException if an IO Error occurred or the user/group does not
150   * match if security is enabled
151   */
152  public static FSDataInputStream openFSDataInputStream(File file,
153      String expectedOwner, String expectedGroup) throws IOException {
154    if (!UserGroupInformation.isSecurityEnabled()) {
155      return rawFilesystem.open(new Path(file.getAbsolutePath()));
156    }
157    return forceSecureOpenFSDataInputStream(file, expectedOwner, expectedGroup);
158  }
159
160  /**
161   * Same as openFSDataInputStream except that it will run even if security is
162   * off. This is used by unit tests.
163   */
164  @VisibleForTesting
165  protected static FSDataInputStream forceSecureOpenFSDataInputStream(
166      File file,
167      String expectedOwner, String expectedGroup) throws IOException {
168    final FSDataInputStream in =
169        rawFilesystem.open(new Path(file.getAbsolutePath()));
170    boolean success = false;
171    try {
172      Stat stat = NativeIO.POSIX.getFstat(in.getFileDescriptor());
173      checkStat(file, stat.getOwner(), stat.getGroup(), expectedOwner,
174          expectedGroup);
175      success = true;
176      return in;
177    } finally {
178      if (!success) {
179        in.close();
180      }
181    }
182  }
183
184  /**
185   * Open the given File for read access, verifying the expected user/group
186   * constraints if security is enabled.
187   *
188   * Note that this function provides no additional checks if Hadoop
189   * security is disabled, since doing the checks would be too expensive
190   * when native libraries are not available.
191   *
192   * @param f the file that we are trying to open
193   * @param expectedOwner the expected user owner for the file
194   * @param expectedGroup the expected group owner for the file
195   * @throws IOException if an IO Error occurred, or security is enabled and
196   * the user/group does not match
197   */
198  public static FileInputStream openForRead(File f, String expectedOwner, 
199      String expectedGroup) throws IOException {
200    if (!UserGroupInformation.isSecurityEnabled()) {
201      return new FileInputStream(f);
202    }
203    return forceSecureOpenForRead(f, expectedOwner, expectedGroup);
204  }
205
206  /**
207   * Same as openForRead() except that it will run even if security is off.
208   * This is used by unit tests.
209   */
210  @VisibleForTesting
211  protected static FileInputStream forceSecureOpenForRead(File f, String expectedOwner,
212      String expectedGroup) throws IOException {
213
214    FileInputStream fis = new FileInputStream(f);
215    boolean success = false;
216    try {
217      Stat stat = NativeIO.POSIX.getFstat(fis.getFD());
218      checkStat(f, stat.getOwner(), stat.getGroup(), expectedOwner,
219          expectedGroup);
220      success = true;
221      return fis;
222    } finally {
223      if (!success) {
224        fis.close();
225      }
226    }
227  }
228
229  private static FileOutputStream insecureCreateForWrite(File f,
230      int permissions) throws IOException {
231    // If we can't do real security, do a racy exists check followed by an
232    // open and chmod
233    if (f.exists()) {
234      throw new AlreadyExistsException("File " + f + " already exists");
235    }
236    FileOutputStream fos = new FileOutputStream(f);
237    boolean success = false;
238    try {
239      rawFilesystem.setPermission(new Path(f.getAbsolutePath()),
240        new FsPermission((short)permissions));
241      success = true;
242      return fos;
243    } finally {
244      if (!success) {
245        fos.close();
246      }
247    }
248  }
249
250  /**
251   * Open the specified File for write access, ensuring that it does not exist.
252   * @param f the file that we want to create
253   * @param permissions we want to have on the file (if security is enabled)
254   *
255   * @throws AlreadyExistsException if the file already exists
256   * @throws IOException if any other error occurred
257   */
258  public static FileOutputStream createForWrite(File f, int permissions)
259  throws IOException {
260    if (skipSecurity) {
261      return insecureCreateForWrite(f, permissions);
262    } else {
263      return NativeIO.getCreateForWriteFileOutputStream(f, permissions);
264    }
265  }
266
267  private static void checkStat(File f, String owner, String group, 
268      String expectedOwner, 
269      String expectedGroup) throws IOException {
270    boolean success = true;
271    if (expectedOwner != null &&
272        !expectedOwner.equals(owner)) {
273      if (Path.WINDOWS) {
274        UserGroupInformation ugi =
275            UserGroupInformation.createRemoteUser(expectedOwner);
276        final String adminsGroupString = "Administrators";
277        success = owner.equals(adminsGroupString)
278            && ugi.getGroups().contains(adminsGroupString);
279      } else {
280        success = false;
281      }
282    }
283    if (!success) {
284      throw new IOException(
285          "Owner '" + owner + "' for path " + f + " did not match " +
286              "expected owner '" + expectedOwner + "'");
287    }
288  }
289
290  /**
291   * Signals that an attempt to create a file at a given pathname has failed
292   * because another file already existed at that path.
293   */
294  public static class AlreadyExistsException extends IOException {
295    private static final long serialVersionUID = 1L;
296
297    public AlreadyExistsException(String msg) {
298      super(msg);
299    }
300
301    public AlreadyExistsException(Throwable cause) {
302      super(cause);
303    }
304  }
305}